前回は3Dっぽい縦シューティングゲームの基本的な部分をつくりました。今回は自機を描画して弾丸を発射させるところまでやります。
メニューの[スタート]をクリックしたらゲームスタートです。視点をプログラム開始の位置に戻して新しく自機が生成されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public partial class Form1 : Form { private void MenuItemGameStart_Click(object sender, EventArgs e) { InitGame(); } void InitGame() { BaseY = 0; Jiki = new Jiki(0.5f, Speed); } } |
では自機を表示するためのクラスとはどのようなものなのでしょうか?
まず画像としてこれをつかいます。
ここから自機として使える部分を切り取って使います。
クラスは自機だけでなく敵や弾丸などのクラスもあります。共通しているのは移動したり描画される部分です。そこでこれらの基底クラスを作成して、これを継承して自機クラスを作成します。
基底クラスにするCharacterクラスは位置情報やXY方向に移動する量、描画処理などをおこないます。各キャラクターのコンストラクタ内でテクスチャを作成し、これをつかって描画をおこないます。テクスチャ作成に必要なBitmapは上記の画像の適切な部分を取り出して使います。
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 |
using OpenTK; using OpenTK.Graphics.OpenGL; public class Character { public float X = 0; public float Y = 0; public float VecX = 0; public float VecY = 0; public bool IsDead = false; public float Radius { get; protected set; } int _life = 0; public int Life { get { return _life; } set { _life = value; if(_life <= 0) IsDead = true; } } protected int MoveCount = 0; public virtual void Move() { X += VecX; Y += VecY; MoveCount++; } public virtual void Draw() { } protected virtual List<Bitmap> GetBitmaps() { List<Bitmap> bitmaps = new List<Bitmap>(); return bitmaps; } protected List<int> CreateTextures() { List<int> textures = new List<int>(); // テクスチャ有効化 GL.Enable(EnableCap.Texture2D); // 透過処理のために必要 GL.AlphaFunc(AlphaFunction.Gequal, 0.5f); GL.Enable(EnableCap.AlphaTest); List<Bitmap> bitmaps = GetBitmaps(); foreach(Bitmap bitmap0 in bitmaps) { // 領域確保 int texture; GL.GenTextures(1, out texture); textures.Add(texture); // アクティブ設定 GL.BindTexture(TextureTarget.Texture2D, texture); System.Drawing.Imaging.BitmapData data = bitmap0.LockBits( new Rectangle(0, 0, bitmap0.Width, bitmap0.Height), // 画像全体をロック System.Drawing.Imaging.ImageLockMode.ReadOnly, // 読み取り専用 System.Drawing.Imaging.PixelFormat.Format32bppArgb); // テクスチャ領域にピクセルデータを貼り付ける GL.TexImage2D( TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, data.Width, data.Height, 0, PixelFormat.Bgra, // Rgbaにすると赤と青が反転するので注意 PixelType.UnsignedByte, data.Scan0); // ロックをしたら必ずアンロックする bitmap0.UnlockBits(data); GL.GenerateMipmap(GenerateMipmapTarget.Texture2D); GL.BindTexture(TextureTarget.Texture2D, 0); } return textures; } } |
では次に自機クラスを作成します。
Move()メソッドのなかで移動量をXY座標にプラスしています。ただし画面の外に出てしまわないように一定数を超える場合はそれ以上の値にならないようにしています。また描画時は左右に移動しているあいだは機体が傾いて描画されるようにしています。
それからIsMutekiというプロパティがありますが、これは敵弾に当たったときに立て続けにやられるのを防ぐためのものです。敵弾に当たった場合は1秒間IsMutekiプロパティがtrueになり、自機は点滅表示となり、そのあいだは当たり判定は行なわれません。また通常であれば背景は黒ですが、自機がダメージをうけたときは背景を赤に変えます。
基底クラスのMove()メソッドで何回Move()メソッドが実行されたかカウントしています。この値を利用して必要な処理をおこなえるようにしているのですが、自機クラスではフィールド変数X、Yに代入できる値を変更するため、基底クラスのMove()メソッドは呼び出さずに自分でMoveCountをインクリメントしています。
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 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
using OpenTK; using OpenTK.Graphics.OpenGL; public class Jiki: Character { public Jiki(float radius, float vecY) { // テクスチャを作成するのは最初の1回だけ。あとは使い回す。 if(Textures.Count == 0) Textures = CreateTextures(); VecY = vecY; Radius = radius; Life = MaxLife; } public int MaxLife = 10; static List<int> Textures = new List<int>(); protected override List<Bitmap> GetBitmaps() { Bitmap bitmap = Properties.Resources.sprite2; List<Bitmap> bitmaps = new List<Bitmap>(); Bitmap bitmap1 = new Bitmap(42, 45); Graphics g = Graphics.FromImage(bitmap1); g.DrawImage(bitmap, new Rectangle(0, 0, 42, 45), new Rectangle(57, 0, 42, 45), GraphicsUnit.Pixel); g.Dispose(); bitmaps.Add(bitmap1); return bitmaps; } public float RotateY = 0; new public void Move() { X += VecX; Y += VecY; if(VecX< 0) RotateY = -10f; if(VecX > 0) RotateY = 10f; if(VecX == 0) RotateY = 0; // 画面外に出てしまったり前に移動しすぎないように調整する if(X < -4) X = -4; if(X > 4) X = 4; if(Y < Form1.BaseY) Y = Form1.BaseY; if(Y > Form1.BaseY + 4) Y = Form1.BaseY + 4; MoveCount++; } public override void Draw() { if(IsMuteki) { if(MoveCount % 4 < 2) return; } GL.PushMatrix(); { GL.Translate(X, Y, 0); GL.Rotate(RotateY, 0, 1, 0); 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, 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(); } bool _isMuteki = false; public bool IsMuteki { get{ return _isMuteki; } set { _isMuteki = value; if(value == true) { System.Windows.Forms.Timer timer1 = new System.Windows.Forms.Timer(); timer1.Tick += Timer_Tick1; timer1.Interval = 1000; timer1.Start(); } void Timer_Tick1(object sender, EventArgs e) { System.Windows.Forms.Timer t = (System.Windows.Forms.Timer)sender; t.Stop(); t.Dispose(); _isMuteki = false; } } } public bool _isBackColorRed = false; public bool IsBackColorRed { get { return _isBackColorRed; } set { _isBackColorRed = value; if(value == true) { System.Windows.Forms.Timer timer1 = new System.Windows.Forms.Timer(); timer1.Tick += Timer_Tick1; timer1.Interval = 100; timer1.Start(); } void Timer_Tick1(object sender, EventArgs e) { System.Windows.Forms.Timer t = (System.Windows.Forms.Timer)sender; t.Stop(); t.Dispose(); _isBackColorRed = false; } } } } |
では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 |
using OpenTK; using OpenTK.Graphics.OpenGL; public partial class Form1 : Form { new void Update() { BaseY += Speed; // 自機が存在するときは移動させる if(Jiki != null && !Jiki.IsDead) Jiki.Move(); } private void glControlEx1_Paint(object sender, PaintEventArgs e) { GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); if(Jiki != null && !Jiki.IsDead && Jiki.IsBackColorRed) GL.ClearColor(Color.Red); else GL.ClearColor(Color.Black); Lighting(); SetSight(); DrawField(); // 自機を描画する if(Jiki != null && !Jiki.IsDead) Jiki.Draw(); glControl.SwapBuffers(); } } |
自機を操縦する機能も追加します。方向キーが押されている間は自機に移動量を設定します。離されたらその方向の移動量をゼロにします。ただし縦方向の移動は視点がつねにY軸方向に移動しているので、なにもないときはForm1.Speed(=0.2f)とし、↑が押されているときはさらに0.1f追加、↓が押されているときは0.1f減速しています。
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 { protected override void OnKeyDown(KeyEventArgs e) { if(Jiki == null) return; if(e.KeyCode == Keys.Left) { Jiki.VecX = -0.1f; } if(e.KeyCode == Keys.Right) { Jiki.VecX = 0.1f; } if(e.KeyCode == Keys.Up) { Jiki.VecY = 0.1f + Speed; } if(e.KeyCode == Keys.Down) { Jiki.VecY = -0.1f + Speed; } if(e.KeyCode == Keys.Space) JikiShot(); base.OnKeyDown(e); } protected override void OnKeyUp(KeyEventArgs e) { if(Jiki == null) return; if(e.KeyCode == Keys.Left) { Jiki.VecX = 0; } if(e.KeyCode == Keys.Right) { Jiki.VecX = 0; } if(e.KeyCode == Keys.Up) { Jiki.VecY = Speed; } if(e.KeyCode == Keys.Down) { Jiki.VecY = Speed; } } } |
次に弾丸を発射する処理をおこないます。
以下は自機から放たれた弾丸を管理するクラスです。弾丸の移動(XY座標の変更)は基底クラスの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 |
using OpenTK.Graphics.OpenGL; using OpenTK; public class JikiBurret : Character { public JikiBurret(float x, float y, float vecX, float vecY, float radius) { X = x; Y = y; VecX = vecX; VecY = vecY; Radius = radius; 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>(); 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 override void Draw() { GL.PushMatrix(); { GL.Translate(X, Y, 0); 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(); } } |
では自機から弾丸を発射する機能を追加してみましょう。スペースキーはおされるとJikiShot()メソッドが実行されます。弾丸はシューティングゲームらしく3方向に飛ぶようにします。Update()メソッドのなかでMoveJikiBurrets()メソッドを呼び出して弾丸を移動させます。
弾丸は敵に命中した場合は消滅します。それだけでなく一定の距離(見えなくなる距離)を飛ぶと描画の処理は不要なのでリストのなかから取り除きます。IsOutOfField(Character character)メソッドは各キャラクターが描画不要の位置に存在するかどうかを判定するメソッドです。
弾丸の描画はglControlEx1_Paint(object sender, PaintEventArgs e)内でDrawJikiBurrets()メソッドを呼び出しておこないます。
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 |
public partial class Form1 : Form { List<JikiBurret> JikiBurrets = new List<JikiBurret>(); void JikiShot() { JikiBurrets.Add(new JikiBurret(Jiki.X, Jiki.Y, 0, 1f, 0.1f)); JikiBurrets.Add(new JikiBurret(Jiki.X, Jiki.Y, 0.1f, 1f, 0.1f)); JikiBurrets.Add(new JikiBurret(Jiki.X, Jiki.Y, -0.1f, 1f, 0.1f)); } new void Update() { BaseY += Speed; if(Jiki != null && !Jiki.IsDead) Jiki.Move(); MoveJikiBurrets(); // 描画不要の弾丸を取り除く JikiBurrets = JikiBurrets.Where(x => !IsBurretOutOfField(x) && !x.IsDead).ToList(); } void MoveJikiBurrets() { foreach(JikiBurret burret in JikiBurrets) { burret.Move(); } } bool IsOutOfField(Character character) { if(character.Y > BaseY + 40 || character.Y < BaseY - 2.0) return true; else return false; } private void glControlEx1_Paint(object sender, PaintEventArgs e) { GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); Lighting(); SetSight(); DrawField(); // 自機を描画 if(Jiki != null && !Jiki.IsDead) Jiki.Draw(); DrawJikiBurrets(); glControl.SwapBuffers(); } void DrawJikiBurrets() { foreach(JikiBurret burret in JikiBurrets) { burret.Draw(); } } } |
はじめまして、森田といいます。こちらのサイトを参考にシューティングゲームを作成中です。
自機をアニメーションできるようにしたいのですが、可能でしょうか?可能であればどのようにすればいいか教えてほしいです。また、自機などを描画する際の横と縦の指定はどこでしているのでしょうか?お手数をおかけしますがよろしくお願いします。
>自機をアニメーションできるようにしたいのですが、可能でしょうか?
自機を描画するときに使う画像を用意してDraw()が実行されるたびに切り替えて使えば可能です。
難しいのはプログラミングよりも画像をどうやって用意するかです。
実際のアニメーション処理は爆発の描画のところを参照してください。
>自機などを描画する際の横と縦の指定はどこでしているのでしょうか?
Radiusというプロパティで指定しています。縦横の比は同じです。
Radiusは半径。実際には自機や敵キャラは円や球ではないのですが、当たり判定を簡単に済ませるために各キャラクターは円であるとみなして処理をしています。