JavaScript ワイはあの娘とつながっているんや!をつくる(1)の続きです。今回はゲームとして動作するようにします。
Contents
ページが読み込まれたときの処理
ページが読み込まれたときの処理を示します。
Playerオブジェクトを生成し、canvasのサイズを設定します。また描画用の画像ファイルを読み込みます。そのあとイベントリスナを追加し、ボリュームを設定できるようにします。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | window.onload = () => {     player = new Player();     $canvas.width = CANVAS_WIDTH;     $canvas.height = CANVAS_HEIGHT;     imagePlayerMain.src = './images/main.png';     imagePlayerSub.src = './images/sub.png';     imageEnemy.src = './images/enemy.png';     imageItem1.src = './images/item1.png';     imageItem2.src = './images/item2.png';     addEventListeners(); // 後述     initVolumeController(0.5); } | 
ボリューム調整可能にする
ボリュームを調整するための処理は以下のとおりです。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | function initVolumeController(initValue){     const $elemVolume = document.getElementById("volume");     const $elemRange = document.getElementById("vol_range");     $elemVolume.addEventListener("change", function(){         const value = $elemVolume.value;         setVolume(value);     }, false);     setVolume(initValue);     function setVolume(value){         $elemRange.textContent = value;         soundGameover.volume = value;         soundByebye.volume = value;         soundHihihi.volume = value;         soundMiss.volume = value;     } } function playSound(){     soundGameover.currentTime = 0;     soundGameover.play(); } | 
イベントリスナの追加
スタートボタンをクリックすることでゲームを開始したり、ボタンをクリックまたはPCでキーを操作することでプレイヤーを操作できるようにするためにイベントリスナを追加します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | function addEventListeners(){     // ゲームスタート     $start.addEventListener('click', () => gameStart())     // ボタンのクリック     const arr1 = ['mousedown', 'mouseup', 'touchstart', 'touchend'];     const arr2 = [true, false, true, false];     for(let i = 0; i < 4; i++){         $up.addEventListener(arr1[i], (ev) => onPressButton(ev, 'up', arr2[i]));         $down.addEventListener(arr1[i], (ev) => onPressButton(ev, 'down', arr2[i]));         $left.addEventListener(arr1[i], (ev) => onPressButton(ev, 'left', arr2[i]));         $right.addEventListener(arr1[i], (ev) => onPressButton(ev, 'right', arr2[i]));     }     window.addEventListener('mouseup', () => {         player.MoveUp = false;         player.MoveDown = false;         player.MoveLeft = false;         player.MoveRight = false;     });     // PCのキー操作     const arr3 = ['keydown', 'keyup'];     const arr4 = [true, false];     for(let i = 0; i < 2; i++){         document.addEventListener(arr3[i], (ev) => {             if(ev.code == 'ArrowLeft')                 onPressButton(ev, 'left', arr4[i]);             if(ev.code == 'ArrowUp')                 onPressButton(ev, 'up', arr4[i]);             if(ev.code == 'ArrowRight')                 onPressButton(ev, 'right', arr4[i]);             if(ev.code == 'ArrowDown')                 onPressButton(ev, 'down', arr4[i]);         });     } } function onPressButton(ev, button, isPress){     ev.preventDefault();     if(button == 'up')         player.MoveUp = isPress;     if(button == 'down')         player.MoveDown = isPress;     if(button == 'left')         player.MoveLeft = isPress;     if(button == 'right')         player.MoveRight = isPress; } | 
ゲームをスタートする処理
スタートボタンを非表示にしてプレイヤーを操作するためのボタンを表示させます。Playerオブジェクトを初期化して、スコアをリセットし、敵とアイテムが格納されている配列をクリアします。そして最初の敵を作り、更新処理を開始させます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | function gameStart(){     // スタートボタンを非表示     // プレイヤーを操作するためのボタンを表示     $up.style.display = 'block';     $down.style.display = 'block';     $left.style.display = 'block';     $right.style.display = 'block';     $start.style.display = 'none';     // 各キャラクタを移動させる     stopUpdate = false;     score = 0;     player.Init();     enemies = [];     items1 = [];     items2 = [];     // 最初の敵をつくる     enemies.push(new Enemy(Math.random() * CANVAS_WIDTH, 0, Math.random() * 4 - 2, Math.random() * 2 + 3));     // setInterval関数が実行されていないのであれば実行する     // すでに実行されているのであれば何もしない     if(setIntervalId == null){         setIntervalId = setInterval(() => {             update();         }, 1000 / 30);     } } | 
描画更新の処理
描画処理はつねにおこないますが、移動の処理はstopUpdate == false のときだけにします。こうすることでゲームオーバー時は動きが止まって見えます。またゲームオーバー時はゲームオーバーの文字列も描画します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | function update(){     clearCanvas();     // stopUpdate == true のときだけ移動処理をおこなう     if(!stopUpdate){         player.Update();         enemies.forEach(enemy => enemy.Update());         items1.forEach(item => item.Update());         items2.forEach(item => item.Update());         createCharcter(); // 後述         checkCrush(); // 後述     }     // 描画処理はつねにおこなう     player.Draw();     enemies.forEach(enemy => enemy.Draw());     items1.forEach(item => item.Draw());     items2.forEach(item => item.Draw());     // スコアの描画     drawScore();     // ゲームオーバー時はゲームオーバーの表示     if(stopUpdate)         drawGameOver(); } | 
clearCanvas関数はcanvas全体を黒で塗りつぶします。
| 1 2 3 4 | function clearCanvas(){     ctx.fillStyle = '#000';     ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); } | 
敵、アイテムの生成
createCharcter関数は敵やアイテムをランダムに生成します。単に乱数で処理をすると敵がいない状態が長く場合があるので、敵がひとつもいないときは常に生成します。アイテムはフィールドにひとつも存在しない場合のみ生成します。
| 1 2 3 4 5 6 7 8 9 10 | function createCharcter(){     if(enemies.length == 0 || (enemies.length < 12 && Math.random() < 0.02))         enemies.push(new Enemy(Math.random() * CANVAS_WIDTH, 0, Math.random() * 4 - 2, Math.random() * 2 + 3));     if(items1.length == 0 && Math.random() < 0.01)         items1.push(new Item1(Math.random() * CANVAS_WIDTH, 0, Math.random() * 4 - 2, Math.random() * 2 + 3));     if(items2.length == 0 && Math.random() < 0.01)         items2.push(new Item2(Math.random() * CANVAS_WIDTH, 0, Math.random() * 4 - 2, Math.random() * 2 + 3)); } | 
当たり判定
敵と衝突した場合は糸の耐久度を1下げます。アイテム1をゲットした場合は糸の耐久度を1上げます。ただし最大値を超えて上がることはありません。アイテム2をゲットした場合は糸を短くします。アイテムをゲットした場合は10点を加点します。いずれの場合も効果音を鳴らします。
そのあと死亡フラグが立っているオブジェクトやフィールドの外に出たオブジェクトを配列から除去します。
当たり判定の処理がおわって糸の耐久度が0以下になっている場合はゲームオーバーの処理をおこないます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | function checkCrush(){     if(player.Life <= 0)         return;     // 敵との当たり判定     const positions = player.GetHitJugePositions();     for(let i = 0; i < positions.length; i++){         const hited = enemies.filter(enemy => !enemy.IsDead && Math.pow(enemy.X - positions[i].x, 2) + Math.pow(enemy.Y - positions[i].y, 2) < Math.pow(CHARCTER_SIZE, 2));         if(hited.length > 0){             hited.forEach(enemy => enemy.IsDead = true);             player.Life--;             if(player.Life > 0){                 // 効果音(player.Life == 0になったときは別の効果音を鳴らす)                 soundMiss.currentTime = 0;                 soundMiss.play();             }         }     }     // アイテム1との当たり判定     for(let i = 0; i < positions.length; i++){         const hited = items1.filter(item => !item.IsDead && Math.pow(item.X - positions[i].x, 2) + Math.pow(item.Y - positions[i].y, 2) < Math.pow(CHARCTER_SIZE, 2));         if(hited.length > 0){             hited.forEach(item => item.IsDead = true);             if(player.Life + 1 <= LIFE_MAX)                 player.Life++;             score += 10 * hited.length;             // 効果音             soundHihihi.currentTime = 0;             soundHihihi.play();         }     }     // アイテム2との当たり判定     for(let i = 0; i < positions.length; i++){         const hited = items2.filter(item => !item.IsDead && Math.pow(item.X - positions[i].x, 2) + Math.pow(item.Y - positions[i].y, 2) < Math.pow(CHARCTER_SIZE, 2));         if(hited.length > 0){             hited.forEach(item => item.IsDead = true);             if(player.Distance - 10 >= DISTANCE_MIN)                 player.Distance -= 10;             else                 player.Distance = DISTANCE_MIN;             score += 10 * hited.length;             // 効果音             soundHihihi.currentTime = 0;             soundHihihi.play();         }     }     // 死亡フラグが立っているオブジェクトやフィールドの外に出たオブジェクトを配列から除去     enemies = enemies.filter(enemy => !enemy.IsDead && !(enemy.Y >= CANVAS_HEIGHT || enemy.X <= 0 || enemy.X >= CANVAS_WIDTH));     items1 = items1.filter(item => !item.IsDead && !(item.Y >= CANVAS_HEIGHT || item.X <= 0 || item.X >= CANVAS_WIDTH));     items2 = items2.filter(item => !item.IsDead && !(item.Y >= CANVAS_HEIGHT || item.X <= 0 || item.X >= CANVAS_WIDTH));     if(player.Life <= 0){         player.Life = 0;         gameOver();     } } | 
drawScore関数はスコアを、drawGameOver関数はゲームオーバーのときにゲームオーバーの文字列を描画します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | function drawScore(){     ctx.font = '24px Arial';     ctx.textBaseline = 'top';     ctx.fillStyle = '#fff';     ctx.fillText('SCORE  ' + score, 20, 10); } function drawGameOver(){     ctx.font = '24px Arial';     ctx.textBaseline = 'top';     ctx.fillStyle = '#fff';     ctx.fillText('今日もめでたくふられました', 25, 170);     ctx.fillText('GAME OVER', 110, 230); } | 
ゲームオーバーの処理
ゲームオーバーになったときは、プレイヤー操作用のボタンを非表示にします。そして糸が切れて「あの娘」がどこかへ飛んでいく演出をします。そのあと移動処理を止めるフラグをセットします。しばらく待機したあとゲームオーバーの効果音と再挑戦用のボタンを表示する処理をおこないます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | function gameOver(){     // プレイヤー操作用のボタンを非表示にする     $up.style.display = 'none';     $down.style.display = 'none';     $left.style.display = 'none';     $right.style.display = 'none';     // 女の子「バイバーイ」     soundByebye.currentTime = 0;     soundByebye.play();     setTimeout(() => {         // キャラクタの移動を停止する(「あの娘」が飛んでいく演出があるのでこのタイミングで止める)         stopUpdate = true;         soundGameover.currentTime = 0;         soundGameover.play();         // 再挑戦用のボタンを表示する         $start.style.display = 'block';     }, 1500); } | 
