今回はボス敵をつくります。大型で少々の命中弾では死にません。
ボス敵の動きは左右にうごきます。そして表示されるY座標は視点のY座標と同じように増えていきます。そのため加速していない自機と相対速度は同じです。ボスにはザコ敵とは比べものにならないくらい派手に弾丸を発射してほしいのですが、この処理は次回おこないます。
以下のようなクラスを作成します。EnemyBaseクラスを継承しています。Move()メソッドが実行されるたびに左または右に0.1移動するのですが、そのとき右に移動するのか左に移動するかを決めるフィールド変数を持たせます。またX座標の範囲は-3より大きく3より小さくします。移動できる限界点に到達したら移動方向を逆にします。
ザコ敵と自機のZ座標は0にしているのですが、ボスは大きいのでZ座標を0にしてしまうと、フィールドに描画されている直線との関係で違和感があります。そこでZ座標は +2 とします。
それからプログラミング講座 第33回【シューティングゲーム作成(13)弾幕/JavaScript】と同様、ボスは画面の上部から下りてくるようにしてあらわれます。動画では黄色いひよこをつかっていますが、こちらでは青いサングラスをしたひよこを使います。こっちのほうがボスらしく見えませんか?
またボスが下りてくる間は双方、攻撃はできません。
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 |
public class Boss : EnemyBase { float InitZ = 10f; float Z = 0; bool isRight = true; // ボスは右に動くか左に動くか? 最初は右 public bool IsReady = false; // 戦闘準備は完了したか? int MaxLife = 0; public Boss(float x, float y, float radius, int life, int score) { X = x; Y = y; Z = InitZ; MaxLife = life; Life = life; IsDead = false; Score = score; Radius = radius; VecX = 0.1f; VecY = Form1.Speed; 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(140, 156); Graphics g = Graphics.FromImage(bitmap1); g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor; g.DrawImage(bitmap, new Rectangle(0, 0, 140, 156), new Rectangle(337, 300, 140, 156), GraphicsUnit.Pixel); g.Dispose(); bitmaps.Add(bitmap1); } return bitmaps; } public override void Move() { // 生成されたばかりのボスは上方にいるので0.1fずつ下に移動させる if(Z > 2) { Z -= 0.1f; Y += Form1.Speed; return; } // 上のif文が実行されなくなったら戦闘準備完了 IsReady = true; // 戦闘中のボスのZ座標は 2f とする Z = 2f; // X座標の範囲は-3より大きく3より小さい。 // 移動できる限界点に到達したら移動方向を転換する if(X > 3) isRight = false; if(X < -3) isRight = true; // 左または右に0.1移動する if(isRight) VecX = 0.1f; else VecX = -0.1f; // 周囲に弾幕を張るように弾丸を配置(内容は次回考える) Shot(); base.Move(); } public override void Draw() { GL.PushMatrix(); { GL.Translate(X, Y, 2); GL.Material(MaterialFace.Front, MaterialParameter.Ambient, Color.White); GL.Material(MaterialFace.Front, MaterialParameter.Diffuse, Color.White); 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(); } } |
ではForm1クラスにボスを描画するための機能を追加します。
ゲームが進行してBaseYが一定の値(今回は500とする)まで増加すると、ザコ敵をつくるのをやめます。そして現在存在するザコ敵が撃墜されるか描画対象領域からいなくなったらボスを出現させます。
ボスのLifeが0になったらボスを爆発させて消滅させる必要がありますが、それだけだと死んだはずのボスが何回も繰り返し出現することになるので現在対ボス戦なのかどうかを示すフィールド変数をつくります。
ボスが出現するのは
BaseYが一定の値を超えている
まだ対ボス戦ははじまっていない
という2つの条件を満たすときです。ボスが死ぬときはボスらしく激しく大爆発してもらい、爆発がおさまったらBaseYを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 42 43 44 45 46 47 48 49 50 |
public partial class Form1 : Form { bool IsBattleAntiBoss = false; new void Update() { BaseY += Speed; if(Jiki != null && !Jiki.IsDead) { Jiki.Move(); // BaseYが一定の値まで増えるとザコ敵をつくるのをやめる if(BaseY < 500) { CreateEnemies(); } else { // ザコ敵がいなくなったらボスを登場させる if(Enemies.Count == 0 && !IsBattleAntiBoss) { // 対ボス戦開始 BeginBattleAntiBoss(); } } } MoveEnemies(); MoveJikiBurrets(); MoveEnemyBurrets(); HitJudeJikiBurrets(); HitJudeEnemyBurrets(); MoveExplosions(); JikiBurrets = JikiBurrets.Where(x => !IsOutOfField(x) && !x.IsDead).ToList(); Enemies = Enemies.Where(x => !IsOutOfField(x) && !x.IsDead).ToList(); EnemyBurrets = EnemyBurrets.Where(x => !IsOutOfField(x) && !x.IsDead).ToList(); Explosions = Explosions.Where(x => !x.IsDead).ToList(); } void BeginBattleAntiBoss() { IsBattleAntiBoss = true; Enemies.Add(new Boss(0, 10 + BaseY, 3, 500, 1000)); } } |
次にボスの移動ですが、ボスのオブジェクトもEnemiesのなかに格納されているのでMoveEnemies()を普通に呼び出すだけでうごきます。
自機の弾丸に命中した敵がみつかったら、それはボスなのかどうか調べます。そしてボスのときはOnBurretHitBoss(Boss boss, JikiBurret burret)メソッドを呼び出します。ボスの死亡判定もこのなかでおこないます。ボスの死亡が確認されたらボスらしく激しく爆発して消えてもらいましょう。
そのあと現在対ボス戦なのかどうかを示すフィールド変数をfalseにしてステージの開始位置に自機と視点を戻すのですが、爆発処理が終わるより先にこの処理をしてしまうと爆発の様子が描画されないので、ボス死亡のあと少しインターバルを置いて以下の処理をおこなっています。
1 2 3 |
IsBattleAntiBoss = false; // いまはもう対ボス戦ではない BaseY = 0; // 視点の位置をスタート地点に戻す Jiki.Y = 0; // 自機のY座標をリセットする |
ではボスを攻撃するときの処理をみてみましょう。
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 partial class Form1 : Form { void HitJudeJikiBurrets() { foreach(JikiBurret burret in JikiBurrets) { foreach(EnemyBase enemy in Enemies) { if(Math.Pow(burret.Radius + enemy.Radius, 2) > Math.Pow(burret.X - enemy.X, 2) + Math.Pow(burret.Y - enemy.Y, 2)) { // 自機の弾丸が当たったのはボスかそれともザコ敵か? if(enemy is Boss) OnBurretHitBoss((Boss)enemy, burret); else OnBurretHitEnemy(enemy, burret); break; } } } } void OnBurretHitBoss(Boss boss, JikiBurret burret) { if(!boss.IsReady) return; boss.Life--; burret.IsDead = true; if(boss.IsDead) { // ボスを倒した場合はボスを大爆発させる OnBossDead(boss); } else { // 単なる命中の場合は自機弾丸を中心に小爆発させる Explosions.Add(new Explosion(burret.X, burret.Y, 0, 0, 0.5f)); } } void OnBossDead(Boss boss) { // ボスを倒した場合はボスを大爆発させる BigExplosionWhenBossDead(boss.X, boss.Y); // 少し時間をおいてからボス死亡後の処理、すなわち // IsBattleAntiBoss = false; // BaseY = 0; // Jiki.Y = 0; を実行する Timer timer = new Timer(); timer.Interval = 2000; timer.Tick += Timer_Tick1; timer.Start(); void Timer_Tick1(object sender, EventArgs e) { Timer t = (Timer)sender; t.Stop(); t.Dispose(); AfterBossDead(); } } void AfterBossDead() { IsBattleAntiBoss = false; BaseY = 0; Jiki.Y = 0; } } |
ボスを爆発させるメソッドを示します。通常の大爆発はX方向とY方向に広がるのですが、それだとあまり大きな爆発には見えないのでX方向とZ方向に広がるようにしています。また広がりも大きくするためにExplosionクラスを継承して新しく作成したExplosion2クラスを使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public partial class Form1 : Form { // BigExplosionメソッドの倍の値を設定している void BigExplosionWhenBossDead(float x, float y) { for(int i = 0; i < 10; i++) { int r = Random.Next(10); float rx = (r - 5) * 0.04f; r = Random.Next(10); float ry = (r - 5) * 0.04f; Explosions.Add(new Explosion2(x, y, rx, ry, 0.5f)); } Explosions.Add(new Explosion2(x, y, 0.2f, 0.2f, .1f)); Explosions.Add(new Explosion2(x, y, -0.2f, -0.2f, 1f)); Explosions.Add(new Explosion2(x, y, -0.2f, 0.2f, 1f)); Explosions.Add(new Explosion2(x, y, 0.2f, -0.2f, 1f)); } } |
Explosion2クラスを示します。より大きな爆発に見えるように描画される時間を基底クラスの2倍にしています。爆発はX軸方向とZ軸方向に広がります。
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 |
public class Explosion2 : Explosion { public Explosion2(float x, float y, float vecX, float vecZ, float radius) :base(x, y, vecX, 0, radius) { X = x; Y = y; VecX = vecX; VecZ = vecZ; Radius = radius; if(Textures.Count == 0) Textures = CreateTextures(); } static List<int> Textures = new List<int>(); float Z = 0; float VecZ = 0; public override void Move() { X += VecX; Z += VecZ; MoveCount++; } public override void Draw() { GL.PushMatrix(); { GL.Translate(X, Y, Z); GL.Material(MaterialFace.Front, MaterialParameter.Ambient, Color.White); GL.Material(MaterialFace.Front, MaterialParameter.Diffuse, Color.White); 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, 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(); } } |