サンタクロースがモンスターにプレゼント箱を投げつけて倒すゲームをつくる(1)の続きです。定義したクラスをつかってゲームを完成させます。
Contents
ページが読み込まれたときの処理
ページが読み込まれたときの処理を示します。Playerオブジェクトを生成し、canvasのサイズを調整して全面を黒で塗りつぶします。そして画像ファイルを読み込んでイメージを初期化する処理、イベントリスナの追加、効果音をレンジスライダーで調整できるようにする処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
window.onload = () => { player = new Player(); $canvas.width = CANVAS_WIDTH; $canvas.height = CANVAS_HEIGHT; ctx.fillStyle = '#000'; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); initImages(); // 画像ファイルを読み込んでイメージを初期化する(後述) addEventListeners(); // イベントリスナの追加(後述) initVolumes(0.5); // レンジスライダーでボリューム調整可に(後述) } |
イメージの初期化
画像ファイルを読み込んでイメージを初期化する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function initImages(){ playerImage.src = './images/player.png'; presentImage.src = './images/present.png'; enemyImage1.src = './images/enemy1.png'; enemyImage2.src = './images/enemy2.png'; enemyBombImage.src = './images/bomb.png'; for(let i = 0; i < 6; i++){ const image = new Image(); image.src = `./images/spark${i+1}.png`; sparkImages.push(image); } } |
イベントリスナの追加
イベントリスナの追加する処理を示します。
PCのマウス操作とスマホのタップ操作に対応できるようにします。スマホ用のボタンの表示非表示を切り替えるチェックボックスの状態が変更されたときにスマホ用の操作ボタンを表示非表示が切り替えるようにします。また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 |
function addEventListeners(){ // PCのマウス操作とスマホのタップ操作に対応できるようにする const arr1 = ['mousedown', 'touchstart']; for(let i=0; i<arr1.length; i++){ $left?.addEventListener(arr1[i], (ev) => player.Move('left')); $right?.addEventListener(arr1[i], (ev) => player.Move('right')); $jump?.addEventListener(arr1[i], (ev) => player.Jump()); $shot?.addEventListener(arr1[i], (ev) => player.Shot()); } const arr2 = ['mouseup', 'touchend']; for(let i=0; i<arr2.length; i++){ $left?.addEventListener(arr2[i], (ev) => player.Stop()); $right?.addEventListener(arr2[i], (ev) => player.Stop()); } // スマホのタップ操作ではデフォルトの動作を抑止する const arr3 = ['touchstart', 'touchend']; for(let i=0; i<arr3.length; i++){ $left?.addEventListener(arr3[i], (ev) => ev.preventDefault()); $right?.addEventListener(arr3[i], (ev) => ev.preventDefault()); $jump?.addEventListener(arr3[i], (ev) => ev.preventDefault()); $shot?.addEventListener(arr3[i], (ev) => ev.preventDefault()); } // PCのマウス操作ではマウスボタンが離されたら移動処理を中止する // これをやらないとマウスでボタンを押したまま移動して離すと移動を中止することができなくなる // (もう一度同じボタンをクリックすれば止まるが、一般ユーザーはそんなことには気づかないと思う) document.addEventListener('mouseup', (ev) => player.Stop()); // スマホ用のボタンの表示非表示を切り替えるチェックボックスを操作したときの処理 // プレイ中でチェックボックスがチェックされているときだけスマホ用のボタンを表示する document.getElementById('show-buttons').addEventListener('change', (ev) => { const display = isPlaying && ev.target.checked ? 'block' : 'none'; $left.style.display = display; $right.style.display = display; $jump.style.display = display; $shot.style.display = display; }) // PCのキー操作でも操作できるようにする document.addEventListener('keydown', (ev) => { if(isPlaying){ ev.preventDefault(); if(ev.code == 'ArrowLeft') player.Move('left'); if(ev.code == 'ArrowRight') player.Move('right'); if(ev.code == 'ArrowUp') player.Jump(); if(ev.code == 'Space') player.Shot(); } }); document.addEventListener('keyup', (ev) => { if(ev.code == 'ArrowLeft') player.Stop(); if(ev.code == 'ArrowRight') player.Stop(); }); } |
効果音をレンジスライダーで調整する
効果音をレンジスライダーで調整できるように処理をおこないます
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function initVolumes(initValue){ const $elemVolume = document.getElementById("volume"); const $elemRange = document.getElementById("vol-range"); $elemVolume.addEventListener('change', () => setVolume($elemVolume.value)); setVolume(initValue); function setVolume(value){ $elemVolume.value = value; $elemRange.textContent = value; shotSound.volume = value; hitSound.volume = value; deadSound.volume = value; gameoverSound.volume = value; } } |
音量テストのボタンをクリックすると設定したボリュームで効果音が再生されます。
1 2 3 4 |
function playSound(){ deadSound.currentTime = 0; deadSound.play(); } |
ゲームスタート時の処理
ゲームスタート時の処理を示します。
プレイ中であるにもかかわらず二重にゲームスタートの処理がおこなわれないようにチェックしています。ゲームが開始されたらスタートボタンは非表示にし、代わりにスマホ用の操作ボタンを表示させます(ただしチェックボックスにチェックがされている場合のみ)。
そのあとisPlayingフラグのセットと、各オブジェクトが格納されている配列をクリアし、スコアと残機数を初期化してPlayer.Init関数を呼び出します。これでプレイヤーが描画されるようになります。
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 |
function gameStart(){ if(isPlaying) return; $start.style.display = 'none'; if(document.getElementById('show-buttons').checked){ $left.style.display = 'block'; $right.style.display = 'block'; $jump.style.display = 'block'; $shot.style.display = 'block'; } isPlaying = true; presents = []; enemies = []; enemyBombs = []; holes = []; rest = INIT_REST; score = 0; player.Init(); } |
更新と描画
更新時の処理を示します。
更新はプレイ中のときだけおこなわれます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
setInterval(() => { if(!isPlaying) return; // 背景を黒で塗りつぶす ctx.fillStyle = '#000'; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); // 地面の描画 ctx.fillStyle = '#800'; ctx.fillRect(0, PLAYER_BASE_Y + PLAYER_SIZE, CANVAS_WIDTH, CANVAS_HEIGHT); update(); // 各オブジェクトの更新(後述) judge(); // 当たり判定(後述) draw(); // 各オブジェクトの描画(後述) },1000 / 30); |
更新処理
各オブジェクトを更新する処理を示します。更新と同時に敵と穴の生成もおこないます。
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 |
function update(){ // プレイヤーの更新 player.Update(); // プレゼント箱の更新と死亡フラグがセットされているオブジェクトの除去 presents.forEach(present => present.Update()); presents = presents.filter(present => !present.IsDead); // updateCountForNextEnemyをデクリメントして0なら敵を生成する // 敵を生成したらupdateCountForNextEnemyに20~69の乱数をセットする if(updateCountForNextEnemy == 0){ updateCountForNextEnemy = Math.floor(Math.random() * 50) + 20; if(Math.random() < 0.5) enemies.push(new Enemy1()); else enemies.push(new Enemy2()); } else updateCountForNextEnemy--; // 敵の更新と死亡フラグがセットされているオブジェクトの除去 enemies.forEach(enemy => enemy.Update()); enemies = enemies.filter(enemy => !enemy.IsDead); // updateCountForNextHoleをデクリメントして0なら穴を生成する // 穴を生成したらupdateCountForNextHoleに200~299の乱数をセットする if(updateCountForNextHole == 0){ updateCountForNextHole = Math.floor(Math.random() * 100) + 200; holes.push(new Hole()); } else updateCountForNextHole--; // 敵の爆弾の更新と死亡フラグがセットされているオブジェクトの除去 enemyBombs.forEach(bomb => bomb.Update()); enemyBombs = enemyBombs.filter(bomb => !bomb.IsDead); // 穴の更新 holes.forEach(hole => hole.Update()); // 火花の更新と死亡フラグがセットされているオブジェクトの除去 sparks.forEach(spark => spark.Update()); sparks = sparks.filter(spark => !spark.IsDead); } |
描画処理
描画処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function draw(){ presents.forEach(present => present.Draw()); enemies.forEach(enemy => enemy.Draw()); enemyBombs.forEach(bomb => bomb.Draw()); holes.forEach(hole => hole.Draw()); sparks.forEach(spark => spark.Draw()); player.Draw(); ctx.font = '20px Arial bold'; ctx.textBaseline = 'top'; ctx.fillStyle = '#fff'; ctx.fillText(`SCORE ${score} 残 ${rest}`, 10, 10); } |
当たり判定
当たり判定の処理を示します。
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 |
function judge(){ // プレゼント箱と敵との当たり判定 for(let i = 0; i < enemies.length; i++){ const enemy = enemies[i]; if(enemy.IsDead) continue; const hits = presents.filter(present => isHit(present, enemy)); // isHit関数は後述 if(hits.length > 0){ hits[0].IsDead = true; onHit(enemy); // 敵を倒したときの処理(後述) } } // プレイヤーと敵との当たり判定 let hits = enemies.filter(enemy => isHit(player, enemy)); if(hits.length > 0){ hits[0].IsDead = true; explode(hits[0].X, hits[0].Y); // 衝突した敵も爆発させる(後述) onDead(false); // ミス時の処理(後述) return; } // プレイヤーと敵の爆弾との当たり判定 hits = enemyBombs.filter(bomb => isHit(player, bomb)); if(hits.length > 0){ hits[0].IsDead = true; onDead(false); return; } // プレイヤーと穴との当たり判定 hits = holes.filter(hole => isFallHole(hole)); // isFallHole関数は後述 if(hits.length > 0){ onDead(true); return; } } |
isHit関数は第一引数のオブジェクトと第二引数のオブジェクトが接触しているかどうかを返します。
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 |
function isHit(object1, object2){ // 少なくとも片方が死亡している場合は false を返す if(object1.IsDead || object2.IsDead) return false; // 両者の中心座標の距離の2乗と両者の半径の和の2乗を比較して接触しているかどうか判定する // 引数がどのクラスのインスタンスなのかを調べて中心座標と半径を取得する let objectCenter1X = object1.X; let objectCenter1Y = object1.Y; let objectRadius1 = 0; if(object1.Name == 'Present'){ objectCenter1X += CHARCTER_SIZE / 2; objectCenter1Y += CHARCTER_SIZE / 2; objectRadius1 = CHARCTER_SIZE / 2; } if(object1.Name == 'Player'){ objectCenter1X += PLAYER_SIZE / 2; objectCenter1Y += PLAYER_SIZE / 2; // 本当は PLAYER_SIZE / 2 だが、当たり判定がシビアになりすぎるのを避ける objectRadius1 = PLAYER_SIZE / 4; } let objectCenter2X = object2.X; let objectCenter2Y = object2.Y; let objectRadius2 = 0; if(object2.Name == 'Enemy1' || object2.Name == 'Enemy2'){ objectCenter2X += ENEMY_SIZE / 2; objectCenter2Y += ENEMY_SIZE / 2; objectRadius2 = ENEMY_SIZE / 2; } if(object2.Name == 'EnemyBomb'){ objectCenter2X += CHARCTER_SIZE / 2; objectCenter2Y += CHARCTER_SIZE / 2; objectRadius2 = CHARCTER_SIZE / 2; } // 取得された両者の中心座標と両者の半径から当たり判定をする if(Math.pow(objectCenter2X - objectCenter1X, 2) + Math.pow(objectCenter2Y - objectCenter1Y, 2) < Math.pow(objectRadius1 + objectRadius2, 2)) return true; else return false; } |
isFallHole関数はプレイヤーが引数で渡されたHoleオブジェクトに落ちていないかを調べて結果を返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function isFallHole(hole){ // プレイヤーがジャンプ中であれば常に false を返す if(player.Y < PLAYER_BASE_Y) return false; // 穴の左右の端のX座標とプレイヤーの左右の端のX座標を比較し、穴の中にある場合は穴に落ちたと判定する const holeLeft = hole.X; const holeRight = hole.X + CHARCTER_SIZE; if(holeLeft < player.X && player.X < holeRight) return true; if(holeLeft < player.X + PLAYER_SIZE && player.X + PLAYER_SIZE < holeRight) return true; return false; } |
当たり判定後の処理
当たり判定後の処理を示します。
爆発の発生
explode関数は爆発を発生させます。この関数は32個の火花を0rad~2πrad(0度~360度)の角度で初速4~6で飛ばします。
1 2 3 4 5 6 7 |
function explode(x, y){ for(let i = 0; i < 32; i++) { const rad = Math.random() * 2 * Math.PI; const v = Math.random() * 2 + 4; sparks.push(new Spark(x, y, v * Math.cos(rad), v * Math.sin(rad))); } } |
敵を倒したときの処理
onHit関数はプレゼント箱が敵に命中したときに呼び出されます。
プレゼント箱が敵に死亡フラグをセットして爆発の火花を発生させます。効果音を鳴らしてスコアを加算します。
1 2 3 4 5 6 7 |
function onHit(enemy){ enemy.IsDead = true; explode(enemy.X, enemy.Y); hitSound.currentTime = 0; hitSound.play(); score += 50; } |
ミス時の処理
onDead関数はプレイヤーが敵、敵の爆弾、穴のいずれかに接触したとき(ミスをしたとき)に呼び出されます。引数がtrueのときは穴に落ちたときです。
ミスをした場合は残機を1減らして残機がある場合は、2秒後にプレイヤーの死亡フラグをクリアするとともにすべての敵オブジェクトをクリアしてゲームを再開します。残機が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 |
function onDead(isFallHoll){ if(player.IsDead) return; player.IsDead = true; rest--; // 爆発はプレイヤーの位置で発生するが、穴に落ちた場合はそれよりも少し下側で発生させる if(isFallHoll) explode(player.X, player.Y + PLAYER_SIZE); else explode(player.X, player.Y); deadSound.currentTime = 0; deadSound.play(); setTimeout(() => { // 2秒後に残機があるならリセットして再開、ない場合はゲームオーバー処理 if(rest > 0){ player.Init(); presents = []; enemies = []; enemyBombs = []; holes = []; } else { onGameOver(); // 後述 } }, 2000); } |
ゲームオーバーの処理
ゲームオーバーの処理を示します。
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 onGameOver(){ // isPlayingをクリア isPlaying = false; // 'GAME OVER'の文字列を描画する ctx.font = '24px Arial bold'; ctx.textBaseline = 'top'; ctx.fillStyle = '#fff'; // 水平方向で中央に文字列を描画するためにX座標を計算する const width = ctx.measureText('GAME OVER').width; ctx.fillText('GAME OVER', (CANVAS_WIDTH - width) / 2, 150); gameoverSound.currentTime = 0; gameoverSound.play(); // 操作用のボタンを非表示にする $left.style.display = 'none'; $right.style.display = 'none'; $jump.style.display = 'none'; $shot.style.display = 'none'; // ゲーム開始用のボタンを表示させる $start.style.display = 'block'; } |