ASP.NET Core版 対戦型ラリーエックスをつくる(1)の続きです。
Playerクラスの定義
Playerクラスを定義します。それから方向にかんする列挙体を定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
namespace RallyX { public class Player { } public enum Direct { None, Up, Down, Left, Right, } } |
以降は名前空間を省略して以下のように書きます。
1 2 3 |
public class Player { } |
主要なプロパティを示します。
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 211 212 213 214 215 216 217 218 219 220 221 |
public class Player { // AspNetCore.SignalRで接続したときに与えられるID public string ConnectionID { private set; get; } // プレイヤーの名前 string _name = ""; public string Name { set { _name = value; } get { if (ConnectionID != "") { if (_name == "") _name = "デフォルトの名無しさん"; return _name; } else return String.Format("NPC {0:000}", _npcNumber); } } // スコア public int Score { set; get; } // 残機 public int Rest { set; get; } // プレイヤーの番号(100番台:青、200番台:赤、300番台:緑) public int PlayerNumber { private set; get; } // 燃料 public int Fuel { set; get; } // マップ上のセルのどの列にいるか? public int CurrentColumn { private set; get; } // マップ上のセルのどの行にいるか? public int CurrentRow { private set; get; } // マップ上のセルのどの列に移動しようとしているか? public int NextColumn { private set; get; } // マップ上のセルのどの行に移動しようとしているか? public int NextRow { private set; get; } // マップ上のX座標 public int X { set; get; } // マップ上のY座標 public int Y { set; get; } // 移動方向 public Direct MovingDirect { private set; get; } // 生きているか死んでいるか? public bool IsDead { private set; get; } // これが0以上の場合、スピン状態である public int TimeToSpin { private set; get; } // これが0以上の場合、無敵状態である public int InvincibleTime { private set; get; } // ノーミスでそのステージで通過したフラッグの数 public int FlagCount { set; get; } // ノーミスでそのステージで撃破した数 public int KillCount { set; get; } // そのステージで通過したフラッグの合計 public int StageFlagCount { set; get; } // そのステージで撃破した合計 public int StageKillCount { set; get; } // ↑キーが押されている bool _isUpKeyDown = false; public bool IsUpKeyDown { set { _isUpKeyDown = value; if (value) { _isDownKeyDown = false; _isLeftKeyDown = false; _isRightKeyDown = false; } } get { return _isUpKeyDown; } } // ↓キーが押されている bool _isDownKeyDown = false; public bool IsDownKeyDown { set { _isDownKeyDown = value; if (value) { _isUpKeyDown = false; _isLeftKeyDown = false; _isRightKeyDown = false; } } get { return _isDownKeyDown; } } // ←キーが押されている bool _isLeftKeyDown = false; public bool IsLeftKeyDown { set { _isLeftKeyDown = value; if (value) { _isUpKeyDown = false; _isDownKeyDown = false; _isRightKeyDown = false; } } get { return _isLeftKeyDown; } } // →キーが押されている bool _isRightKeyDown = false; public bool IsRightKeyDown { set { _isRightKeyDown = value; if (value) { _isUpKeyDown = false; _isDownKeyDown = false; _isLeftKeyDown = false; } } get { return _isRightKeyDown; } } } |
初期化
コンストラクタを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class Player { static int _nextNpcNumber = 1; int _npcNumber; public Player(string connectionId, int playerNumber) { ConnectionID = connectionId; Name = ""; Score = 0; _npcNumber = _nextNpcNumber; _nextNpcNumber++; Rest = RallyXGame.REST_MAX; PlayerNumber = playerNumber; Fuel = RallyXGame.FUEL_MAX; Reset(); } } |
Resetメソッドはプレイヤーの初期位置を設定します。このメソッドが実行されると、RallyXGameクラスのXXXPositionsプロパティからプレイヤーの初期位置を取得し、これをXプロパティとYプロパティにセットします。また初期の移動方向の設定もおこないます。
それと同時に死亡フラグのクリアして燃料を満タンにします。FlagCountプロパティとKillCountプロパティをリセットして、しばらくのあいだ無敵状態にします。
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 |
public class Player { // 前回、移動処理が成功した方向 Direct _beforeDirect = Direct.None; public void Reset() { Reset(PlayerNumber); } void Reset(int i) { if (i == 101) { X = RallyXGame.BluePositions[0].X; Y = RallyXGame.BluePositions[0].Y; MovingDirect = Direct.Up; } else if (i == 102) { X = RallyXGame.BluePositions[1].X; Y = RallyXGame.BluePositions[1].Y; MovingDirect = Direct.Up; } else if (i == 103) { X = RallyXGame.BluePositions[2].X; Y = RallyXGame.BluePositions[2].Y; MovingDirect = Direct.Up; } else if (i == 201) { X = RallyXGame.RedPositions[0].X; Y = RallyXGame.RedPositions[0].Y; MovingDirect = Direct.Down; } else if (i == 202) { X = RallyXGame.RedPositions[1].X; Y = RallyXGame.RedPositions[1].Y; MovingDirect = Direct.Up; } else if (i == 203) { X = RallyXGame.RedPositions[2].X; Y = RallyXGame.RedPositions[2].Y; MovingDirect = Direct.Up; } else if (i == 301) { X = RallyXGame.GreenPositions[0].X; Y = RallyXGame.GreenPositions[0].Y; MovingDirect = Direct.Up; } else if (i == 302) { X = RallyXGame.GreenPositions[1].X; Y = RallyXGame.GreenPositions[1].Y; MovingDirect = Direct.Down; } else if (i == 303) { X = RallyXGame.GreenPositions[2].X; Y = RallyXGame.GreenPositions[2].Y; MovingDirect = Direct.Down; } NextColumn = CurrentColumn = X / RallyXGame.CHARACTER_SIZE; NextRow = CurrentRow = Y / RallyXGame.CHARACTER_SIZE; _beforeDirect = Direct.None; IsDead = false; Fuel = RallyXGame.FUEL_MAX; TimeToSpin = -1; FlagCount = 0; KillCount = 0; InvincibleTime = RallyXGame.INVINCIBLE_TIME_MAX; } } |
移動に関する処理
CanMoveXXXメソッドはその方向に移動できるかどうかを調べて結果を返します。
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 |
public class Player { bool CanMoveUp(int col, int row) { if (row > 0 && !RallyXGame.Walls.Any(wall => wall.Column == col && wall.Row == row - 1)) return true; return false; } bool CanMoveDown(int col, int row) { if (row + 1 < RallyXGame.RowMax && !RallyXGame.Walls.Any(wall => wall.Column == col && wall.Row == row + 1)) return true; return false; } bool CanMoveLeft(int col, int row) { if (col > 0 && !RallyXGame.Walls.Any(wall => wall.Column == col - 1 && wall.Row == row)) return true; return false; } bool CanMoveRight(int col, int row) { if (col + 1 < RallyXGame.ColMax && !RallyXGame.Walls.Any(wall => wall.Column == col + 1 && wall.Row == row)) return true; return false; } } |
ラリーエックスの場合は途中で立ち止まることができません。壁に当たったら移動可能な方向に方向転換して移動し続けます。キー操作がされていないのであればそのまま直進し、それができない場合は移動できる方向をランダムに取得して返す処理を示します。基本的にUターンはしません。
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 |
public class Player { static Random _random = new Random(); Direct GetMoveDirect() { List<Direct> directs = new List<Direct>(); if (CanMoveDown(CurrentColumn, CurrentRow)) { directs.Add(Direct.Down); // 現在の移動方向で移動できるならそれを維持する if (MovingDirect == Direct.Down) return Direct.Down; } if (CanMoveUp(CurrentColumn, CurrentRow)) { directs.Add(Direct.Up); if (MovingDirect == Direct.Up) return Direct.Up; } if (CanMoveLeft(CurrentColumn, CurrentRow)) { directs.Add(Direct.Left); if (MovingDirect == Direct.Left) return Direct.Left; } if (CanMoveRight(CurrentColumn, CurrentRow)) { directs.Add(Direct.Right); if (MovingDirect == Direct.Right) return Direct.Right; } if (directs.Count == 0) return Direct.None; if (directs.Count == 1) return directs[0]; // 壁にぶつかった場合は適当にdirectsのなかから選ぶ // 基本的にUターンはしない if (MovingDirect == Direct.Up) directs.Remove(Direct.Down); if (MovingDirect == Direct.Down) directs.Remove(Direct.Up); if (MovingDirect == Direct.Left) directs.Remove(Direct.Right); if (MovingDirect == Direct.Right) directs.Remove(Direct.Left); int r = _random.Next(directs.Count); return directs[r]; } } |
煙幕のなかに入ってしまったときの処理
IsSmokeメソッドは煙幕のなかにいるかどうかを調べて結果を返します。煙幕のなかにいても自身に効果がない煙幕であればfalseを返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class Player { bool IsSmoke(int col, int row) { List<Smoke> smokes = RallyXGame.GetSmokes(); smokes = smokes.Where(smoke => smoke.Column == col && smoke.Row == row).ToList(); foreach (Smoke smoke in smokes) { if (PlayerNumber / 100 == 2 && smoke.PlayerNumber / 100 == 1) return true; if (PlayerNumber / 100 == 3 && smoke.PlayerNumber / 100 == 2) return true; if (PlayerNumber / 100 == 1 && smoke.PlayerNumber / 100 == 3) return true; } return false; } } |
RallyXGame.GetSmokesメソッドは以下のように定義されています。
1 2 3 4 5 6 7 8 9 |
public class RallyXGame { public static List<Smoke> GetSmokes() { List<Smoke> smokes = new List<Smoke>(Smokes); smokes.AddRange(_newSmokes); return smokes; } } |
煙幕のなかに入ってしまうと車はしばらくのあいだスピンしてその後移動方向を反転させます。GetDirectInSmokeメソッドは煙幕に突っ込んだ場合、移動可能であればこれまでの移動方向とは逆の方向を返します。移動できない場合は移動できる方向を取得してそれを返します。
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 |
public class Player { Direct GetDirectInSmoke(int col, int row) { // 煙幕に突っ込んだ場合は移動可能であれば逆方向に移動する if (MovingDirect == Direct.Up) { if (CanMoveDown(col, row)) return Direct.Down; } else if (MovingDirect == Direct.Down) { if (CanMoveUp(col, row)) return Direct.Up; } else if (MovingDirect == Direct.Left) { if (CanMoveRight(col, row)) return Direct.Right; } else if (MovingDirect == Direct.Right) { if (CanMoveLeft(col, row)) return Direct.Left; } // 逆方向に移動できない場合は移動できる方向に移動する if (CanMoveDown(col, row)) return Direct.Down; if (CanMoveUp(col, row)) return Direct.Up; if (CanMoveRight(col, row)) return Direct.Right; if (CanMoveLeft(col, row)) return Direct.Left; return MovingDirect; } } |
更新時の処理
プレイヤーの更新時の処理を示します。
死亡時にはなにもしません。それ以外のときは燃料を減らして無敵状態であるならInvincibleTimeを減らします。スピンしているのであればTimeToSpinを減らします。
X座標とY座標が両方ともCHARACTER_SIZEで割り切れる場合は、煙幕のなかに突入していないか調べたあと、方向転換の処理をおこないます。方向キーがおされていてその方向に方向転換できるときだけ方向転換の処理をおこないます。ただしスピンしている状態の場合はなにもしません。
方向キーがおされていない場合や押されているけど移動可能な方向でない場合は、前回と同じ方向に移動させることができるならそのまま維持し、壁にぶつかった場合は移動可能な方向に転換します。
X座標とY座標の両方がCHARACTER_SIZEで割り切れる場合でないときは方向転換はできません。その方向に4ピクセル移動します。移動した結果、X座標とY座標が両方ともCHARACTER_SIZEで割り切れる場合はCurrentColumnとCurrentRowを更新します。
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 |
public class Player { public void UpdatePlayer() { if (IsDead) return; Fuel--; InvincibleTime--; TimeToSpin--; if (X % RallyXGame.CHARACTER_SIZE == 0 && Y % RallyXGame.CHARACTER_SIZE == 0) { int col = X / RallyXGame.CHARACTER_SIZE; int row = Y / RallyXGame.CHARACTER_SIZE; if (TimeToSpin < 0) { if (IsSmoke(col, row)) { MovingDirect = GetDirectInSmoke(col, row); TimeToSpin = RallyXGame.SPIN_COUNT_MAX; return; } } else if (TimeToSpin > 0) { return; } Direct next = Direct.None; if (IsDownKeyDown && CanMoveDown(col, row)) { NextRow = row + 1; MovingDirect = Direct.Down; next = Direct.Down; } if (IsUpKeyDown && CanMoveUp(col, row)) { NextRow = row - 1; MovingDirect = Direct.Up; next = Direct.Up; } if (IsLeftKeyDown && CanMoveLeft(col, row)) { NextColumn = col - 1; MovingDirect = Direct.Left; next = Direct.Left; } if (IsRightKeyDown && CanMoveRight(col, row)) { NextColumn = col + 1; MovingDirect = Direct.Right; next = Direct.Right; } // 次の移動方向が設定されていない場合は前回と同じ方向に移動させる if (next == Direct.None) { MovingDirect = GetMoveDirect(); } } if (MovingDirect == Direct.Down) Y += 4; if (MovingDirect == Direct.Up) Y -= 4; if (MovingDirect == Direct.Left) X -= 4; if (MovingDirect == Direct.Right) X += 4; if (X % RallyXGame.CHARACTER_SIZE == 0 && Y % RallyXGame.CHARACTER_SIZE == 0) { CurrentColumn = X / RallyXGame.CHARACTER_SIZE; CurrentRow = Y / RallyXGame.CHARACTER_SIZE; } } } |
死亡時の処理
死亡時の処理を示します。
死亡時はいったんIsDeadフラグをtrueにします。そして残機を1減らし、2秒後に復活させます。ただし残機1減の結果、0になってしまった場合はゲームオーバーです。ゲームオーバーの場合はイベントを発生させます。ただしNPC(non player character)の場合はゲームオーバーはありません。
復活させるときはResetメソッドを実行させます。リセットすべきプロパティがリセットされてゲームが続行されます。
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 |
public class Player { public delegate void GameHandler(Player player); public event GameHandler? GameOverEvent; public void Dead() { IsDead = true; Rest--; System.Timers.Timer timer = new System.Timers.Timer(); timer.Interval = 2000; timer.Elapsed += EventHandlerRevive; timer.Start(); } private void EventHandlerRevive(object? sender, System.Timers.ElapsedEventArgs e) { System.Timers.Timer? t = (System.Timers.Timer ?)sender; if (t != null) { t.Stop(); t.Dispose(); if (ConnectionID == "" || Rest > 0) Reset(); else GameOverEvent?.Invoke(this); } } } |
NPCの動作
NPCの動作にかんする処理を示します。
NPCが次に移動する方向を取得するための処理を示します。
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 |
public class Player { Direct GetDirectNPC(int col, int row) { List<Direct> vs = new List<Direct>(); // 移動できる方向を取得する if (CanMoveUp(col, row)) vs.Add(Direct.Up); if (CanMoveDown(col, row)) vs.Add(Direct.Down); if (CanMoveLeft(col, row)) vs.Add(Direct.Left); if (CanMoveRight(col, row)) vs.Add(Direct.Right); // Uターンはしないので現在の逆方向は取り除く if (_beforeDirect == Direct.Up) vs = vs.Where(direct => direct != Direct.Down).ToList(); if (_beforeDirect == Direct.Down) vs = vs.Where(direct => direct != Direct.Up).ToList(); if (_beforeDirect == Direct.Left) vs = vs.Where(direct => direct != Direct.Right).ToList(); if (_beforeDirect == Direct.Right) vs = vs.Where(direct => direct != Direct.Left).ToList(); if (vs.Count == 0) return MovingDirect; Direct nextDirect = Direct.None; // 近くにいる獲物を探す Player target = RallyXGame.BluePlayers[0]; List<Player> targets = new List<Player>(); if (PlayerNumber / 100 == 2) targets = RallyXGame.BluePlayers; if (PlayerNumber / 100 == 3) targets = RallyXGame.RedPlayers; if (PlayerNumber / 100 == 1) targets = RallyXGame.GreenPlayers; double d = double.MaxValue; foreach (Player player in targets) { if (d > Math.Pow(player.X - X, 2) + Math.Pow(player.Y - Y, 2)) { d = Math.Pow(player.X - X, 2) + Math.Pow(player.Y - Y, 2); target = player; } } // targetが一番近くにいる獲物である // targetを捕まえるためにその方向に移動できるなら方向転換する // できない場合は乱数で適当に選ぶ if (X - target.X > 0 && CanMoveLeft(col, row) && _beforeDirect != Direct.Right && PlayerNumber % 100 != 1) nextDirect = Direct.Left; if (X - target.X < 0 && CanMoveRight(col, row) && _beforeDirect != Direct.Left && PlayerNumber % 100 != 2) nextDirect = Direct.Right; if (Y - target.Y > 0 && CanMoveUp(col, row) && _beforeDirect != Direct.Down && PlayerNumber % 100 != 1) nextDirect = Direct.Up; if (Y - target.Y < 0 && CanMoveDown(col, row) && _beforeDirect != Direct.Up && PlayerNumber % 100 != 2) nextDirect = Direct.Down; if (nextDirect != Direct.None) return nextDirect; else return vs[_random.Next(vs.Count)]; } } |
NPCの更新処理を示します。プレイヤーのときと同様、X座標とY座標がCHARACTER_SIZEで割り切れるときだけ方向転換の処理をして、それ以外のときは移動方向に4ピクセル移動するだけです。
X座標とY座標がCHARACTER_SIZEで割り切れるときは現在スピン状態ではなく、煙幕に突入していない状態であることを確認して上記のメソッドで取得した方向をMovingDirectにセットします。
それから背後から敵が迫っているかもしれません。そこでNPCの後ろ側の近い位置に敵がいる場合は煙幕を放出させます。
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 |
public class Player { public void UpdateNPC() { InvincibleTime--; TimeToSpin--; if (X % RallyXGame.CHARACTER_SIZE == 0 && Y % RallyXGame.CHARACTER_SIZE == 0) { int col = X / RallyXGame.CHARACTER_SIZE; int row = Y / RallyXGame.CHARACTER_SIZE; if (TimeToSpin < 0) { if (IsSmoke(col, row)) { MovingDirect = GetDirectInSmoke(col, row); TimeToSpin = RallyXGame.SPIN_COUNT_MAX; return; } else MovingDirect = GetDirectNPC(col, row); } else if (TimeToSpin > 0) { return; } // 敵が背後に迫っているかもしれない List<Player>? targets = null; if (PlayerNumber / 100 == 1) targets = RallyXGame.RedPlayers; if (PlayerNumber / 200 == 1) targets = RallyXGame.GreenPlayers; if (PlayerNumber / 300 == 1) targets = RallyXGame.BluePlayers; // 上に移動しているときは下側160ピクセルに敵が迫っている場合は煙幕を発射する if (MovingDirect == Direct.Up) { NextRow = row - 1; if (targets != null && targets.Any(player => Y <= player.Y && player.Y <= Y + 160 && X == player.X)) RallyXGame.SetSmoke(col, row, PlayerNumber); } if (MovingDirect == Direct.Down) { NextRow = row + 1; if (targets != null && targets.Any(player => Y - 160 <= player.Y && player.Y <= Y && X == player.X)) RallyXGame.SetSmoke(col, row, PlayerNumber); } if (MovingDirect == Direct.Left) { NextColumn = col - 1; if (targets != null && targets.Any(player => X <= player.X && player.X <= X + 160 && Y == player.Y)) RallyXGame.SetSmoke(col, row, PlayerNumber); } if (MovingDirect == Direct.Right) { NextColumn = col + 1; if (targets != null && targets.Any(player => X - 160 <= player.X && player.X <= X && Y == player.Y)) RallyXGame.SetSmoke(col, row, PlayerNumber); } _beforeDirect = MovingDirect; } if (MovingDirect == Direct.Down) Y += 4; if (MovingDirect == Direct.Up) Y -= 4; if (MovingDirect == Direct.Left) X -= 4; if (MovingDirect == Direct.Right) X += 4; if (X % RallyXGame.CHARACTER_SIZE == 0 && Y % RallyXGame.CHARACTER_SIZE == 0) { CurrentColumn = X / RallyXGame.CHARACTER_SIZE; CurrentRow = Y / RallyXGame.CHARACTER_SIZE; } } } |
煙幕を放出する処理を示します。RallyXGameクラス内で以下のように定義しています。
1 2 3 4 5 6 7 8 9 |
public class RallyXGame { static List<Smoke> _newSmokes = new List<Smoke>(); public static void SetSmoke(int col, int row, int playerNumber) { _newSmokes.Add(new Smoke(col, row, playerNumber)); } } |