ボスコニアンは以下のようなゲームです。
1981年11月に日本のナムコから稼働されたアーケード用多方向スクロールシューティングゲーム。
8方向レバーと1ボタン(ショット)で自機を操作。自機を中心に画面が8方向へスクロールする。画面レイアウトは同社が前年に発表した『ラリーX』を踏襲し、画面右側のレーダー表示を頼りに、ステージ内に配置された敵基地を全て破壊するとステージクリアとなる。
ラリーXと異なり、レーダーの上下および左右はそれぞれ繋がっている。
敵基地は規則的に開閉するシェルターを持つ中心部と、6箇所の砲台で構成されている。レーダー上では緑色の点で表示。シェルターが開いた時に中心部を攻撃するか、砲台を全て破壊すると倒せる。これを全滅させるとラウンドクリア。
とこのようなものになっています。そのまま同じものをつくっても面白くないので前回の縦シューティングゲームのように原作にはない3Dの要素を加えます。
するとこんなゲームができました!
ではOpenTKで作成してみましょう。
まず基本的な部分です。ここでは自機や敵を移動させたり死亡判定をするUpdate()メソッドや描画をおこなうDraw()メソッド、描画するにあたってどこからどのように見たものを描画するかを決めるSetSight()メソッドを考えることになります。以下のコードはそれ以外の決まり切った部分です。
1 2 3 4 5 6 7 8 9 10 11 |
using OpenTK; using OpenTK.Graphics.OpenGL; public class GLControlEx: OpenTK.GLControl { public GLControlEx() { // ないとForm1でKeyDown、KeyUpイベントが捕捉できない SetStyle(ControlStyles.Selectable, false); } } |
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 |
using OpenTK; using OpenTK.Graphics.OpenGL; public partial class Form1 : Form { public Form1() { InitializeComponent(); Timer.Interval = 1000 / 60; Timer.Tick += Timer_Tick; Timer.Start(); } private void glControlEx1_Load(object sender, EventArgs e) { GL.ClearColor(glControl.BackColor); // Projection の設定 SetProjection(); // デプスバッファの使用 GL.Enable(EnableCap.DepthTest); } private void glControl_Resize(object sender, EventArgs e) { SetProjection(); glControl.Refresh(); } private void SetProjection() { GL.Viewport(0, 0, glControl.Width, glControl.Height); GL.MatrixMode(MatrixMode.Projection); Matrix4 proj = Matrix4.CreatePerspectiveFieldOfView(MathHelper.PiOver4, (float)glControl.Size.Width / (float)glControl.Size.Height, 0.01f, 80.0f); GL.LoadMatrix(ref proj); GL.MatrixMode(MatrixMode.Modelview); } private void glControlEx1_Paint(object sender, PaintEventArgs e) { GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); Update(); SetSight(); Draw(); glControl.SwapBuffers(); } // タイマーを使って再描画をさせる Timer Timer = new Timer(); private void Timer_Tick(object sender, EventArgs e) { glControl.Refresh(); } protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); } void SetSight() { } new void Update() { } void Draw() { } } |
ではさっそく自機を描画して表示させてみましょう。自機はXY座標のみが変化します。
どのように描画するかですが、視点はつねに自機の進行方向に対して後ろ側とし、一定の距離を維持します。Z座標は固定の値とし、自機座標(0,0,0)のとき、視点の座標は(0,-20, 6)とします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
using OpenTK; using OpenTK.Graphics.OpenGL; public partial class Form1 : Form { void SetSight() { // 自機の回転軸はZ軸 // 自機座標(0,0,0)Rotate=90 のとき、カメラの座標は(0,-15, 4) float rad = (float)(Jiki.Rotate * Math.PI) / 180; float ex = -15 * (float)Math.Cos(rad) + Jiki.X; float ey = -15 * (float)Math.Sin(rad) + Jiki.Y; Vector3 eye = new Vector3(ex, ey, 4); Vector3 target = new Vector3(Jiki.X, Jiki.Y, 0); // Jikiオブジェクトについては後述 Vector3 up = Vector3.UnitZ; Matrix4 look = Matrix4.LookAt(eye, target, up); GL.LoadMatrix(ref look); } } |
自機や弾丸の大きさや速度はほかのクラスからもアクセスできるように別のクラスにまとめます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
static public class Config { static public float FieldWidth = 100; // 自機が移動できる範囲 static public float FieldHeight = 160; // 自機が移動できる範囲 static public int MaxJikiRest = 5; // 自機の残機 static public int MaxEnemiesCount = 10; // 出現する敵の上限 static public float JikiRadius = 1.0f; // 自機の大きさ static public float JikiSpeed = 0.15f; // 自機の移動速度 static public float BurretSpeed = 0.6f; // 自機から発射された弾丸の速度 static public float BurretRadius = 0.2f; // 自機から発射された弾丸の大きさ static public float EnemyRadius = 1.0f; // ザコ敵の大きさ static public float EnemySpeedA = 0.12f; // ザコ敵Aの移動速度 static public float EnemySpeedB = 0.18f; // ザコ敵Bの移動速度 static public float FortressRadius = 4.0f; // 敵要塞の大きさ static public float FortressCoreRadius = 1.0f; // 敵要塞のコアの大きさ static public float FortressCannonRadius = 1.0f; // 敵要塞の砲台の大きさ static public float FortressBurretRadius = 0.2f; // 敵要塞から発射された弾丸の大きさ static public float ExplosionRadius = 1.0f; // 爆発の大きさ } |
自機と敵のクラスをつくろうとすると共通した部分が多くできます。そこでこれらの基底クラスとしてCharacterクラスを作成します。
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 Character { public Character(Form1 form1, float x, float y, float vecX, float vecY) { MainForm = form1; X = x; Y = y; VecX = vecX; VecY = vecY; } protected Form1 MainForm = null; public float X = 0; public float Y = 0; public float VecX = 0; public float VecY = 0; public bool IsDead = false; public float Radius = 0; protected int UpdateCount = 0; public virtual void Update() { X += VecX; Y += VecY; UpdateCount++; } 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); bitmap0.RotateFlip(RotateFlipType.RotateNoneFlipY); 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, PixelType.UnsignedByte, data.Scan0); bitmap0.UnlockBits(data); GL.GenerateMipmap(GenerateMipmapTarget.Texture2D); GL.BindTexture(TextureTarget.Texture2D, 0); } return textures; } } |
では自機クラスを作成します。
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 Jiki : Character { public Jiki(Form1 form1, float x, float y, float vecX, float vecY) : base(form1, x, y, vecX, vecY) { // テクスチャを作成するのは最初の1回だけ。あとは使い回す。 if(Textures.Count == 0) Textures = CreateTextures(); Radius = Config.JikiRadius; } 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 Rotate = 90; // 最初は上向きなので90°とする public override void Update() { // 自機が向いている方向から自機の移動量を求める double rad = Math.PI * Rotate / 180; VecX = Config.JikiSpeed * (float)Math.Cos(rad); VecY = Config.JikiSpeed * (float)Math.Sin(rad); // 移動量を自機の座標に反映させる base.Update(); } public override void Draw() { GL.PushMatrix(); { GL.Translate(X, Y, 0); GL.Rotate(Rotate - 90, 0, 0, 1); GL.BindTexture(TextureTarget.Texture2D, Textures[0]); GL.Begin(BeginMode.Quads); { GL.TexCoord2(1, 1); GL.Vertex3(Radius, Radius, 0); GL.TexCoord2(0, 1); GL.Vertex3(-Radius, Radius, 0); GL.TexCoord2(0, 0); GL.Vertex3(-Radius, -Radius, 0); GL.TexCoord2(1, 0); GL.Vertex3(Radius, -Radius, 0); } GL.End(); GL.BindTexture(TextureTarget.Texture2D, 0); } GL.PopMatrix(); } } |
あとは表示させて動かすだけです。方向転換をする場合、1回のUpdate()メソッドごとに9°ずつ5回にわけて回転させます。
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 |
public partial class Form1 : Form { Jiki Jiki = null; int UpdateCountEndLeft = -1; int UpdateCountEndRight = -1; int UpdateCount = 0; private void glControlEx1_Load(object sender, EventArgs e) { GL.ClearColor(glControl.BackColor); // Projection の設定 SetProjection(); // デプスバッファの使用 GL.Enable(EnableCap.DepthTest); Jiki = new Jiki(0, 0, 0, 0, JikiRadius, 1); } // 左右のキーで方向転換 protected override void OnKeyDown(KeyEventArgs e) { if (e.KeyCode == Keys.Left) { UpdateCountEndLeft = UpdateCount + 5; } if (e.KeyCode == Keys.Right) { UpdateCountEndRight = UpdateCount + 5; } if (e.KeyCode == Keys.Down) { UpdateCountEndRight = UpdateCount + 5 * 4; } if (e.KeyCode == Keys.Space) Shot(); base.OnKeyDown(e); } new void Update() { MoveJiki(); UpdateCount++; } // 方向転換するのであれば9°ずつ5回にわけて回転させる void MoveJiki() { if (UpdateCountEndLeft > UpdateCount) Jiki.Rotate += 9; if (UpdateCountEndRight > UpdateCount) Jiki.Rotate -= 9; Jiki.Update(); } void Draw() { DrawFieldLines(); if(!Jiki.IsDead) Jiki.Draw(); } // フィールドに縦横の直線を描画する void DrawFieldLines() { GL.Begin(BeginMode.Lines); { GL.Color3(Color.Green); GL.Material(MaterialFace.Front, MaterialParameter.Ambient, Color.Green); GL.Material(MaterialFace.Front, MaterialParameter.Diffuse, Color.Green); for(int i = -50; i < 50; i++) { GL.Vertex3(4 * i, 200, 0); GL.Vertex3(4 * i, -200, 0); } GL.Color3(Color.Blue); GL.Material(MaterialFace.Front, MaterialParameter.Ambient, Color.Blue); GL.Material(MaterialFace.Front, MaterialParameter.Diffuse, Color.Blue); for(int i = -100; i < 100; i++) { GL.Vertex3(200, 4 * i, 0); GL.Vertex3(-200, 4 * i, 0); } GL.Color3(Color.White); } GL.End(); } } |