ミサイルコマンドのようなゲームをつくる 敵ミサイルの描画の続きです。
ミサイルを描画するためのMyMissileクラス
迎撃ミサイルを発射する処理を考えます。
迎撃ミサイルは点ではなく線で描画します。発射基地から直線が伸びていき目標点に到達すると爆発して敵ミサイルを撃墜します。
まずはミサイルを描画するためのクラスを作成します。
敵ミサイルと同じようにコンストラクタ内で発射位置と目標から移動方向と移動量を求めます。そのあとUpdateメソッドが呼び出されたら移動量だけ現在位置を変更します。このとき到達点と同じかこれを通過した場合は現在位置を目標と同じ座標にしてIsReachedプロパティを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 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 MyMissile { int StartX = 0; int StartY = 0; int TargetX = 0; int TargetY = 0; double VX = 0; double VY = 0; // 発射位置と目標からXY座標の移動量を求める public MyMissile(int startX, int startY, int targetX, int targetY) { // 現在位置を設定 X = startX; Y = startY; // 初期位置を保存 StartX = startX; StartY = startY; // 最終位置を保存 TargetX = targetX; TargetY = targetY; // 移動方向と移動量を算出する double angle = Math.Atan2(targetY - startY, targetX - startX); VX = Math.Cos(angle) * 20; VY = Math.Sin(angle) * 20; IsReached = false; } public double X { get; private set; } public double Y { get; private set; } public bool IsReached { get; private set; } public bool IsDead { get; set; } public void Update() { // 移動させて目標を通過していたら目標と同じ座標にすると同時にIsReached = trueにする X += VX; Y += VY; if (Y < TargetY) { X = TargetX; Y = TargetY; IsReached = true; } } // ミサイルの描画処理 static Pen Pen = new Pen(Color.Blue, 3); public void Draw(Graphics graphics) { if(!IsDead) graphics.DrawLine(Pen, new Point(StartX, StartY), new Point((int)X, (int)Y)); } } |
爆発処理をするためのExplosionクラス
次に爆発処理をするためのExplosionクラスを作成します。
爆発によって光っている感じを出すために1フレームごとに色を変えます。同じ6種類のPenを作成してこれを使い回します。1フレームで半径が2ピクセルずつ広がります。Explosionオブジェクトの寿命は場合によって異なります。また爆発の輪が重なると不自然なので内部を背景色と同じ黒で塗りつぶします。
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 |
public class Explosion { int X = 0; int Y = 0; int Radius = 0; int Life = 0; static SolidBrush Brush = new SolidBrush(Color.Black); static List<Pen> Pens = new List<Pen>(); int UpdateCount = 0; public Explosion(int x, int y, int life) { if (Pens.Count == 0) { Pens.Add(new Pen(Color.FromArgb(0xff, 0x00, 0x00), 3)); Pens.Add(new Pen(Color.FromArgb(0xff, 0xff, 0x00), 3)); Pens.Add(new Pen(Color.FromArgb(0x00, 0xff, 0x00), 3)); Pens.Add(new Pen(Color.FromArgb(0x00, 0xff, 0xff), 3)); Pens.Add(new Pen(Color.FromArgb(0x00, 0x00, 0xff), 3)); Pens.Add(new Pen(Color.FromArgb(0xff, 0x00, 0xff), 3)); } X = x; Y = y; Life = life; } public bool IsDead { get; private set; } public void Update() { UpdateCount++; Radius = UpdateCount * 2; if (UpdateCount >= Life) IsDead = true; } public void Draw(Graphics graphics) { int i = UpdateCount % Pens.Count; graphics.FillEllipse(Brush, new Rectangle(X - Radius, Y - Radius, Radius * 2, Radius * 2)); graphics.DrawEllipse(Pens[i], new Rectangle(X - Radius, Y - Radius, Radius * 2, Radius * 2)); } // 引数で渡されたPointは円の内部かどうか? public bool IsContactPoint(Point point) { double distance2 = Math.Pow(X - point.X, 2) + Math.Pow(Y - point.Y, 2); double radius2 = Math.Pow(Radius, 2); if (distance2 < radius2) return true; return false; } } |
Form1クラスの処理
Form1クラスの処理を示します。
スペースキーが押されたら迎撃ミサイルを発射します。MyMissilesに新しいミサイルを追加します。ただし全都市が壊滅している場合はなにも起きません。
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 |
public partial class Form1 : Form { protected override void OnKeyDown(KeyEventArgs e) { if (e.KeyCode == Keys.Left) TargetLeft = true; if (e.KeyCode == Keys.Right) TargetRight = true; if (e.KeyCode == Keys.Up) TargetUp = true; if (e.KeyCode == Keys.Down) TargetDown = true; if (e.KeyCode == Keys.Space) Shot(); base.OnKeyDown(e); } WMPLib.WindowsMediaPlayer playerShot = new WMPLib.WindowsMediaPlayer(); void Shot() { if (Cities.Any(x => !x.IsDead)) { MyMissiles.Add(new MyMissile(LauncherStartPoint.X, LauncherStartPoint.Y, TargetX, TargetY)); // 音を鳴らす playerShot.URL = Application.StartupPath + "\\shot.mp3"; playerShot.controls.play(); } } } |
ミサイルを移動と敵ミサイルの撃墜ができたかどうかの判定処理を行ないます。
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 partial class Form1 : Form { List<Explosion> Explosions = new List<Explosion>(); List<MyMissile> MyMissiles = new List<MyMissile>(); private void Timer_Tick(object sender, EventArgs e) { // IsDeadプロパティがtrueのものは対象からはずす MyMissiles = MyMissiles.Where(x => !x.IsDead).ToList(); Explosions = Explosions.Where(x => !x.IsDead).ToList(); EnemyMissiles = EnemyMissiles.Where(x => !x.IsDead).ToList(); CreateEnemy(); MoveTarget(); MoveEnemies(); MoveMyMissiles(); // 後述 CheckEnemyMissileLanding(); CheckHitEnemyMissile(); // 後述 TickCount++; Invalidate(); } } |
迎撃ミサイルを移動させる処理を示します。ミサイルが目標に到達しているのであればMyMissile.IsReached = trueになっているのでこのIsDeadプロパティをtrueにして爆発の処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public partial class Form1 : Form { WMPLib.WindowsMediaPlayer playerHit = new WMPLib.WindowsMediaPlayer(); void MoveMyMissiles() { foreach (MyMissile myMissile in MyMissiles) { myMissile.Update(); if (myMissile.IsReached && !myMissile.IsDead) { myMissile.IsDead = true; Explosions.Add(new Explosion((int)myMissile.X, (int)myMissile.Y, 20)); // 音を鳴らす playerHit.controls.play(); } } } } |
爆発の処理が行なわれている場合は敵ミサイルを撃墜できたかを調べます。爆発の円内に敵ミサイルが存在する場合は撃墜されたことになります。敵ミサイルも爆発し、この爆発によって他の敵ミサイルが誘爆することもあります。
爆発の処理をおこなうときは誘爆で最初の爆発が上書きされないように最後に追加するのではなく最初に挿入します。
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 partial class Form1 : Form { void CheckHitEnemyMissile() { List<Point> newPoints = new List<Point>(); // foreach文のなかでExplosionsに要素を追加すると例外が発生するので // 追加したいデータはいったんnewPointsに格納する foreach (Explosion explosion in Explosions) { explosion.Update(); foreach (EnemyMissile missile in EnemyMissiles) { if (missile.IsDead) continue; Point enemyPoint = new Point((int)missile.X, (int)missile.Y); if (explosion.IsContactPoint(enemyPoint)) { newPoints.Add(enemyPoint); missile.IsDead = true; OnHitEnemyMissile(); break; } } } // 追加するときは一番最後ではなく最初に foreach (Point newPoint in newPoints) Explosions.Insert(0, new Explosion(newPoint.X, newPoint.Y, 15)); } } |
描画の処理を示します。
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 partial class Form1 : Form { protected override void OnPaint(PaintEventArgs e) { // 迎撃ミサイルの描画 foreach (MyMissile myMissile in MyMissiles) myMissile.Draw(e.Graphics); // 爆発の描画 foreach (Explosion explosion in Explosions) explosion.Draw(e.Graphics); // 都市の描画 foreach (City city in Cities) { if(!city.IsDead) city.Draw(e.Graphics); } // 敵ミサイルの描画 foreach (EnemyMissile missile in EnemyMissiles) missile.Draw(e.Graphics); // 迎撃ミサイルのターゲットの描画 DrawTarget(e.Graphics); base.OnPaint(e); } } |
ゲームとして完成させます。スコアの表示とゲームオーバー処理を追加します。
まずスコアを表示させます。敵ミサイルを撃墜したら25点追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public partial class Form1 : Form { int Score = 0; Font ScoreFont = new Font("MS ゴシック", 16, FontStyle.Bold); SolidBrush ScoreBrush = new SolidBrush(Color.White); void OnHitEnemyMissile() { Score += 25; playerHit.URL = Application.StartupPath + "\\hit3.mp3"; } void DrawScore(Graphics graphics) { graphics.DrawString("Score " + Score.ToString("00000"), ScoreFont, ScoreBrush, new Point(10, 10)); } } |
すべての都市が壊滅した場合はゲームオーバーです。ゲームオーバーの場合は「Game Over」「Press S key to try again」と表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public partial class Form1 : Form { Font GameOverFont1 = new Font("MS ゴシック", 32); Font GameOverFont2 = new Font("MS ゴシック", 20); void DrawStringIfGameOver(Graphics graphics) { if (!Cities.Any(x => !x.IsDead)) { // 実際に文字を描画してX座標は中心にY座標は中心よりも少し上の座標を取得する Size size = TextRenderer.MeasureText("Game Over", GameOverFont1, new Size(this.Width, this.Height), TextFormatFlags.NoPadding); int x = (this.Width - size.Width) / 2; int y = (this.Height - size.Height) / 2; TextRenderer.DrawText(graphics, "Game Over", GameOverFont1, new Point(x, y - 60), Color.White); y = y + size.Height + 20; size = TextRenderer.MeasureText("Press S key to try again", GameOverFont2, new Size(this.Width, this.Height), TextFormatFlags.NoPadding); x = (this.Width - size.Width) / 2; TextRenderer.DrawText(graphics, "Press S key to try again", GameOverFont2, new Point(x, y - 60), Color.White); } } } |
ゲームオーバーの状態でSキーが押されたら新しくゲームを開始します。スコアをリセットして都市を復元し、ターゲットを画面の中心に設定してゲームを開始します。
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 partial class Form1 : Form { protected override void OnKeyDown(KeyEventArgs e) { if (e.KeyCode == Keys.Left) TargetLeft = true; if (e.KeyCode == Keys.Right) TargetRight = true; if (e.KeyCode == Keys.Up) TargetUp = true; if (e.KeyCode == Keys.Down) TargetDown = true; if (e.KeyCode == Keys.Space) Shot(); if (e.KeyCode == Keys.S && !Cities.Any(x => !x.IsDead)) Retry(); base.OnKeyDown(e); } void Retry() { Score = 0; TargetX = Width / 2; TargetY = Height / 2; TickCount = 0; Cities.Select(x => x.IsDead = false).ToList(); } } |
描画処理を示します。
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 partial class Form1 : Form { protected override void OnPaint(PaintEventArgs e) { foreach (MyMissile myMissile in MyMissiles) myMissile.Draw(e.Graphics); foreach (Explosion explosion in Explosions) explosion.Draw(e.Graphics); foreach (City city in Cities) { if(!city.IsDead) city.Draw(e.Graphics); } foreach (EnemyMissile missile in EnemyMissiles) missile.Draw(e.Graphics); DrawTarget(e.Graphics); // スコアとゲームオーバー表示 DrawScore(e.Graphics); DrawStringIfGameOver(e.Graphics); base.OnPaint(e); } } |