ミサイルコマンド (Missile Command) は、1980年にアタリが発売したアーケードゲームです。上空から落下してくる弾道弾ミサイルから都市を守るために迎撃ミサイルで迎撃する内容になっています。
画面下部の両端と中央に3つの自軍砲台基地があり、その間には都市が計6つあります。迎撃ミサイルは砲台基地毎に発射数が決まっていて、3つボタンで迎撃ミサイルの発射を操作します。ただし今回は砲台基地はひとつだけ発射数の制限なしということで進めます。
Contents
都市を描画する
守るべき都市は6つです。Bitmapは同じものを使うので静的変数にしてあります。GetBitmapメソッドで元になる画像ファイルからBitmapを取得しています。
元になる画像ファイルはこれを使います。
コンストラクタで指定されたサイズに変更してから青の部分だけを描画します。CenterXプロパティとCenterYプロパティは都市が描画される矩形の中心の座標です。敵のミサイルはここをめがけて進みます。敵のミサイルが着弾したらIsDeadプロパティがtrueになります。敵弾が都市に到達したかどうかはIsInsidePointメソッドで判定します。
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 City { Rectangle Rectangle = Rectangle.Empty; static Bitmap Bitmap = null; public City(Rectangle rect) { Rectangle = rect; CenterX = rect.X + rect.Width / 2; CenterY = rect.Y + rect.Height / 2; if (Bitmap == null) Bitmap = GetBitmap(rect.Width, rect.Height); } Bitmap GetBitmap(int width, int height) { // width, heightで指定したサイズに拡大縮小する // 色が変化しないようにInterpolationMode.NearestNeighborにする Bitmap bitmap = new Bitmap(width, height); Graphics graphics = Graphics.FromImage(bitmap); graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor; graphics.DrawImage(Properties.Resources.city, new Rectangle(0, 0, width, height)); graphics.Dispose(); // 青い部分だけ取り出す Bitmap newBitmap = new Bitmap(width, height); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { if (bitmap.GetPixel(x, y) == Color.FromArgb(0, 0, 255)) newBitmap.SetPixel(x, y, Color.Blue); } } bitmap.Dispose(); return newBitmap; } public int CenterX { get; } public int CenterY { get; } public bool IsDead { get; set; } // 引数で渡されたPointは矩形の内部かどうか? public bool IsInsidePoint(Point point) { if (point.X < Rectangle.Left) return false; if (point.Y < Rectangle.Top) return false; if (Rectangle.Right < point.X) return false; if (Rectangle.Bottom < point.Y) return false; return true; } // 都市が壊滅していなければ描画する public void Draw(Graphics graphics) { if (!IsDead) graphics.DrawImage(Bitmap, Rectangle); } } |
次にForm1クラスを考えます。方向キーをおすとターゲットがその方向に移動し、スペースキーを押すとミサイルがそこへむけて飛んでいき、ターゲットに到達すると爆発して敵のミサイルを撃墜します。
コンストラクタ内でタイマーを初期化して1秒間に60回再描画させます。
そのあと都市を描画する準備をします。等間隔で6個分の都市を描画できるような矩形を求めます。
また迎撃ミサイルを発射するときのターゲットを+印で表示します。最初の座標は画面の中央です。LauncherStartPointは迎撃ミサイルが発射される場所の座標です。実際のミサイルコマンドでは3箇所から弾数に制限がありますが、ここでは弾数制限なし、中央下部から発射される仕様にします。
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 |
public partial class Form1 : Form { Timer Timer = new Timer(); List<City> Cities = new List<City>(); Point LauncherStartPoint = Point.Empty; public Form1() { InitializeComponent(); Timer.Interval = 1000/60; Timer.Tick += Timer_Tick; Timer.Start(); this.DoubleBuffered = true; this.BackColor = Color.Black; int spacing = (this.ClientSize.Width - 50 * 2 - 45) / 5; Cities.Add(new City(new Rectangle(50 + spacing * 0, 350, 45, 30))); Cities.Add(new City(new Rectangle(50 + spacing * 1, 350, 45, 30))); Cities.Add(new City(new Rectangle(50 + spacing * 2, 350, 45, 30))); Cities.Add(new City(new Rectangle(50 + spacing * 3, 350, 45, 30))); Cities.Add(new City(new Rectangle(50 + spacing * 4, 350, 45, 30))); Cities.Add(new City(new Rectangle(50 + spacing * 5, 350, 45, 30))); TargetX = this.ClientSize.Width / 2; TargetY = this.ClientSize.Height / 2; LauncherStartPoint = new Point(this.ClientSize.Width / 2, 350); } } |
TargetXとTargetYはターゲットの座標を示すプロパティです。値を設定する前に画面の外側にでないようにしています。
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 { int _targetX = 0; int TargetX { get { return _targetX; } set { if (0 < value && value < this.ClientSize.Width) _targetX = value; } } int _targetY = 0; int TargetY { get { return _targetY; } set { if (0 < value && value < Height) _targetY = value; } } } |
キーを押すとターゲットが移動します。キーを押すとどの方向に移動するかがセットされます。この状態でInvalidateメソッドが呼び出されるとその方向にターゲットが移動します。
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 partial class Form1 : Form { bool TargetLeft = false; bool TargetRight = false; bool TargetUp = false; bool TargetDown = false; 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); } protected override void OnKeyUp(KeyEventArgs e) { if (e.KeyCode == Keys.Left) TargetLeft = false; if (e.KeyCode == Keys.Right) TargetRight = false; if (e.KeyCode == Keys.Up) TargetUp = false; if (e.KeyCode == Keys.Down) TargetDown = false; base.OnKeyUp(e); } } |
ターゲットを移動させて描画する処理を示します。Timer.Tickイベントが発生したらInvalidateメソッドを呼び出し、OnPaintメソッドのなかで移動先の座標にターゲットを描画するとともに都市を描画します。
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 |
public partial class Form1 : Form { private void Timer_Tick(object sender, EventArgs e) { MoveTarget(); Invalidate(); } void MoveTarget() { if (TargetLeft) TargetX -= 5; if (TargetRight) TargetX += 5; if (TargetUp) TargetY -= 5; if (TargetDown) TargetY += 5; } protected override void OnPaint(PaintEventArgs e) { foreach (City city in Cities) { if(!city.IsDead) city.Draw(e.Graphics); } DrawTarget(e.Graphics); base.OnPaint(e); } Pen TargetPen = new Pen(Color.Cyan); void DrawTarget(Graphics graphics) { graphics.DrawLine(TargetPen, new Point(TargetX, TargetY), new Point(TargetX - 5, TargetY)); graphics.DrawLine(TargetPen, new Point(TargetX, TargetY), new Point(TargetX + 5, TargetY)); graphics.DrawLine(TargetPen, new Point(TargetX, TargetY), new Point(TargetX, TargetY + 5)); graphics.DrawLine(TargetPen, new Point(TargetX, TargetY), new Point(TargetX, TargetY - 5)); } } |
敵のミサイルを描画する
敵のミサイルを描画するためのクラスを作成します。
コンストラクタのなかで敵ミサイルが出現する座標とミサイルがどの都市に向かって落下するかを設定します。また落下速度も指定できるようにします。
コンストラクタのなかで敵ミサイルが出現する座標とどこへ向かって落下するかで移動方向と移動量を算出します。ミサイルは赤い線で描画され、撃墜されると消えます。
Form1クラスでTimer_Tickメソッドが呼び出されるとUpdateメソッドが呼び出されます。Updateメソッドのなかで現在座標がコンストラクタ内で算出された移動量だけ変化します。そのあとDrawメソッドで描画させます。
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 |
public class EnemyMissile { int StartX = 0; int StartY = 0; double VX = 0; double VY = 0; public EnemyMissile(int x, int y, int targetX, int targetY, double speed) { X = x; Y = y; StartX = x; StartY = y; TargetX = targetX; TargetY = targetY; double angle = Math.Atan2(targetY - y, targetX - x); VX = Math.Cos(angle) * speed; VY = Math.Sin(angle) * speed; } public double X { get; private set; } public double Y { get; private set; } public int TargetX { get; } public int TargetY { get; } public bool IsDead { get; set; } public bool SpeedUp = false; public void Update() { X += VX; Y += VY; if (SpeedUp) { X += VX * 3; Y += VY * 3; } } static Pen Pen = new Pen(Color.Red, 3); public void Draw(Graphics graphics) { graphics.DrawLine(Pen, new Point(StartX, StartY), new Point((int)X, (int)Y)); } } |
Form1クラスでは敵ミサイルの移動と生成をおこないます。Timer_Tickが何回呼び出されたかをあとで使用するので保存しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public partial class Form1 : Form { int TickCount = 0; List<EnemyMissile> EnemyMissiles = new List<EnemyMissile>(); private void Timer_Tick(object sender, EventArgs e) { EnemyMissiles = EnemyMissiles.Where(x => !x.IsDead).ToList(); CreateEnemy(); // 敵ミサイルを生成 MoveTarget(); MoveEnemies(); // 敵ミサイルを移動 CheckEnemyMissileLanding(); // 敵ミサイルが着弾したか調べる TickCount++; Invalidate(); } } |
敵ミサイルを生成する処理を示します。ミサイルが生成される座標はY座標は0、X座標は乱数で求めます。また新しいミサイルを生成するタイミングで落下中のミサイルのどれかを分岐させます。
TickCountが増えるとそれだけミサイルが生成される間隔が短くなり、落下速度もアップします。
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 partial class Form1 : Form { Random Random = new Random(); void CreateEnemy() { int i = 50 - TickCount / 50; // 敵ミサイルが生成される間隔 5より短くはならない if (i < 5) i = 5; // 敵ミサイルを生成する if (TickCount % i == 0) { // 敵ミサイルの落下速度 double speed = 1 + TickCount / 1000d; // 敵ミサイルの出現場所 int startX = Random.Next(this.Width); // 敵ミサイルの落下ポイント // ランダムに選んでいてはいつまでたっても終わらないので生き残っている都市を目指すが // ときどきそうではない場所にも落下させる List<City> cities = Cities.Where(x => !x.IsDead).ToList(); if (cities.Count != 0) { City city; if (Random.Next(3) == 0) city = cities[Random.Next(cities.Count)]; // 生き残っている都市が目標 else city = Cities[Random.Next(Cities.Count)]; // 6箇所から適当に選ぶ EnemyMissiles.Add(new EnemyMissile(startX, 0, city.CenterX, city.CenterY, speed)); // 落下中のミサイルのどれかを分岐させる if (EnemyMissiles.Count != 0) { EnemyMissile enemy = EnemyMissiles[Random.Next(EnemyMissiles.Count)]; if (enemy.TargetX != city.CenterX) EnemyMissiles.Add(new EnemyMissile((int)enemy.X, (int)enemy.Y, city.CenterX, city.CenterY, speed)); } } } } } |
敵のミサイルを落下させる処理を示します。もし都市が全滅している場合は早く終わらせるためにミサイルの落下速度を速くします。
1 2 3 4 5 6 7 8 9 10 11 12 |
public partial class Form1 : Form { void MoveEnemies() { foreach (EnemyMissile enemy in EnemyMissiles) { if (!Cities.Any(x => !x.IsDead)) enemy.SpeedUp = true; enemy.Update(); } } } |
CheckEnemyMissileLandingメソッドは敵ミサイルが都市に到達しているか貫通しているかどうかを調べます。そして該当するミサイルと都市が存在する場合はミサイルを爆発させて都市を消滅させます。
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 { WMPLib.WindowsMediaPlayer playerDamage = new WMPLib.WindowsMediaPlayer(); void CheckEnemyMissileLanding() { EnemyMissiles = EnemyMissiles.Where(x => !x.IsDead).ToList(); foreach (EnemyMissile missile in EnemyMissiles) { City city = null; // 敵ミサイルのY座標が都市の中心Y座標より大きければ貫通している // その場合はどの都市を貫通しているかを調べる if(missile.TargetY < missile.Y) city = Cities.First(x => x.CenterX == missile.TargetX); // 敵ミサイルが貫通している都市があればミサイルを爆発させて都市を消滅させる if (city != null) { city.IsDead = true; missile.IsDead = true; // 爆発の描画(次の記事を参照) Explosions.Insert(0, new Explosion(city.CenterX, city.CenterY, 20)); // 爆発の音を鳴らす playerDamage.URL = Application.StartupPath + "\\damage.mp3"; playerDamage.controls.play(); } } } } |
描画の処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public partial class Form1 : Form { protected override void OnPaint(PaintEventArgs e) { 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); } } |