ASP.NET Coreでボンバーマンのような対戦型ゲームをつくります。
仕様は以下のとおりです。
同時にプレイできるのは4人まで。
プレイヤーが4人に満たない場合はNPCを入れる。
NPCが存在するときにゲーム開始ボタンを押すとプレイに参加できる。
残機制。残機0になったらゲームオーバー。その場合はNPCに入れ替える。
スコアは自分で仕掛けた爆弾で他のプレイヤーを倒したら1000点。
自分以外のプレイヤーによって他のプレイヤーが倒されたときは10点。
スコアランキングは30位まで表示。
プレイに参加できない場合も観戦は可能。
これまでは同時にプレイできるのは7人までで定員オーバーのときは他のページにリダイレクトさせていましたが、今回は4人までと少ないので観戦はできるようにします。
Contents
準備として
ゲームはBomberGame名前空間を定義して、そこで作成します。
最初に爆弾やブロックの位置、状態を管理するためのクラスを定義します。
Positionクラスは壁、爆弾、爆発時の火花の位置を管理するためのものです。キャラクタのサイズは32とし、15×15のマスのどこかに存在します。そのためX座標ならColumn * Game.CHARACTER_SIZEという計算式になります。
| 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 | namespace BomberGame {     public class Position     {         public Position()         {         }         public Position(int col, int row)         {             Column = col;             Row = row;         }         public int Column         {             protected set;             get;         }         public int Row         {             protected set;             get;         }         public int X         {             get { return Column * Game.CHARACTER_SIZE; }         }         public int Y         {             get { return Row * Game.CHARACTER_SIZE; }         }     } } | 
壁の位置、座標を管理するためのWallクラス(Positionクラスを継承)を定義します。
| 1 2 3 4 5 6 7 8 9 10 11 | namespace BomberGame {     public class Wall : Position     {         public Wall(int col, int row)         {             Column = col;             Row = row;         }     } } | 
爆弾の位置、座標、状態を管理するためのBombクラス(Positionクラスを継承)を定義します。スコアを計算するときに誰が仕掛けた爆弾なのかがわかるようにPlayerプロパティを定義しています。
TimeToExplodeプロパティは爆発するまでの時間です。Updateメソッドが実行されるごとに減り0になると爆発しますが、近くにある爆弾の爆発で誘爆することもあります。その場合はExplodeメソッドを実行して爆発するまでの時間を短くします。
| 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 | namespace BomberGame {     public class Bomb : Position     {         public Bomb(int col, int row, Player player)         {             Column = col;             Row = row;             Player = player;             TimeToExplode = Game.TIME_UNTIL_BOMB_EXPLODE; // 定数64。Gameクラスは後述         }         public int TimeToExplode         {             private set;             get;         }         public Player Player         {             private set;             get;         }         // 誘爆         public void Explode()         {             if (TimeToExplode > 4)                 TimeToExplode = 4;         }         public void Update()         {             TimeToExplode--;             if (TimeToExplode <= 0)                 Game.ExplodeBomb(this);         }     } } | 
爆弾によって発生する火花の位置、座標、状態を管理するためのFireクラス(Positionクラスを継承)を定義します。火花が発生した場所のColumnとRowが一致したプレイヤーは死亡します。火花がどのプレイヤーによって発生したのかがわかるようにPlayerプロパティを定義しています。
Distanceプロパティは爆弾本体との距離です。0なら爆弾と同じ位置です。TimeToDisappearanceプロパティは火花が消滅するまでの時間です。
| 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 | namespace BomberGame {     public class Fire : Position     {         public Fire(int col, int row, int distance, Player player)         {             Column = col;             Row = row;             Distance = distance;             Player = player;             TimeToDisappearance = Game.TIME_UNTIL_EXPLOSION_DISAPPEARS; // 12         }         public Player Player         {             private set;             get;         }         public int Distance         {             private set;             get;         }         public int TimeToDisappearance         {             private set;             get;         }         public void Update()         {             TimeToDisappearance--;         }     } } | 
プレイヤーが爆弾によって撃破されたときに周囲に発生する火花の位置、座標、状態を管理するためのDeadFireクラスを定義します。この場合、火花は移動(等速直線運動)するので発生場所と初速をコンストラクタで設定します。Updateメソッドが呼び出されると初期位置と初速から現在位置を計算します。TimeToDisappearanceプロパティは消滅までの時間です。
| 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 | namespace BomberGame {     public class DeadFire     {         int _x = 0;         int _y = 0;         double _vx = 0;         double _vy = 0;         int _update = 0;         public DeadFire(int x, int y, double vx, double vy)         {             TimeToDisappearance = Game.TIME_UNTIL_EXPLOSION_DISAPPEARS; // 12             _x = x;             _y = y;             _vx = vx;             _vy = vy;         }         public int X         {             private set;             get;         }         public int Y         {             private set;             get;         }         public int TimeToDisappearance         {             private set;             get;         }         public void Update()         {             _update++;             X = (int)(_x + _vx * _update);             Y = (int)(_y + _vy * _update);             TimeToDisappearance--;         }     } } | 
Gameクラスの定義
次にゲームを管理するためのGameクラスを定義します。
| 1 2 3 4 5 6 | namespace BomberGame {     public class Game     {     } } | 
インデントが深くなるので以降は以下のように名前空間を省略します。
| 1 2 3 | public class Game { } | 
定数部分
まず定数部分を示します。
| 1 2 3 4 5 6 7 8 9 10 | public class Game {     public const int CHARACTER_SIZE = 32;     public const int RowMax = 15;     public const int ColMax = 15;     public const int BOMB_POWER = 4;     public const int TIME_UNTIL_EXPLOSION_DISAPPEARS = 12;     public const int TIME_UNTIL_BOMB_EXPLODE = 64; } | 
プロパティ
各プロパティを示します。
IndestructibleWallsは爆弾でも破壊することができない壁、Wallsは破壊できる壁、BrokenWallsは1回の更新処理によって破壊された壁、Bombsは設置されている爆弾、Firesは爆発で発生した火花、DeadFiresはプレイヤーが撃破されて発生した周囲へ広がっていく火花です。
| 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 Game {     static List<Wall> _indestructibleWalls = new List<Wall>();     public static List<Wall> IndestructibleWalls     {         get { return _indestructibleWalls; }     }     static List<Wall> _walls = new List<Wall>();     public static List<Wall> Walls     {         get { return _walls; }     }     static List<Wall> _brokenWalls = new List<Wall>();     public static List<Wall> BrokenWalls     {         get { return _brokenWalls; }     }     static List<Bomb> _bombs = new List<Bomb>();     public static List<Bomb> Bombs     {         get { return _bombs; }     }     static List<Fire> _fires = new List<Fire>();     public static List<Fire> Fires     {         get { return _fires.Where(fire => fire.TimeToDisappearance > 0).ToList(); }     }     static List<DeadFire> _deadFires = new List<DeadFire>();     public static List<DeadFire> DeadFires     {         get{ return _deadFires.Where(fire => fire.TimeToDisappearance > 0).ToList(); }     } } | 
マップの初期化
マップを初期化する処理を示します。初期化がおこなわれるのは誰も接続されていない状態から最初のユーザーが訪問してきたときです。
mapTextの1の部分が爆弾でも破壊されない壁、0の部分がそれ意外の部分です。最初に4体のプレイヤーは四隅に登場するのですが、そのとき上下に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 | public class Game {     static string[] mapText = {         "111111111111111",         "100000000000001",         "101010101010101",         "100000000000001",         "101010101010101",         "100000000000001",         "101010101010101",         "100000000000001",         "101010101010101",         "100000000000001",         "101010101010101",         "100000000000001",         "101010101010101",         "100000000000001",         "111111111111111",     };     public static string[] MapText     {         get { return mapText; }     }     public static void Init()     {         // 破壊できない壁を追加する         IndestructibleWalls.Clear();         string[] vs1 = mapText;         for (int row = 0; row < RowMax; row++)         {             string str = vs1[row];             char[] vs2 = str.ToArray();             for (int col = 0; col < ColMax; col++)             {                 if (vs2[col] == '1')                 {                     IndestructibleWalls.Add(new Wall(col, row));                 }             }         }         // 破壊できる壁を追加する         // 初期状態のプレイヤーが上下に2つ分だけ移動できるようにして、それ以外の部分は破壊できる壁ですべて埋める         Walls.Clear();         for (int row = 0; row < RowMax; row++)         {             string str = vs1[row];             char[] vs2 = str.ToArray();             for (int col = 0; col < ColMax; col++)             {                 if (col >= 1 && col <= 3 && row == 1)                     continue;                 if (col >= 11 && col <= 13 && row == 1)                     continue;                 if (col >= 1 && col <= 3 && row == 13)                     continue;                 if (col >= 11 && col <= 13 && row == 13)                     continue;                 if (col == 1 && row >= 1 && row <= 3)                     continue;                 if (col == 1 && row >= 11 && row <= 13)                     continue;                 if (col == 13 && row >= 1 && row <= 3)                     continue;                 if (col == 13 && row >= 11 && row <= 13)                     continue;                 if (vs2[col] == '0')                     Walls.Add(new Wall(col, row));             }         }         // もし爆弾、火花などがあれば消去する         Bombs.Clear();         Fires.Clear();         BrokenWalls.Clear();     } } | 
爆弾のセットと除去
爆弾をセットしたり除去するための処理を示します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class Game {     public static void SetBomb(Bomb bomb)     {         if (!Bombs.Any(b => b.Column == bomb.Column && b.Row == bomb.Row))             Bombs.Add(bomb);     }     public static void CrearBombs()     {         Bombs.Clear();         Fires.Clear();         DeadFires.Clear();     } } | 
爆発時の処理
爆発に関する処理を示します。まず爆弾が爆発することで発生する火花の位置を取得する処理を示します。
| 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 Game {     public static List<Fire> GetFirePositions(Bomb bomb)     {         List<Fire> fires = new List<Fire>();         fires.Add(new Fire(bomb.Column, bomb.Row, 0, bomb.Player));         for (int i = 0; i < 4; i++)         {             for (int k = 0; k < BOMB_POWER; k++)             {                 int dx = 0;                 int dy = 0;                 if (i == 0)                     dx = (1 + k);                 if (i == 1)                     dx = -(1 + k);                 if (i == 2)                     dy = (1 + k);                 if (i == 3)                     dy = -(1 + k);                 if (bomb.Column + dx < 0 || bomb.Column + dx >= ColMax)                     break;                 if (bomb.Row + dy < 0 || bomb.Row + dy >= RowMax)                     break;                 if (IndestructibleWalls.FirstOrDefault(wall => wall.Column == bomb.Column + dx && wall.Row == bomb.Row + dy) != null)                     break;                 fires.Add(new Fire(bomb.Column + dx, bomb.Row + dy, k + 1, bomb.Player));                 Wall? wall = Walls.FirstOrDefault(wall => wall.Column == bomb.Column + dx && wall.Row == bomb.Row + dy);                 if (wall != null)                     break;             }         }         return fires;     } } | 
爆弾が爆発したときの処理を示します。
まず該当する爆弾をBombsから取り除きます。そして上記 GetFirePositionsメソッドで取得したFireオブジェクトを_firesリストのなかに格納します。
このとき壁が破壊されるかもしれないのでそのチェックもしています。破壊された壁は一時的にBrokenWallsリストのなかに格納しておき、更新処理のときにクライアントサイドに送信できるようにしています。また爆発音を鳴らしたいのでイベントで通知できるようにもしています。
| 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 | public class Game {     public delegate void GameEventHandler();     public static event GameEventHandler? BombExploded;     public static void ExplodeBomb(Bomb bomb)     {         Bombs.Remove(bomb);         BombExploded?.Invoke();         List<Fire> fires = GetFirePositions(bomb);         foreach (Fire fire in fires)         {             _fires.Add(fire);             Wall? wall = Walls.FirstOrDefault(wall => wall.Column == fire.Column && wall.Row == fire.Row);             if (wall != null)             {                 Walls.Remove(wall);                 BrokenWalls.Add(wall);             }             List<Bomb> bombs = Bombs.Where(b => b.Column == fire.Column && b.Row == fire.Row).ToList();             foreach (Bomb b in bombs)                 b.Explode();         }     }     // 一時的に保存しておいたBrokenWallsをクリアする     public static void ClearBrokenWalls()     {         BrokenWalls.Clear();     } } | 
プレイヤーが撃破されたときの処理
プレイヤーが撃破されたときに四方八方に飛び散る火花を生成する処理を示します。
引数は撃破されたプレイヤーがいた座標です。ここを中心に乱数で初速を設定して24個のDeadFireを生成して周囲に火花が飛ぶようにしています。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class Game {     static Random _random = new Random();     public static void SetDeadFire(int x, int y)     {         for (int i = 0; i < 24; i++)         {             int r = _random.Next(100);             double rad = Math.PI * 2 * r / 100;             double v = 8 + _random.Next(8);             _deadFires.Add(new DeadFire(x, y, v * Math.Cos(rad), v * Math.Sin(rad)));         }     } } | 
移動できない位置と危険な位置を取得する
CreateDangerMapメソッドはNPCの動作を決定するときに必要になります。二次元配列を生成して移動できない場所に-1をセットします。移動できない場所とは壁がある位置、爆弾がセットされている位置、火花が存在する位置です。また爆弾が爆発すると火花が飛んでくる位置には爆弾との距離をセットしています。それ以外の移動可能で安全な位置にはint.MaxValueをセットしています。
| 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 | public class Game {     public static int[,] CreateDangerMap()     {         int[,] dangerMap = new int[RowMax, ColMax];         for (int r = 0; r < RowMax; r++)         {             for (int c = 0; c < ColMax; c++)                 dangerMap[r, c] = int.MaxValue;         }         foreach (Wall wall in Game.IndestructibleWalls)             dangerMap[wall.Row, wall.Column] = -1;         foreach (Wall wall in Game.Walls)             dangerMap[wall.Row, wall.Column] = -1;         List<Fire> fires = new List<Fire>();         foreach (Bomb bomb in Bombs)             fires.AddRange(GetFirePositions(bomb));         foreach (Fire fire in fires)         {             if (dangerMap[fire.Row, fire.Column] > fire.Distance)                 dangerMap[fire.Row, fire.Column] = fire.Distance;         }         foreach (Bomb bomb in Game.Bombs)             dangerMap[bomb.Row, bomb.Column] = -1;         foreach (Fire fire in Game.Fires)             dangerMap[fire.Row, fire.Column] = -1;         return dangerMap;     } } | 
