今回は敵をつくります。Enemyクラスを作成します。
Contents
Enemyクラスの作成
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 |
public class Enemy : GameCharacter { static List<int> Textures = new List<int>(); public float X = 0; public float Z = 0; float Radius = 1.0f; public Enemy(float x, float z) { X = x; Z = z; // テクスチャを作成するのは最初の1回だけ。あとは使い回す。 if (Textures.Count == 0) Textures = CreateTextures(); } protected override List<Bitmap> GetBitmaps() { Bitmap bitmap = Properties.Resources.sprite2; List<Bitmap> bitmaps = new List<Bitmap>(); // 敵(ピンク色のひよこ) { 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(4, 94, 25, 28), GraphicsUnit.Pixel); g.Dispose(); bitmaps.Add(bitmap1); } return bitmaps; } public void Draw() { if (EnemyStatus == EnemyStatus.Dead) return; GL.PushMatrix(); { GL.Translate(X, 0, Z); GL.BindTexture(TextureTarget.Texture2D, Textures[0]); GL.Begin(BeginMode.Quads); { GL.Color3(Color.White); GL.TexCoord2(0, 0); GL.Vertex3(-Radius, Radius, 0); GL.TexCoord2(1, 0); GL.Vertex3(Radius, Radius, 0); GL.TexCoord2(1, 1); GL.Vertex3(Radius, -Radius, 0); GL.TexCoord2(0, 1); GL.Vertex3(-Radius, -Radius, 0); } GL.End(); GL.BindTexture(TextureTarget.Texture2D, 0); } GL.PopMatrix(); } } |
敵を移動させる
敵を描画するだけであればこれで完成です。しかし敵は攻撃をしかけてくるのでその部分も作る必要があります。
敵の攻撃はほとんど垂直に弾丸を発射しながら落下し、地面にあたるまえに進路を上方に変更して退却します。弾丸はまっすぐ下に落ちるものだけではなく斜めに飛ぶ物もあります。
退却のときに機雷をおとしていきます。機雷は卵をかぶったひよこをつかいます。ひよこが卵を産むというのはおかしいかもしれませんが、今回はそのようにします。
上方に退却すると編隊の一番後ろに配置されます。攻撃は前列から順におこなわれ、前列がいなくなると編隊全体が下に向かって移動していきます。
敵は上空で待機している状態、下にむけて攻撃をしている状態、上方へ退却している状態、自機によって撃墜されてしまった状態の4つがあります。そこで以下のような列挙体を作成します。
1 2 3 4 5 6 7 |
public enum EnemyStatus { Standby, Attack, Retreat, Dead, } |
退却してきた敵は最後尾に配置されますが、それぞれの敵が何列目にいるかわかるようにしておかないと最後尾がどの部分になるかわかりません。そこでEnemyクラスのフィールド変数にその敵が前(下)から何列目に配置されているかを格納しておきます。
Moveメソッドでは攻撃と退却時の動きだけ実装しています。待機状態でも前列の敵は前後(というか上下)に往復運動をしているのですが、この部分はGameManagerクラスで実装します。
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 |
public class Enemy : GameCharacter { public EnemyStatus EnemyStatus = EnemyStatus.Standby; public int Line = 0; // 何列目に配置されているか? public bool IsAttackRight = true; // 攻撃時は右方向か逆か? int MoveDownCount = 0; int MoveUpCount = 0; int ShotCount = 0; // 弾丸を何回発射したか? // 攻撃と退却時の動きだけ public void Move() { if (EnemyStatus == EnemyStatus.Dead) return; if (EnemyStatus == EnemyStatus.Standby) return; if (EnemyStatus == EnemyStatus.Attack) { MoveDownCount++; if (IsAttackRight) X += 0.05f; else X -= 0.05f; Z += 0.3f; // 弾丸を発射する。EnemyBurretクラスとGameManager.EnemyBurretsは後述 if (MoveDownCount % 20 == 0) { ShotCount++; if (ShotCount != 3) GameManager.EnemyBurrets.Add(new EnemyBurret(X, Z, 0, 0.7f)); else { if (IsAttackRight) GameManager.EnemyBurrets.Add(new EnemyBurret(X, Z, 0.3f, 0.5f)); else GameManager.EnemyBurrets.Add(new EnemyBurret(X, Z, -0.3f, 0.5f)); } } } else { if (Z > -30) { if (IsAttackRight) X += 0.05f; else X -= 0.05f; Z -= 0.3f; MoveUpCount++; if (MoveUpCount % 60 == 0) { // 退却時にときどき機雷を投下させる。GameManager.DropMineメソッドは後述 GameManager.DropMine(this); } } else { // Z座標が-30まで退却したら編隊の後部に再配置する // GameManager.SetNewPositionメソッドは後述 EnemyStatus = EnemyStatus.Standby; MoveDownCount = 0; MoveUpCount = 0; GameManager.SetNewPosition(this); } } // Z座標が15になるまで下降したら退却する if (Z > 15) EnemyStatus = EnemyStatus.Retreat; } } |
当たり判定
自機の弾丸に当たったかどうかを判定する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class Enemy : GameCharacter { public bool IsHited(List<JikiBurret> jikiBurrets) { jikiBurrets = jikiBurrets.Where(x => !x.IsDead).ToList(); if (jikiBurrets.Count == 0) return false; foreach (JikiBurret burret in jikiBurrets) { double a = Math.Pow(this.Radius + burret.Radius, 2); double b = Math.Pow(this.X - burret.X, 2) + Math.Pow(this.Z - burret.Z, 2); if (a > b) { burret.IsDead = true; return true; } } return false; } } |
敵弾を描画するためのEnemyBurretクラス
次に敵の弾丸を描画するためのEnemyBurretクラスを示します。直線を描画することでこれを敵弾とします。
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 class EnemyBurret { public float X = 0; public float Z = 0; public float VecX = 0; public float VecZ = 0; public EnemyBurret(float x, float z, float vecX, float vecZ) { X = x; Z = z; VecX = vecX; VecZ = vecZ; } public void Move() { X += VecX; Z += VecZ; } public void Draw() { double ang = Math.Atan2(VecX, VecZ); float x0 = (float)(X + 1 * Math.Sin(ang)); float z0 = (float)(Z + 1 * Math.Cos(ang)); GL.LineWidth(2); GL.Begin(BeginMode.Lines); GL.Color3(Color.White); GL.Vertex3(X, 0, Z); GL.Vertex3(x0, 0, z0); GL.End(); GL.LineWidth(1); } } |
GameManagerクラスにおける処理
GameManagerクラスの処理ですが、まずゲームが開始されたときに敵を表示させる処理をおこないます。
初期位置に敵を表示させる
敵は縦5列、横9列です。初期状態で何列目に存在するかで初期座標を求めます。
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 static class GameManager { static List<Enemy> Enemies = new List<Enemy>(); const int RowMax = 5; const int ColumMax = 9; public static Random Random = new Random(); public static void Init() { Enemies.Clear(); InitEnemies(); } static void InitEnemies() { UpdateCount = 0; for (int row = 0; row < RowMax; row++) { for (int colum = 0; colum < ColumMax; colum++) { Enemy enemy = new Enemy(-12 + colum * 3, -16 - 3 * row); enemy.Line = row; Enemies.Add(enemy); } } } } |
攻撃を開始させる
次に敵に攻撃を開始させる処理を示します。
1000 / 60 * 40秒おきに乱数で攻撃を開始する敵を求め、これを攻撃に参加させます。
まず最初に待機状態の敵のリストを取得してそのなかからEnemy.Lineの最小値を求めます。この最小値がEnemy.Lineである敵が攻撃開始可能な敵です。あとはこのなかから適当に選びます。
攻撃に参加する敵がきまったらEnemyStatusをAttackに変更して、右側に向かうか左に向かうかを決めます。あとはEnemy.Moveメソッドで処理がおこなわれます。攻撃を開始した場合、編隊全体を下方向に前進させます。
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 { static void EnemyBeginAttack() { List<Enemy> enemies = Enemies.Where(x => x.EnemyStatus == EnemyStatus.Standby).ToList(); if (enemies.Count == 0) return; int line = enemies.Min(x => x.Line); if (UpdateCount % 40 == 10) { enemies = Enemies.Where(x => x.EnemyStatus == EnemyStatus.Standby && x.Line == line).ToList(); if (enemies.Count > 0) { // 攻撃に参加する敵はどれか? int r = Random.Next(enemies.Count); enemies[r].EnemyStatus = EnemyStatus.Attack; enemies[r].IsAttackRight = Random.Next(2) == 0 ? true : false; // 全体を前進させる enemies = Enemies.Where(x => x.EnemyStatus == EnemyStatus.Standby).ToList(); foreach (Enemy enemy in enemies) enemy.Z += 3.0f / ColumMax; } } } } |
待機状態の敵の動き
待機状態にある敵の動きに関する処理を示します。上下に移動するのは前の2列だけです。そこで該当する敵を取得します。前の2列がどの敵を指すのかは攻撃を開始する敵や自機に撃墜されることで変化していきます。上下に移動している敵が一番上にきているタイミングで前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 |
public static class GameManager { static int FrontLine = 0; static void MoveStandbyEnemies() { // 前2列が一番上にきているタイミングで前2列の敵を取得する List<Enemy> enemies = Enemies.Where(x => x.EnemyStatus == EnemyStatus.Standby).ToList(); if (enemies.Count == 0) return; int line = enemies.Min(x => x.Line); if (UpdateCount % 120 == 0) FrontLine = line; List<Enemy> frontLineEnemies = Enemies.Where(x => x.EnemyStatus == EnemyStatus.Standby && (x.Line == FrontLine || x.Line == FrontLine + 1)).ToList(); // 1秒間下に移動、そのあと上に移動 foreach (Enemy enemy in frontLineEnemies) { int cnt = UpdateCount % 120; if (cnt < 60) enemy.Z += 0.10f; else enemy.Z -= 0.10f; } } } |
敵に動き全体をまとめるとこうなります。自機が死亡しているあいだは敵が新たに攻撃をしかけてくることはありません。これは立て続けに自機が死ぬことを避けるためです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public static class GameManager { static bool IsJikiDead = false; static void MoveEnemies() { if (Enemies.Count == 0) return; foreach (Enemy enemy in Enemies) enemy.Move(); // 攻撃開始 if (!IsJikiDead) EnemyBeginAttack(); // 前2列を上下に移動させる MoveStandbyEnemies(); } } |
敵が退却したら編隊の後列に再配置されます。そのための処理を示します。
Enemy.Lineの最大値を求め、これと同じ敵がどれだけ存在するか調べます。もしその数がGameManager.ColumMaxよりも小さければ空きがあるということなのでその列に再配置します。もし空きがあければその後ろの列に配置します。最初は中央に、それ以降は左右交互の位置に配置できるようにNewPositionXという配列を作っています。
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 static class GameManager { static int[] NewPositionX = new int[] { 4, 3, 5, 2, 6, 1, 7, 0, 8 }; public static void SetNewPosition(Enemy enemy) { List<Enemy> enemies = Enemies.Where(x => x.EnemyStatus == EnemyStatus.Standby).ToList(); if (enemies.Count > 0) { int line = enemies.Max(x => x.Line); enemies = enemies.Where(x => x.Line == line).ToList(); if (enemies.Count < ColumMax) { enemy.Line = line; enemy.Z = enemies[0].Z; enemy.X = -12 + 3 * NewPositionX[enemies.Count]; } else { enemy.Line = line + 1; enemy.Z = enemies[0].Z - 3; enemy.X = -12 + 3 * NewPositionX[0]; } } else { enemy.Line = 1; enemy.Z = -16; enemy.X = -12 + 3 * NewPositionX[0]; } enemy.EnemyStatus = EnemyStatus.Standby; } } |
敵弾と機雷の投下
次に敵の弾丸を移動させる処理を示します。敵が攻撃を開始するとEnemy.MoveメソッドのなかでGameManager.EnemyBurretsに弾丸オブジェクトを格納するのでforeach文で移動処理をおこなうだけです。
1 2 3 4 5 6 7 8 9 10 |
public static class GameManager { public static List<EnemyBurret> EnemyBurrets = new List<EnemyBurret>(); static void MoveEnemyBurrets() { foreach (EnemyBurret burret in EnemyBurrets) burret.Move(); } } |
それから機雷投下の処理も必要です。
まず機雷を描画するためのクラスを示します。
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 |
public class Mine : GameCharacter { static List<int> Textures = new List<int>(); public float X = 0; public float Z = 0; public float Radius = 1.0f; public bool IsDead = false; public Mine(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(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(4, 260, 25, 28), GraphicsUnit.Pixel); g.Dispose(); bitmaps.Add(bitmap1); } return bitmaps; } public void Draw() { 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, 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(); } public bool IsHited(List<JikiBurret> jikiBurrets) { jikiBurrets = jikiBurrets.Where(x => !x.IsDead).ToList(); if (jikiBurrets.Count == 0) return false; foreach (JikiBurret burret in jikiBurrets) { double a = Math.Pow(this.Radius + burret.Radius, 2); double b = Math.Pow(this.X - burret.X, 2) + Math.Pow(this.Z - burret.Z, 2); if (a > b) { burret.IsDead = true; return true; } } return false; } } |
機雷を投下する示します。Enemy.Moveメソッドのなかで退却時にGameManager.DropMineメソッドが呼び出されます。そのとき50%の確率で機雷を投下します。ただし敵のX座標の絶対値が15を超えると自機の移動範囲を超えるのでその場合は投下しません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public static class GameManager { public static List<Mine> Mines = new List<Mine>(); static int ProbabilityOfDropBomb = 50; public static void DropMine(Enemy enemy) { if (Math.Abs(enemy.X) >= 15 || enemy.Z < -10) return; if (Random.Next(100) < ProbabilityOfDropBomb) Mines.Add(new Mine(enemy.X, enemy.Z)); } } |
機雷を移動させる処理を示します。Z座標が21を超えると着弾したことになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public static class GameManager { static void MoveMines() { Mines.Select(x => x.Z += 0.2f).ToList(); Mine mine = Mines.FirstOrDefault(x => !x.IsDead && x.Z > 21); if (mine != null) { mine.IsDead = true; OnMineLanding(); } } } |
機雷の着弾
機雷が着弾したときの処理を示します。機雷が着弾したときForm1クラスで処理ができるようにイベントを発生させています。GameOverになっているときはイベントは発生しません。現在GameOverなのかどうかは残機が0かどうかで判断しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public static class GameManager { public delegate void MineLandingHandler(); public static event MineLandingHandler MineLanding; static void OnMineLanding() { if(!IsGameOver()) MineLanding?.Invoke(); } public static int RestMax = 3; public static int Rest = RestMax; static bool IsGameOver() { return Rest == 0; } } |
GameManager.UpdateメソッドとGameManager.Drawメソッド
これまでをまとめるとこうなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public static class GameManager { public static void Update() { UpdateCount++; MoveJiki(); MoveEnemies(); MoveEnemyBurrets(); MoveMines(); } } |
それから描画の処理も必要です。
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 static class GameManager { public static void Draw() { SetSight(); DrawField(); DrawEnemies(); DrawEnemyBurrets(); DrawMine(); if (!IsJikiDead) Jiki.Draw(); } public static void DrawEnemies() { foreach (Enemy enemy in Enemies) enemy.Draw(); } public static void DrawEnemyBurrets() { foreach (EnemyBurret burret in EnemyBurrets) burret.Draw(); } public static void DrawMine() { foreach (Mine mine in Mines) mine.Draw(); } } |