今回はディフェンダーに似たオンライン対戦ゲームをつくります。
『ディフェンダー』(Defender)は1980年にウィリアムスが開発したアーケードゲームで任意横スクロールのシューティングゲームです。ランダーという敵が常に地上にいる人間を画面上部へとさらおうとする要素は入れていません。原作では横スクロールですが、これだとスマホで遊びにくいので縦スクロールにしています。左右に移動可能、上下は自機の向きと進行方向、移動速度が変わります。単純に敵を倒すだけでなく複数のプレイヤー同士で互いを攻撃することができる仕様にしています。
ちなみに元々の『ディフェンダー』(Defender)はこのようなゲームです。
ではさっそくつくっていきましょう。名前空間はDefenderとします。
Contents
Constクラスの定義
定数に関する定義をします。
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 |
namespace Defender { public class Const { // フィールドの幅と高さ public const int FIELD_WIDTH = 360; public const int FIELD_HEIGHT = 10000; // 各キャラクターの大きさ public const int PLAYER_SIZE = 40; public const int ENEMY_SIZE = 38; public const int BULLET_SIZE = 14; // 当たり判定で使うキャラクターの大きさ public const int CHARCTER_SIZE = 32; // 自機、敵機、弾丸の移動速度 public const double PLAYER_SPEED = 4; public const double ENEMY_SPEED = 1.1; public const double PLAYER_BULLET_SPEED = 5; public const double ENEMY_BULLET_SPEED = 3.1; // 残機の初期数 public const int LIFE_MAX = 5; // フィールドに存在するプレイヤー機と敵機の数 public const int PLAYER_MAX = 12; public const int ENEMY_MAX = 12; // ひとつの爆発で生成される火花の数 public const int SPARK_COUNT_PER_EXPLODE = 32; // 火花の総数(想定される最大数:自機と敵機がすべて爆発したとしてもこの数で足りるはず) public const int SPARK_COUNT = 32 * 24; } } |
Bulletクラスの定義
弾丸を移動させ描画するためにBulletクラスを定義します。
1 2 3 4 5 6 |
namespace Defender { public class Bullet { } } |
以降は名前空間の部分は省略して書きます。
1 2 3 |
public class Bullet { } |
コンストラクタとプロパティ
コンストラクタとプロパティを示します。
最初はインスタンスだけ生成して弾丸自体は存在しないのでIsDead = true;をセットします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Bullet { public Bullet() { IsDead = true; } // 弾丸の座標 public double X { get; set; } public double Y { get; set; } // 弾丸の移動速度 public double VX { get; set; } public double VY { get; set; } // 死亡フラグ IsDead == trueなら弾丸は存在しない public bool IsDead { get; set; } // 更新回数(発射されて一定時間が経過したら死亡フラグがセットされる) public int UpdateCount { get; set; } } |
発射位置と発射速度の設定
Initメソッドは弾丸が発射されたときに各プロパティに初期座標(発射位置)と移動速度(発射速度)をセットするためのものです。
1 2 3 4 5 6 7 8 9 10 11 12 |
public class Bullet { public void Init(double x, double y, double vx, double vy) { X = x; Y = y; VX = vx; VY = vy; IsDead = false; UpdateCount = 0; } } |
更新処理
Updateメソッドは移動速度分、弾丸の座標を移動させます。このときフィールドの一番上または下に移動したものは反対側へワープさせます。左右からはみ出したものは死亡フラグをセットします。またどこまでも飛び続けないように更新回数や移動距離が一定数に達した場合も死亡フラグをセットしています。
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 |
public class Bullet { public void Update() { // 死亡フラグがセットされているなら更新処理はしない if (IsDead) return; this.UpdateCount++; X += this.VX; Y += this.VY; if (Y < 0) Y += Const.FIELD_HEIGHT; else if (Y > Const.FIELD_HEIGHT) Y -= Const.FIELD_HEIGHT; if (X < 0) IsDead = true; if (X > Const.FIELD_WIDTH) IsDead = true; if (UpdateCount > Math.Abs(640 / VY) || UpdateCount > 180) IsDead = true; } } |
Sparkクラスの定義
爆発したときに発生する火花の描画処理をするためにSparkクラスを定義します。
1 2 3 4 5 6 |
namespace Defender { public class Spark { } } |
以降は名前空間の部分は省略して書きます。
1 2 3 |
public class Spark { } |
コンストラクタとプロパティ
コンストラクタとプロパティを示します。最初はインスタンスを生成だけで火花は存在しないので IsDead = true;をセットします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Spark { public Spark() { IsDead = true; } // 火花の座標 public double X { get; set; } public double Y { get; set; } // 火花の移動速度 public double VX { get; set; } public double VY { get; set; } // 死亡フラグ public bool IsDead { get; set; } // 火花の状態 public int Type { get; set; } } |
Initメソッドは火花が発生したときに発生場所の座標と移動速度を各プロパティにセットします。
発生場所の座標と移動速度の設定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class Spark { int _updateCount = 0; public void Init(double x, double y, double vx, double vy) { X = x; Y = y; VX = vx; VY = vy; IsDead = false; _updateCount = 0; } } |
更新処理
Updateメソッドは移動速度分、火花の座標を移動させます。また更新回数によって火花の状態を変化させます。火花が消滅する段階で死亡フラグをセットしています。
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 Spark { public void Update() { // 死亡フラグがセットされているなら更新処理はしない if (IsDead) return; this._updateCount++; X += this.VX; Y += this.VY; if (Y < 0) Y += Const.FIELD_HEIGHT; else if (Y > Const.FIELD_HEIGHT) Y -= Const.FIELD_HEIGHT; Type = _updateCount / 4; // 火花の消滅 if(Type >= 6) IsDead = true; } } |
Playerクラスの定義
各プレイヤーの移動処理や描画処理をするためにPlayerクラスを定義します。
1 2 3 4 5 6 |
namespace Defender { public class Player { } } |
以降は名前空間の部分は省略して書きます。
1 2 3 |
public class Player { } |
コンストラクタと各プロパティを示します。
最初はインスタンスを生成するでプレイヤーはゲームに参加していません。なのでIsGameOvered = true;をセットします。またサーバーサイドアプリケーションでオブジェクトを生成したり破棄するのはあまりよくないので、手持ちの弾丸を最初に生成し、途中で追加の生成はしないでこれを使い回すことにします。
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 |
public class Player { public Player() { IsGameOvered = true; ConnectionId = ""; Name = ""; // 最初に必要な弾丸のオブジェクトを10個つくっておく Bullets = new List<Bullet>(); for (int i = 0; i < 10; i++) Bullets.Add(new Bullet()); } // ASP.NET SignalR の接続ID public string ConnectionId { get; set; } // プレイヤー名、スコア、残機 public string Name { get; set; } public int Score { get; set; } public int Life { get; set; } // プレイヤーの座標 public double X { get; set; } public double Y { get; set; } // プレイヤーの移動速度 public double VX { get; set; } public double VY { get; set; } // 死亡フラグ public bool IsDead { get; set; } // 無敵状態か?(ゲーム開始とミスからの復帰直後は一定時間無敵状態とする) public bool IsInvincible { get; set; } // 描画するかどうか?死亡時は非表示、無敵状態のときは点滅させる public bool IsShow { get; set; } // ゲームオーバー状態かどうか? public bool IsGameOvered { get; set; } // 弾丸を発射できるか? 連射の制限 bool _allowShot = true; // 弾丸オブジェクトのリスト public List<Bullet> Bullets { get; } } |
新規参加と復帰時の処理
引数ありのInitメソッドは新しくプレイヤーがゲームに参加したときに呼び出されます。プレイヤー名とconnectionIdをプロパティにセットします。また残機数、スコアを初期値にセットし、IsGameOveredフラグをクリアします。そのあと引数なしのInitメソッドを呼び出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Player { public void Init(string name, string connectionId) { ConnectionId = connectionId; Name = name.Replace(",", "_").Replace("\n", "").Replace("\r", ""); if (name == "") Name = "名無しさん"; Life = Const.LIFE_MAX; IsGameOvered = false; Score = 0; Init(); } } |
引数なしの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 |
public class Player { public void Init() { // 死亡フラグをクリア IsDead = false; // 初期座標と初期速度をセット X = (Const.FIELD_WIDTH - Const.PLAYER_SIZE) / 2; Y = 0; VX = 0; VY = -Const.PLAYER_SPEED / 2; // 弾丸の発射可能 _allowShot = true; // 無敵状態にする IsInvincible = true; // 無敵状態のときは点滅表示させるが、最初は表示する IsShow = true; // これまでに発射したすべての弾丸に死亡フラグをセット(存在しない扱いにする) foreach (Bullet bullet in Bullets) bullet.IsDead = true; // 無敵状態は3秒で解除 Task.Run(async () => { await Task.Delay(3000); IsInvincible = false; }); } } |
更新処理
Updateメソッドは更新処理をおこないます。
死亡フラグがセットされていないときだけ移動処理をおこないます。無敵状態のときは点滅させ、そうでないときはつねに表示させますが、死亡時は非表示とします。縦方向の移動はフィールドの上下からはみ出したら反対側にワープさせますが、横方向の移動はフィールドからはみ出さない場合だけ行なうことにします。
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 Player { public void Update() { // 死亡フラグがセットされていないときだけ移動処理をおこなう if (!IsDead) { // 無敵状態のときは点滅させる(表示/非表示を入れ替える) // そうでないときはつねに表示 if (IsInvincible) IsShow = !IsShow; else IsShow = true; // 縦方向に移動 Y += VY; // フィールドの上下にはみ出したら反対側にワープさせる if (Y < 0) Y += Const.FIELD_HEIGHT; else if (Y > Const.FIELD_HEIGHT) Y -= Const.FIELD_HEIGHT; // 横方向の移動はフィールドからはみ出さない場合だけ行なう if (X + VX >= 0 && X + VX <= Const.FIELD_WIDTH - Const.PLAYER_SIZE) X += VX; } // 弾丸も移動させる foreach (Bullet bullet in Bullets) bullet.Update(); // 死亡時は非表示 if (IsDead) IsShow = false; } } |
弾丸の発射
Shotメソッドは弾丸の発射処理をおこないます。
自機死亡時または発射不能時はなにもしません。無制限に連射できてしまうと困るので、発射後0.2秒経過しないと発射できない仕様にします。発射地点の座標と発射方向が求められたら、これらをBullet.Initメソッドに引数として渡します。発射処理が正常に完了したらtrue、そうでない場合は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 |
public class Player { public bool Shot() { // 自機死亡時または発射不能時はなにもしない if (!_allowShot || IsDead) return false; _allowShot = false; // 発射後0.2秒経過したらふたたび発射可能とする Task.Run(async () => { await Task.Delay(200); _allowShot = true; }); // 発射地点の座標を求める(発射地点は自機の中央部とする) double sx = X + (Const.PLAYER_SIZE - Const.BULLET_SIZE) / 2; double sy = Y + (Const.PLAYER_SIZE - Const.BULLET_SIZE) / 2; // 発射する弾丸オブジェクトを探す(見つからないことはないはず) Bullet? bullet = Bullets.FirstOrDefault(bullet => bullet.IsDead); if(bullet == null) return false; // 自機の移動方向で発射方向を変える if (VY > 0) bullet.Init(sx, sy, 0, Const.PLAYER_BULLET_SPEED + VY); else bullet.Init(sx, sy, 0, -Const.PLAYER_BULLET_SPEED + VY); return true; } } |
移動方向の変更
ユーザーのキー操作、ボタンのクリック、タップで移動方向を変更できるようにします。X方向は一定速度で移動しますが、Y方向は加速、減速があるのでメソッドをわけています。AddVelocityYメソッドは加速処理の結果、速度の絶対値が大きくなったときはtrueを返します(スピードアップ時に効果音を再生したいので)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Player { public void SetVelocityX(double value) { VX = value; } public bool AddVelocityY(double value) { bool ret = false; if ((VY > 0 && value > 0) || (VY < 0 && value < 0)) ret = true; VY += value; return ret; } } |
Enemyクラスの定義
敵の移動処理や描画処理をするためにEnemyクラスを定義します。
1 2 3 4 5 6 |
namespace Defender { public class Enemy { } } |
以降は名前空間の部分は省略して書きます。
1 2 3 |
public class Enemy { } |
コンストラクタとプロパティ
コンストラクタとプロパティを示します。
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 Enemy { static Random _random = new Random(); int _updateCount = 0; public Enemy() { // 発射する弾丸は最初に24個生成してこれを使い回す Bullets = new List<Bullet>(); for (int i = 0; i < 6 * 4; i++) Bullets.Add(new Bullet()); Init(); // 後述 } // 敵の座標と移動速度 public double X { get; set; } public double Y { get; set; } public double VX { get; set; } public double VY { get; set; } // 敵のタイプ(黄色、ピンク、青) public int Type { get; set; } // 死亡フラグ public bool IsDead { get; set; } // 敵を表示するか? 生成されたばかりの敵は点滅させる public bool IsShow { get; set; } // 生成されたばかりの敵かどうか? public bool IsJustBorn { get; set; } // 弾丸のリスト public List<Bullet> Bullets { get; } } |
初期化直後と復帰時の処理
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 |
public class Enemy { public void Init() { // どこに出現させるか? 移動速度は? X = _random.Next(Const.FIELD_WIDTH); Y = _random.Next(Const.FIELD_HEIGHT); VX = Const.ENEMY_SPEED + (0.1 * _random.Next(10)); VY = Const.ENEMY_SPEED * 0.5 + (0.1 * _random.Next(10)); // 1 / 2の確率で逆方向にする if (_random.Next(2) == 0) VX *= -1; if (_random.Next(2) == 0) VY *= -1; // 敵のタイプ(特に意味はない。見た目の問題) Type = _random.Next(3); _updateCount = 0; // 死亡フラグをクリアし、可視化する IsShow = true; IsDead = false; // 生成されたばかりの敵であることを意味するフラグをセットし、3秒後にクリアする IsJustBorn = true; Task.Run(async () => { await Task.Delay(3000); IsJustBorn = 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 36 37 38 39 40 41 42 43 |
public class Enemy { public void Update() { if (!IsDead) { _updateCount++; // 生成されたばかりの敵は点滅させる // そうでないなら常に表示 if (IsJustBorn) IsShow = !IsShow; else IsShow = true; // 横方向の移動。端にきたら移動方向を逆にする if (X + VX < 0) VX = Math.Abs(this.VX); if (X + VX > Const.FIELD_WIDTH - Const.ENEMY_SIZE) VX = -Math.Abs(this.VX); X += VX; // 縦方向の移動。端にきたら逆側にワープさせる Y += VY; if (Y < 0) Y += Const.FIELD_HEIGHT; else if (Y > Const.FIELD_HEIGHT) Y -= Const.FIELD_HEIGHT; // 更新回数が120の倍数の場合は弾丸を発射する // ただし生成されたばかりの敵の場合はなにもしない if (_updateCount % 120 == 0 && !IsJustBorn) Shot(); } else IsShow = false; // 死亡している敵は表示させない // 弾丸も移動させる foreach (Bullet bullet in Bullets) bullet.Update(); } } |
弾丸を発射する処理
弾丸を発射する処理を示します。弾丸は放射状に6個発射します。リストのなかに死亡フラグがセットされている弾丸があるはずなので、最初にみつかったものに新たな初期座標と初期速度を設定し
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class Enemy { public void Shot() { for (int i = 0; i < 6; i++) { double rad = 2 * Math.PI / 6 * i; double x = X + Const.ENEMY_SIZE / 2; double y = Y + Const.ENEMY_SIZE / 2; Bullet? bullet = Bullets.FirstOrDefault(bullet => bullet.IsDead); if (bullet == null) break; bullet.Init(x, y, Const.ENEMY_BULLET_SPEED * Math.Cos(rad) + VX, Const.ENEMY_BULLET_SPEED * Math.Sin(rad) + VY); } } } |