レーザーと爆発のエフェクトをつかってゲームをつくる(2) の続きです。今回は前回定義したクラスを使ってゲームを完成させます。
ページが読み込まれたときの処理
ページが読み込まれたときにおこなわれる処理を示します。
自機た敵、敵弾の描画処理ができるようにImageオブジェクトに画像ファイルを読み込ませます。そのあとcanvasのサイズの調整、イベントリスナの追加、チェックボックスの初期化、連続発射処理の初期化をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
window.onload = () => { playerImage.src = './images/player.png'; enemyImage0.src = './images/enemy0.png'; enemyImage1.src = './images/enemy1.png'; enemyBullet.src = './images/enemy-bullet.png'; $canvas.width = CANVAS_WIDTH; $canvas.height = CANVAS_HEIGHT; addEventListeners(); // 後述 document.getElementById('hide-buttons').checked = false;; initAutoShot(); // 後述 update(); // 後述 } |
addEventListeners関数は必要なイベントリスナを追加します。各ボタンがクリックされたとき、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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
function addEventListeners(){ // ゲーム開始の処理 $start.addEventListener('click', () => { $start.style.display = 'none'; player = new Player(); lasers = []; enemies = []; enemyBullets = []; fireballs = []; }); // 自機操作用のボタンが押下(PCとスマホ)されたときの処理 const arr1 = ['mousedown', 'touchstart']; const arr2 = ['mouseup', 'touchend']; for(let i = 0; i < 2; i++){ $up.addEventListener(arr1[i], () => player.MoveUp = true); $down.addEventListener(arr1[i], () => player.MoveDown = true); $left.addEventListener(arr1[i], () => player.MoveLeft = true); $right.addEventListener(arr1[i], () => player.MoveRight = true); $up.addEventListener(arr2[i], () => player.MoveUp = false); $down.addEventListener(arr2[i], () => player.MoveDown = false); $left.addEventListener(arr2[i], () => player.MoveLeft = false); $right.addEventListener(arr2[i], () => player.MoveRight = false); } const arr3 = [$up, $down, $left, $right]; for(let i = 0; i < 2; i++){ arr3[i].addEventListener('touchstart', (e) => e.preventDefault()); arr3[i].addEventListener('touchend', (e) => e.preventDefault()); } // PCで自機操作用のボタンが押下され、別の場所で離されたときは自機の移動フラグをすべてクリアする document.addEventListener('mouseup', () => { if(player == null) return; player.MoveUp = false; player.MoveDown = false; player.MoveLeft = false; player.MoveRight = false; }); // PCでキー操作されたときに自機を操作する document.onkeydown = (ev) => { if(ev.code == 'Space') player.Shot(); if(ev.code == 'ArrowLeft') player.MoveLeft = true; if(ev.code == 'ArrowRight') player.MoveRight = true; if(ev.code == 'ArrowUp') player.MoveUp = true; if(ev.code == 'ArrowDown') player.MoveDown = true; // プレイ中にキー操作をしたときはスクロールしないようにする if(player != null && !player.Gameovered) ev.preventDefault(); } document.onkeyup = (ev) => { if(ev.code == 'ArrowLeft') player.MoveLeft = false; if(ev.code == 'ArrowRight') player.MoveRight = false; if(ev.code == 'ArrowUp') player.MoveUp = false; if(ev.code == 'ArrowDown') player.MoveDown = false; } // レンジスライダーを操作したときは効果音のボリュームを調整する const $volumeRange = document.getElementById('volume-range'); const $volumeValue = document.getElementById('volume-value'); $volumeRange.addEventListener('input', () => { volume = Number($volumeRange.value); $volumeValue.innerText = volume; }); $volumeValue.innerText = volume; $volumeRange.value = volume; // レンジスライダーで設定したボリュームをテスト再生する document.getElementById('test').addEventListener('click', () => { gameoverSound.volume = volume / 100; gameoverSound.play(); }); } |
スマホで自機の操作をしながらレーザーの発射をするのは難しいので自動的に連続発射するようにします。PCユーザーの場合はチェックボックスで切り替えることができるようにします。
自動で連続発射処理をするのは、自機が生存している場合で、チェックボックスがONのときだけです。またlasers内にLaserオブジェクトが18個以上格納されている場合も発射はしません。lasers.length < 18という条件を入れているのは、requestAnimationFrameが一時的に止まったときに次から次へとLaserオブジェクトが配列内に追加されるのを防ぐためです。
1 2 3 4 5 6 7 8 |
function initAutoShot(){ const $autoShot = document.getElementById('auto-shot'); $autoShot.checked = true; setInterval(() => { if(player != null && !player.IsDead && $autoShot.checked && lasers.length < 18) player.Shot(); }, 300); } |
更新処理
更新処理を示します。
まずゲームがプレイ中のときは[スマホ用の操作ボタンを隠す]がチェックされている場合、操作用のボタンを非表示にします。またプレイ中でない場合も操作用のボタンは非表示にします。
ゲームオーバーのときはそれ以上の処理はしません。
プレイ中のときは各オブジェクトの状態を更新します。その後、当たり判定をして死亡フラグがセットされているオブジェクトを配列のなかから取り除きます。そのあと敵の弾丸発射の処理と新しい敵を生成する処理をして描画処理をおこないます。
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 |
function update(){ requestAnimationFrame(update); const hideButtons = player == null || player.IsDead || document.getElementById('hide-buttons').checked; ctrlButtons.forEach(button => button.style.display = hideButtons ? 'none' : 'block'); if(player == null || player.Gameovered) return; updateCount++; // 各オブジェクトの状態を更新する lasers.forEach(laser => laser.Update()); player.Update(); enemies.forEach(enemy => enemy.Update()) ; fireballs.forEach(fireball => fireball.Update()) ; enemyBullets.forEach(bullet => bullet.Update()); judge(); // 当たり判定(後述) enemies = enemies.filter(enemy => !enemy.IsDead); lasers = lasers.filter(bullet => !bullet.IsDead); enemyBullets = enemyBullets.filter(bullet => !bullet.IsDead); fireballs = fireballs.filter(fireball => !fireball.IsDead); enemiesShot(); // 後述 if(updateCount % 96 == 1) createEnemy(); // 後述 draw(); // 後述 } |
当たり判定
当たり判定の処理を示します。
ここでは ① 自機から発射された弾丸は敵に命中したか? ② 敵から発射された弾丸は自機に命中したか? ③ 自機と敵は衝突したか?を調べますが、自機死亡時と自機が無敵状態のときは ② と ③ はおこなわれません。
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 |
function judge(){ // 自機から発射された弾丸は敵に命中したか? for(let i = 0; i < lasers.length; i++){ if(lasers[i].IsDead) continue; // (敵とレーダーの先端部の距離)が(敵の半径)より小さければ命中していると判定する const hits = enemies.filter(enemy => Math.pow(enemy.CenterX - lasers[i].HeadX, 2) + Math.pow(enemy.CenterY - lasers[i].HeadY, 2) < Math.pow(ENEMY_SIZE / 2, 2)); if(hits.length > 0){ hits[0].IsDead = true; // 双方に死亡フラグをセット lasers[i].IsDead = true; onHit(hits[0].CenterX, hits[0].CenterY); // 爆発の発生、スコア加算などの処理(後述) } } if(player.IsMuteki || player.IsDead) return; // 敵から発射された弾丸は自機に命中したか? // (自機と敵弾の中心部の距離)が(自機の半径)より小さければ命中していると判定する let hits = enemyBullets.filter(bullet => Math.pow(bullet.CenterX - player.CenterX, 2) + Math.pow(bullet.CenterY - player.CenterY, 2) < Math.pow(player.Size / 2, 2)); if(hits.length > 0){ hits[0].IsDead = true; onMiss(hits[0].CenterX, hits[0].CenterY); // 爆発の発生、Life減算などの処理(後述) return; } // 自機と敵は衝突したか? // (自機と敵の中心部の距離)が(自機と敵の半径の合計)より小さければ命中していると判定する hits = enemies.filter(enemy => Math.pow(enemy.CenterX - player.CenterX, 2) + Math.pow(enemy.CenterY - player.CenterY, 2) < Math.pow((player.Size + enemy.Size) / 2, 2)); if(hits.length > 0){ hits[0].IsDead = true; onMiss(hits[0].CenterX, hits[0].CenterY); // 爆発の発生、Life減算などの処理(後述) return; } } |
命中時の処理
命中時の処理を示します。敵を倒したときはスコアを加算し、爆発の処理と効果音の再生をおこないます。
1 2 3 4 5 |
function onHit(bx, by){ player.Score += 50; sound.PlayHitSound(); explode(bx, by); // 後述 } |
敵や敵弾に当たったときはLifeを減算し、爆発の処理と効果音の再生をおこないます。またLife減算の結果、Lifeが0以下になったときはゲームオーバーの処理をおこないます。
ゲームオーバーになったら自機を大爆発させ、2秒後に’GAME OVER’の文字列を描画します。player.Gameovered = trueとすることで描画更新処理を実質的に停止させます。またゲーム再挑戦用のボタンを表示させます。
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 |
function onMiss(bx, by){ player.IsMuteki = true; setTimeout(() => { player.IsMuteki = false; }, 2000); // 小爆発の発生 smallExplode(bx, by); // 後述 sound.PlayDamageSound(); // 0.2秒だけ背景色を赤に変える backColor = '#f00'; setTimeout(() => backColor = '#000', 200); if(--player.Life <=0){ // Lifeが0になったら自機を大爆発させる bigExplode(player.CenterX, player.CenterY); // 後述 player.IsDead = true; // 2秒後に'GAME OVER'を表示。描画更新処理の実質的な停止 setTimeout(() => { player.Gameovered = true; const gameover = 'GAME OVER'; ctx.textBaseline = "top"; ctx.font = '36px Arial'; const textWidth = ctx.measureText(gameover).width; ctx.fillText(gameover, (CANVAS_WIDTH - textWidth) / 2, 100); sound.PlayGameoverSound(); $start.style.display = 'block'; }, 2000); } } |
爆発のエフェクトを描画する処理を示します。敵を倒したときの火球と自機が被弾したときの火球に色の違いをつけて視覚的にわかりやすくします。火球の色はFireballクラスのコンストラクタに渡す第三引数が0か1かで変わります。
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 |
async function explode(x, y){ for(let i = 0; i < settingExplode['fireballs-count']; i++){ const radius = settingExplode['explosion-radius']; const dx = Math.random() * radius * 2 - radius; const dy = Math.random() * radius * 2 - radius; fireballs.push(new Fireball(x + dx, y + dy, 0)); await new Promise(resolve => setTimeout(() => resolve(''), settingExplode['fireball-delay'])); } } async function smallExplode(x, y){ // 小爆発なので火球はひとつしか発生しない fireballs.push(new Fireball(x, y, 1)); } async function bigExplode(x, y){ // 大爆発なので通常の爆発と比較して2倍の範囲に2倍の数の火球を発生させる for(let i = 0; i < settingExplode['fireballs-count'] * 2; i++){ const radius = settingExplode['explosion-radius'] * 2; const dx = Math.random() * radius * 2 - radius; const dy = Math.random() * radius * 2 - radius; fireballs.push(new Fireball(x + dx, y + dy, 1)); await new Promise(resolve => setTimeout(() => resolve(''), settingExplode['fireball-delay'])); } } |
敵弾発射の処理
敵に弾丸を発射させる処理を示します。更新処理がおこなわれるごとに0.5%の確率で弾丸を発射させます。弾丸は自機がいる方向にむけて発射されます。
1 2 3 4 5 6 7 8 |
function enemiesShot(){ for(let i = 0; i < enemies.length; i++){ if(Math.random() < 0.005){ const rad = Math.atan2(player.CenterY - enemies[i].CenterY, player.CenterX - enemies[i].CenterX); enemyBullets.push(new EnemyBullet(enemies[i].CenterX, enemies[i].CenterY, ENEMY_BULLET_SPEED * Math.cos(rad), ENEMY_BULLET_SPEED * Math.sin(rad))); } } } |
敵の生成
新しい敵を生成する処理を示します。
生成する敵のタイプと、EnemyXクラスのコンストラクタに渡す第一引数を乱数で決定しています。これによって8機で構成される敵の編隊が生成されます。
1 2 3 4 5 6 7 8 9 10 11 |
function createEnemy(){ const initX = Math.floor(Math.random() * CANVAS_WIDTH); // 出現位置のX座標 if(Math.random() < 0.5){ for(let i = 0; i < 8; i++) enemies.push(new Enemy0(initX, 24 * i)); } else { for(let i = 0; i < 8; i++) enemies.push(new Enemy1(initX, 24 * i)); } } |
描画処理
描画の処理を示します。canvas全体をbackColorで塗りつぶし、各オブジェクトのDraw関数を呼び出して描画処理をおこないます。そのあとスコア関連の描画をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 |
function draw(){ ctx.fillStyle = backColor; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); enemyBullets.forEach(bullet => bullet.Draw()); lasers.forEach(laser => laser.Draw()); player.Draw(); enemies.forEach(enemy => enemy.Draw()) ; fireballs.forEach(fireball => fireball.Draw()) ; drawScore(); } |
スコア関連の描画をおこなう処理を示します。canvas上部にスコアとLifeを描画します。
1 2 3 4 5 6 |
function drawScore(){ ctx.textBaseline = "top"; ctx.font = '24px Arial'; ctx.fillStyle = '#fff'; ctx.fillText(`Score ${player.Score} Life ${player.Life}`, 10, 10); } |