ASP.NET coreで対戦型『スペースウォー!』(Spacewar!)をつくる(1)の続きです。
今回はゲーム全体を管理するGameクラスを定義します。ゲームは対戦者以外の人も観戦できるようにします。
| 1 2 3 4 5 6 7 8 | using System.Drawing; namespace SpacewarApp {     public class Game     {     } } | 
以降は名前空間部分を省略して表記します。
| 1 2 3 | public class Game { } | 
Contents
フィールド変数とプロパティ
フィールド変数とプロパティを示します。
| 1 2 3 4 5 6 7 8 9 10 11 | public class Game {     public Player? Player0 { set; get; } // 2人のプレーヤー     public Player? Player1 { set; get; }     public List<Fireball> Fireballs { set; get; } // 爆発で発生した火球のリスト     public int GameNumber { set; get; } // Gameオブジェクトの通し番号     // ASP.NET SignalRで使われる接続の一意のIDのリスト(観戦者)     public List<string> WatcherIds = new List<string>(); } | 
コンストラクタ
コンストラクタを示します。
| 1 2 3 4 5 6 7 8 9 10 | public class Game {     public Game(Player player0, Player player1, int gameNumber)     {         Player0 = player0;         Player1 = player1;         GameNumber = gameNumber;         Fireballs = new List<Fireball>();     } } | 
更新処理
更新時におこなわれる処理を示します。
2つのプレーヤーに太陽の重力を作用させ移動速度を変更させます。そのあとPlayer.Updateメソッドを呼び出してプレーヤーの座標を移動させます。そのあとCheckメソッドで当たり判定をおこないます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class Game {     public void Update()     {         PullToSun(Player0); // 太陽の重力を作用させる(後述)         PullToSun(Player1);         Player0?.Update();         Player1?.Update();         Check(); // 当たり判定(後述)         // 火球オブジェクトを更新         // 死亡フラグがセットされたものをリストから取り除く         foreach (Fireball fireball in Fireballs)             fireball?.Update();         Fireballs = Fireballs.Where(fireball => !fireball.IsDead).ToList();     } } | 
太陽の重力を作用させる処理を示します。重力は距離の2乗に反比例し、中心にむかって作用します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class Game {     void PullToSun(Player? player)     {         if (player == null)             return;         // 重力の大きさ         double distance = Math.Sqrt(Math.Pow(player.CenterX - Const.FIELD_WIDTH / 2, 2) + Math.Pow(player.CenterY - Const.FIELD_HEIGHT / 2, 2));         double gravity = 80 / Math.Pow(distance, 2);         // 方向(当然中心へ)         double angle = Math.Atan2(Const.FIELD_HEIGHT / 2 - player.CenterY, Const.FIELD_WIDTH / 2 - player.CenterX);         player.VX += gravity * Math.Cos(angle);         player.VY += gravity * Math.Sin(angle);     } } | 
当たり判定
当たり判定の処理を示します。
片方がすでに死亡している場合は勝負がついているので当たり判定はしません。当たり判定は太陽に衝突したか?他のプレーヤーが発射した弾丸に接触したか?プレーヤー同士が衝突したか?を判定します。そのあと両プレーヤーの死亡フラグを調べてどちらが勝ったのかを判定します。
| 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 | public class Game {     void Check()     {         // 片方がすでに死亡している場合は勝負がついているので当たり判定はしない         if (Player0 == null || Player1 == null)             return;         if (Player0.IsDead || Player1.IsDead)             return;         // 太陽に衝突したか?         if (IsPlayerCrashSun(Player0))         {             Player0.IsDead = true;             Explode(Player0.CenterX, Player0.CenterY, 1);         }         if (IsPlayerCrashSun(Player1))         {             Player1.IsDead = true;             Explode(Player1.CenterX, Player1.CenterY, 0);         }         // 弾丸は命中したか?         if (IsBulletHit(Player0.Bullets, Player1))         {             Player1.IsDead = true;             Explode(Player1.CenterX, Player1.CenterY, 0);         }         if (IsBulletHit(Player1.Bullets, Player0))         {             Player0.IsDead = true;             Explode(Player0.CenterX, Player0.CenterY, 1);         }         // 自機と敵が衝突したか?         if (IsCrashPlayers())         {             Player0.IsDead = true;             Explode(Player0.CenterX, Player0.CenterY, 1);             Player1.IsDead = true;             Explode(Player1.CenterX, Player1.CenterY, 0);         }         if (Player0.IsDead && Player1.IsDead) // 共倒れ             OnSettled(-1); // 終戦処理(後述)         else if (Player1.IsDead) // Player0が勝ち             OnSettled(0);         else if (Player0.IsDead) // Player1が勝ち             OnSettled(1);     } } | 
太陽に衝突したか?
プレーヤーが太陽に衝突したかを判定する処理を示します。
Player.OuterPointsをAngleだけ回転させてCenterX、CenterYだけ平行移動させるとプレーヤーの外周部分の座標を取得することができます。これら座標と中心座標と太陽の座標(中心部分)で一番距離が小さい部分と太陽の半径を比較します(実際には平方根を取る処理を省略して2乗を比較している)。太陽の半径よりも太陽との距離が小さい場合は太陽に衝突していることになります。
| 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 Game {     bool IsPlayerCrashSun(Player player)     {         if (player.IsDead)             return false;         List<PointF> pts = new List<PointF>();         foreach (PointF pt in player.OuterPoints)         {             double x = pt.X * Math.Cos(player.Angle) - pt.Y * Math.Sin(player.Angle) + player.CenterX;             double y = pt.X * Math.Sin(player.Angle) + pt.Y * Math.Cos(player.Angle) + player.CenterY;             pts.Add(new PointF((float)x, (float)y));         }         pts.Add(new PointF((float)player.CenterX, (float)player.CenterY));         double min = pts.Min(pt => Math.Pow(pt.X - Const.FIELD_WIDTH / 2, 2) + Math.Pow(pt.Y - Const.FIELD_HEIGHT / 2, 2));         if(min < Math.Pow(Const.SUN_RADIUS, 2))             return true;         else             return false;     } } | 
弾丸は命中したか?
ある点が複数の点で構成される図形の内部にあるかどうかを調べるメソッドを示します。これはC# JavaScript 不規則な形をした図形との当たり判定のものとほとんど同じです。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class Game {     bool PointInArea(List<PointF> vertices, PointF point)     {         int count = 0;         for (int i = 0; i < vertices.Count - 1; i++)         {             if (((vertices[i].Y <= point.Y) && (vertices[i + 1].Y > point.Y))                 || ((vertices[i].Y > point.Y) && (vertices[i + 1].Y <= point.Y)))             {                 float vt = (point.Y - vertices[i].Y) / (vertices[i + 1].Y - vertices[i].Y);                 if (point.X < (vertices[i].X + (vt * (vertices[i + 1].X - vertices[i].X))))                     count++;             }         }         return count % 2 == 1;     } } | 
弾丸がプレーヤーに命中したかを調べる処理を示します。
Player.OuterPointsをAngleだけ回転させてCenterX、CenterYだけ平行移動させてプレーヤーの外周部分の座標を取得します。これらの点で構成される図形の内部に弾丸の先端部があれば弾丸は命中していることになります。
| 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 Game {     bool IsBulletHit(List<Bullet> bullets, Player player)     {         if (player.IsDead)             return false;         List<PointF> pts = new List<PointF>();         foreach (PointF pt in player.OuterPoints)         {             double x = pt.X * Math.Cos(player.Angle) - pt.Y * Math.Sin(player.Angle) + player.CenterX;             double y = pt.X * Math.Sin(player.Angle) + pt.Y * Math.Cos(player.Angle) + player.CenterY;             pts.Add(new PointF((float)x, (float)y));         }         foreach (Bullet bullet in bullets)         {             if(PointInArea(pts, new PointF((float)bullet.HeadX, (float)bullet.HeadY)))                 return true;         }         return false;     } } | 
プレーヤー同士が衝突したか?
プレーヤー同士が衝突したかを判定する処理を示します。
Player0.InnerPoints(OuterPointsよりも内部より)を回転させて平行移動させた点の座標を取得し、これらの点で構成される図形の内部にPlayer1の内部にある点が存在するかを調べます。存在する場合は両者は衝突しています。
| 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 Game {     bool IsCrashPlayers()     {         if (Player0 == null || Player1 == null || Player0.IsDead || Player1.IsDead)             return false;         List<PointF> pts0 = new List<PointF>();         foreach (PointF pt in Player0.InnerPoints)         {             double x = pt.X * Math.Cos(Player0.Angle) - pt.Y * Math.Sin(Player0.Angle) + Player0.CenterX;             double y = pt.X * Math.Sin(Player0.Angle) + pt.Y * Math.Cos(Player0.Angle) + Player0.CenterY;             pts0.Add(new PointF((float)x, (float)y));         }         List<PointF> pts1 = new List<PointF>();         foreach (PointF pt in Player1.InnerPoints)         {             double x = pt.X * Math.Cos(Player1.Angle) - pt.Y * Math.Sin(Player1.Angle) + Player1.CenterX;             double y = pt.X * Math.Sin(Player1.Angle) + pt.Y * Math.Cos(Player1.Angle) + Player1.CenterY;             pts1.Add(new PointF((float)x, (float)y));         }         foreach (PointF pt in pts1)         {             if (PointInArea(pts0, pt))                 return true;         }         return false;     } } | 
爆発発生の処理
爆発を発生させる処理を示します。
乱数を生成して爆発の中心点の周囲に時間差をつけて火球を生成します。爆発発生時には効果音も鳴らしたいのでExplodedイベントを定義します。
| 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 | public class Game {     public event EventHandler? Exploded;     Random _random = new Random();     async void Explode(double x, double y, int type)     {         double Random()         {             return 1d * _random.Next(1000) / 1000;         }         Exploded?.Invoke(this, new EventArgs());         const int fireballDelay = 10; // 次の火球が出現するまでの時間(ミリ秒)         const int fireballsCount = 8; // ひとつの爆発で出現する火球の数         const int explosionRadius = 16; // 爆発の中心点と火球が出現する位置の距離の最大値         for (int i = 0; i < fireballsCount; i++)         {             double dx = Random() * explosionRadius * 2 - explosionRadius;             double dy = Random() * explosionRadius * 2 - explosionRadius;             Fireballs.Add(new Fireball(x + dx, y + dy, type));             await Task.Delay(fireballDelay);         }     } } | 
終戦処理
ひとつの戦闘が終了したあとにおこなわれる処理を示します。
OnSettledメソッドの引数は勝者の番号です。-1のときはプレーヤー同士が衝突したり相打ちで引き分けの場合です。勝者がいる場合はそのプレーヤーのスコアを1増やします。MAX_SCOREに達した場合はゲーム終了です。GameFinishedイベントを発生させて勝者と敗者を確定させてゲームを終了させます。そうでない場合は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 | public class Game {     public event GameFinishedHandler? GameFinished;     public delegate void GameFinishedHandler(Game game, GameFinishedArgs args);     async void OnSettled(int winner)     {         if (winner == 0 && Player0 != null)             Player0.Score++;         if (winner == 1 && Player1 != null)             Player1.Score++;         await Task.Delay(2000);         if (Player0?.Score < Const.MAX_SCORE && Player1?.Score < Const.MAX_SCORE)         {             Player0?.Init();             Player1?.Init();         }         if (Player0?.Score >= Const.MAX_SCORE && Player1 != null)             GameFinished?.Invoke(this, new GameFinishedArgs(this, Player0, Player1));         if (Player1?.Score >= Const.MAX_SCORE && Player0 != null)             GameFinished?.Invoke(this, new GameFinishedArgs(this, Player1, Player0));     } } | 
イベントハンドラの引数に使われるGameFinishedArgsクラスを示します。勝者と敗者、Gameオブジェクトをプロパティとしています。
| 1 2 3 4 5 6 7 8 9 10 11 12 | public class GameFinishedArgs : EventArgs {     public Player Winner { get; }     public Player Loser { get; }     public Game Game { get; }     public GameFinishedArgs(Game game, Player winner, Player loser)     {         Game = game;         Winner = winner;         Loser = loser;     } } | 
