前回、Gameクラス ASP.NET Coreでクラッシュローラーをつくる(1)の続きです。
Rollerクラス
先に比較的簡単なRollerクラスを示します。
XとYはローラーの座標でInitXとInitYは初期状態のローラーの座標です。
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 |
namespace CrashRoller { public class Roller { public Roller() { } public int InitX { protected set; get; } public int InitY { protected set; get; } public int X { set; get; } public int Y { set; get; } } } |
前述のRollerクラスを継承して縦方向のみと横方向のみに移動できるローラーを生成するためのクラスを定義します。xxEndプロパティは移動できるXY座標の最大値と最小値です。
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 |
namespace CrashRoller { public class RollerWE : Roller { public RollerWE(int initX, int initY, int leftEndX, int rightEndX) { X = InitX = initX; Y = InitY = initY; LeftEndX = leftEndX; RightEndX = rightEndX; } public int LeftEndX { get; } public int RightEndX { get; } } public class RollerNS : Roller { public RollerNS(int initX, int initY, int topEndY, int bottomEndY) { X = InitX = initX; Y = InitY = initY - 1; // 微調整 TopEndY = topEndY; BottomEndY = bottomEndY - 2; // 微調整 } public int TopEndY { get; } public int BottomEndY { get; } } } |
Enemyクラス
次にEnemyクラスを示します。
Positionクラス
まず座標を管理するためのPositionクラスを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
namespace CrashRoller { public class Position { public Position(int x, int y) { X = x; Y = y; } public int X { private set; get; } public int Y { private set; get; } } } |
フィールド変数とプロパティ
次にEnemyクラスの本体を示します。
1 2 3 4 5 6 |
namespace CrashRoller { public class Enemy { } } |
と書くべきところを
1 2 3 |
public class Enemy { } |
と省略して書きます。
最初にフィールド変数とプロパティを示します。
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 |
public class Enemy { // すぐに巣から敵がでてこないように適当に時間を消費する // NumberOfTimesToMoveが0になったら巣から出てくる const int MaxNumberOfTimesToMove = 64; int NumberOfTimesToMove = 0; public Game Game { private set; get; } // 敵の初期のX座標 public int InitX { private set; get; } // 敵の初期のY座標 public int InitY { private set; get; } // 敵の現在のX座標 public int X { private set; get; } // 敵の現在のY座標 public int Y { private set; get; } // 敵を識別するID。1か2か? public int ID { private set; get; } public Direct EnemyDirect { private set; get; } } |
初期化
コンストラクタを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Enemy { public Enemy(Game game, int initX, int initY, int id) { Game = game; InitX = initX; InitY = initY; ID = id; Reset(); } public void Reset() { X = InitX; Y = InitY + 16; // 最初は巣のなかにいるように見せるために初期座標より少し下にする EnemyDirect = Direct.None; NumberOfTimesToMove = MaxNumberOfTimesToMove; } } |
移動方向の取得
次に移動させるための処理をおこないます。ここではプレイヤーの位置から移動する方向を決める、実際に移動させるという2段階で処理をおこないます。
以下は敵が移動する方向を取得するためのメソッドです。ふたつの橋をそれぞれとおる場合と通らない場合でプレイヤーにたどりつくための最短コストを取得することで最終的な最短経路を取得しています。
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 |
public class Enemy { Direct GetDirect(int targetX, int targetY, ref int minCost) { // ふたつの橋をそれぞれとおる場合と通らない場合で最小のコストでプレイヤーにたどりつけるルートを探す minCost = int.MaxValue; bool[] isUseBridgeNSs = { true, true, false, false, }; bool[] isUseBridgeWEs = { true, false, true, false, }; Direct direct = Direct.None; for (int i = 0; i < isUseBridgeNSs.Length; i++) { int tempMin = 0; Direct tempDirect = GetDirect(targetX, targetY, ref tempMin, isUseBridgeNSs[i], isUseBridgeWEs[i]); if (minCost > tempMin) { minCost = tempMin; direct = tempDirect; } } // 敵の移動方向が確定する return direct; } Direct GetDirect(int targetX, int targetY, ref int minCost, bool isUseBridgeNS, bool isUseBridgeWE) { List<Position> path = GetShortestPath(targetX, targetY, isUseBridgeNS, isUseBridgeWE, ref minCost); // プレイヤーを捕まえた場合、またはプレイヤーの位置にたどり着けない場合 if (path.Count < 2) return Direct.None; int lastX = path[1].X; int lastY = path[1].Y; // 敵の現在位置とプレイヤーを捕まえるために次に移動すべき座標を比較すると // 次の移動方向が決定する if (X < lastX) return Direct.Right; else if (X > lastX) return Direct.Left; else if (Y < lastY) return Direct.Down; else if (Y > lastY) return Direct.Up; else return EnemyDirect; } } |
敵がプレイヤーがいる位置にたどり着くための最短経路を取得するための処理を示します。
挟み撃ちを仕掛けるために2体の敵のうち、一方は右側と下側から、他方には左側と上側からプレイヤーを捕まえるための最短経路を求めます。ただしプレイヤーが角にいる場合は捕まえるための最短経路が存在しない場合があるので、その場合はどの方向からも捕まえることができるようにします。
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 |
public class Enemy { List<Position> GetShortestPath(int targetX, int targetY, bool isUseBridgeNS, bool isUseBridgeWE, ref int minCost) { if (Game.Map == null) return new List<Position>(); int[,] map = Game.Map; int h = map.GetLength(0); int w = map.GetLength(1); int[,] costs = new int[h, w]; bool[,] allowPath = new bool[h, w]; // falseの場合は通れない Position[,] from = new Position[h, w]; for (int y = 0; y < map.GetLength(0); y++) { for (int x = 0; x < map.GetLength(1); x++) { costs[y, x] = int.MaxValue; allowPath[y, x] = true; } } for (int y = 0; y < map.GetLength(0); y++) { for (int x = 0; x < map.GetLength(1); x++) { if (map[y, x] == (int)Road.BRIDGE_NS_CROSS) { if (isUseBridgeNS) { allowPath[y, x + 1] = false; allowPath[y, x - 1] = false; } else { allowPath[y + 1, x] = false; allowPath[y - 1, x] = false; } } if (map[y, x] == (int)Road.BRIDGE_WE_CROSS) { if (isUseBridgeWE) { allowPath[y + 1, x] = false; allowPath[y - 1, x] = false; } else { allowPath[y, x + 1] = false; allowPath[y, x - 1] = false; } } } } costs[Y, X] = 0; from[Y, X] = new Position(0, 0); Queue<int> vsX = new Queue<int>(); Queue<int> vsY = new Queue<int>(); vsX.Enqueue(X); vsY.Enqueue(Y); bool isMovementRestrictions1 = true; // ID が 1 の敵はプレイヤーの右側と下側は通行禁止とする bool isMovementRestrictions2 = true; // ID が 1 の敵はプレイヤーの左側と上側は通行禁止とする // プレイヤーは角にいるかもしれないのでチェック if (Game.IsInMap(targetY, targetX)) { // プレイヤーは角にいてその方向からでは捕まえることができない場合は通行禁止解除する if ((map[targetY - 1, targetX] == (int)Road.NONE && map[targetY, targetX - 1] == (int)Road.NONE)) isMovementRestrictions1 = false; if ((map[targetY + 1, targetX] == (int)Road.NONE && map[targetY, targetX + 1] == (int)Road.NONE)) isMovementRestrictions2 = false; } while (true) { int x = vsX.Dequeue(); int y = vsY.Dequeue(); int[] nextXs = { 0, 0, 1, -1 }; int[] nextYs = { 1, -1, 0, 0 }; for (int i = 0; i < 4; i++) { // 現在調査している座標のとなりの点が移動可能であればQueueに追加する int nextX = x + nextXs[i]; int nextY = y + nextYs[i]; // 移動先の候補が配列の範囲外の場合、ワープ処理をおこなう if (nextX < 0) nextX = w - 1; else if (nextY < 0) nextY = h - 1; else if (nextX >= w) nextX = 0; else if (nextY >= h) nextY = 0; // 移動先の候補が通路ではない場合、移動先の候補からはずす if (map[nextY, nextX] == (int)Road.NONE) continue; // 移動先の候補が通行禁止の場合も、移動先の候補からはずす if (!allowPath[nextY, nextX]) continue; // 敵のIDによってプレイヤーを捕まえにいく方向をかえる // これで挟み撃ちを狙っているような敵の動きを実現できる // ただしプレイヤーが角にいる場合は捕まえにいく方向を限定しない if (ID == 1) { // ID が 1 のときはプレイヤーの右側と下側は通行止めとする if ((targetX + 1 == nextX && targetY == nextY) || (targetX == nextX && targetY + 1 == nextY)) { if (isMovementRestrictions1) continue; } } else { // ID が 2 のときはプレイヤーの左側と上側は通行止めとする if ((targetX - 1 == nextX && targetY == nextY) || (targetX == nextX && targetY - 1 == nextY)) { if (isMovementRestrictions2) continue; } } // 移動可能で既存のコストも小さい場合は移動先候補に加える if (costs[y, x] + 1 < costs[nextY, nextX]) { costs[nextY, nextX] = costs[y, x] + 1; vsX.Enqueue(nextX); vsY.Enqueue(nextY); from[nextY, nextX] = new Position(x, y); } } // 移動先候補がなくなったら終了 if (vsX.Count == 0) break; } Position last = from[targetY, targetX]; // プレイヤーの位置に到達するまでのコスト minCost = costs[targetY, targetX]; List<Position> path = new List<Position>(); while (true) { if (last == null) break; var temp = from[last.Y, last.X]; int tempX = temp.X; int tempY = temp.Y; if (tempX == 0 && tempY == 0) break; last = new Position(tempX, tempY); path.Insert(0, last); } return path; } } |
移動の処理
実際に敵を移動させる処理を示します。敵が移動方向を変更することができるのは角、十字路、丁字路にいるときです。それ以外の場所にいるときはそのまま直進します。
方向転換できる状態のときは前述のGetDirectメソッドで移動すべき方向を取得してEnemyDirectにセットします。そのあとEnemyDirectにセットされている値によって適切に敵が存在する座標を変更します。
永久パターン対策として次に敵を倒したときに加算される点数が3200点を超えた場合はローラーから逃げる動作をさせています。逃げた敵がプレイヤーがローラーから離れたとたん引き返してくるので深追いには注意が必要です。
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 |
public class Enemy { public void Move(int targetX, int targetY) { if (Game.Map == null) return; // 巣のなかにいるときは巣の外に出す if (Game.Map[Y, X] == (int)Road.NONE) { // NumberOfTimesToMove > 0のあいだは移動を開始しない if (NumberOfTimesToMove > 0) NumberOfTimesToMove--; // 巣の外へ移動させる if (NumberOfTimesToMove <= 0) Y--; return; } bool canUp = false; bool canRight = false; bool canDown = false; bool canLeft = false; // 敵は、上下左右にそれぞれ移動できるか? if (Game.IsInMap(Y, X) && Game.IsInMap(Y + 1, X) && Game.IsInMap(Y - 1, X) && Game.IsInMap(Y, X + 1) && Game.IsInMap(Y, X - 1)) { canUp = Game.Map[Y - 1, X] != (int)Road.NONE && Game.Map[Y, X] != (int)Road.BRIDGE_NS_CROSS && Game.Map[Y, X] != (int)Road.BRIDGE_WE_CROSS; canRight = Game.Map[Y, X + 1] != (int)Road.NONE && Game.Map[Y, X] != (int)Road.BRIDGE_NS_CROSS && Game.Map[Y, X] != (int)Road.BRIDGE_WE_CROSS; canDown = Game.Map[Y + 1, X] != (int)Road.NONE && Game.Map[Y, X] != (int)Road.BRIDGE_NS_CROSS && Game.Map[Y, X] != (int)Road.BRIDGE_WE_CROSS; canLeft = Game.Map[Y, X - 1] != (int)Road.NONE && Game.Map[Y, X] != (int)Road.BRIDGE_NS_CROSS && Game.Map[Y, X] != (int)Road.BRIDGE_WE_CROSS; } // 敵は停止している場合と角、丁字路、十字路にいるときだけ方向転換できる if (EnemyDirect == Direct.None || canUp && canRight || canRight && canDown || canDown && canLeft || canLeft && canUp) { // 新しい移動方向を取得 int minCost = 0; EnemyDirect = GetDirect(targetX, targetY, ref minCost); } // 次回の加点が3200点を超えたローラーから逃げる処理を追加する if (Game.AddPointCrashEnemy >= 3200) { if (Game.IsHoldRollerW) { if (Y == Game.PlayerY) EnemyDirect = Direct.Left; } if (Game.IsHoldRollerE) { if (Y == Game.PlayerY) EnemyDirect = Direct.Right; } if (Game.IsHoldRollerN) { if (X == Game.PlayerX) EnemyDirect = Direct.Up; } if (Game.IsHoldRollerS) { if (X == Game.PlayerX) EnemyDirect = Direct.Down; } } int h = Game.Map.GetLength(0); int w = Game.Map.GetLength(1); // 移動方向に応じて現在座標から移動する // ワープ処理をしなければならない場合があるので注意する if (EnemyDirect == Direct.Right) { if (X >= w - 2) X = 0; else X++; } if (EnemyDirect == Direct.Left) { if (X <= 1) X = w - 1; else X--; } if (EnemyDirect == Direct.Down) { if (Y >= h - 2) Y = 0; else Y++; } if (EnemyDirect == Direct.Up) { if (Y <= 1) Y = h - 1; else Y--; } } } |