敵を攻撃して撃墜できるようにします。また敵弾に接触するとミスとなります。今回はこの機能を追加します。
Contents
爆発を描画するためのExplosionクラス
そのまえに爆発を描画するためのクラスを作成します。これは弾丸の当たり判定と爆発の処理 3Dっぽい縦シューティングゲームをつくるとほとんど同じです。
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 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
public class Explosion : GameCharacter { float X = 0; float Z = 0; float VecX = 0; float VecY = 0; float Radius = 1.0f; public bool IsDead = false; int MoveCount = 0; public Explosion(float x, float y, float vecX, float vecY) { X = x; Z = y; VecX = vecX; VecY = vecY; if (Textures.Count == 0) Textures = CreateTextures(); } static List<int> Textures = new List<int>(); protected override List<Bitmap> GetBitmaps() { Bitmap bitmap = Properties.Resources.sprite2; List<Bitmap> bitmaps = new List<Bitmap>(); // 爆発のテクスチャ 0 { Bitmap bitmap1 = new Bitmap(25, 28); Graphics g = Graphics.FromImage(bitmap1); g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor; g.DrawImage(bitmap, new Rectangle(0, 0, 25, 28), new Rectangle(17, 340, 24, 27), GraphicsUnit.Pixel); g.Dispose(); bitmaps.Add(bitmap1); } // 爆発のテクスチャ 1 { Bitmap bitmap1 = new Bitmap(25, 28); Graphics g = Graphics.FromImage(bitmap1); g.DrawImage(bitmap, new Rectangle(0, 0, 25, 28), new Rectangle(44, 340, 30, 30), GraphicsUnit.Pixel); g.Dispose(); bitmaps.Add(bitmap1); } // 爆発のテクスチャ 2 { Bitmap bitmap1 = new Bitmap(25, 28); Graphics g = Graphics.FromImage(bitmap1); g.DrawImage(bitmap, new Rectangle(0, 0, 25, 28), new Rectangle(79, 340, 32, 32), GraphicsUnit.Pixel); g.Dispose(); bitmaps.Add(bitmap1); } // 爆発のテクスチャ 3 { Bitmap bitmap1 = new Bitmap(25, 28); Graphics g = Graphics.FromImage(bitmap1); g.DrawImage(bitmap, new Rectangle(0, 0, 25, 28), new Rectangle(116, 340, 36, 36), GraphicsUnit.Pixel); g.Dispose(); bitmaps.Add(bitmap1); } // 爆発のテクスチャ 4 { Bitmap bitmap1 = new Bitmap(25, 28); Graphics g = Graphics.FromImage(bitmap1); g.DrawImage(bitmap, new Rectangle(0, 0, 25, 28), new Rectangle(153, 340, 34, 34), GraphicsUnit.Pixel); g.Dispose(); bitmaps.Add(bitmap1); } // 爆発のテクスチャ 5 { Bitmap bitmap1 = new Bitmap(25, 28); Graphics g = Graphics.FromImage(bitmap1); g.DrawImage(bitmap, new Rectangle(0, 0, 25, 28), new Rectangle(188, 340, 28, 30), GraphicsUnit.Pixel); g.Dispose(); bitmaps.Add(bitmap1); } return bitmaps; } public void Move() { MoveCount++; X += VecX; Z += VecY; } public void Draw() { GL.PushMatrix(); { GL.Translate(X, 0, Z); if (MoveCount < 4) GL.BindTexture(TextureTarget.Texture2D, Textures[0]); else if (MoveCount < 8) GL.BindTexture(TextureTarget.Texture2D, Textures[1]); else if (MoveCount < 12) GL.BindTexture(TextureTarget.Texture2D, Textures[2]); else if (MoveCount < 16) GL.BindTexture(TextureTarget.Texture2D, Textures[3]); else if (MoveCount < 20) GL.BindTexture(TextureTarget.Texture2D, Textures[4]); else if (MoveCount < 24) GL.BindTexture(TextureTarget.Texture2D, Textures[5]); else { IsDead = true; return; } GL.Begin(BeginMode.Quads); { GL.Normal3(Vector3.UnitY); GL.TexCoord2(1, 0); GL.Vertex3(Radius, Radius, 0); GL.TexCoord2(0, 0); GL.Vertex3(-Radius, Radius, 0); GL.TexCoord2(0, 1); GL.Vertex3(-Radius, -Radius, 0); GL.TexCoord2(1, 1); GL.Vertex3(Radius, -Radius, 0); } GL.End(); GL.BindTexture(TextureTarget.Texture2D, 0); } GL.PopMatrix(); } } |
自機から弾丸を発射する
自機から弾丸を発射する処理を実装していなかったので、ここで実装します。
まずは自機から発射された弾丸を描画するためのクラスを示します。
JikiBurretクラス
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 |
public class JikiBurret : GameCharacter { static List<int> Textures = new List<int>(); public float X = 0; public float Z = 0; public float Radius = 0.3f; public bool IsDead = false; public JikiBurret(float x, float z) { if (Textures.Count == 0) Textures = CreateTextures(); X = x; Z = z; } protected override List<Bitmap> GetBitmaps() { Bitmap bitmap = Properties.Resources.sprite2; List<Bitmap> bitmaps = new List<Bitmap>(); Bitmap bitmap1 = new Bitmap(5, 5); Graphics g = Graphics.FromImage(bitmap1); g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor; g.DrawImage(bitmap, new Rectangle(0, 0, 5, 5), new Rectangle(4, 50, 5, 5), GraphicsUnit.Pixel); g.Dispose(); bitmaps.Add(bitmap1); return bitmaps; } public void Move() { if (IsDead) return; Z -= 0.5f; } public void Draw() { if (IsDead) return; GL.PushMatrix(); { GL.Translate(X, 0, Z); GL.BindTexture(TextureTarget.Texture2D, Textures[0]); GL.Begin(BeginMode.Quads); { GL.Normal3(Vector3.UnitY); GL.TexCoord2(1, 0); GL.Vertex3(Radius, 0, Radius); GL.TexCoord2(0, 0); GL.Vertex3(-Radius, 0, Radius); GL.TexCoord2(0, 1); GL.Vertex3(-Radius, 0, -Radius); GL.TexCoord2(1, 1); GL.Vertex3(Radius, 0, -Radius); } GL.End(); GL.BindTexture(TextureTarget.Texture2D, 0); } GL.PopMatrix(); } } |
弾丸を発射する処理を示します。Shotメソッドが実行されるとJikiBurretsに弾丸オブジェクトが格納されます。あとはMoveJikiBurretsメソッドが呼び出されると上へ飛んでいきます。
それからこれは弾丸をバンバン発射するシューティングゲームではないので連射はできない仕様にしています。JikiBurretsのなかに弾丸オブジェクトがある場合はなにもおきません。Shotメソッドは弾丸を発射することができたかどうかその結果も返します。
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 static class GameManager { public static List<JikiBurret> JikiBurrets = new List<JikiBurret>(); public static bool Shot() { // 連射できないようにする。当然のことながら自機死亡時にも弾丸は発射されない。 if (JikiBurrets.Count == 0 && !IsJikiDead) { JikiBurret burret = new JikiBurret(Jiki.X, 20); JikiBurrets.Add(burret); return true; } return false; } static void MoveJikiBurrets() { foreach (JikiBurret burret in JikiBurrets) { burret.Move(); } } } |
自機から発射された弾丸が命中したときの処理
次に自機から発射された弾丸が敵に当たったかどうかの判定と、命中した場合の処理を示します。
敵に命中したときは爆発の処理をするとともにScoredイベントを発生させます。イベントハンドラの引数は追加される点数です。待機中の敵と退却中の敵は30点、攻撃中の敵は50点にします。
撃墜した場合、敵は全滅しているかもしれません。その場合は次のステージに進みます。
自機から発射された弾丸の当たり判定
自機から発射された弾丸の当たり判定の処理を示します。
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 static class GameManager { public delegate void ScoredHandler(int add); public static event ScoredHandler Scored; static void CheckJikiBurretsHit() { JikiBurrets = JikiBurrets.Where(x => !x.IsDead && x.Z > -16).ToList(); Enemies = Enemies.Where(x => x.EnemyStatus != EnemyStatus.Dead).ToList(); Mines = Mines.Where(x => !x.IsDead).ToList(); Enemy hitedEnemy = Enemies.FirstOrDefault(x => x.IsHited(JikiBurrets)); if (hitedEnemy != null) { // 敵に命中したらイベントを発生させる if (hitedEnemy.EnemyStatus == EnemyStatus.Standby) Scored?.Invoke(30); else if (hitedEnemy.EnemyStatus == EnemyStatus.Attack) Scored?.Invoke(50); else if (hitedEnemy.EnemyStatus == EnemyStatus.Retreat) Scored?.Invoke(30); // 敵を死んだことにする hitedEnemy.EnemyStatus = EnemyStatus.Dead; // 爆発させる CreateExplosion(hitedEnemy.X, hitedEnemy.Z); // 敵が全滅したかどうか調べる CheckClear(); } Mine mine = Mines.FirstOrDefault(x => x.IsHited(JikiBurrets)); if (mine != null) { mine.IsDead = true; Scored?.Invoke(30); CreateExplosion(mine.X, mine.Z); } } } |
爆発の処理
爆発の処理を示します。爆発が4方向へ広がるように、そして乱数をつかって単調な爆発描画にならないようにしています。
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 |
public static class GameManager { public static List<Explosion> Explosions = new List<Explosion>(); static void CreateExplosion(float x, float y) { // 4方向へ広がるように Explosions.Add(new Explosion(x, y, 0.1f, 0.1f)); Explosions.Add(new Explosion(x, y, -0.1f, -0.1f)); Explosions.Add(new Explosion(x, y, -0.1f, 0.1f)); Explosions.Add(new Explosion(x, y, 0.1f, -0.1f)); // 上の処理だけでは単調な爆発描画しかできないので乱数で広がる量を決める for (int i = 0; i < 10; i++) { int r = Random.Next(10); float rx = (r - 5) * 0.02f; r = Random.Next(10); float ry = (r - 5) * 0.02f; Explosions.Add(new Explosion(x, y, rx, ry)); } } // 爆発を中心から周囲に移動させる public static void MoveExplosions() { Explosions = Explosions.Where(x => !x.IsDead).ToList(); foreach (Explosion explosion in Explosions) explosion.Move(); } // 爆発を描画する public static void DrawExplosions() { foreach (Explosion explosion in Explosions) explosion.Draw(); } } |
敵が全滅したら次ステージへ
敵が全滅したかどうか調べる処理を示します。全滅している場合は3秒後にInitEnemiesメソッドを実行して初期状態と同じように敵を生成します。
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 |
public static class GameManager { public delegate void StageClearedHandle(); public static event StageClearedHandle StageCleared; static void CheckClear() { if (!Enemies.Any(x => x.EnemyStatus != EnemyStatus.Dead)) { StageCleared?.Invoke(); Timer timer = new Timer(); timer.Interval = 3000; timer.Tick += Timer_Tick; timer.Start(); } void Timer_Tick(object sender, EventArgs e) { Timer t = (Timer)sender; t.Stop(); t.Dispose(); InitEnemies(); } } } |
被弾したときの処理
敵にやられたときの処理も必要です。敵の弾丸や機雷に当たった場合はミスとなります。
自機被弾を判定するCheckJikiDeadメソッド
自機が被弾したかどうかを判定するCheckJikiDeadメソッドを示します。
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 static class GameManager { static void CheckJikiDead() { // すでに死亡しているときは処理をおこなわない if (IsJikiDead) return; Mines = Mines.Where(x => !x.IsDead && x.Z < 25).ToList(); EnemyBurrets = EnemyBurrets.Where(x => x.Z < 25).ToList(); foreach (Mine mine in Mines) { double r = Math.Pow(Jiki.Radius + mine.Radius, 2); double dis = Math.Pow(Jiki.X - mine.X, 2) + Math.Pow(Jiki.Z - mine.Z, 2); if (dis < r) { OnJikiDead(); return; } } foreach (EnemyBurret burret in EnemyBurrets) { double r = Math.Pow(Jiki.Radius, 2); double dis = Math.Pow(Jiki.X - burret.X, 2) + Math.Pow(Jiki.Z - burret.Z, 2); if (dis < r) { OnJikiDead(); return; } } } } |
自機死亡時の処理
自機が死亡した場合の処理を示します。自機死亡の場合はイベントを発生させます。残機を1減らして0になっていなければ3秒後にゲームを再開します。残機が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 40 41 |
public static class GameManager { public delegate void JikiDeadHandler(); public static event JikiDeadHandler JikiDead; public static int RestMax = 3; public static int Rest = RestMax; public static void OnJikiDead() { CreateExplosion(Jiki.X, Jiki.Z); IsJikiDead = true; Rest--; JikiDead?.Invoke(); if (Rest == 0) { OnGameOver(); return; } Timer timer = new Timer(); timer.Interval = 3000; timer.Tick += Timer_Tick; timer.Start(); static void Timer_Tick(object sender, EventArgs e) { Timer t = (Timer)sender; t.Stop(); t.Dispose(); Jiki.X = 0; Mines.Clear(); EnemyBurrets.Clear(); IsJikiDead = false; } } } |
ゲームオーバー時の処理
ゲームオーバーになったときの処理です。これもイベントを発生させます。
1 2 3 4 5 6 7 8 9 10 |
public static class GameManager { public delegate void GameOverHandler(); public static event GameOverHandler GameOver; static void OnGameOver() { GameOver?.Invoke(); } } |
それからゲームをもう一回するときのメソッドも作成しておきます。残機を最大数に戻してIsJikiDeadをfalseに変更して敵と敵弾、機雷をクリアします。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public static class GameManager { public static void GameRetry() { Rest = RestMax; IsJikiDead = false; Enemies.Clear(); EnemyBurrets.Clear(); Mines.Clear(); JikiBurrets.Clear(); InitEnemies(); } } |
あとは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 |
public static class GameManager { public static void Update() { UpdateCount++; MoveJiki(); CheckJikiDead(); MoveEnemies(); MoveEnemyBurrets(); MoveMines(); MoveJikiBurrets(); CheckJikiBurretsHit(); MoveExplosions(); } public static void Draw() { SetSight(); DrawField(); DrawEnemies(); DrawEnemyBurrets(); DrawMine(); if (!IsJikiDead) Jiki.Draw(); DrawJikiBurrets(); DrawExplosions(); } } |
Form1クラスでイベント処理をする
Form1クラスでイベントの処理をおこないます。ついでに音も鳴らしましょう。
まずイベント処理ができるようにイベントハンドラを追加します。
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 |
public partial class Form1 : Form { WMPLib.WindowsMediaPlayer Player = new WMPLib.WindowsMediaPlayer(); string pathShotSound = ""; string pathHitSound = ""; string pathDeadSound = ""; protected override void OnLoad(EventArgs e) { InitForm(); Score = 0; // 効果音のmp3ファイルのパスを取得する pathDeadSound = Application.StartupPath + "\\sound\\dead.mp3"; pathHitSound = Application.StartupPath + "\\sound\\hit.mp3"; pathShotSound = Application.StartupPath + "\\sound\\shot.mp3"; GameManager.Init(); // イベントハンドラを追加 GameManager.Scored += GameManager_Scored; GameManager.JikiDead += GameManager_JikiDead; GameManager.MineLanding += GameManager_MineLanding; GameManager.StageCleared += GameManager_StageCleared; GameManager.GameOver += GameManager_GameOver; // ダメージメーターをつくる(後回し) base.OnLoad(e); } private void GameManager_Scored(int add) { } private void GameManager_JikiDead() { } private void GameManager_MineLanding() { } private void GameManager_StageCleared() { } private void GameManager_GameOver() { } } |
スコアと残機の表示
スコアと残機を表示するための処理を示します。得点が追加されたり残機が変更されたら表示されているスコアと残機数を変更します。
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 _score = 0; int Score { get { return _score; } set { _score = value; ShowScore(); } } void ShowScore() { label1.Text = String.Format("Score {0:00000} 残 {1}", _score, GameManager.Rest); } private void GameManager_Scored(int add) { Player.URL = pathHitSound; Score += add; } } |
自機が死亡したら0.1秒間隔で10回背景の色を変えます。
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 |
public partial class Form1 : Form { private void GameManager_JikiDead() { Player.URL = pathDeadSound; // 残機の表示を変更 ShowScore(); // 背景の色を変える。0.1秒間隔で10回 int count = 0; Timer timer = new Timer(); timer.Interval = 100; timer.Tick += Timer_Tick1; timer.Start(); void Timer_Tick1(object sender, EventArgs e) { glControlEx1.ChangeBackColor(Color.Blue, 50); if (count > 10) { Timer t = (Timer)sender; t.Stop(); t.Dispose(); } count++; } } } |
スペースキーがおされたときは実際に自機から弾丸が発射されたことを確認してから発射音を鳴らします。
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 OnKeyDown(KeyEventArgs e) { if (e.KeyCode == Keys.Left) GameManager.MoveLeft = true; if (e.KeyCode == Keys.Right) GameManager.MoveRight = true; if (e.KeyCode == Keys.Space) { if (GameManager.Shot()) Player.URL = pathShotSound; } if (e.KeyCode == Keys.S) Retry(); base.OnKeyDown(e); } } |
機雷が着弾したときの処理は次回示します。