ボスコニアンのようなオンライン対戦ゲームをつくる(2)の続きです。今回はゲーム全体を管理するGameクラスを定義します。
Contents
コンストラクタとプロパティ
Gameクラスの定義を定義します。
1 2 3 4 5 6 |
namespace Bosconian { public class Game { } } |
以降は名前空間の部分は省略して書きます。
1 2 3 |
public class Game { } |
コンストラクタと各プロパティを示します。
Player、Enemy、Fortress、Sparkオブジェクトを生成してリストに格納します。そして以降はこのオブジェクトを使い回します。
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 Game { List<Player> _players = new List<Player>(); List<Spark> _sparks = new List<Spark>(); Random _random = new Random(); public Game() { for (int i = 0; i < Const.PLAYER_MAX; i++) _players.Add(new Player()); Enemies = new List<Enemy>(); for (int i = 0; i < Const.ENEMY_MAX; i++) Enemies.Add(new Enemy()); Fortresses = new List<Fortress>(); for (int i = 0; i < Const.FORTRESS_MAX; i++) Fortresses.Add(new Fortress()); for (int i = 0; i < Const.SPARK_COUNT; i++) _sparks.Add(new Spark()); } // ゲームに参加しているプレイヤーのリスト public List<Player> Players { get { return _players.Where(player => player.ConnectionId != "").ToList(); } } // 敵のリスト public List<Enemy> Enemies { private set; get; } // 要塞のリスト public List<Fortress> Fortresses { private set; get; } // フィールド上に存在する火花のリスト public List<Spark> Sparks { get { return _sparks.Where(spark => !spark.IsDead).ToList(); } } } |
プロパティではありませんが、以下はプレイに参加していないPlayerオブジェクトを取得するメソッドです。
1 2 3 4 5 6 7 |
public class Game { public Player? GetNewPlayer() { return _players.FirstOrDefault(player => player.ConnectionId == ""); } } |
更新処理
更新処理時の処理を示します。プレイヤーと敵、要塞の更新処理をしたあと当たり判定をおこないます。最後に火花の更新処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Game { public void Update() { // 各メソッドは後述 UpdatePlayers(); UpdateEnemies(); UpdateFortresses(); HitJudgeEnemiesAndBullets(); HitJudgeFortressesAndBullets(); HitJudgePlayersAndBullets(); HitJudgePlayers(); HitJudgePlayersAndEnemies(); HitJudgePlayersAndFortresses(); foreach (Spark spark in Sparks) spark.Update(); } } |
プレイヤーの更新処理
プレイヤーを更新する処理を示します。
プレイヤーと弾丸を移動させます。そのあとどのプレイヤーからも見えない弾丸を消去する処理をおこないます。if文のなかで32を足しているのはコメントにあるとおりで、敵から逃げながら弾丸を発射すると命中扱いにならないことが多くなるので、それを避けるためです。
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 { public void UpdatePlayers() { foreach (Player player in Players) { player.Move(); foreach (Bullet bullet in player.Bullets) bullet.IsDead = !IsExistPlayer(bullet); } } int[] _offsets = { 0, Const.FIELD_SIZE, -Const.FIELD_SIZE }; // 弾丸はプレイヤーの近くに存在するか? // ワープした弾丸はワープ前に戻して距離を測る // if文のなかで32を足しているのはcanvas上から少しでも外れた消えた弾丸を消去してしまうと // 当たり判定がシビアになりすぎるのを避けるため bool IsExistPlayer(Bullet bullet) { foreach (Player player in Players) { for (int i = 0; i < _offsets.Length; i++) { for (int k = 0; k < _offsets.Length; k++) { if (Math.Abs(bullet.X + _offsets[i] - player.X) < Const.CANVAS_SIZE / 2 + 32 && Math.Abs(bullet.Y + _offsets[k] - player.Y) < Const.CANVAS_SIZE / 2 + 32) return true; } } } return false; } } |
敵の更新処理
敵の更新処理を示します。
敵と敵の弾丸を移動させます。また48更新に1回の割合で敵に弾丸を発射させます(ただし生成されたばかりの敵は弾丸を発射しない)。弾丸を発射するときは射程距離内(canvasのサイズの半分の距離以内)にプレイヤーが存在するときにほぼプレイヤーがいる方向にむけておこないます。
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 Game { public void UpdateEnemies() { foreach (Enemy enemy in Enemies) { enemy.Move(); EnemyShot(enemy); foreach (Bullet bullet in enemy.Bullets) bullet.IsDead = !IsExistPlayer(bullet); } } void EnemyShot(Enemy enemy) { if (!enemy.IsJustBorn && enemy.UpdateCount % 48 == 0) { // 近くにプレイヤーがいるか調べる Player? player = GetTargetPlayer(enemy); if (player != null) { // 近くにプレイヤーがいる場合はだいたいその方向にむけて発射する double rad = Math.Atan2(player.Y - enemy.Y, player.X - enemy.X); rad += (_random.Next(20) - 10) * 0.01; // -0.1 ~ 0.1 の乱数をプラスする enemy.GetNewBullet()?.Init(enemy.X, enemy.Y, Const.ENEMY_BULLET_SPEED * Math.Cos(rad), Const.ENEMY_BULLET_SPEED * Math.Sin(rad)); } } } Player? GetTargetPlayer(Enemy enemy) { for (int i = 0; i < _offsets.Length; i++) { for (int k = 0; k < _offsets.Length; k++) { Player? player = Players.FirstOrDefault(player => Math.Abs(enemy.X - player.X + _offsets[i]) < Const.CANVAS_SIZE / 2 && Math.Abs(enemy.Y - player.Y + _offsets[k]) < Const.CANVAS_SIZE / 2); if(player != null) return player; } } return null; } } |
要塞の更新処理
敵の要塞の更新処理を示します。
砲台から発射された弾丸を移動させます。更新回数を30で割ったときの剰余が砲台の番号×5のときは弾丸を発射させます(ただし生成されたばかりの要塞は弾丸を発射しない)。弾丸を発射するときは射程距離内(canvasのサイズの半分の距離以内)にプレイヤーが存在するときにほぼプレイヤーがいる方向にむけておこないます。
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 |
public class Game { public void UpdateFortresses() { foreach (Fortress fortress in Fortresses) { foreach (Bullet bullet in fortress.Bullets) { bullet.Move(); bullet.IsDead = !IsExistPlayer(bullet); } if (!fortress.IsJustBorn) { fortress.Update(); FortressShot(fortress); } } } void FortressShot(Fortress fortress) { foreach (Cannon cannon in fortress.Cannons) { if (cannon.IsDead) continue; if (fortress.UpdateCount % 30 != 5 * cannon.Number) continue; Bullet? bullet = fortress.GetNewBullet(); Player? player = GetTargetPlayer(cannon); if (bullet != null && player != null) { double d1 = Math.Pow(player.X - cannon.X, 2) + Math.Pow(player.Y - cannon.Y, 2); double d2 = Math.Pow(player.X - fortress.X, 2) + Math.Pow(player.Y - fortress.Y, 2); if (d1 < d2) { double rad = Math.Atan2(player.Y - cannon.Y, player.X - cannon.X); rad += (_random.Next(20) - 10) * 0.01; // -0.1 ~ 0.1 の乱数をプラスする bullet.Init(cannon.X, cannon.Y, Const.FORTRESS_BULLET_SPEED * Math.Cos(rad), Const.FORTRESS_BULLET_SPEED * Math.Sin(rad)); } } } } Player? GetTargetPlayer(Cannon cannon) { for (int i = 0; i < _offsets.Length; i++) { for (int k = 0; k < _offsets.Length; k++) { Player? player = Players.FirstOrDefault(player => Math.Abs(cannon.X - player.X + _offsets[i]) < Const.CANVAS_SIZE / 2 && Math.Abs(cannon.Y - player.Y + _offsets[k]) < Const.CANVAS_SIZE / 2); if (player != null) return player; } } return null; } } |
当たり判定
更新処理のあとは当たり判定をおこないます。
プレイヤーの弾丸は敵に命中したか?
プレイヤーの弾丸は敵に命中したかどうかを判定する処理と命中時の処理を示します。
弾丸に死亡フラグがセットされている場合や敵が生成されたばかりの場合は当たり判定は「なし」と判定します。命中したときはプレイヤーに点数を加算して爆発を発生させます。撃墜された敵はすぐにランダムな位置で復活させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Game { void HitJudgeEnemiesAndBullets() { foreach (Player player in Players) { foreach (Bullet bullet in player.Bullets) { Enemy? enemy = Enemies.FirstOrDefault(enemy => IsHit(bullet, enemy)); if (enemy != null) { bullet.IsDead = true; OnHitEnemy(player); Explode(enemy.X, enemy.Y); enemy.Init(); } } } } } |
弾丸が敵に命中したかどうかを返すメソッドを示します。
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 { bool IsHit(Bullet bullet, Enemy enemy) { // 弾丸に死亡フラグがセットされている場合や敵が生成されたばかりの場合はfalseを返す if (bullet.IsDead || enemy.IsJustBorn) return false; // 敵と弾丸の中心の距離と双方の半径の和の2乗を比較している(ただし弾丸の半径は0としている) return Math.Pow(enemy.X - bullet.X, 2) + Math.Pow(enemy.Y - bullet.Y, 2) < Math.Pow(Const.CHARACTER_SIZE / 2, 2); } public event EventHandler? HitEnemy; void OnHitEnemy(Player player) { player.Score += 50; // 効果音を鳴らすためにイベントを送信している HitEnemy?.Invoke(player, new MyEventArgs(player.ConnectionId)); } 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 = 3 + _random.Next(1000) * 0.003; // 使用されていないオブジェクトを探して初期座標と初速を設定する Spark? spark = _sparks.FirstOrDefault(spark => spark.IsDead); if (spark == null) break; spark.Init(x, y, r * Math.Cos(rad), r * Math.Sin(rad)); } } } |
イベントの引数に使われているMyEventArgsクラスの定義を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
namespace Bosconian { public class MyEventArgs : EventArgs { public MyEventArgs(string connectionId) { ConnectionId = connectionId; } public string ConnectionId { 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 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 |
public class Game { void HitJudgeFortressesAndBullets() { foreach (Player player in Players) { foreach (Bullet bullet in player.Bullets) { foreach (Fortress fortress in Fortresses) { Cannon? cannon = fortress.Cannons.FirstOrDefault(cannon => IsHit(bullet, cannon)); if (cannon != null) { Explode(cannon.X, cannon.Y); cannon.IsDead = true; bullet.IsDead = true; OnHitEnemy(player); if (fortress.Cannons.Count == 0) { ExplodeFortress(fortress); fortress.Init(); break; } } if (IsHitFortressCore(bullet, fortress)) { bullet.IsDead = true; ExplodeFortress(fortress); fortress.Init(); OnHitCore(player); break; } // 要塞の内部で中心部に到達することがない部分や // 弾丸の進行方向が上または下ではない場合は弾丸は消滅させる if (IsHitFortressNonCore(bullet, fortress)) { bullet.IsDead = true; break; } } } } } bool IsHit(Bullet bullet, Cannon cannon) { if (bullet.IsDead || cannon.IsDead) return false; return Math.Pow(cannon.X - bullet.X, 2) + Math.Pow(cannon.Y - bullet.Y, 2) < Math.Pow(Const.CHARACTER_SIZE / 2, 2); } bool IsHitFortressCore(Bullet bullet, Fortress fortress) { return bullet.VX == 0 && Math.Abs(bullet.X - fortress.X) <= Const.MOVE_SPEED * 2 + 1 && Math.Pow(fortress.X - bullet.X, 2) + Math.Pow(fortress.Y - bullet.Y, 2) < Math.Pow(Const.CHARACTER_SIZE / 2, 2); } bool IsHitFortressNonCore(Bullet bullet, Fortress fortress) { return (bullet.VX != 0 || Math.Abs(bullet.X - fortress.X) > Const.MOVE_SPEED * 2 + 1) && Math.Pow(fortress.X - bullet.X, 2) + Math.Pow(fortress.Y - bullet.Y, 2) < Math.Pow(fortress.Radius - 10, 2); } void ExplodeFortress(Fortress fortress) { Explode(fortress.X + fortress.Radius * 0.75, fortress.Y + fortress.Radius * 0.75); Explode(fortress.X + fortress.Radius * 0.75, fortress.Y - fortress.Radius * 0.75); Explode(fortress.X - fortress.Radius * 0.75, fortress.Y + fortress.Radius * 0.75); Explode(fortress.X - fortress.Radius * 0.75, fortress.Y - fortress.Radius * 0.75); } public event EventHandler? HitCore; void OnHitCore(Player player) { player.Score += 1000; HitCore?.Invoke(player, new MyEventArgs(player.ConnectionId)); } } |
弾丸はプレイヤーに命中したか?
敵や要塞の砲台、他のプレイヤーが発射した弾丸がプレイヤーに命中したかを判定する処理と命中時の処理を示します。
命中時は機体を中心に爆発を発生させます。またOnPlayerDeadメソッドを呼び出して自機死亡のイベントを送信します。残機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 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 |
public class Game { void HitJudgePlayersAndBullets() { // 敵の弾丸がプレイヤーに命中したか? foreach (Enemy enemy in Enemies) { foreach (Bullet bullet in enemy.Bullets) { Player? player = Players.FirstOrDefault(player => IsHit(bullet, player)); if (player != null) { bullet.IsDead = true; Explode(player.X, player.Y); OnPlayerDead(player); } } } // 要塞の砲台の弾丸がプレイヤーに命中したか? foreach (Fortress fortress in Fortresses) { foreach (Bullet bullet in fortress.Bullets) { Player? player = Players.FirstOrDefault(player => IsHit(bullet, player)); if (player != null) { bullet.IsDead = true; Explode(player.X, player.Y); OnPlayerDead(player); } } } // 他のプレイヤーが発射した弾丸がプレイヤーに命中したか? foreach (Player enemy in Players) { foreach (Bullet bullet in enemy.Bullets) { Player? player = Players.FirstOrDefault(player => IsHit(bullet, player)); if (player != null && player != enemy) { bullet.IsDead = true; Explode(player.X, player.Y); OnPlayerDead(player); OnHitEnemy(enemy); } } } } bool IsHit(Bullet bullet, Player player) { if (player.IsDead || player.IsInvincible) return false; return Math.Pow(player.X - bullet.X, 2) + Math.Pow(player.Y - bullet.Y, 2) < Math.Pow(Const.CHARACTER_SIZE / 2, 2); } public event EventHandler? PlayerDead; public event EventHandler? PlayerGameOvered; void OnPlayerDead(Player player) { player.OnDead(); PlayerDead?.Invoke(player, new MyEventArgs(player.ConnectionId)); if (player.Rest <= 0) PlayerGameOvered?.Invoke(player, new MyEventArgs(player.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 |
public class Game { void HitJudgePlayers() { foreach (Player player in Players) { Player? player2 = Players.FirstOrDefault(p => IsHit(player, p)); if (player2 != null) { OnPlayerDead(player); OnPlayerDead(player2); Explode(player.X, player.Y); Explode(player2.X, player2.Y); } } } bool IsHit(Player player1, Player player2) { if (player1 == player2) return false; if (player1.IsDead || player2.IsDead || player1.IsInvincible || player2.IsInvincible) return false; return Math.Pow(player1.X - player2.X, 2) + Math.Pow(player1.Y - player2.Y, 2) < Math.Pow(Const.CHARACTER_SIZE, 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 |
public class Game { void HitJudgePlayersAndEnemies() { foreach (Player player in Players) { Enemy? enemy = Enemies.FirstOrDefault(enemy => IsHit(player, enemy)); if (enemy != null) { OnPlayerDead(player); enemy.Init(); Explode(player.X, player.Y); Explode(enemy.X, enemy.Y); } } } bool IsHit(Player player, Enemy enemy) { if (player.IsDead || player.IsInvincible || enemy.IsJustBorn) return false; return Math.Pow(player.X - enemy.X, 2) + Math.Pow(player.Y - enemy.Y, 2) < Math.Pow(Const.CHARACTER_SIZE, 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 { void HitJudgePlayersAndFortresses() { foreach (Player player in Players) { Fortress? fortress = Fortresses.FirstOrDefault(fortress => IsHit(player, fortress)); if (fortress != null) { OnPlayerDead(player); Explode(player.X, player.Y); } } } bool IsHit(Player player, Fortress fortress) { if (player.IsDead || player.IsInvincible || fortress.IsJustBorn) return false; return Math.Pow(fortress.X - player.X, 2) + Math.Pow(fortress.Y - player.Y, 2) < Math.Pow(Const.CHARACTER_SIZE / 2 + fortress.Radius, 2); } } |