ディフェンダーに似たオンライン対戦ゲームをつくる(1)の続きです。
今回はゲーム全体の処理をおこなうGameクラスを定義します。ここでやるのは各キャラクターの移動と更新、クライアントサイドで描画処理をするために必要なデータの取得などです。
1 2 3 4 5 6 |
namespace Defender { public class Game { } } |
以降は名前空間部分は省略します。
1 2 3 |
public class Game { } |
コンストラクタ
コンストラクタを示します。最初に必要な数のPlayer、Enemy、Sparkオブジェクトを生成しておき、以降はこれらを使い回します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class Game { Random _random = new Random(); List<Player> _players = new List<Player>(); List<Enemy> _enemies = new List<Enemy>(); List<Spark> _sparks = new List<Spark>(); public Game() { for (int i = 0; i < Const.PLAYER_MAX; i++) _players.Add(new Player()); for (int i=0; i< Const.ENEMY_MAX; i++) _enemies.Add(new Enemy()); for (int i = 0; i < Const.SPARK_COUNT; i++) _sparks.Add(new Spark()); } } |
プレイヤーの追加と削除
InitPlayerメソッドはユーザーがプレイを開始したときに呼び出され、UninitPlayerメソッドはユーザーがページからの離脱したときに呼び出されます。「追加と削除」という表現をしていますが、リストに追加または削除されるわけではなく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 |
public class Game { public bool InitPlayer(string name, string connectionId) { Player? player = _players.FirstOrDefault(player => player.IsGameOvered); if (player != null) { player.Init(name, connectionId); return true; } else return false; } public bool UninitPlayer(string connectionId) { Player? player = _players.FirstOrDefault(player => player.ConnectionId == connectionId); if (player != null) { player.IsGameOvered = true; player.ConnectionId = ""; return true; } else return false; } } |
GetPlayerCountメソッドは現在プレイしているユーザーの数を返します。
1 2 3 4 5 6 7 |
public class Game { public int GetPlayerCount() { return _players.Count(player => !player.IsGameOvered); } } |
自機の操作
ユーザーが自機を操作(移動、加速、弾丸の発射)するための処理を示します。
PlayerSetVelocityXメソッドは自機を左右に移動させようとしたときに呼び出されます。”left”や”right”であれば左右に移動するための速度を与え、”none”なら停止させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Game { public void PlayerSetVelocityX(string connectionId, string direct) { Player? player = _players.FirstOrDefault(player => player.ConnectionId == connectionId); if (player == null || player.IsDead) return; if(direct == "left") player.SetVelocityX(-Const.PLAYER_SPEED); if (direct == "right") player.SetVelocityX(Const.PLAYER_SPEED); if (direct == "none") player.SetVelocityX(0); } } |
PlayerAddVelocityYは自機を加速、減速させようとしたときに呼び出されます。”up”なら上方向へ加速、”down”なら下方向へ加速です。速度の絶対値が増加する場合はtrueを返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class Game { public bool PlayerAddVelocityY(string connectionId, string direct) { Player? player = _players.FirstOrDefault(player => player.ConnectionId == connectionId); if (player == null || player.IsDead) return false; if (direct == "up" && Math.Abs(player.VY - Const.PLAYER_SPEED) < 15) return player.AddVelocityY(-Const.PLAYER_SPEED); if (direct == "down" && Math.Abs(player.VY + Const.PLAYER_SPEED) < 15) return player.AddVelocityY(Const.PLAYER_SPEED); return false; } } |
PlayerShotメソッドは弾丸を発射しようとしたときに呼び出されます。発射処理が行なわれたときはtrueを返します。
1 2 3 4 5 6 7 8 9 10 11 |
public class Game { public bool PlayerShot(string connectionId) { Player? player = _players.FirstOrDefault(player => player.ConnectionId == connectionId); if (player == null) return false; return player.Shot(); } } |
イベントの送信
敵機(他のプレイヤーを含む)を撃ち落とした場合や自機が死亡した場合はイベントを送信します。
イベントとイベントハンドラに渡す引数のクラスを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class Game { public event EventHandler? HitEnemy; // 敵を撃ち落とした public event EventHandler? PlayerDead; // 自機が死亡した public event EventHandler? PlayerGameOvered; // ゲームオーバーになった // どのプレイヤーによって発生したイベントなのかがわかるようにする public class MyEventArgs : EventArgs { public MyEventArgs(string connectionId) { ConnectionId = connectionId; } public string ConnectionId { get; } } } |
敵を撃ち落としたときは100点を加算します。自機が死亡したときは残機1を減らして3秒後に自機を初期位置に戻して復活させます。残機が0の場合は復活することなくゲームオーバーイベントを送信します。
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 { void OnHitEnemy(Player player) { player.Score += 100; HitEnemy?.Invoke(this, new MyEventArgs(player.ConnectionId)); } void OnPlayerDead(Player player) { player.Life--; PlayerDead?.Invoke(this, new MyEventArgs(player.ConnectionId)); Task.Run(async () => { await Task.Delay(3000); if (player.Life > 0) player.Init(); else { // 残機0ならゲームオーバー(二重に送信しないように注意) if (!player.IsGameOvered) { PlayerGameOvered?.Invoke(this, new MyEventArgs(player.ConnectionId)); player.IsGameOvered = true; player.ConnectionId = ""; } } }); } } |
当たり判定
当たり判定の処理をおこないます。当たり判定はプレイヤーが発射した弾丸が敵に命中したか? 敵が発射した弾丸がプレイヤーに命中したか? プレイヤーと敵、プレイヤー同士が衝突したか?があります。
最初に爆発による火花を発生させる処理をおこなうExplodeメソッドを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class Game { void Explode(double x, double y) { for (int i = 0; i < Const.SPARK_COUNT_PER_EXPLODE; i++) { // 乱数で火花がとぶ方向と速度を決定する double rad = Math.PI * 2 * _random.Next(1000) * 0.001; double r = 2 + _random.Next(1000) * 0.002; // 使用されていないオブジェクトを探して初期座標と初速を設定する Spark? spark = _sparks.FirstOrDefault(spark => spark.IsDead); if (spark == null) break; spark.Init(x, y, r * Math.Cos(rad), r * Math.Sin(rad)); } } } |
CheckBulletsHit1メソッドはプレイヤーが発射した弾丸が敵や他のプレイヤーに命中したかを調べます。キャラクターの中心座標を求めて、両者の距離の二乗とConst.CHARCTER_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 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 { void CheckBulletsHit1() { foreach (Player player in _players) { // 死亡したプレイヤーの弾丸は考慮しない if (player.IsDead) continue; foreach (Bullet bullet in player.Bullets) { // 死亡フラグが立っている弾丸は判定の対象外 if (bullet.IsDead) continue; // 中心座標を求める double x1 = bullet.X + Const.BULLET_SIZE / 2; double y1 = bullet.Y + Const.BULLET_SIZE / 2; // 敵に命中したか? foreach (Enemy enemy in _enemies) { // 死亡フラグが立っている敵は判定の対象外 if (enemy.IsDead) continue; // 中心座標を求める double x2 = enemy.X + Const.CHARCTER_SIZE / 2; double y2 = enemy.Y + Const.CHARCTER_SIZE / 2; // 中心同士の距離と比較する if (Math.Pow(x1 - x2, 2) + Math.Pow(y1 - y2, 2) < Math.Pow(Const.CHARCTER_SIZE / 2, 2)) { // 命中している場合は死亡フラグをセット bullet.IsDead = true; enemy.IsDead = true; // 爆発とイベントの送信 OnHitEnemy(player); Explode(x1, y1); // 敵は2秒後に復活させる Task.Run(async () => { await Task.Delay(2000); enemy.Init(); }); break; } } if (bullet.IsDead) continue; // 他のプレイヤーに命中したか? foreach (Player enemy in _players) { // 自分自身に命中した場合や他のプレイヤーが死亡または無敵状態のときは判定の対象外 if (enemy == player || enemy.IsDead || enemy.IsInvincible) continue; double x2 = enemy.X + Const.CHARCTER_SIZE / 2; double y2 = enemy.Y + Const.CHARCTER_SIZE / 2; if (Math.Pow(x1 - x2, 2) + Math.Pow(y1 - y2, 2) < Math.Pow(Const.CHARCTER_SIZE / 2, 2)) { // 死亡フラグをセットする bullet.IsDead = true; enemy.IsDead = true; // 撃墜した側に加点、された側に自機死亡のイベントを送信 OnHitEnemy(player); OnPlayerDead(enemy); // 爆発の発生 Explode(x1, y1); break; } } } } } } |
CheckBulletsHit2メソッドは敵が発射した弾丸がプレイヤーに命中したかを調べます。キャラクターの中心座標を求めて、両者の距離の二乗とConst.CHARCTER_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 |
public class Game { void CheckBulletsHit2() { foreach (Enemy enemy in _enemies) { foreach (Bullet bullet in enemy.Bullets) { if (bullet.IsDead) continue; double x1 = bullet.X + Const.BULLET_SIZE / 2; double y1 = bullet.Y + Const.BULLET_SIZE / 2; foreach (Player player in _players) { if (player.IsDead || player.IsInvincible) continue; double x2 = player.X + Const.CHARCTER_SIZE / 2; double y2 = player.Y + Const.CHARCTER_SIZE / 2; if (Math.Pow(x1 - x2, 2) + Math.Pow(y1 - y2, 2) < Math.Pow(Const.CHARCTER_SIZE / 2, 2)) { bullet.IsDead = true; player.IsDead = true; OnPlayerDead(player); Explode(x1, y1); break; } } } } } } |
CheckCrushメソッドはプレイヤーと敵、プレイヤー同士が衝突しているかどうかを判定します。
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 |
public class Game { void CheckCrush() { foreach (Player player in _players) { // プレイヤーが死亡または無敵状態のときは判定の対象外 if (player.IsInvincible || player.IsDead) continue; double x1 = player.X + Const.CHARCTER_SIZE / 2; double y1 = player.Y + Const.CHARCTER_SIZE / 2; foreach (Enemy enemy in _enemies) { // 敵が死亡または生まれたばかりのときは判定の対象外 if (enemy.IsDead || enemy.IsJustBorn) continue; double x2 = enemy.X + Const.CHARCTER_SIZE / 2; double y2 = enemy.Y + Const.CHARCTER_SIZE / 2; if (Math.Pow(x1 - x2, 2) + Math.Pow(y1 - y2, 2) < Math.Pow(Const.CHARCTER_SIZE, 2)) { player.IsDead = true; enemy.IsDead = true; OnPlayerDead(player); Explode(x1, y1); Task.Run(async () => { await Task.Delay(2000); enemy.Init(); }); break; } } if (player.IsDead) continue; foreach (Player enemy in _players) { // プレイヤーが自分自身または死亡フラグがセットされているときは判定の対象外 if (enemy == player || enemy.IsDead) continue; double x2 = enemy.X + Const.CHARCTER_SIZE / 2; double y2 = enemy.Y + Const.CHARCTER_SIZE / 2; if (Math.Pow(x1 - x2, 2) + Math.Pow(y1 - y2, 2) < Math.Pow(Const.CHARCTER_SIZE, 2)) { player.IsDead = true; enemy.IsDead = true; OnPlayerDead(player); OnPlayerDead(enemy); Explode(x1, y1); Task.Run(async () => { await Task.Delay(2000); enemy.Init(); }); break; } } } } } |
更新処理
プレイヤーと敵、火花をそれぞれ更新します。そのあと当たり判定をおこないます。非同期処理でforeach文を回すと例外が発生する場合があるので例外処理をおこなっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class Game { public void TryUpdate() { try { foreach (Player player in _players) player.Update(); foreach (Enemy enemy in _enemies) enemy.Update(); foreach (Spark spark in _sparks) spark.Update(); CheckBulletsHit1(); CheckBulletsHit2(); CheckCrush(); } catch { } } } |
文字列の送信
描画処理をおこなうためにクライアントサイドに文字列を送信するのですが、その文字列を取得するための処理を示します。
各キャラクターの座標や状態をカンマ区切りの文字列に変換しています。
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 |
public class Game { public string TryGetSendData() { try { return GetSendData(); } catch { return ""; } } string GetSendData() { System.Text.StringBuilder sb = new System.Text.StringBuilder(); // プレイヤーの座標 foreach (Player player in _players) { if (!player.IsGameOvered) { string direct = player.VY > 0 ? "Down" : "Up"; string isShow = player.IsShow ? "true" : "false"; string isDead = player.IsDead ? "true" : "false"; sb.Append($"{player.Name},{player.Score},{player.ConnectionId},{player.X},{player.Y},{direct},{player.Life},{isDead},{isShow}\t"); } } sb.Append("\n"); // プレイヤーの弾丸の座標 foreach (Player player in _players) { foreach (Bullet bullet in player.Bullets) { if (!bullet.IsDead) sb.Append($"{player.ConnectionId},{bullet.X},{bullet.Y},{bullet.UpdateCount}\t"); } } sb.Append("\n"); // 敵の座標 foreach (Enemy enemy in _enemies) { string isJustBorn = enemy.IsJustBorn ? "true" : "false"; if (enemy.IsShow) sb.Append($"{enemy.X},{enemy.Y},{enemy.Type},{isJustBorn}\t"); } sb.Append("\n"); // 敵の弾丸の座標 foreach (Enemy enemy in _enemies) { foreach (Bullet bullet in enemy.Bullets) { if (!bullet.IsDead) sb.Append($"{bullet.X},{bullet.Y},{bullet.UpdateCount}\t"); } } sb.Append("\n"); // 火花の座標 foreach (Spark spark in _sparks) { if (!spark.IsDead) sb.Append($"{spark.X},{spark.Y},{spark.Type}\t"); } return sb.ToString(); } } |