前回、JavaScriptで『スペースウォー!』(Spacewar!)を完成させたので、次に対戦型の『スペースウォー!』を作ります。
スペースウォー!は1962年、当時マサチューセッツ工科大学(MIT)の学生であったスティーブ・ラッセルを中心に、DEC社のミニコンPDP-1上で稼動するデモンストレーションプログラムとして開発されました。世界初のシューティングゲームとされています。
特徴として、方向転換しただけでは進行方向を変えることはできません。また中心には太陽があり、プレイヤーの移動に影響を与えます。近づきすぎると重力に引き込まれてミスとなります。操作が難しいゲームといえます。
ではさっそくつくっていきましょう。名前空間はSpacewarAppとします。
Contents
定数の定義
ゲームで使う定数を定義します。
1 2 3 4 5 6 7 8 9 10 11 |
namespace SpacewarApp { public class Const { public const double FIELD_WIDTH = 360; // フィールドのサイズ public const double FIELD_HEIGHT = 360; public const double BULLET_SPEED = 4; // 弾丸の速さ public const double SUN_RADIUS = 24; // 太陽の半径 public const int MAX_SCORE = 5; // 先に5ポイント先取で勝利 } } |
Bulletクラスの定義
発射された弾丸の状態を取得、更新するためのBulletクラスを定義します。弾丸はレーザーのように一定の長さの線状に伸びたものとし、先端部に当たり判定があるものとします。
1 2 3 4 5 6 |
namespace SpacewarApp { 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 |
public class Bullet { double X = 0; // 弾丸の座標 double Y = 0; double VX = 0; // 弾丸の移動速度 double VY = 0; public bool IsDead = false; // 死亡フラグ int Life = 48; // 48回更新されたら消滅させる public double HeadX { get { return X; } } // 弾丸の先端部(当たり判定がある部分)の座標 public double HeadY { get { return Y; } } public double TailX { get { return HistoryXs.Last(); } } // 弾丸の最後尾部の座標 public double TailY { get { return HistoryYs.Last(); } } // 弾丸の最後尾部の座標を取得できるように更新時に先端部の座標をリストに保存する List<double> HistoryXs = new List<double>(); List<double> HistoryYs = new List<double>(); } |
コンストラクタ
コンストラクタを示します。コンストラクタの引数をフィールド変数に代入し、リストに現在の座標を格納します。
1 2 3 4 5 6 7 8 9 10 11 12 |
public class Bullet { public Bullet(double x, double y, double vx, double vy) { X = x; Y = y; VX = vx; VY = vy; HistoryXs.Insert(0, X); HistoryYs.Insert(0, Y); } } |
更新時の処理
更新時におこなわれる処理を示します。
現在の座標をリストの先頭に追加し、リストの大きさが20を超えたらそれ以降は切り捨てます。これで弾丸の先頭部分と最後尾部分が取得できるようになります。
そのあと移動速度分だけ座標を変更します。もしLifeが0になったりフィールドの外に出た場合は死亡フラグをセットします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class Bullet { public void Update() { Life--; HistoryXs.Insert(0, X); HistoryYs.Insert(0, Y); if (HistoryXs.Count > 20) { HistoryXs = HistoryXs.Take(20).ToList(); HistoryYs = HistoryYs.Take(20).ToList(); } X += VX; Y += VY; if (Life <= 0 || X < 0 || X > Const.FIELD_WIDTH || Y < 0 || Y > Const.FIELD_HEIGHT) IsDead = true; } } |
Playerクラスの定義
プレーヤーの状態を取得、更新するためのPlayerクラスを定義します。
1 2 3 4 5 6 |
namespace SpacewarApp { public class Player { } } |
以降は名前空間部分を省略して表記します。
1 2 3 |
public class Player { } |
フィールド変数とプロパティ
フィールド変数とプロパティを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class Player { public string ConnectionId = ""; // ASP.NET SignalRで使われる接続の一意のID public string Name { set; get; } // プレーヤー名 public int Type = 0; // プレーヤーは 0:wedge か 1:needleか? public bool IsDead { set; get; } // 死亡フラグ bool AllowShot = true; // 射撃は可能か?(連射制限) public double Angle { set; get; } // プレーヤーの向き public double CenterX { set; get; } // プレーヤーの中心座標 public double CenterY { set; get; } public double VX = 0; // プレーヤーの移動速度 public double VY = 0; public int Score { set; get; } // スコア public bool PressLeft = false; // ユーザーはArrowLeftキーを押下しているか? public bool PressRight = false; // ユーザーはArrowRightキーを押下しているか? public List<Bullet> Bullets { set; get; } // 弾丸オブジェクトを格納しているリスト public PointF[] OuterPoints = { }; // プレーヤーの周囲の座標(当たり判定で使用) public PointF[] InnerPoints = { }; } |
コンストラクタ
コンストラクタを示します。
OuterPointsとInnerPointsに設定している座標は以下の部分です。
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 |
public class Player { public Player(string name, string connectionId, int type) { Name = name; ConnectionId = connectionId; Type = type; IsDead = false; AllowShot = true; Bullets = new List<Bullet>(); if (type == 0) { // 初期の方向、座標、画像を設定 Angle = 0; CenterX = 36; CenterY = 36; PointF[] points1 = { new PointF(-26,-15), new PointF(-11,-15), new PointF(26,-3), new PointF(26,2), new PointF(-11,14), new PointF(-26,14), new PointF(-26,-15), }; OuterPoints = points1.ToArray(); PointF[] points2 = { new PointF(-24,-12), new PointF(-12,-12), new PointF(24,-1), new PointF(24,2), new PointF(-12,13), new PointF(-24,13), new PointF(-24,-12), }; InnerPoints = points2.ToArray(); } else { Angle = Math.PI; CenterX = Const.FIELD_WIDTH - 36; CenterY = Const.FIELD_HEIGHT - 36; PointF[] points = { new PointF(-32,-9), new PointF(-15,-9), new PointF(-13,-7), new PointF(23,-5), new PointF(32,-3), new PointF(32,2), new PointF(23,4), new PointF(-13,6), new PointF(-15,8), new PointF(-32,8), new PointF(-32,-9), }; OuterPoints = points.ToArray(); PointF[] points2 = { new PointF(-30,-6), new PointF(-30,7), new PointF(-17,-6), new PointF(-17,7), new PointF(31,0), new PointF(21,0), new PointF(11,0), new PointF(1,0), new PointF(-9,0), }; InnerPoints = points2.ToArray(); } VX = 0; VY = 0; Score = 0; } } |
初期化の処理
ひとつのゲームが終わって次のゲームに移行するときにプレーヤーの状態を初期化するための処理を示します。
ここでやっていることはプレーヤーの座標と機体の方向を最初の状態に戻す、死亡フラグのクリア、移動速度のリセット、発射された弾丸のリストのクリアです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class Player { public void Init() { if (Type == 0) { Angle = 0; CenterX = 36; CenterY = 36; } else { Angle = Math.PI; CenterX = Const.FIELD_WIDTH - 36; CenterY = Const.FIELD_HEIGHT - 36; } IsDead = false; VX = 0; VY = 0; Bullets.Clear(); } } |
更新処理
更新時におこなわれる処理を示します。
方向転換をするキーが押下されていたらその方向に機体を回転させます。そして移動速度分だけ座標を移動させます。このときフィールドの外に出てしまう場合は反対側にワープさせます。同時に弾丸の状態も更新し、死亡フラグがセットされている弾丸オブジェクトはリストから取り除きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Player { public void Update() { if(PressLeft) Angle -= 0.04; if (PressRight) Angle += 0.04; this.CenterX += this.VX + Const.FIELD_WIDTH; this.CenterY += this.VY + Const.FIELD_HEIGHT; this.CenterX %= Const.FIELD_WIDTH; this.CenterY %= Const.FIELD_HEIGHT; foreach (var bullet in this.Bullets) bullet.Update(); Bullets = Bullets.Where(bullet => !bullet.IsDead).ToList(); } } |
弾丸発射時の処理
弾丸発射時におこなわれる処理を示します。
自機死亡時と射撃不能時はなにもおこなわれません。弾丸発射の処理をするときは自機の中心座標と同じ座標に新しい弾丸オブジェクトを生成します。弾丸の移動速度は自機の方向と三角関数から得られた値に機体の移動速度を合成したものとします。また弾丸を発射したら0.1秒間は射撃不能とします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class Player { public bool Shot() { if (this.IsDead || !this.AllowShot) return false; this.AllowShot = false; Task.Run(async () => { await Task.Delay(100); this.AllowShot = true; }); double vx = this.VX + Const.BULLET_SPEED * Math.Cos(this.Angle); double vy = this.VY + Const.BULLET_SPEED * Math.Sin(this.Angle); this.Bullets.Add(new Bullet(this.CenterX, this.CenterY, vx, vy)); return true; } } |
加速処理
加速時におこなわれる処理を示します。機体の方向を三角関数に渡してそのぶん加速させます。
1 2 3 4 5 6 7 8 9 10 11 12 |
public class Player { public bool Accelerate() { if (this.IsDead) return false; this.VX += 0.5 * Math.Cos(this.Angle); this.VY += 0.5 * Math.Sin(this.Angle); return true; } } |
Fireballクラスの定義
爆発時に発生する火球の状態を取得、更新するためのFireballクラスを定義します。
火球は一度生成されると中心座標は変えずに寿命が尽きるまで半径を1ずつ大きくしていきます。
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 |
namespace SpacewarApp { public class Fireball { public int Type { set; get; } public double X { set; get; } public double Y { set; get; } public double Radius { set; get; } public bool IsDead = false; int Life = 28; // 火球の寿命 public Fireball(double x, double y, int type) { Type = type; X = x; Y = y; Radius = 1; } public void Update() { // 火球の寿命が尽きるまで半径を大きくする Life--; if (Life > 0) Radius += 1; else IsDead = true; } } } |