前回は敵の追尾を実装しました。今回はローラーを使った反撃と当たり判定を実装します。
Contents
ローラーを描画する
まずローラーを表示させます。ローラーの位置情報を管理するためのクラスをつくります。
XとYは現在のローラーの座標、InitXとInitYは初期座標です。LeftEndX、RightEndX、TopEndY、BottomEndYはローラーが移動できる左右上下の限界値です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class RollerWE { constructor(initX, initY, leftEndX, rightEndX) { this.X = this.InitX = initX; this.Y = this.InitY = initY; this.LeftEndX = leftEndX; this.RightEndX = rightEndX; } } class RollerNS { constructor(initX, initY, topEndY, bottomEndY) { this.X = this.InitX = initX; this.Y = this.InitY = initY; this.TopEndY = topEndY; this.BottomEndY = bottomEndY; } } |
Rollerオブジェクトを生成します。ページが読み込まれたらローラーが初期座標の位置に描画されるようにコンストラクタに適切な値を渡します。初期座標は東西にかかる橋の左端と南北にかかる橋の下端です。
インスタンスを生成したらグローバル変数rollerNSとrollerWEに格納しておきます。
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 |
let rollerWE = null; let rollerNS = null; function CreateRollerWE() { // 二次元配列 map のなかから描画すべき座標(東西にかかる橋の左端)を探す for (let y = 0; y < mapSourceHeight; y++) { let left = -1; let right = -1; for (let x = 0; x < mapSourceWidth; x++) { if (map[y][x] == POSITION.POSITION_BRIDGE_EDGE && map[y][x + 1] == POSITION.POSITION_BRIDGE_WE) left = x; if (map[y][x] == POSITION.POSITION_BRIDGE_EDGE && map[y][x - 1] == POSITION.POSITION_BRIDGE_WE) right = x; } if (left != -1 && right != -1) { rollerWE = new RollerWE(left + ROLLER_THICKNESS / EXPANSION_RATE, y, left + ROLLER_THICKNESS / EXPANSION_RATE, right - ROLLER_THICKNESS / EXPANSION_RATE); return; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function CreateRollerNS() { for (let x = 0; x < mapSourceWidth; x++) { let top = -1; let bottom = -1; for (let y = 0; y < mapSourceHeight; y++) { if (map[y][x] == POSITION.POSITION_BRIDGE_EDGE && map[y + 1][x] == POSITION.POSITION_BRIDGE_NS) top = y; if (map[y][x] == POSITION.POSITION_BRIDGE_EDGE && map[y - 1][x] == POSITION.POSITION_BRIDGE_NS) bottom = y; } if (top != -1 && bottom != -1) { rollerNS = new RollerNS(x, bottom - ROLLER_THICKNESS / EXPANSION_RATE-1, top, bottom - ROLLER_THICKNESS / EXPANSION_RATE); return; } } } |
1 2 3 4 5 |
function InitRollers() { CreateRollerNS(); CreateRollerWE(); } |
次にローラーを描画するための関数を作成します。
1 2 3 4 5 6 7 8 9 10 11 12 |
function DrawRollers() { ctx.fillStyle = "rgb(0, 0, 255)"; ctx.fillRect( rollerWE.X * EXPANSION_RATE + LEFT_MARGIN, rollerWE.Y * EXPANSION_RATE + TOP_MARGIN, ROLLER_THICKNESS, CHARACTOR_SIZE); ctx.fillRect( rollerNS.X * EXPANSION_RATE + LEFT_MARGIN, rollerNS.Y * EXPANSION_RATE + TOP_MARGIN, CHARACTOR_SIZE, ROLLER_THICKNESS); } |
既存の関数を修正します。
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 |
window.addEventListener('load', function(){ Init(); InitEnemies(); InitRollers(); // 追加 InitSound(); Draw(); }); function Draw() { DrawClear(); DrawRoad(); DrawPlayer(); DrawEnemies() DrawBridgeNS(); DrawBridgeWE(); ReDrawBridgeEdgeIfNeed(); ReDrawPlayerIfNeed(); ReDrawEnemiesIfNeed(); DrawRollers(); // 追加 } |
ローラーを移動させる
上記の処理でローラーが描画されるようになりましたが移動させることができません。そこで移動させる処理を追加します。
ローラーが移動するのはプレイヤーがローラーの移動可能範囲内で押している場合です。押しているかどうかはローラーの座標とプレイヤーの座標を比較することで判断できます。プレイヤーがローラーを押しているかどうかを格納するフラグを作成し、以下のような関数を作成します。
座標を比較するときにC#だと変数をint型にすると端数は丸められて整数になるのですが、JavaScriptの場合はそうはならないのでMath.floor関数で整数に変換してから比較します。
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 |
let isHoldRollerE = false; let isHoldRollerW = false; let isHoldRollerN = false; let isHoldRollerS = false; function MoveRollerIfPlayerHold() { // ローラーを東側(右側)から押しているか? if ( playerDirect == Direct.Right && rollerWE.Y == playerY && rollerWE.X + EXPANSION_RATE < rollerWE.RightEndX && Math.floor(rollerWE.X) == Math.floor(playerX + CHARACTOR_SIZE / EXPANSION_RATE) ) { // ローラーを東側(右側)から押している場合、ローラーの座標を変更する rollerWE.X += 1; isHoldRollerE = true; } else isHoldRollerE = false; if ( playerDirect == Direct.Left && rollerWE.Y == playerY && rollerWE.X - EXPANSION_RATE > rollerWE.LeftEndX && Math.floor(rollerWE.X) == Math.floor(playerX - ROLLER_THICKNESS / EXPANSION_RATE) ) { rollerWE.X -= 1; isHoldRollerW = true; } else isHoldRollerW = false; if ( playerDirect == Direct.Up && rollerNS.X == playerX && Math.floor(rollerNS.Y) == Math.floor(playerY - ROLLER_THICKNESS / EXPANSION_RATE) && rollerNS.Y - EXPANSION_RATE > rollerNS.TopEndY + CHARACTOR_SIZE / EXPANSION_RATE ) { rollerNS.Y -= 1; isHoldRollerN = true; } else isHoldRollerN = false; if ( playerDirect == Direct.Down && rollerNS.X == playerX && rollerNS.Y - EXPANSION_RATE < rollerNS.BottomEndY - ROLLER_THICKNESS / EXPANSION_RATE && Math.floor(rollerNS.Y) == Math.floor(playerY + CHARACTOR_SIZE / EXPANSION_RATE) ) { rollerNS.Y += 1; isHoldRollerS = true; } else isHoldRollerS = false; } |
それからローラーを押しているときに効果音を鳴らします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function PlayRollerSoundIfNeed() { if(isHoldRollerE || isHoldRollerW || isHoldRollerN || isHoldRollerS) { if(!isPlayingRollerSound) { isPlayingRollerSound = true; rollerSound.currentTime = 0; rollerSound.play(); } } else { isPlayingRollerSound = false; rollerSound.currentTime = 0; rollerSound.pause(); } } |
プレイヤーの加速
ローラーを押しているときはプレイヤーの速度をアップさせ、ローラーを押し終わるまで方向転換ができないようにします。
キャラクタを移動させるためにタイマーを使っていますが、MovePlayer2関数はもうひとつ別に作っているタイマーによって呼び出されます。ローラーを押しているときにMovePlayer2関数でもプレイヤーを移動させるとそのときだけ加速している処理ができるようになります。
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 MovePlayer2() { if (IsInMap(playerY, playerX)) { if (isHoldRollerN && playerDirect == Direct.Up) playerY--; if (isHoldRollerS && playerDirect == Direct.Down) playerY++; if (isHoldRollerW && playerDirect == Direct.Left) playerX--; if (isHoldRollerE && playerDirect == Direct.Right) playerX++; } MoveRollerIfPlayerHold(); } function SetPlayerDirect() { if(isHoldRollerN || isHoldRollerS || isHoldRollerW || isHoldRollerE) return; // 以下は既存の関数と同じ } |
既存の関数を修正します。
1 2 3 4 5 6 7 8 9 10 11 |
function Update() { MovePlayer(); MoveEnemies(); MoveRollerIfPlayerHold(); // 追加 Draw(); PlayBGMIfNeed(); PlayRollerSoundIfNeed(); // 追加 } |
当たり判定とそのあとの処理
ローラーが移動するようになりましたが、いまは当たり判定が実装されていないので敵に接触してもなにも起きません。ローラーを押しているときに敵に接触したら敵を撃退し、そうでないときに敵に接触したらミス時の処理がおこなわれるようにします。
敵を倒したとき
まず敵を倒したときの処理をする関数を作成します。
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 |
let pointDrawAddScore = null; let addPointCrashEnemy = INIT_ADD_POINT_CRASH_ENEMY; function OnHitEmemy(enemy) { StopTimer1(); StopTimer2(); // 効果音を鳴らす hitSound.play(); // 倒した敵の近くに加算される点数を表示させたいのでそのための座標を記憶しておく // pointDrawAddScore != null のあいだだけ獲得した点数が表示される pointDrawAddScore = new Position(enemy.X, enemy.Y); setTimeout(() =>{ // 撃退された敵を巣に戻す // 加算された点数の表示を終了する enemy.Reset(); pointDrawAddScore = null; // 得点追加の処理 次回の得点は倍(ただし上限は9900点)になる score += addPointCrashEnemy; addPointCrashEnemy *= 2; if (addPointCrashEnemy > 9900) addPointCrashEnemy = 9900; // 停止させていたタイマーをStartさせてゲームを続行 StartTimer1(interval); StartTimer2(); }, 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 25 26 27 28 29 30 31 32 33 34 35 36 37 |
let rest = MAX_REST; function OnDeadPlayer() { StopTimer1(); StopTimer2(); // 効果音を鳴らす deadSound.play(); // 残機を1減らす。敵を倒したときに加算される点数をリセットする rest--; addPointCrashEnemy = INIT_ADD_POINT_CRASH_ENEMY; setTimeout(() => { if(rest <= 0) { isGaming = false; StopBGM(); gameOverSound.play(); // 再生 Draw(); return; } playerX = INIT_PLAYER_X; playerY = INIT_PLAYER_Y; playerDirect = Direct.Stop; enemies[0].Reset(); enemies[1].Reset(); // 停止させていたタイマーをStartさせてゲームを続行 StartTimer1(interval); StartTimer2(); }, 2000); } |
加算される点数の表示
敵を倒したときは近くに加算される点数を表示させます。それからスコアや残機、ゲームオーバー時にはその表示もここでやってしまいましょう。関連の関数を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function DrawScore() { ctx.font="bold 20px MS ゴシック"; ctx.fillStyle = "rgb(255, 255, 222)"; // スコア表示用 let scoreText = 'SCORE ' + String(score).padStart(5, '0'); if(score >= 100000) scoreText = 'SCORE ' + score; ctx.fillText(scoreText, 10, 380); // 敵を倒したときに加算される点数を表示する if(pointDrawAddScore != null) ctx.fillText(addPointCrashEnemy, pointDrawAddScore.X * EXPANSION_RATE + LEFT_MARGIN, pointDrawAddScore.Y * EXPANSION_RATE + TOP_MARGIN + 10); } |
残機の表示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function DrawRest() { ctx.font="bold 20px MS ゴシック"; ctx.fillStyle = "rgb(255, 255, 222)"; let scoreText = '残 ' + rest ctx.fillText(scoreText, 10, 410); ctx.font="bold 20px MS ゴシック"; ctx.fillStyle = "rgb(255, 0, 0)"; if(pointDrawAddScore != null) ctx.fillText(addPointCrashEnemy, pointDrawAddScore.X * EXPANSION_RATE + LEFT_MARGIN, pointDrawAddScore.Y * EXPANSION_RATE + TOP_MARGIN + 10); } |
ゲームオーバー表示
1 2 3 4 5 6 7 8 |
function DrawGameOverIfNeed() { if(!isGaming) { ctx.fillStyle = "rgb(255, 0, 0)"; ctx.fillText('GAME OVER',10,450); } } |
既存のDraw関数を修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function Draw() { DrawClear(); DrawRoad(); DrawPlayer(); DrawEnemies(); DrawBridgeNS(); DrawBridgeWE(); ReDrawBridgeEdgeIfNeed(); ReDrawPlayerIfNeed(); ReDrawEnemiesIfNeed(); DrawRollers(); DrawScore(); // 追加 DrawRest(); // 追加 DrawGameOverIfNeed(); // 追加 } |
ステージクリア判定
ステージクリア時には次のステージへ進むことになりますが、ステージクリア判定の処理が必要です。
ステージクリアの条件は通路をすべて塗りつぶしたときです。通路であって二次元配列 isVisits がすべてtrueになっているとき・・・と言いたいのですが、実際にはちょっと違います。交差点の部分はisVisits[y][x]がfalseでも交差点を通過していれば色が塗られているように見えてしまうのです。
そこでisVisits[y][x]がfalseでも近くにtrueの座標があればtrueと見なすことにします。そうでないとすべて塗り潰れているのになぜかステージクリア判定されないという問題がおきてしまうので…。
これは引数の座標に相当するisVisits[y][x]はtrueか、trueでなくてもtrueと見なすべきなのかを返す関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function IsVisitSurroundingPosittions(x, y) { for (let i = 0; i <= CHARACTOR_SIZE / EXPANSION_RATE; i++) { for (let j = 0; j <= CHARACTOR_SIZE / EXPANSION_RATE; j++) { if (y + i < 0) continue; if (y + i >= mapSourceHeight) continue; if (x + j < 0) continue; if (x + j >= mapSourceWidth) continue; if (isVisits[y + i][x + j]) return true; } } return false; } |
これはステージクリアかどうかを判定する関数です。POSITION.POSITION_NONEではないすべての座標でIsVisitSurroundingPosittions関数を実行してひとつもfalseが返されなかった場合はステージクリアです。
1 2 3 4 5 6 7 8 9 10 11 12 |
function IsStageClear() { for (let y = 0; y < mapSourceHeight; y++) { for (let x = 0; x < mapSourceWidth; x++) { if (!isVisits[y][x] && map[y][x] != POSITION.POSITION_NONE && !IsVisitSurroundingPosittions(x, y)) return false; } } return true; } |
Update関数が実行されたときにステージクリアかどうかをチェックします。ステージクリア時はそのときにする処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function CheckStageClear() { if(IsStageClear()) { stageClearSound.play() StopTimer1(); StopTimer2(); eatCount = 0; addPointCrashEnemy = INIT_ADD_POINT_CRASH_ENEMY; setTimeout(()=>{ InitStage(); interval -= 5; if(interval <= 10) interval = 10; StartTimer1(interval); StartTimer2(); Draw(); }, 2000); } } |
既存の関数を修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function Update() { MovePlayer(); MoveEnemies(); MoveRollerIfPlayerHold(); CheckHit(); CheckStageClear(); // 追加 Draw(); PlayBGMIfNeed(); PlayRollerSoundIfNeed(); } |
1 2 3 4 5 6 7 8 9 10 11 12 |
function InitStage() { ClearVisits(); InitPlayerPosition(); playerDirect = Direct.Stop; // 追加 敵とローラーを初期座標に戻す InitEnemies(); CreateRollerWE(); CreateRollerNS(); } |
またGameStart関数も修正が必要です。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function GameStart() { if(!isGaming) isGaming = true; score = 0; // スコアをリセット rest = MAX_REST; // 残機を最大値に interval = INIT_INTERVAL; // タイマーのintervalを初期値に InitStage(); StartTimer1(interval); StartTimer2(); } |