JavaScript スクランブルのようなゲームをつくる(2)の続きです。今回は更新処理と当たり判定を実装してゲームを完成させます。
Contents
更新処理
1秒間に30回更新処理をおこないます。
1 2 3 4 5 6 7 8 9 |
setInterval(() => { // プレイ中ではない場合は何もしない if(!isPlaying) return; update(); // 更新処理(後述) hitJudge(); // 当たり判定(後述) draw(); // 描画処理(後述) }, 1000 / 30); |
更新処理ではcanvas左端に描画される部分の座標をSPEEDぶん増やしますが、敵司令部を破壊しないで右端まで来てしまった場合は最終ステージの最初に戻します(違和感がないようにループさせる)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function update(){ curPositionX += SPEED; // 敵司令部を破壊しないで右端まで来てしまった場合は最終ステージの最初に戻す if(curPositionX > startPositions[6].X){ player.X -= curPositionX - startPositions[5].X; curPositionX -= curPositionX - startPositions[5].X; } // 以下の関数は後述 updatePlayer(); updateBullets(); updateBombs(); updateMissiles(); updateUfos(); updateFireballs(); updateSparks(); } |
プレイヤーの更新
プレイヤーの更新処理を示します。ボタンの押下状態によってplayerの座標を移動させます。また燃料も減らし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 updatePlayer(){ // プレイしていないとき、自機死亡時は何もしない if(player.IsDead || !isPlaying) return; // 燃料があるなら減らす if(fuel > 0) fuel -= 1; // 加速していないときは自機のX座標を curPositionX + 20 に設定する if(curPositionX + 20 > player.X) player.X = curPositionX + 20; // 通常時と加速時(つまり加速が終わって減速しているとき以外)は自機のX座標を SPEED だけ増やす // これによってcanvas上の表示位置は変わらない if(curPositionX + 20 == player.X || pressRight) player.X += SPEED; // 燃料がある場合はユーザーの操作にあわせて自機の座標を変更する // 燃料切れの場合は墜落させる(PLAYER_SPEED / 2 で下に移動させる) if(fuel > 0){ if(pressRight && player.X - curPositionX < CANVAS_WIDTH / 2) player.X += PLAYER_SPEED; if(pressUp && player.Y > 20) player.Y -= PLAYER_SPEED; if(pressDown) player.Y += PLAYER_SPEED; } else player.Y += PLAYER_SPEED / 2; } |
敵の更新処理
敵のミサイルを更新する処理を示します。
まず発射すべきミサイルがあるか探します。UFOとファイアボールのゾーン以外はミサイルが発射されます。条件を満たしたものがある場合は発射させます。発射されたミサイルはMissile.Update関数が呼びだされるたびに上昇していきます。
1 2 3 4 5 6 7 8 |
function updateMissiles(){ missiles.forEach(missile => missile.Update()); // 発射すべきミサイルがあるか探す(自機からmissile.Shiftだけズレた部分に命中するタイミングで発射する) const objs = missiles.filter(missile => (missile.X < startPositions[1].X || missile.X > startPositions[3].X) && (missile.X - player.X - PLAYER_WIDTH * 0.5 + missile.Shift) * (MISSILE_SPEED / SPEED) < missile.Y - player.Y); // 発射させる objs.forEach(missile => missile.IsMoving = true); } |
UFOの更新処理を示します。
新しいUFOを生成する必要がある場合は生成します。そのあと配列内に格納されているUFOオブジェクトを移動させます。そのあと死亡フラグがセットされているものは取り除きます。
1 2 3 4 5 6 7 8 |
function updateUfos(){ // UFOのゾーンで 80更新に1回の割合で新しいUFOを生成する if(startPositions[1].X < curPositionX && curPositionX < startPositions[2].X && curPositionX % 160 == 0) ufos.push(new UFO()); ufos.forEach(ufo => ufo.Update()); ufos = ufos.filter(ufo => !ufo.IsDead); } |
ファイアボールの更新処理を示します。
新しいファイアボールを生成する必要がある場合は生成します。そのあと配列内に格納されているファイアボールオブジェクトを移動させます。そのあと死亡フラグがセットされているものは取り除きます。
1 2 3 4 5 6 7 8 |
function updateFireballs(){ if(startPositions[2].X < curPositionX && curPositionX < startPositions[3].X && curPositionX % 32 == 0){ ufos = []; // ファイアボールのゾーンにきたらUFOは消滅させる fireballs.push(new FireBall()); } fireballs.forEach(ball => ball.Update()); fireballs = fireballs.filter(ball => !ball.IsDead); } |
火花を更新する処理を示します。Typeが2になったものは配列内から取り除きます。
1 2 3 4 |
function updateSparks(){ sparks.forEach(spark => spark.Update()); sparks = sparks.filter(spark => spark.Type != 2); } |
弾丸・爆弾の更新と当たり判定
弾丸・爆弾の座標を更新したあと後述するJudgeHitEnemies関数で敵に命中しているかを調べます。そのあと死亡フラグがセットされたオブジェクトを配列から取り除いています。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function updateBullets(){ bullets.forEach(bullet => bullet.Update()); for(let i =0; i<bullets.length; i++) JudgeHitEnemies(bullets[i]); // 後述 bullets = bullets.filter(bullet => !bullet.IsDead); } function updateBombs(){ bombs.forEach(bomb => bomb.Update()); for(let i =0; i<bombs.length; i++) JudgeHitEnemies(bombs[i]); bombs = bombs.filter(bomb => !bomb.IsDead); } |
当たり判定
敵に弾丸や爆弾が命中しているかを調べるJudgeHitEnemies関数を示します。
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 |
function JudgeHitEnemies(bulletOrBomb){ // 弾丸または爆弾の中心が敵オブジェクトの内部にあるか調べる const x = bulletOrBomb.X + bulletOrBomb.Width / 2; const y = bulletOrBomb.Y + bulletOrBomb.Height / 2; // 地面との当たり判定 if(obstaclePositions.filter(obstacle => obstacle.IsInside(x, y)).length > 0){ createSpark(x, y); // 火花を発生させる bulletOrBomb.IsDead = true; // 命中した弾丸または爆弾には死亡フラグをセットする return; } // ミサイルとの当たり判定 let hits = missiles.filter(missile => !missile.IsDead && missile.IsInside(x, y)); if(hits.length > 0){ createSpark(x, y); bulletOrBomb.IsDead = true; hits[0].IsDead = true; // 弾丸または爆弾が命中したミサイルは消滅させる onHitMissile(hits[0].IsMoving); // 後述 return; } // 燃料タンクとの当たり判定 hits = fuelTanks.filter(tank => !tank.IsDead && tank.IsInside(x, y)); if(hits.length > 0){ createSpark(x, y); bulletOrBomb.IsDead = true; hits[0].IsDead = true; onHitFuelTank(); // 後述 return; } // UFOとの当たり判定 hits = ufos.filter(ufo => !ufo.IsDead && ufo.IsInside(x, y)); if(hits.length > 0){ createSpark(x, y); bulletOrBomb.IsDead = true; hits[0].IsDead = true; onHitUfo(); // 後述 return; } // ファイアボールとの当たり判定(ファイアボールに弾丸・爆弾が命中しても消滅しない) hits = fireballs.filter(ball => !ball.IsDead && ball.IsInside(x, y)); if(hits.length > 0){ createSpark(x, y); bulletOrBomb.IsDead = true; return; } // 敵司令部との当たり判定 hits = finalbases.filter(finalbase => !finalbase.IsDead && finalbase.IsInside(x, y)); if(hits.length > 0){ createSpark(x, y); bulletOrBomb.IsDead = true; hits[0].IsDead = true; onHitFinalbase(); // 後述 return; } } |
命中後の処理
爆発で発生する火花を生成する処理を示します。
1 2 3 |
function createSpark(centerX, centerY){ sparks.push(new Spark(centerX - SPARK_SIZE / 2, centerY - SPARK_SIZE / 2)); } |
敵のミサイルなどを破壊したら加点処理をおこないます。
1 2 3 4 5 6 7 8 9 10 |
function onHitMissile(isMoving){ // 発射前と後で点数を変える if(!isMoving) score += 50; else score += 80; hitSound.currentTime = 0; hitSound.play(); } |
1 2 3 4 5 6 7 8 9 10 11 |
function onHitFuelTank(){ score += 150; // 燃料タンクを破壊したら自機の燃料を増やす fuel += INIT_FUEL / 8; if(fuel > INIT_FUEL) fuel = INIT_FUEL; hitSound.currentTime = 0; hitSound.play(); } |
1 2 3 4 5 6 |
function onHitUfo(){ score += 100; hitSound.currentTime = 0; hitSound.play(); } |
敵司令部を破壊したらステージクリアです。5秒経過したら次のステージの先頭から再開されます。
1 2 3 4 5 6 7 8 9 10 11 |
function onHitFinalbase(){ score += 800; cleared = true; // ステージクリアのフラグをセット hitSound.currentTime = 0; hitSound.play(); setTimeout(()=> { restart(); // 後述 }, 5000); } |
ステージクリア後または自機死亡後の処理
ステージクリア後または自機死亡後にゲームを再開する処理を示します。
clearedフラグが立っている場合はステージ数を1増やしてcurPositionXを0にリセットします。そのあとgetObjects関数を呼び出して各オブジェクトの初期化をして燃料を満タンにします。そのあとcurPositionXから自機を復活させる場所を取得します。最後に死亡フラグをクリアします。
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 |
function restart(){ if(cleared){ stage++; curPositionX = 0; } cleared = false; getObjects(); fuel = INIT_FUEL; // どの座標で自機を復活させるかを取得する let index = 0; for(let i = 0; i < startPositions.length; i++){ if(curPositionX > startPositions[i].X) index = i; else break; } // 自機を取得した座標にセットする curPositionX = startPositions[index].X; player.X = startPositions[index].X; player.Y = startPositions[index].Y; player.IsDead = 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 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 |
function hitJudge(){ // 自機死亡時は何もしない if(player.IsDead) return; // ミサイル、燃料タンク、UFO、ファイアボールの四隅が自機の内部にあるか調べる // あれば自機は死亡 const objects = [missiles, fuelTanks, ufos, fireballs]; for(let i = 0; i < objects.length; i++){ const hits = objects[i].filter(obj => !obj.IsDead && ( player.IsInside(obj.X, obj.Y) || player.IsInside(obj.X, obj.Bottom) || player.IsInside(obj.X, (obj.Y + obj.Bottom) / 2) || player.IsInside(obj.Right, obj.Y) || player.IsInside(obj.Right, (obj.Y + obj.Bottom) / 2) || player.IsInside(obj.Right, obj.Bottom) )); if(hits.length > 0){ sparks.push(new Spark(hits[0].X, hits[0].Y)); hits[0].IsDead = true; onDead(); } } // 敵司令部の四隅が自機の内部にあるか調べる。 // あれば自機は死亡。死亡するのは自機のみ。司令部は破壊されない。 let hits = finalbases.filter(obj => !obj.IsDead && ( player.IsInside(obj.X, obj.Y) || player.IsInside(obj.X, obj.Bottom) || player.IsInside(obj.X, (obj.Y + obj.Bottom) / 2) || player.IsInside(obj.Right, obj.Y) || player.IsInside(obj.Right, (obj.Y + obj.Bottom) / 2) || player.IsInside(obj.Right, obj.Bottom) )); if(hits.length > 0){ sparks.push(new Spark(hits[0].X, hits[0].Y)); onDead(); } // 地面の角が自機の内部にあるか調べる // 地形の形状でどの角を当たり判定の対象とするのかを変える hits = obstaclePositions.filter(obj => { const checks0 = [ {x:obj.X, y:obj.Y}, {x:obj.Right, y:obj.Y}, {x:obj.X, y:(obj.Y + obj.Bottom) / 2}, {x:obj.Right, y:(obj.Y + obj.Bottom) / 2}, {x:obj.X, y:obj.Bottom}, {x:obj.Right, y:obj.Bottom}, ]; let checks = []; if(obj.Type == '■' || obj.Type == '●' || obj.Type == '○') checks = checks0; if(obj.Type == '▲') // 左上は当たり判定の対象からはずす checks = [checks0[1],checks0[3],checks0[4],checks0[5]]; if(obj.Type == '△') // 右上は当たり判定の対象からはずす checks = [checks0[0],checks0[2],checks0[4],checks0[5]]; if(obj.Type == '▼') // 左下は当たり判定の対象からはずす checks = [checks0[0],checks0[1],checks0[3],checks0[5]]; if(obj.Type == '▽') // 右下は当たり判定の対象からはずす checks = [checks0[0],checks0[1],checks0[2],checks0[4]]; for(let i = 0; i < checks.length; i++){ if(player.IsInside(checks[i].x, checks[i].y)) return true; } return false; }); // 地面に当たっていたら自機死亡 if(hits.length > 0) onDead(); } |
自機死亡時の処理
自機死亡時の処理を示します。
原作ではステージが進行すると燃費が悪くなり、敵の司令部を破壊したあと上昇しようとしても燃料が足りず墜落してしまいます。この場合はステージクリア後に死亡であれば残機は減らず、ステージクリアの処理がおこなわれます。このゲームの同様にステージクリア後に死亡であれば残機は減らず、ステージクリアの処理をおこないます。といっても燃費云々の処理はこのゲームでは実装していません(おいっ)。
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 |
function onDead(){ // すでに自機死亡の場合はなにもしない if(player.IsDead) return; // 火花を発生させる createSpark(player.X + PLAYER_WIDTH / 2, player.Y + PLAYER_HEIGHT / 2); createSpark(player.X + PLAYER_WIDTH / 2 - 20, player.Y + PLAYER_HEIGHT / 2); createSpark(player.X + PLAYER_WIDTH / 2 + 20, player.Y + PLAYER_HEIGHT / 2); deadSound.currentTime = 0; deadSound.play(); // 死亡フラグのセット player.IsDead = true; // クリアフラグが立っていないなら残機を減らす if(!cleared) rest--; // 残機がある場合は 3秒後に再開。ない場合はゲームオーバー処理をする if(rest > 0) setTimeout(() => restart(), 3000); else gameOver(); // 後述 } |
ゲームオーバー時の処理
ゲームオーバー時の処理を示します。
ゲームオーバー時は3秒後にcanvas中央に’GAME OVER’という文字列を描画します。isPlayingフラグをクリアしてゲームオーバーの効果音を鳴らし、自機操作用のボタンを非表示にしてゲームスタート用のボタンを表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function gameOver(){ setTimeout(() => { drawGameover(); // 'GAME OVER'と表示(後述) isPlaying = false; gameoverSound.currentTime = 0; gameoverSound.play(); const buttons = [$up, $down, $accelerate, $shot, $bomb]; buttons.forEach(button => button.style.display = 'none'); $start.style.display = 'block'; }, 3000); } function drawGameover(){ ctx.font = '28px bold MS ゴシック'; // 文字列を中央に描画する const width = ctx.measureText('GAME OVER').width; ctx.fillStyle = '#fff'; ctx.fillText('GAME OVER', (CANVAS_WIDTH - width) / 2, 120); } |
通常の描画処理
通常の描画をする処理を示します。
canvas全体を黒で塗りつぶし、地形、ミサイルなどの敵、自機(プレイ中で自機が生存しているときのみ)を描画します。そのあとスコア、残機、残り燃料を示すメーターを描画します。またステージクリア後はステージクリアを示す文字列を描画します。
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 |
function draw(){ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); obstaclePositions.forEach(o => o.Draw()); // 遊び心(敵司令部の近くのビルに「鳩でもわかるC#」の名前を入れる) const hato = obstaclePositions.filter(o => o.Type == '鳩'); ctx.drawImage(hatoNameImage, hato[0].X - curPositionX - BLOCK_WIDTH, hato[0].Y, BLOCK_WIDTH * 7, BLOCK_HEIGHT); // 敵の描画 missiles.forEach(missile => missile.Draw()); fuelTanks.forEach(tank => tank.Draw()); ufos.forEach(ufo => ufo.Draw()); fireballs.forEach(ball => ball.Draw()); finalbases.forEach(finalbase => finalbase.Draw()); // 自機の弾丸と爆弾の描画 bullets.forEach(bullet => bullet.Draw()); bombs.forEach(bomb => bomb.Draw()); // 自機の描画 if(isPlaying && !player.IsDead) ctx.drawImage(playerImage, player.X - curPositionX, player.Y, PLAYER_WIDTH, PLAYER_HEIGHT); // 火花の描画 sparks.forEach(spark => spark.Draw()); drawScore(); drawFuel(); drawStageClear(); } |
スコアを描画する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function drawScore(){ let num = 1; for(let i=0; i<startPositions.length; i++){ if(curPositionX > startPositions[i].X) num = i + 1; else break; } ctx.fillStyle = '#fff'; ctx.font = '20px bold MS ゴシック'; ctx.textBaseline = 'top'; ctx.fillText('score ' + score, 10, 10); ctx.fillText(stage + '-' + num, 200, 10); ctx.fillText('残 ' + rest, 280, 10); } |
残り燃料のメーターを描画する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function drawFuel(){ ctx.font = '20px bold MS ゴシック'; ctx.fillStyle = '#000'; ctx.fillRect(0, CANVAS_HEIGHT - 30, CANVAS_WIDTH, 30); ctx.fillStyle = '#fff'; ctx.fillText('FUEL' ,10, CANVAS_HEIGHT - 20); ctx.fillStyle = '#00f'; ctx.fillRect(70, CANVAS_HEIGHT - 25, CANVAS_WIDTH - 80, 20); ctx.fillStyle = '#ff0'; if(fuel > 0) ctx.fillRect(70, CANVAS_HEIGHT - 25, (CANVAS_WIDTH - 80) * (fuel / INIT_FUEL), 20); } |
ステージクリア後の文字列を描画する処理を示します。
1 2 3 4 5 6 7 8 |
function drawStageClear(){ if(cleared){ ctx.font = '28px bold MS ゴシック'; const width = ctx.measureText('STAGE CLEAR').width; ctx.fillStyle = '#fff'; ctx.fillText('STAGE CLEAR', (CANVAS_WIDTH - width) / 2, 120); } } |