ステージの上方に、次に配置可能なブロックを表示しておく。
次に来るブロックは、ブロックを配置する際(ステージクリック)にランダムで設定するようになっている。
ソースコード
index.html
1 | <!DOCTYPE html> |
2 | < html lang = "ja" > |
3 | < head > |
4 | < meta charset = "UTF-8" > |
5 | < meta name = "viewport" content = "width=device-width, initial-scale=1.0" > |
6 | < title >OTIMONO(09次のブロックを表示する)</ title > |
7 | < style > |
8 | *{ |
9 | margin: 0; |
10 | padding: 0; |
11 | } |
12 | </ style > |
13 | < script src = "main.js" type = "text/javascript" ></ script > |
14 | </ head > |
15 | < body > |
16 | < canvas id = "canvas" width = "512" height = "640" ></ canvas > |
17 | </ body > |
18 | </ html > |
main.js
1 | // 09次のブロックを表示する |
2 | let isDebug = true ; // デバッグモードON/OFF |
3 | let canvas = null ; |
4 | let g = null ; |
5 | let pos = {x:0, y:0}; // キャンバス上のマウス座標 |
6 | let dropSpeed = 3; // 落下速度(10-->1秒, 1-->0.1秒) |
7 | let timer = {start: 0, end: 0}; // 落下速度計測用 |
8 |
9 | const BLOCK_SIZE = 64; // 1ブロックのサイズ(=パズルキャラのサイズ)64x64ピクセル |
10 | const ROW_SIZE = 8; // 行数 |
11 | const COLUMN_SIZE = 8; // 列数 |
12 | const BLOCK_LENGTH = ROW_SIZE * COLUMN_SIZE; // ステージ全体のブロック長(行数×列数) |
13 |
14 | // ゲームの状態(game.stateで使用: Symbolの引数文字列は状態表示に利用) |
15 | const TITLE = Symbol( 'title' ); // タイトル画面(未使用) |
16 | const WAITING = Symbol( 'waiting' ); // ブロック配置待ち |
17 | const SET_BLOCK = Symbol( 'set_block' ); // ブロック配置 |
18 | const DROP = Symbol( 'drop' ); // 落下中 |
19 | const CHECK_BLOCK = Symbol( 'check_block' ); // 消せるブロックをチェック |
20 | const DELETE_BLOCK = Symbol( 'delete_block' ); // ブロックを消去 |
21 |
22 | // ステージ背景色(市松模様) |
23 | const BACKGROUND_COLOR1 = "pink" ; |
24 | const BACKGROUND_COLOR2 = "skyblue" ; |
25 |
26 | // ゲーム全体の設定や情報 |
27 | let game = { |
28 | state: WAITING, // ゲーム中の状態 |
29 | hiscore: 0, // 未使用 |
30 | offsetY: 128, // キャンバス左上からのステージ座標オフセット値(Y座標) |
31 | }; |
32 |
33 | let currentBlockType = 0; // 配置するブロックタイプ(1~6) |
34 | let nextBlockType = 0; // 次に配置するブロックタイプ(1~6) |
35 |
36 | // ゲームに使う画像ファイル |
37 | const image_files = [ "space.png" , "alien.png" , "apple.png" , "dog.png" , "obake.png" , "ultra.png" , "usi.png" , "delete.png" ]; |
38 |
39 | // 画像オブジェクト(画像ファイルをゲームで利用するため) |
40 | let images = []; |
41 |
42 | // ステージ情報配列 |
43 | let stageState = new Array(BLOCK_LENGTH); |
44 |
45 | /* |
46 | * マウスを動かしている時の処理 |
47 | */ |
48 | const mouseMoveListener = (e) => { |
49 | // オフセット位置:キャンバスがブラウザの左上からどれくらいの位置にあるか?(rect.left, rect.top) |
50 | const rect = e.target.getBoundingClientRect(); |
51 |
52 | // e.clientXとe.clientYはブラウザ左上からのキャンバスクリック位置なのでオフセット分を引くとキャンバス上の座標が分かる |
53 | pos.x = e.clientX - rect.left; |
54 | pos.y = e.clientY - rect.top; |
55 | }; |
56 |
57 | /* |
58 | * マウスボタンを押した時の処理 |
59 | */ |
60 | const mouseDownListener = () => { |
61 | // ブロックが配置できる条件 |
62 | // 1)ステージ上にマウスポインタがある かつ |
63 | // 2)全てのブロックが落下して消去ブロックが無い場合 |
64 | if (pos.y >= game.offsetY && game.state == WAITING) game.state = SET_BLOCK; |
65 | }; |
66 |
67 | /* |
68 | * 背景を描画 |
69 | */ |
70 | const drawBackground = ()=> { |
71 | let x = 0, y = 0; // ステージ上のブロック座標 |
72 |
73 | // 市松模様のステージ色 |
74 | let color1 = BACKGROUND_COLOR1; |
75 | let color2 = BACKGROUND_COLOR2; |
76 |
77 | // 市松模様を描画 |
78 | for (i=0; i<BLOCK_LENGTH; i++){ |
79 | // 1段下がると先頭の色を入れ替える |
80 | if (i % COLUMN_SIZE == 0) [color1, color2] = [color2, color1]; |
81 | // 格子状にブロックを表示 |
82 | x = i % COLUMN_SIZE; // x方向ブロック座標 |
83 | y = Math.floor(i / ROW_SIZE); // Y方向ブロック座標 |
84 |
85 | // 色を交互に設定 |
86 | bgColor = (i % 2 == 0) ? color1: color2; |
87 | g.fillStyle = bgColor; |
88 | g.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE + game.offsetY, BLOCK_SIZE, BLOCK_SIZE); |
89 | } |
90 | } |
91 |
92 | /* |
93 | * カーソルを描画 |
94 | */ |
95 | const drawCursor = ()=> { |
96 | // ステージ外ではなにもしない |
97 | if (pos.y < game.offsetY) return ; |
98 |
99 | // マウス座標からブロック位置を求める |
100 | const x = Math.floor(pos.x / BLOCK_SIZE); |
101 | const y = Math.floor(pos.y / BLOCK_SIZE); |
102 |
103 | g.fillStyle = "rgba(128, 128, 128, 0.5)" ; // グレー色で透過 |
104 | g.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); |
105 | } |
106 |
107 | /* |
108 | * ステージを表示 |
109 | */ |
110 | const drawStage = ()=> { |
111 | // キャンバスクリア |
112 | g.clearRect(0, 0, canvas.width, canvas.height); |
113 | // 背景描画 |
114 | drawBackground(); |
115 | // カーソル描画 |
116 | drawCursor(); |
117 | // 次のブロックを表示 |
118 | showNextBlock(); |
119 |
120 | // ステージ配列に基づき、ブロック画像を表示 |
121 | for (let i=0; i<BLOCK_LENGTH; i++){ |
122 | x = i % COLUMN_SIZE; // x方向ブロック座標 |
123 | y = Math.floor(i / ROW_SIZE); // Y方向ブロック座標 |
124 |
125 | // ブロックを表示 |
126 | g.drawImage(images[stageState[i]], x*BLOCK_SIZE, y*BLOCK_SIZE + game.offsetY); |
127 |
128 | // ゲーム状態を表す配列内容を表示(デバッグ用) |
129 | if (isDebug){ |
130 | g.fillStyle = "#fffa" ; |
131 | g.fillText(stageState[i], x*BLOCK_SIZE+20, y*BLOCK_SIZE+20+game.offsetY); |
132 | } |
133 | } |
134 | } |
135 |
136 | /* |
137 | * 画像ファイル読み込み |
138 | */ |
139 | const loadImages = async ()=> { |
140 | const images = []; |
141 | const promises = []; |
142 |
143 | image_files.forEach((url) =>{ |
144 | const promise = new Promise((resolve)=>{ |
145 | const img = new Image(); |
146 | img.addEventListener( "load" , ()=>{ |
147 | console.log( "loaded " + url); |
148 | resolve(); // 読み込み完了通知 |
149 | }); |
150 | img.src = "./asset/" + url; // 画像ファイル読み込み開始 |
151 | images.push(img); // 画像オブジェクト配列に格納 |
152 | }); |
153 | promises.push(promise); |
154 | }); |
155 | await Promise.all(promises); // 全ての画像ファイル読み込みが完了してから |
156 | return images; // 画像オブジェクトを返す |
157 | } |
158 |
159 | /* |
160 | * ステージ情報を生成 |
161 | */ |
162 | const createStage = (stage)=> { |
163 | stageState = []; |
164 | stageState = [...stage]; // ステージ情報をコピー(stageStage <-- stage) |
165 | console.log(stageState); |
166 | } |
167 |
168 | /* |
169 | * ブロックを落とす |
170 | */ |
171 | const dropBlock = ()=> { |
172 | // 落下タイミングチェック |
173 | timer.end = new Date().getTime(); |
174 | const passedTime = (timer.end - timer.start) / 100; // 10分の1秒に直す |
175 |
176 | if (passedTime <= dropSpeed){ |
177 | return ; // 落下しない |
178 | } |
179 |
180 | let dropCount = 0; // 落としたブロックの数 |
181 | for (let y=ROW_SIZE-2; y>=0; y--){ // ステージの下方向からチェック |
182 | for (let x=0; x<COLUMN_SIZE; x++){ |
183 | const upIndex = y*ROW_SIZE+x; |
184 | const downIndex = (y+1)*ROW_SIZE+x; |
185 | if (stageState[upIndex] != 0 && stageState[downIndex] == 0){ |
186 | // 入れ替え |
187 | [ stageState[upIndex], stageState[downIndex] ]= [ stageState[downIndex], stageState[upIndex] ]; |
188 | dropCount++; // 落とすブロックの数をカウント |
189 | } |
190 | } |
191 | } |
192 | timer.start = new Date().getTime(); // 落下タイマー再スタート |
193 |
194 | return dropCount; // 落としたブロック数を返す |
195 | } |
196 |
197 | /* |
198 | * ブロックが消去できるかチェックする |
199 | */ |
200 | const checkBlock = () =>{ |
201 | // ステージの状態をコピー |
202 | const check = [...stageState]; |
203 | // 消すブロックの数 |
204 | let deleteBlockCount = 0; |
205 | // 横方向チェック |
206 | for (let y=0; y<ROW_SIZE; y++){ |
207 | for (let x=1; x<COLUMN_SIZE-1; x++){ |
208 | const currentIndex = y*ROW_SIZE + x; // 中央 |
209 | const rightIndex = y*ROW_SIZE + (x + 1); // 右側 |
210 | const leftIndex = y*ROW_SIZE + (x - 1); // 左側 |
211 | if (stageState[currentIndex] > 0){ |
212 | if ((stageState[currentIndex] === stageState[rightIndex] && stageState[currentIndex] === stageState[leftIndex])){ |
213 | check[currentIndex] = check[rightIndex] = check[leftIndex] = 7; // 7はこれから消す予定の×マーク画像 |
214 | deleteBlockCount++; |
215 | } |
216 | } |
217 | } |
218 | } |
219 | // 縦方向チェック |
220 | for (let y=1; y<ROW_SIZE-1; y++){ |
221 | for (let x=0; x<COLUMN_SIZE; x++){ |
222 | const currentIndex = y*ROW_SIZE + x; // 中央 |
223 | const upIndex = (y-1)*ROW_SIZE + x; // 上側 |
224 | const downIndex = (y+1)*ROW_SIZE + x; // 下側 |
225 | if (stageState[currentIndex] > 0){ |
226 | if ((stageState[currentIndex] === stageState[upIndex] && stageState[currentIndex] === stageState[downIndex])){ |
227 | check[currentIndex] = check[upIndex] = check[downIndex] = 7; // 7はこれから消す予定の×マーク画像 |
228 | deleteBlockCount++; |
229 | } |
230 | } |
231 | } |
232 | } |
233 |
234 | // チェック後の状態をステージ情報に戻す |
235 | stageState = [...check]; |
236 | return deleteBlockCount; |
237 | } |
238 |
239 | /* |
240 | * ブロックを消去する |
241 | */ |
242 | const deleteBlock = () =>{ |
243 | for (let i=0; i<BLOCK_LENGTH; i++){ |
244 | if (stageState[i] == 7){ // ×画像だったら |
245 | stageState[i] = 0; // 何もない画像にする |
246 | } |
247 | } |
248 | } |
249 |
250 | /* |
251 | * 最上段にランダムにブロックを配置する |
252 | */ |
253 | const setRandomBlock = () =>{ |
254 | for (let i=0; i<COLUMN_SIZE; i++){ |
255 | const blockType = Math.floor(Math.random() * (images.length-1)); // 0~6 |
256 | stageState[i] = blockType; |
257 | } |
258 | } |
259 |
260 | /* |
261 | * 次のブロックをランダムで決定する |
262 | */ |
263 | const setNextBlock = () =>{ |
264 | // 次のブロックタイプをランダムで設定 |
265 | nextBlockType = Math.floor(Math.random() * (images.length-2)) + 1; // 1~6 |
266 | console.log( "NEXT: " + image_files[nextBlockType]); |
267 | } |
268 |
269 | /* |
270 | * 次のブロックを表示する |
271 | */ |
272 | const showNextBlock = () =>{ |
273 | // 文字と背景 |
274 | g.fillStyle = "#555" ; |
275 | g.fillText( "NEXT" , 324, 50); |
276 |
277 | g.fillStyle = "#ddd" ; |
278 | g.fillRect(384, 0, 128, 128); |
279 |
280 | // 次のブロックを表示 |
281 | g.drawImage(images[nextBlockType], 424, 35, 50, 50); |
282 | } |
283 |
284 | /* |
285 | * ゲーム状態を表示(デバッグ表示用) |
286 | */ |
287 | const showGameState = () =>{ |
288 | // Symbolのdescriptionを表示する |
289 | g.fillStyle = "yellow" ; |
290 | g.fillText(game.state.description, 220, 220 + game.offsetY); |
291 | } |
292 |
293 | /* |
294 | * ゲームのメイン処理(少しだけゲームっぽくなってきている) |
295 | */ |
296 | const GameMain = () =>{ |
297 | // ステージ表示 |
298 | drawStage(); |
299 |
300 | // ゲーム状態を表示(デバッグ用) |
301 | if (isDebug){ |
302 | showGameState(); |
303 | } |
304 |
305 | // ゲーム状態(game.state)による分岐処理 |
306 | if (game.state == DROP){ |
307 | // ブロックを落とす |
308 | if (dropBlock() == 0){ // 落とすブロックが無くなったら |
309 | game.state = CHECK_BLOCK; |
310 | } |
311 | } |
312 | else if (game.state == CHECK_BLOCK){ |
313 | // 消せるブロックをチェックする |
314 | if (checkBlock() != 0){ |
315 | game.state = DELETE_BLOCK; |
316 | } |
317 | else { |
318 | game.state = WAITING; // 消すブロックがないなら入力待ち |
319 | } |
320 | } |
321 | else if (game.state == DELETE_BLOCK){ |
322 | // ブロックを消去 |
323 | setTimeout( function (){ |
324 | deleteBlock(); |
325 | game.state = DROP; |
326 | }, 200); // すぐに消去せず、どれが消去ブロックが分かるように200ミリ秒の間×画像を表示している |
327 | } |
328 | else if (game.state == SET_BLOCK){ |
329 | // ステージ最上段にランダムにブロックを配置 |
330 | setRandomBlock(); |
331 | // ブロックタイプを更新 |
332 | currentBlockType = nextBlockType; |
333 | // 次のブロックタイプを設定 |
334 | setNextBlock(); |
335 | // ステージのクリック位置に画像ブロックを設定 |
336 | const blockPos = { |
337 | x: Math.floor(pos.x / BLOCK_SIZE), //ブロックx座標 |
338 | y: Math.floor((pos.y-game.offsetY) / BLOCK_SIZE) // ブロックのy座標 |
339 | }; |
340 |
341 | stageState[blockPos.x + blockPos.y * COLUMN_SIZE] = currentBlockType; |
342 | console.log(blockPos); |
343 |
344 | // ブロックを落とすモードに移行 |
345 | game.state = DROP; |
346 | } |
347 | else if (game.state == WAITING){ |
348 | // ブロック配置待ち(現時点では何もしない) |
349 | } |
350 | // フレーム再描画 |
351 | requestAnimationFrame(GameMain); |
352 | } |
353 |
354 | /* |
355 | * 起動時の処理 |
356 | */ |
357 | window.addEventListener( "load" , async ()=>{ |
358 | // キャンバス取得 |
359 | canvas = document.getElementById( "canvas" ); |
360 | g = canvas.getContext( "2d" ); |
361 | g.font = "normal 30px Impact" ; // フォントサイズ・フォント種類 設定 |
362 | g.textBaseline = "top" ; // テキスト描画時のベースライン |
363 |
364 | // マウスイベント設定 |
365 | canvas.addEventListener( "mousemove" , mouseMoveListener, false ); |
366 | canvas.addEventListener( "mousedown" , mouseDownListener, false ); |
367 |
368 | // 画像ファイルを読み込む |
369 | images = await loadImages(image_files); |
370 | console.log(images); |
371 |
372 | // ステージ情報を生成する |
373 | const stage1 = [ |
374 | 0, 0, 0, 0, 0, 0, 0, 0, |
375 | 0, 0, 0, 0, 0, 0, 0, 0, |
376 | 0, 0, 0, 0, 0, 0, 0, 0, |
377 | 0, 0, 0, 0, 0, 0, 0, 0, |
378 | 0, 0, 0, 0, 0, 0, 0, 0, |
379 | 0, 0, 0, 0, 0, 0, 0, 0, |
380 | 0, 0, 0, 0, 0, 0, 0, 0, |
381 | 0, 0, 0, 0, 0, 0, 0, 0 |
382 | ]; |
383 | createStage(stage1); |
384 |
385 | // 落下タイマースタート |
386 | timer.start = new Date().getTime(); |
387 |
388 | // 最上段にランダムにブロックを配置 |
389 | setRandomBlock(); |
390 |
391 | // 最初のブロックを決定 |
392 | setNextBlock(); |
393 |
394 | // 落下モード |
395 | game.state = DROP; |
396 |
397 | // ゲーム開始 |
398 | GameMain(); |
399 | }); |
コメント