今回はボスコニアンに似たオンライン対戦ゲームをつくります。
『ボスコニアン』(BOSCONIAN)は、1981年11月にナムコから稼働されたアーケード用多方向スクロールシューティングゲームです。全方向任意スクロールの2Dシューティングゲームとなっており、3038年を舞台に自機「スペースパトロール機」を操作して「宇宙海賊ボスコニアン」の基地を破壊する事を目的としています。
単純に敵を倒すだけでなく複数のプレイヤー同士で互いを攻撃することができる仕様にしています。
ちなみに元々の『ボスコニアン』(BOSCONIAN)はこのようなゲームです。
ではさっそくつくっていきましょう。名前空間はBosconianとします。
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 |
namespace Bosconian { public class Const { public const int PLAYER_MAX = 16; // 参加できるプレイヤーの最大数 public const int ENEMY_MAX = 100; // ザコ敵の総数 public const int FORTRESS_MAX = 10; // 要塞の総数 public const int REST_MAX = 5; // ゲーム開始時の残機数 public const int FIELD_SIZE = 3200; // フィールドの大きさ(幅高さ共通) public const int CANVAS_SIZE = 360; // 描画用のcanvasサイズ(幅高さ共通) // 発射される弾丸の速度 public const int MOVE_SPEED = 4; public const int PLAYER_BULLET_SPEED = 10; public const int ENEMY_BULLET_SPEED = 4; public const int FORTRESS_BULLET_SPEED = 5; public const int CHARACTER_SIZE = 32; // 弾丸以外の各キャラクターの大きさ public const int SPARK_COUNT_PER_EXPLODE = 32; // ひとつの爆発で生成される火花の数 // 火花の総数(想定される最大数:敵機がすべて爆発した場合の2倍を確保。これなら足りるはず) public const int SPARK_COUNT = SPARK_COUNT_PER_EXPLODE * ENEMY_MAX * 2; } } |
Bulletクラスの定義
弾丸を移動させたり描画するためにBulletクラスを定義します。
1 2 3 4 5 6 |
namespace Bosconian { public class Bullet { } } |
以降は名前空間の部分は省略して書きます。
1 2 3 |
public class Bullet { } |
コンストラクタとプロパティ
コンストラクタと各プロパティを示します。
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 |
public class Bullet { public Bullet() { IsDead = true; } // X座標とY座標 public double X { private set; get; } public double Y { private set; get; } // X方向とY方向の移動速度 public double VX { private set; get; } public double VY { private set; get; } // 死亡フラグ public bool IsDead { set; get; } } |
発射時の処理
弾丸が発射されたらInitメソッドを呼び出し、初期座標と速度を設定します。発射以降は弾丸は存在するので死亡フラグはfalseにします。
1 2 3 4 5 6 7 8 9 10 11 |
public class Bullet { public void Init(double x, double y, double vx, double vy) { IsDead = false; X = x; Y = y; VX = vx; VY = vy; } } |
移動処理
弾丸を移動させる処理を示します。速度だけ座標を変更するのですが、フィールドの端に来たら反対側に移動させます。
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 Bullet { public void Move() { // 死亡フラグが立っている弾丸(存在しない弾丸)は無視 if (IsDead) return; // 速度ぶん移動させる X += VX; Y += VY; // フィールドの端に来たら反対側にワープさせる if (X > Const.FIELD_SIZE) X -= Const.FIELD_SIZE; else if (X < 0) X += Const.FIELD_SIZE; if (Y > Const.FIELD_SIZE) Y -= Const.FIELD_SIZE; else if (Y < 0) Y += Const.FIELD_SIZE; } } |
Sparkクラスの定義
爆発によって発生する火花を移動させたり描画するためにSparkクラスを定義します。
1 2 3 4 5 6 |
namespace Bosconian { public class Spark { } } |
以降は名前空間の部分は省略して書きます。
1 2 3 |
public class Spark { } |
コンストラクタとプロパティ
コンストラクタと各プロパティを示します。Bulletクラスと似ていますが、火花は発生から時間の経過とともに描画に使うイメージが変化します。2回更新すると別のものに変化します。そして6回変化(12回更新)したら消滅することにします。
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 |
public class Spark { public Spark() { IsDead = true; } // 座標 public double X { private set; get; } public double Y { private set; get; } // 速度 public double VX { private set; get; } public double VY { private set; get; } // どのイメージを描画するか?(時間の経過によって変化する) public int Type { private set; get; } public bool IsDead { private set; get; } } |
爆発が発生時の処理
爆発が発生したらInitメソッドを呼び出し、初期座標と速度を設定します。火花が消滅するまでは火花は存在するので死亡フラグはfalseにします。また何回更新したか数えられるようにフィールド変数を0で初期化します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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; } } |
火花の更新
火花の状態を更新する処理を示します。速度だけ座標を変更するのですが、フィールドの端に来たら反対側に移動させます。また2回更新されたらTypeを1増加させ6になったら死亡フラグをセットして消滅させます。
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 Spark { public void Update() { if (IsDead) return; _updateCount++; X += VX; Y += VY; if (X < 0) X += Const.FIELD_SIZE; else if (X > Const.FIELD_SIZE) X -= Const.FIELD_SIZE; if (Y < 0) Y += Const.FIELD_SIZE; else if (Y > Const.FIELD_SIZE) Y -= Const.FIELD_SIZE; Type = _updateCount / 2; if(Type >= 6) IsDead = true; } } |
Playerクラスの定義
プレイヤーの機体を移動させたり描画するためにPlayerクラスを定義します。
1 2 3 4 5 6 |
namespace Bosconian { public class Player { } } |
以降は名前空間の部分は省略して書きます。
1 2 3 |
public class Player { } |
コンストラクタとプロパティ
コンストラクタと各プロパティを示します。
コンストラクタのなかでプレイヤーが発射できる最大数より少し多い数のBulletオブジェクトを生成しておきます。そしてこれらを使い回します(サーバーサイドの処理でオブジェクトを次から次へと大量生成するのはよくないらしい)。
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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
public class Player { static Random _random = new Random(); int _moveCount = 0; // 更新回数 List<Bullet> _bullets = new List<Bullet>(); // 弾丸 public Player() { // 最初に弾丸を生成してしまう for (int i = 0; i < 32; i++) _bullets.Add(new Bullet()); // 各プロパティ(後述)の初期化 IsDead = true; ConnectionId = ""; Name = ""; Score = 0; IsInvincible = false; Init(); // 後述 } // 死亡フラグ public bool IsDead { get; private set; } // ASP.NET SignalR の接続ID public string ConnectionId { get; set; } // プレイヤー名 public string Name { get; set; } // スコア public int Score { get; set; } // 残機 public int Rest { get; private set; } // 座標 public double X { get; private set; } public double Y { get; private set; } // 移動速度 public double VX { get; private set; } public double VY { get; private set; } // 無敵状態かどうか? public bool IsInvincible { get; private set; } // 飛行中の弾丸のリストを返す public List<Bullet> Bullets { get { return _bullets.Where(bullet => !bullet.IsDead).ToList(); } } // 自機を描画するかどうか?(死亡時は描画しない。無敵状態のときは2回に1回は描画しない) public bool IsShow { get { if (IsDead || (IsInvincible && _moveCount % 2 == 0)) return false; else return true; } } } |
プレイ開始時の処理
プレイヤーがゲームに参加したときに行なわれる処理を示します。
死亡フラグをクリアして各プロパティを初期化します。引数のプレイヤー名とASP.NET SignalR の接続IDをセットしたあと、乱数を生成してフィールド上にランダムに配置します(復活時の狙い撃ちを避けるため)。初期の移動方向は上です。残機をREST_MAXに設定して5秒間は無敵状態とし、描画処理では点滅表示させます。無敵状態のときは当たり判定をおこないません。
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 |
public class Player { public void GameStart(string playerName, string connectionId) { _moveCount = 0; // 各プロパティを初期化 IsDead = false; Score = 0; IsInvincible = true; Rest = Const.REST_MAX; // プレイヤー名とASP.NET SignalR の接続IDをセット Name = playerName; ConnectionId = connectionId; // 乱数でフィールド上にランダムに配置(ただし端の100ピクセルは避ける) X = _random.Next(Const.FIELD_SIZE - 200) + 100; Y = _random.Next(Const.FIELD_SIZE - 200) + 100; VX = 0; VY = -Const.MOVE_SPEED; // 最初の5秒間は無敵状態をする Task.Run(async () => { await Task.Delay(5000); IsInvincible = false; }); } } |
プレイヤー死亡時の処理
プレイヤー死亡時の処理を示します。
死亡フラグをセットして残機を1減らします。それでも残機がある場合は2秒後に死亡フラグをクリアしてフィールド上にランダムに配置します。そして3秒間は無敵状態とします。残機0になった場合はゲームオーバーです。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 28 29 30 31 32 33 34 35 36 |
public class Player { public void OnDead() { IsDead = true; Rest--; Task.Run(async () => { await Task.Delay(2000); if (Rest > 0) { _moveCount = 0; IsDead = false; X = _random.Next(Const.FIELD_SIZE - 200) + 100; Y = _random.Next(Const.FIELD_SIZE - 200) + 100; VX = 0; VY = -Const.MOVE_SPEED; IsInvincible = true; await Task.Run(async () => { await Task.Delay(3000); IsInvincible = false; }); } else { // ゲームオーバーの処理 ConnectionId = ""; Name = ""; } }); } } |
プレイヤーの移動処理
プレイヤーを移動させる処理を示します。
すでに発射された弾丸を移動させます。もし死亡フラグがセットさせていたらそれ以外にすることはありません。自機が生きている場合は移動速度分だけ座標を変更します。このときフィールドの端を超えたら反対側にワープさせるのは弾丸の処理と同じです。
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 Player { public void Move() { foreach (Bullet bullet in Bullets) bullet.Move(); if (IsDead) return; _moveCount++; X += VX; Y += VY; if (X > Const.FIELD_SIZE) X -= Const.FIELD_SIZE; else if (X < 0) X += Const.FIELD_SIZE; if (Y > Const.FIELD_SIZE) Y -= Const.FIELD_SIZE; else if (Y < 0) Y += Const.FIELD_SIZE; } } |
移動方向変更時の処理
ユーザーのキー操作で自機の移動方向を変更する処理を示します。押されたキーで機体の方向が決まるので、三角関数で自機のXY方向の移動速度を求めてセットしています。回転時は現在の機体方向を取得して45度左右に回転させています。このときdouble型だとcos(π/2)がちょうど0にならないので絶対値を調べて1未満の場合は強制的に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 |
public class Player { public void ChangeDirect(string direct) { double rad = 0; if (direct == "up") rad = Math.PI * 3 / 2; if (direct == "down") rad = Math.PI * 1 / 2; if (direct == "left") rad = Math.PI * 2 / 2; if (direct == "right") rad = Math.PI * 0 / 2; if (direct == "upleft") rad = Math.PI * 5 / 4; if (direct == "upright") rad = Math.PI * 7 / 4; if (direct == "downleft") rad = Math.PI * 3 / 4; if (direct == "downright") rad = Math.PI * 1 / 4; if (direct == "rotateLeft") { rad = Math.Atan2(VY , VX); rad -= Math.PI / 4; } if (direct == "rotateRight") { rad = Math.Atan2(VY, VX); rad += Math.PI / 4; } VX = Const.MOVE_SPEED * Math.Cos(rad); VY = Const.MOVE_SPEED * Math.Sin(rad); if (Math.Abs(VX) < 1) VX = 0; if (Math.Abs(VY) < 1) VY = 0; } } |
弾丸発射時の処理
弾丸を発射する処理を示します。
無制限に連射できるとゲーム的におもしろくないので制限をかけています。IsDeadフラグがセットされているBulletオブジェクトを探して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 |
public class Player { bool _allowShot = true; public bool Shot() { // 死亡時は発射不可。0.2秒以上開けないと連射不可 if (IsDead || !_allowShot) return false; _allowShot = false; Task.Run(async () => { await Task.Delay(200); _allowShot = true; }); Bullet[] bullets = _bullets.Where(bullet => bullet.IsDead).ToArray(); if (bullets.Length < 2) return false; double rad = 0; if (VX == 0) rad = Math.PI / 2; if (VY == 0) rad = 0; if (VX * VY > 0) rad = Math.PI * 1 / 4; if (VX * VY < 0) rad = Math.PI * 3 / 4; // 弾丸の初速を計算する double vx = Const.PLAYER_BULLET_SPEED * Math.Cos(rad); if(Math.Abs(vx) < 1) vx = 0; double vy = Const.PLAYER_BULLET_SPEED * Math.Sin(rad); if (Math.Abs(vy) < 1) vy = 0; // 機体の速度に上記の計算結果をプラスマイナスしたものが弾丸の発射速度 bullets[0].Init(X, Y, vx + VX, vy + VY); bullets[1].Init(X, Y, -vx + VX, -vy + VY); return true; } } |