前回は通路の描画とプレイヤーの移動と描画処理を実装したので、今回は敵の動作と描画の処理の実装を考えます。
Enemyクラス
Enemyクラスを作成してそのなかで移動に関する処理をおこないます。
初期化
まずコンストラクタを示します。
コンストラクタの引数はXYの初期座標と識別IDです。InitXとInitYは初期座標、XとYは現在の座標です。MaxNumberOfTimesToMoveはこの回数分のUpdate関数が呼び出されないと巣のなかから外には出てこない回数です。最初はNumberOfTimesToMoveはMaxNumberOfTimesToMoveと同じで、Update関数が呼び出されるたびに1少なくなります。そして0になると巣の外にでてプレイヤーを追跡しはじめます。
1 2 3 4 5 6 7 8 9 10 11 12 |
class Enemy { constructor(initX, initY, id) { this.X = this.InitX = initX; this.Y = this.InitY = initY; this.ID = id; this.MaxNumberOfTimesToMove = 64; this.NumberOfTimesToMove = this.MaxNumberOfTimesToMove; this.EnemyDirect = Direct.Stop; } } |
敵の初期化をおこないます。プレイヤーによって撃退されたときや、ゲームスタート時やステージクリア時やミス時に敵を初期位置に戻すときに実行されます。
InitXとInitYは通路上における敵の初期座標です。巣のなかにいるときはそのように見せるためにそれよりもY座標を大きな値を設定してそのよに見せかけます。
1 2 3 4 5 6 7 8 9 10 |
class Enemy { Reset() { this.X = this.InitX; this.Y = this.InitY + 16; // 最初は巣のなかにいるように見せるために初期座標より少し下にする this.EnemyDirect = Direct.Stop; this.NumberOfTimesToMove = this.MaxNumberOfTimesToMove; } } |
移動方向を取得する
GetDirect関数は敵が次に移動する方向を決めるためのものです。通路が立体交差になっているので南北、東西の橋と通路の交差点について南北にしか通行できない、東西にしか通行できない場合にわけてそのなかから最短経路を求めています。
立体交差が2箇所あるので全部で4とおり調べないといけません。もっと効率的なやり方があるかもしれないのですが、空を飛ぶために身体を軽量化し、その結果脳みそも小さくなってしまった鳩にはこんな方法しか思いつきません。もっと効率よく計算する方法があるならこっそり教えてください。
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 |
class Enemy { GetDirect() { let obj = { Direct : Direct.Stop, Path : [], Cost : 100000000, // 暫定値として巨大な値を淹れておく } // ふたつの橋をそれぞれとおる場合と通らない場合で最小のコストでプレイヤーにたどりつけるルートを探す let isUseBridgeNSs = [true, true, false, false, ]; let isUseBridgeWEs = [true, false, true, false, ]; for (let i = 0; i < isUseBridgeNSs.length; i++) { let result = this.GetDirect2(isUseBridgeNSs[i], isUseBridgeWEs[i]); // 暫定値よりも少ないコストで移動できる場合は入れ替える if (obj.Cost > result.Cost) { obj.Cost = result.Cost; obj.Path = result.Path; obj.Direct = result.Direct; } } // これで敵の移動方向と最短経路とコストが確定する return obj; } } |
GetDirect関数内でGetDirect2関数が呼び出されていますが、その部分を示します。
引数は南北にかかっている橋を通るのか否か、東西にかかっている橋を通るのか否かです。あとはダイクストラ法で最短経路を求めています。現在位置からプレイヤーのいる座標にたどり着くために最初に移動すべき方向がその敵が移動すべき方向です。このような方法で敵が次に移動する方向を求めています。
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 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
class Enemy { GetDirect2(isUseBridgeNS, isUseBridgeWE) { let costs = []; for(let y = 0; y < mapSourceHeight; y++) { let temp = []; for(let x = 0; x < mapSourceWidth; x++) temp.push(10000000); // 最初に巨大な値を代入しておく costs.push(temp); } // 通行止めの場所を設定する let isAllows = []; // falseの場合は通れない for(let y = 0; y < mapSourceHeight; y++) { let temp = []; for(let x = 0; x < mapSourceWidth; x++) temp.push(true); isAllows.push(temp); } for (let y = 0; y < mapSourceHeight; y++) { for (let x = 0; x < mapSourceWidth; x++) { if (map[y][x] == POSITION.POSITION_BRIDGE_NS_CROSS) { if (isUseBridgeNS) { isAllows[y][x + 1] = false; isAllows[y][x - 1] = false; } else { isAllows[y + 1][x] = false; isAllows[y - 1][x] = false; } } if (map[y][x] == POSITION.POSITION_BRIDGE_WE_CROSS) { if (isUseBridgeWE) { isAllows[y + 1][x] = false; isAllows[y - 1][x] = false; } else { isAllows[y][x + 1] = false; isAllows[y][x - 1] = false; } } } } let from = []; for(let y = 0; y < mapSourceHeight; y++) { let temp = []; for(let x = 0; x < mapSourceWidth; x++) { temp.push(null); } from.push(temp); } costs[this.Y][this.X] = 0; let arrX = []; let arrY = []; arrX.push(this.X); arrY.push(this.Y); let isMovementRestrictions1 = true; // ID が 1 の敵はプレイヤーの右側と下側は通行禁止とする let isMovementRestrictions2 = true; // ID が 1 の敵はプレイヤーの左側と上側は通行禁止とする // プレイヤーは角にいるかもしれないのでチェック if (IsInMap(playerY, playerX) && IsInMap(playerY + 1, playerX) && IsInMap(playerY, playerX + 1) && IsInMap(playerY - 1, playerX) && IsInMap(playerY, playerX - 1)) { // プレイヤーは角にいてその方向からでは捕まえることができない場合は通行禁止解除する if ((map[playerY - 1][playerX] == POSITION.POSITION_NONE && map[playerY][playerX - 1] == POSITION.POSITION_NONE)) isMovementRestrictions1 = false; if ((map[playerY + 1][playerX] == POSITION.POSITION_NONE && map[playerY][playerX + 1] == POSITION.POSITION_NONE)) isMovementRestrictions2 = false; } while (true) { let x = arrX.shift(); let y = arrY.shift(); let nextXs = [0, 0, 1, -1]; let nextYs = [1, -1, 0, 0]; for (let i = 0; i < 4; i++) { // 現在調査している座標のとなりの点が移動可能であれば追加する let nextX = x + nextXs[i]; let nextY = y + nextYs[i]; // 移動先の候補が配列の範囲外の場合、ワープ処理をおこなう if (nextX < 0) nextX = mapSourceWidth - 1; else if (nextY < 0) nextY = mapSourceHeight - 1; else if (nextX >= mapSourceWidth) nextX = 0; else if (nextY >= mapSourceHeight) nextY = 0; // 移動先の候補が通路ではない場合、移動先の候補からはずす if (map[nextY][nextX] == POSITION.POSITION_NONE) continue; // 移動先の候補が通行禁止の場合も、移動先の候補からはずす if (!isAllows[nextY][nextX]) continue; // 敵のIDによってプレイヤーを捕まえにいく方向をかえる // これで挟み撃ちを狙っているような敵の動きを実現できる // ただしプレイヤーが角にいる場合は捕まえにいく方向を限定しない if (this.ID == 1) { // ID が 1 のときはプレイヤーの右側と下側は通行止めとする if ((playerX + 1 == nextX && playerY == nextY) || (playerX == nextX && playerY + 1 == nextY)) { if (isMovementRestrictions1) continue; } } else { // ID が 2 のときはプレイヤーの左側と上側は通行止めとする if ((playerX - 1 == nextX && playerY == nextY) || (playerX == nextX && playerY - 1 == nextY)) { if (isMovementRestrictions2) continue; } } // 移動可能で既存のコストも小さい場合は移動先候補に加える if (costs[y][x] + 1 < costs[nextY][nextX]) { costs[nextY][nextX] = costs[y][x] + 1; arrX.push(nextX); arrY.push(nextY); from[nextY][nextX] = new Position(x, y); } } // 移動先候補がなくなったら終了 if (arrX.length == 0) break; } // プレイヤーの位置に到達するまでのコスト let cost = costs[playerY][playerX]; let lastPosition = from[playerY][playerX]; let path = []; while (true) { if (lastPosition == null) break; let temp = from[lastPosition.Y][lastPosition.X]; if (temp == null) break; let tempX = temp.X; let tempY = temp.Y; lastPosition = new Position(tempX, tempY); path.unshift(lastPosition) ; } let obj = { Direct : Direct.Stop, Path : path, Cost : cost, } // プレイヤーを捕まえた場合、またはプレイヤーの位置にたどり着けない場合 if (path.length < 2) return obj; let lastX = path[1].X; let lastY = path[1].Y; // 敵の現在位置とプレイヤーを捕まえるために次に移動すべき座標を比較すると // 次の移動方向が決定する let direct; if (this.X < lastX) direct = Direct.Right; else if (this.X > lastX) direct = Direct.Left; else if (this.Y < lastY) direct = Direct.Down; else if (this.Y > lastY) direct = Direct.Up; else direct = this.EnemyDirect; obj.Direct = direct; return obj; } } |
移動させる
Move関数は実際に敵が別の座標に移動するための関数です。
まず敵がいる座標が通路ではない場合(map[this.Y][this.X] == POSITION.POSITION_NONEの場合)、巣のなかにいると判断します。敵が巣のなかにいるときはNumberOfTimesToMoveを減算します。すでに0になっているときは上方向に移動させます。
すでに敵が巣の外に出ている場合(map[this.Y][this.X] != POSITION.POSITION_NONEの場合)は敵を移動させます。敵が方向転換できるのは角や丁字路、十字路にいる場合です。敵がいる座標の上下左右がPOSITION.POSITION_NONEであるかどうかで上下左右にそれぞれ移動できるかはわかります。
角や丁字路、十字路にいる場合は前述のGetDirect関数で方向転換の処理をおこないます。取得された新しい進行方向をメンバ変数EnemyDirectに格納しておきます。
いまはまだ実装していませんがプレイヤーがローラーをつかって敵に反撃しようとしているかもしれません。実際に自分でプレイしてみましたがある特定のパターンに入ると永久に点数稼ぎができてしまうため、敵を倒し続けた場合、ローラーから逃げる動作を追加します。関数内のif文で使われているaddPointCrashEnemyはグローバル変数です。
敵を移動させる方向が決まったらに現在の座標から移動させる処理をおこないますが、このとき通路の端にいる場合は反対側にワープさせる処理をおこないます。
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 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
class Enemy { Move() { // 巣のなかにいるときは巣の外に出す if (map[this.Y][this.X] == POSITION.POSITION_NONE) { // NumberOfTimesToMove > 0のあいだは移動を開始しない if(this.NumberOfTimesToMove > 0) this.NumberOfTimesToMove--; // 巣の外へ移動させる if(this.NumberOfTimesToMove <= 0) this.Y--; return; } let canUp = false; let canRight = false; let canDown = false; let canLeft = false; // 敵は、上下左右にそれぞれ移動できるか? if (IsInMap(this.Y, this.X) && IsInMap(this.Y + 1, this.X) && IsInMap(this.Y - 1, this.X) && IsInMap(this.Y, this.X + 1) && IsInMap(this.Y, this.X - 1)) { if(map[this.Y][this.X] != POSITION.POSITION_BRIDGE_NS_CROSS && map[this.Y][this.X] != POSITION.POSITION_BRIDGE_WE_CROSS) { canUp = map[this.Y - 1][this.X] != POSITION.POSITION_NONE; canRight = map[this.Y][this.X + 1] != POSITION.POSITION_NONE; canDown = map[this.Y + 1][this.X] != POSITION.POSITION_NONE; canLeft = map[this.Y][this.X - 1] != POSITION.POSITION_NONE; } } // 敵は停止している場合と角、丁字路、十字路にいるときだけ方向転換できる if ( this.EnemyDirect == Direct.Stop || (canUp && canRight) || (canRight && canDown) || (canDown && canLeft) || (canLeft && canUp)) { // 新しい移動方向を取得 let obj = this.GetDirect(); this.EnemyDirect = obj.Direct; } // 次回の加点が3200点を超えたローラーから逃げる処理を追加する if (addPointCrashEnemy >= 3200) { if (isHoldRollerW) { if (this.Y == playerY) this.EnemyDirect = Direct.Left; } if (isHoldRollerE) { if (this.Y == playerY) this.EnemyDirect = Direct.Right; } if (isHoldRollerN) { if (this.X == playerX) this.EnemyDirect = Direct.Up; } if (isHoldRollerS) { if (this.X == playerX) this.EnemyDirect = Direct.Down; } } // 移動方向に応じて現在座標から移動する // ワープ処理をしなければならない場合があるので注意する if (this.EnemyDirect == Direct.Right) { if (this.X >= mapSourceWidth - 2) this.X = 0; else this.X++; } if (this.EnemyDirect == Direct.Left) { if (this.X <= 1) this.X = mapSourceWidth - 1; else this.X--; } if (this.EnemyDirect == Direct.Down) { if (this.Y >= mapSourceHeight - 2) this.Y = 0; else this.Y++; } if (this.EnemyDirect == Direct.Up) { if (this.Y <= 1) this.Y = mapSourceHeight - 1; else this.Y--; } } } |
クラスの外での処理
ここからはクラスの外での処理になります。
初期化の処理
まず初期化の処理をおこないます。Enemyオブジェクトを格納する配列を作成します。InitEnemies関数は配列の中身を空にして新しく作成したふたつのEnemyオブジェクトを格納します。そしてEnemyクラスのメンバ関数Resetを呼び出して巣のなかに敵を移動させます。
1 2 3 4 5 6 7 8 9 10 11 |
let addPointCrashEnemy = 0; let enemies = []; function InitEnemies() { enemies = []; enemies.push(new Enemy(INIT_ENEMY1_X, INIT_ENEMY1_Y, 1)); enemies.push(new Enemy(INIT_ENEMY2_X, INIT_ENEMY2_Y, 2)); for(let i=0; i<enemies.length; i++) enemies[i].Reset(); } |
移動処理
MoveEnemies関数はenemiesに格納されているEnemyオブジェクトを移動させます。
1 2 3 4 5 |
function MoveEnemies() { for(let i = 0; i< enemies.length; i++) enemies[i].Move(); } |
描画処理
DrawEnemy関数はEnemyオブジェクトを描画します。IDを調べて適切なイメージで描画処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function DrawEnemy(enemy) { let enemyImg; if(enemy.ID == 1) enemyImg = enemy1Img; else enemyImg = enemy2Img; ctx.drawImage( enemyImg, enemy.X * EXPANSION_RATE + LEFT_MARGIN - 5, enemy.Y * EXPANSION_RATE + TOP_MARGIN - 5, CHARACTOR_SIZE + 10, CHARACTOR_SIZE + 10); } |
DrawEnemiesはまとめて描画処理をおこなうためのものです。
1 2 3 4 5 |
function DrawEnemies() { DrawEnemy(enemies[0]); DrawEnemy(enemies[1]); } |
ReDrawEnemiesIfNeedは橋を描画することで本当は描画されなければならない敵が隠れてしまったときに再描画をするためのものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function ReDrawEnemiesIfNeed() { for (let i=0; i<enemies.length; i++) { let enemy = enemies[i]; if (IsInMap(enemy.Y, enemy.X) && (map[enemy.Y][enemy.X] == POSITION.POSITION_BRIDGE_EDGE || map[enemy.Y][enemy.X] == POSITION.POSITION_BRIDGE_NEAR_EDGE || map[enemy.Y][enemy.X] == POSITION.POSITION_BRIDGE_NS || map[enemy.Y][enemy.X] == POSITION.POSITION_BRIDGE_WE)) { DrawEnemy(enemy); } } } |
既存の関数の修正
描画対象に敵を追加したので、必要に応じて既存の関数を修正します。
Update関数を修正します。MoveEnemies関数を追加します。
1 2 3 4 5 6 7 |
function Update() { MovePlayer(); MoveEnemies(); // 追加 Draw(); PlayBGMIfNeed(); } |
敵を描画する処理を追加しないといけないのでDraw関数も修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function Draw() { DrawClear(); DrawRoad(); DrawPlayer(); DrawEnemies(); // 追加 DrawBridgeNS(); DrawBridgeWE(); ReDrawBridgeEdgeIfNeed(); ReDrawPlayerIfNeed(); ReDrawEnemiesIfNeed(); // 追加 } |
ページが読み込まれたときに敵を初期化する処理を追加します。
1 2 3 4 5 6 |
window.addEventListener('load', function(){ Init(); InitEnemies(); // 追加 InitSound(); Draw(); }); |