ASP.NET Core版 対戦型Pengoをつくる(1)の続きです。
Pengoでは氷のブロックを飛ばして敵を倒します。そこでブロックの座標と状態を格納するためのBlockクラスを定義します。
Contents
Blockクラスの定義
1 2 3 4 5 6 |
namespace PengoGame { public class Block { } } |
以降は名前空間を省略して書きます。
1 2 3 |
public class Block { } |
コンストラクタとプロパティ
コンストラクタを示します。
1 2 3 4 5 6 7 8 9 10 |
public class Block { Column = col; Row = row; X = Column * Game.CHARACTER_SIZE; Y = Row * Game.CHARACTER_SIZE; TimeUntilDisappears = Game.TIME_UNTIL_DISAPPEARS; KickPlayer = null; } |
Blockクラスのプロパティです。Pengoではブロックは移動する場合があります。フィールドは15×15ですが、ブロックがこのどの部分にあるのかを示しているのがColumnとRowです。また表示される座標を示しているのがXとYです。TimeUntilDisappearsは破壊されたブロックが消滅するまでの時間であり、KickPlayerはブロックを飛ばしたプレイヤーです(点数計算のときに必要になる)。
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 |
public class Block { public int Column { protected set; get; } public int Row { protected set; get; } public int X { protected set; get; } public int Y { protected set; get; } public int TimeUntilDisappears { private set; get; } public Player? KickPlayer { private set; get; } } |
ブロックを飛ばしたときの処理
ブロックはそのむこうがわに壁や他のブロックが存在しない場合、飛ばすことができます。一度動き始めたブロックは壁かほかのブロックにぶつかるまで動き続けます。
プレイヤーがブロックを飛ばしたときの処理を示します。
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 |
public class Block { bool _breaking = false; bool _movingUp = false; bool _movingDown = false; bool _movingLeft = false; bool _movingRight = false; public delegate void BlockStartHandle(Block block); public event BlockStartHandle? BlockStart; public void MoveStart(Direct direct, Player? kickPlayer) { KickPlayer = kickPlayer; if (direct == Direct.Up) _movingUp = true; if (direct == Direct.Down) _movingDown = true; if (direct == Direct.Left) _movingLeft = true; if (direct == Direct.Right) _movingRight = true; // イベント発生 BlockStart?.Invoke(this); } } |
移動中の処理
ブロックが移動している場合、どこかで止めなければならないのですが、それをチェックするための処理を示します。
ブロックのXY座標がCHARACTER_SIZEで割り切れるときにColumnとRowの座標を更新します。そして他のプレイヤーに衝突していないか調べます。そのあとさらなる移動先が壁やその他のブロックでないか調べます。それ以上移動できない場合はそこで動きを止めます。
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 |
public class Block { void CheckStop(Direct direct) { if (X % Game.CHARACTER_SIZE == 0 && Y % Game.CHARACTER_SIZE == 0) { int col = X / Game.CHARACTER_SIZE; int row = Y / Game.CHARACTER_SIZE; Column = col; Row = row; CheckHit(); // 壁やその他のブロックに衝突したら移動終了 bool isStop = false; if (direct == Direct.Up && (Row == 0 || Game.Blocks.Any(wall => wall.Column == col && wall.Row == row - 1))) Stop(); else if (direct == Direct.Down && (Row == Game.RowMax - 1 || Game.Blocks.Any(wall => wall.Column == col && wall.Row == row + 1))) Stop(); else if (direct == Direct.Left && (Column == 0 || Game.Blocks.Any(wall => wall.Column == col - 1 && wall.Row == row))) Stop(); else if (direct == Direct.Right && (Column == Game.ColMax - 1 || Game.Blocks.Any(wall => wall.Column == col + 1 && wall.Row == row))) Stop(); } } } |
当たり判定
移動中のブロックが他のプレイヤーに衝突していないか調べる処理を示します。すでに衝突されたプレイヤーを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 |
public class Block { public delegate void HitPlayerHandle(Player player, Player hitedPlayer); public event HitPlayerHandle? HitPlayer; List<Player> _hitedPlayers = new List<Player>(); void CheckHit() { if (X % Game.CHARACTER_SIZE == 0 && Y % Game.CHARACTER_SIZE == 0) { Column = X / Game.CHARACTER_SIZE; Row = Y / Game.CHARACTER_SIZE; List<Player> players = Game.AllPlayers.Where(player => player.NextColumn == Column && player.NextRow == Row).ToList(); foreach (Player player in players) { // 二重カウントしないようにする if (player.IsCatched) continue; // 無敵状態であるならカウントしない if (player.InvincibleTime > 0) continue; _hitedPlayers.Add(player); if (KickPlayer != null) HitPlayer?.Invoke(KickPlayer, player); player.IsCatched = true; } } } } |
ブロックを止める
ブロックを止める処理を示します。フラグをクリアしてイベントを発生させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Block { public delegate void BlockStopedHandle(Block block, Player kickPlayer, int hitCount); public event BlockStopedHandle? BlockStoped; public void Stop() { _movingUp = false; _movingDown = false; _movingLeft = false; _movingRight = false; int hitCount = _hitedPlayers.Count; _hitedPlayers.Clear(); // 倒した敵の数をイベントで送信する if (KickPlayer != null) BlockStoped?.Invoke(this, KickPlayer, hitCount); } } |
IsMovingメソッドはブロックが移動中かどうかを返すだけです。
1 2 3 4 5 6 7 |
public class Block { public bool IsMoving() { return (_breaking || _movingUp || _movingDown || _movingLeft || _movingRight); } } |
ブロックを壊す
移動させることができないブロックは壊すことになります。Breakメソッドでは_breakingフラグをセットしているだけです。また移動中のブロックを壊すことはできません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class Block { public delegate void BlockBreakHandle(Block block); public event BlockStartHandle? BlockBreak; public void Break() { if (!IsMoving()) { _breaking = true; BlockBreak?.Invoke(this); } } } |
更新時の処理
ブロックの更新処理を示します。
_breakingフラグがセットされているのであれば崩壊を進行させます。TimeUntilDisappears(消滅までの時間)が0になったらGame.Blocksから取り除きます。
移動中である場合はその方向に移動させます。そのあとCheckStopメソッドで停止するのかどうか、他のプレイヤーとの当たり判定をおこないます。
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 |
public class Block { public void Update() { if (_breaking) { TimeUntilDisappears--; if (TimeUntilDisappears <= 0) Game.Blocks.Remove(this); } else { if (_movingUp) { Y -= 16; CheckStop(Direct.Up); } else if (_movingDown) { Y += 16; CheckStop(Direct.Down); } else if (_movingLeft) { X -= 16; CheckStop(Direct.Left); } else if (_movingRight) { X += 16; CheckStop(Direct.Right); } } } } |
Playerクラスの定義
1 2 3 4 5 6 |
namespace PengoGame { public class Player { } } |
以降は名前空間を省略して書きます。
1 2 3 |
public class Player { } |
コンストラクタとプロパティ
コンストラクタを示します。引数はAspNetCore.SignalRで接続したときに定まるconnectionIdです。コンストラクタのなかでプレイヤー名とスコアを初期化して残機数に最大値をセットします。
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 |
public class Player { public Player(string connectionId, int playerNumber) { ConnectionID = connectionId; Name = ""; Score = 0; Rest = Game.REST_MAX; Reset(playerNumber); } // 初期座標にセットする void Reset(int playerNumber) { if (playerNumber == 0) { X = 0; Y = 0; } else if (playerNumber == 1) { X = 32 * 14; Y = 0; } else if (playerNumber == 2) { X = 0; Y = 32 * 14; } else if (playerNumber == 3) { X = 32 * 14; Y = 32 * 14; } else if (playerNumber == 4) { X = 32 * 14; Y = 32 * 7; } else if (playerNumber == 5) { X = 32 * 7; Y = 32 * 14; } else if (playerNumber == 6) { X = 32 * 7; Y = 0; } else if (playerNumber == 7) { X = 0; Y = 32 * 7; } // フィールド変数、その他プロパティの初期化 _beforeDirect = Direct.None; _movingDown = false; _movingUp = false; _movingLeft = false; _movingRight = false; NextColumn = CurrentColumn = X / Game.CHARACTER_SIZE; NextRow = CurrentRow = Y / Game.CHARACTER_SIZE; PlayerNumber = playerNumber; IsCatched = false; // もし出現位置にブロックがある場合は取り除く Block? block = Game.Blocks.FirstOrDefault(b => b.Column == CurrentColumn && b.Row == CurrentRow); if (block != null) Game.Blocks.Remove(block); } } |
プロパティを示します。
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 |
public class Player { public string ConnectionID { private set; get; } // プレイヤー名はユーザーが指定した名前。NPCの場合は "NPC ○○"という名前にする string _name = ""; public string Name { set { _name = value; } get { if (ConnectionID != "") return _name; else return String.Format("NPC {0:000}", PlayerNumber + 1); } } // プレイヤーの番号(全部で8人なので 0~7 ) public int PlayerNumber { private set; get; } // スコア public int Score { set; get; } // 残機 public int Rest { set; get; } // 15×15のフィールド上の現在位置(縦列) public int CurrentColumn { private set; get; } // 15×15のフィールド上の移動先の位置(横列) public int CurrentRow { private set; get; } // 15×15のフィールド上の移動先の位置(縦列) public int NextColumn { private set; get; } // 15×15のフィールド上の移動先の位置(横列) public int NextRow { private set; get; } // X座標 public int X { set; get; } // Y座標 public int Y { set; get; } // 他のプレイヤーが飛ばしたブロックに捕まっているか? public bool IsCatched { 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; } } // 無敵状態の時間(0より大きい場合は無敵) public int InvincibleTime { private set; get; } } |
プレイヤーは移動できるか?
上下左右に移動できるかどうかを調べる処理を示します。
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 |
public class Player { bool CanMove(int col, int row, Direct direct) { if (direct == Direct.Up) { if (row <= 0) return false; return !Game.Blocks.Any(wall => wall.Column == col && wall.Row == row - 1); } else if (direct == Direct.Down) { if (row >= Game.RowMax - 1) return false; return !Game.Blocks.Any(wall => wall.Column == col && wall.Row == row + 1); } else if (direct == Direct.Left) { if (col <= 0) return false; return !Game.Blocks.Any(wall => wall.Column == col - 1 && wall.Row == row); } else if (direct == Direct.Right) { if (col >= Game.ColMax - 1) return false; return !Game.Blocks.Any(b => b.Column == col + 1 && b.Row == row); } else return 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 |
public class Player { Block? GetBlockOnMove(int col, int row, Direct direct) { if (direct == Direct.Up) { if (row <= 0) return null; return Game.Blocks.FirstOrDefault(wall => !wall.IsMoving() && wall.Column == col && wall.Row == row - 1); } else if (direct == Direct.Down) { if (row >= Game.RowMax - 1) return null; return Game.Blocks.FirstOrDefault(wall => !wall.IsMoving() && wall.Column == col && wall.Row == row + 1); } else if (direct == Direct.Left) { if (col <= 0) return null; return Game.Blocks.FirstOrDefault(wall => !wall.IsMoving() && wall.Column == col - 1 && wall.Row == row); } else if (direct == Direct.Right) { if (col >= Game.ColMax - 1) return null; return Game.Blocks.FirstOrDefault(wall => !wall.IsMoving() && wall.Column == col + 1 && wall.Row == row); } return null; } } |
プレイヤーがブロックを蹴ったとしてブロックを飛ばすことができるかどうかを返すメソッドを示します。
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 |
public class Player { bool CanKick(Block block, Direct direct) { if (direct == Direct.Up) { if (block.IsMoving() || block.Row <= 0) return false; return !Game.Blocks.Any(b => b.Column == block.Column && b.Row == block.Row - 1); } else if (direct == Direct.Down) { if (block.IsMoving() || block.Row >= Game.RowMax - 1) return false; return !Game.Blocks.Any(b => b.Column == block.Column && b.Row == block.Row + 1); } else if (direct == Direct.Left) { if (block.IsMoving() || block.Column <= 0) return false; return !Game.Blocks.Any(b => b.Column == block.Column - 1 && b.Row == block.Row); } else if (direct == Direct.Right) { if (block.IsMoving() || block.Column >= Game.ColMax - 1) return false; return !Game.Blocks.Any(b => b.Column == block.Column + 1 && b.Row == block.Row); } return false; } } |
ブロックを飛ばす or 壊す
ブロックがあるならブロックを飛ばす、または壊す処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Player { void KickBlock(int col, int row, Direct direct) { // 進行方向にブロックがあるか調べる。ないならなにもしない Block? block = GetBlockOnMove(col, row, direct); if (block != null) { // ブロックを飛ばすことができるならその方向に飛ばす。できないなら壊す。 if (CanKick(block, direct)) block.MoveStart(direct, this); else block.Break(); } } } |
プレイヤーを移動させる
プレイヤーを移動させる処理を示します。
キーが押されているかどうかを調べて移動できるのであれば移動させます。移動が開始されたら移動が完了するまで他の動作はできません。移動先にブロックがある場合は飛ばすことができるのであれば飛ばして、できない場合は破壊します。
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 |
public class Player { bool _movingUp = false; bool _movingDown = false; bool _movingLeft = false; bool _movingRight = false; public void UpdatePlayer() { InvincibleTime--; if (X % Game.CHARACTER_SIZE == 0 && Y % Game.CHARACTER_SIZE == 0) { int col = X / Game.CHARACTER_SIZE; int row = Y / Game.CHARACTER_SIZE; if (IsUpKeyDown) { if (CanMove(col, row, Direct.Up)) { NextRow = row - 1; _movingUp = true; } else { KickBlock(col, row, Direct.Up); return; } } if (IsDownKeyDown) { if (CanMove(col, row, Direct.Down)) { NextRow = row + 1; _movingDown = true; } else { KickBlock(col, row, Direct.Down); return; } } if (IsLeftKeyDown) { if (CanMove(col, row, Direct.Left)) { NextColumn = col - 1; _movingLeft = true; } else { KickBlock(col, row, Direct.Left); return; } } if (IsRightKeyDown) { if (CanMove(col, row, Direct.Right)) { NextColumn = col + 1; _movingRight = true; } else { KickBlock(col, row, Direct.Right); return; } } } if (_movingDown) Y += 4; if (_movingUp) Y -= 4; if (_movingLeft) X -= 4; if (_movingRight) X += 4; if (X % Game.CHARACTER_SIZE == 0 && Y % Game.CHARACTER_SIZE == 0) { _movingDown = false; _movingUp = false; _movingLeft = false; _movingRight = false; CurrentColumn = X / Game.CHARACTER_SIZE; CurrentRow = Y / Game.CHARACTER_SIZE; } } } |
プレイヤー死亡時の処理
プレイヤーが死亡時の処理を示します。
プレイヤー死亡時は2秒後に残機1を減らして2秒後に復活させます。ただしプレイヤー(NPCは対象外)の残機が0になった場合はゲームオーバーとします。
復活時は後述するResetメソッドによって、ゲームに参加したときの初期位置に戻されます。ゲームに参加したときの初期位置はPlayerNumberプロパティで決まります。また復活したところを狙い撃ちさせないために、復活してから約3秒間(1 / 24秒 × 24 × 3)は無敵状態とし、他のプレイヤーが飛ばしたブロックにあたってもミスにはならないようにします。
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 |
public class Player { public delegate void GameHandler(Player player); public event GameHandler? GameOverEvent; public void Dead() { Rest--; System.Timers.Timer timer = new System.Timers.Timer(); timer.Interval = 2000; timer.Elapsed += Timer_Elapsed; timer.Start(); void Timer_Elapsed(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(); Invincible(); } else GameOverEvent?.Invoke(this); } } } public void Reset() { Reset(PlayerNumber); } public void Invincible() { InvincibleTime = Game.INVINCIBLE_TIME; } } |
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 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 |
public class Player { public void UpdateNPC() { if (IsCatched) return; InvincibleTime--; if (X % Game.CHARACTER_SIZE == 0 && Y % Game.CHARACTER_SIZE == 0) { int col = X / Game.CHARACTER_SIZE; int row = Y / Game.CHARACTER_SIZE; List<Direct> vs1 = new List<Direct>(); Direct[] directs = { Direct.Up, Direct.Down, Direct.Left, Direct.Right, }; foreach(Direct direct0 in directs) { if (CanMove(col, row, direct0)) vs1.Add(direct0); } foreach (Direct direct0 in directs) { Block? block0 = GetBlockOnMove(col, row, direct0); if (block0 != null && CanKick(block0, direct0)) { CanKick(block0, direct0); vs1.Add(direct0); } } vs1 = vs1.Distinct().ToList(); if (vs1.Count == 0) { if (col > 0) vs1.Add(Direct.Left); if (row > 0) vs1.Add(Direct.Up); if (col < Game.ColMax - 1) vs1.Add(Direct.Right); if (row < Game.RowMax - 1) vs1.Add(Direct.Down); } Direct direct = Direct.None; if (vs1.Count > 0) { if (vs1.Count == 1) direct = vs1[0]; else { if (_beforeDirect == Direct.Up) vs1.Remove(Direct.Down); if (_beforeDirect == Direct.Down) vs1.Remove(Direct.Up); if (_beforeDirect == Direct.Left) vs1.Remove(Direct.Right); if (_beforeDirect == Direct.Right) vs1.Remove(Direct.Left); int r = _random.Next(vs1.Count); direct = vs1[r]; } } _movingDown = false; _movingUp = false; _movingLeft = false; _movingRight = false; Block? block = GetBlockOnMove(col, row, direct); if (block != null) { if (CanKick(block, direct)) block.MoveStart(direct, this); else block.Break(); return; } if (direct == Direct.Up) { NextRow = row - 1; _movingUp = true; } if (direct == Direct.Down) { NextRow = row + 1; _movingDown = true; } if (direct == Direct.Left) { NextColumn = col - 1; _movingLeft = true; } if (direct == Direct.Right) { NextColumn = col + 1; _movingRight = true; } _beforeDirect = direct; } if (_movingDown) Y += 4; if (_movingUp) Y -= 4; if (_movingLeft) X -= 4; if (_movingRight) X += 4; if (X % Game.CHARACTER_SIZE == 0 && Y % Game.CHARACTER_SIZE == 0) { _movingDown = false; _movingUp = false; _movingLeft = false; _movingRight = false; CurrentColumn = X / Game.CHARACTER_SIZE; CurrentRow = Y / Game.CHARACTER_SIZE; } } } |