レーダースコープは1980年に任天堂が開発したシューティングゲームです。3Dっぽいギャラクシアンのようなゲームです。敵はときどき機雷を投下してきてこれを打ち漏らすと下にある自分の基地がダメージをうけます。ただ敵を撃つだけではなく自分の基地も守らないといけない・・・というゲームです。
こちらは本物のレーダースコープのプレイ動画。
ただこのゲーム、技術的に優れていたけどあまり売れなかったそうです。なぜでしょうか? 基地がダメージをうけると自機の動きが遅くなるという仕様になっているのですが、自機が死亡して次に自機が登場すると前機のときにうけた基地のダメージも回復してしまい、あまり基地を守るという緊張感がないということではないでしょうか?
その後大量に余ったこのゲームを利用してドンキーコングが作られたのはゲーム通のあいだでは有名な話です。
それでは前置きはこの程度にしてさっそくつくっていきましょう。
3Dっぽさを出すためにOpenTKを使う
3Dっぽい仕様なのでOpenTKを使います。OpenTK.GLControlを継承して以下のようなクラスを作成します。
最後のChangeBackColorメソッドは第二引数で指定したミリ秒のあいだ背景を変更するためのメソッドです。自機がやられたときに背景をチカチカさせるための処理でつかいます。
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 |
public class GLControlEx : OpenTK.GLControl { public GLControlEx() { SetStyle(ControlStyles.Selectable, false); } public GLControlEx(IContainer container) { SetStyle(ControlStyles.Selectable, false); } protected override void OnLoad(EventArgs e) { GL.ClearColor(this.BackColor); SetProjection(); base.OnLoad(e); } protected override void OnResize(EventArgs e) { SetProjection(); // 再描画 this.Refresh(); base.OnResize(e); } protected override void OnPaint(PaintEventArgs e) { try { GL.Clear(ClearBufferMask.ColorBufferBit); GameManager.Draw();// 描画処理 GameManagerクラスは後述 this.SwapBuffers(); } catch { } base.OnPaint(e); } private void SetProjection() { try { // ビューポートの設定 GL.Viewport(0, 0, this.Width, this.Height); // 視体積の設定 GL.MatrixMode(MatrixMode.Projection); float h = 4.0f, w = h * this.AspectRatio; Matrix4 proj = Matrix4.CreatePerspectiveFieldOfView(MathHelper.PiOver3, this.AspectRatio, 0.01f, 100.0f); GL.LoadMatrix(ref proj); // MatrixMode を元に戻す GL.MatrixMode(MatrixMode.Modelview); } catch { } } Color OldColor = Color.Empty; // 元の色を保存しておく public void ChangeBackColor(Color color, int ms) { if (OldColor == Color.Empty) OldColor = BackColor; BackColor = color; GL.ClearColor(BackColor); Timer timer = new Timer(); timer.Interval = ms; timer.Tick += Timer_Tick; timer.Start(); void Timer_Tick(object sender, EventArgs e) { Timer t = (Timer)sender; t.Stop(); t.Dispose(); BackColor = OldColor; GL.ClearColor(BackColor); } } } |
上記を作成するとデザイナのツールボックスからフォーム上にドラッグアンドドロップすることができるので以下のように配置します。上にあるのはLabelでスコアと残機表示用、下にあるのはPictureBoxでダメージメーター表示用です。
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 |
public partial class Form1 : Form { Timer timer = new Timer(); public Form1() { InitializeComponent(); timer.Interval = 1000/60; timer.Tick += Timer_Tick; timer.Start(); } protected override void OnLoad(EventArgs e) { InitForm(); base.OnLoad(e); } Color GameBackColor = Color.DarkBlue; void InitForm() { Size controlSize = new Size(620, 430); glControlEx1.Location = new Point(0, 0); glControlEx1.Size = controlSize; glControlEx1.BackColor = GameBackColor; GL.ClearColor(GameBackColor); label1.AutoSize = true; label1.Location = new Point(10, 10); label1.Font = new Font("MS ゴシック", 16, FontStyle.Bold); label1.Text = ""; label1.BackColor = GameBackColor; label1.ForeColor = Color.White; // ダメージメーターを中央に表示させるためのX座標を求める Size damageMeterSize = new Size(500, 60); int damageMeterX = (controlSize.Width - damageMeterSize.Width)/2; pictureBox1.Location = new Point(damageMeterX, 360); pictureBox1.Size = damageMeterSize; // フォームのサイズを変更すると表示されない部分が出るのでサイズ変更不可にする this.Size = new Size(637, 468); this.FormBorderStyle = FormBorderStyle.FixedSingle; } private void Timer_Tick(object sender, EventArgs e) { // 再描画 glControlEx1.Refresh(); } } |
ゲームで使う変数(自機や敵機の位置や状態)を扱うクラスとして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 |
public static class GameManager { public static void SetSight() { // 視界の設定 Matrix4 look = Matrix4.LookAt( new Vector3(0, 18, 30), new Vector3(0, -3.4f, 2.1f), Vector3.UnitY); GL.LoadMatrix(ref look); } static int UpdateCount = 0; public static void Update() { UpdateCount++; } public static void Draw() { SetSight(); // このあと描画の処理がつづく } } |
自機を作成する
それでは自機を作成して表示させます。
GameCharacterクラスをつくる
そのまえに自機や敵機を描画するときに必要なテクスチャを取得するためのクラスをつくります。これを継承して自機を描画するためのJikiクラスをつくります。
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 |
using System.Drawing; using OpenTK.Graphics.OpenGL; public class GameCharacter { protected virtual List<Bitmap> GetBitmaps() { return new List<Bitmap>(); } 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; } } |
Jikiクラスをつくる
つぎにJikiクラスを作成します。GetBitmapsメソッドのなかで使用しているProperties.Resources.sprite2は以下のファイルをリソースに追加して使います。
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 |
using System.Drawing; using OpenTK.Graphics.OpenGL; using OpenTK; public class Jiki: GameCharacter { static List<int> Textures = new List<int>(); public float X = 0; public const float Z = 18; public float Radius = 1.0f; public Jiki() { X = 0; // テクスチャを作成するのは最初の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(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 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, 1); GL.Vertex3(Radius, 0, Radius); GL.TexCoord2(0, 1); GL.Vertex3(-Radius, 0, Radius); GL.TexCoord2(0, 0); GL.Vertex3(-Radius, 0, -Radius); GL.TexCoord2(1, 0); GL.Vertex3(Radius, 0, -Radius); } GL.End(); GL.BindTexture(TextureTarget.Texture2D, 0); } GL.PopMatrix(); } } |
自機を操作できるようにする
方向キーを押したら自機を操作できるようにするためにForm1クラスとGameManagerクラスに以下を追加します。
方向キーを押しているときだけMoveLeftとMoveRightがtrueになります。そのときにForm1クラスでTimer.Tickイベントが発生したらGameManager.Updateメソッドを呼び出して自機を移動させます。
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 |
public static class GameManager { public static Jiki Jiki = new Jiki(); public static bool MoveLeft = false; public static bool MoveRight = false; public static void Init() { } public static void Update() { UpdateCount++; MoveJiki(); } static void MoveJiki() { // 自機が画面の端まで移動できないように制限を加える if (GameManager.MoveLeft && Jiki.X > -15) Jiki.X -= 0.2f; if (GameManager.MoveRight && Jiki.X < 15) Jiki.X += 0.2f; } // 自機から弾丸発射 public static void 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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
public static class GameManager { static bool IsJikiDead = false; public static void Draw() { SetSight(); DrawField(); // 自機死亡時は自機を描画しない if (!IsJikiDead) Jiki.Draw(); } public static void DrawField() { GL.Enable(EnableCap.LineStipple); GL.LineStipple(1, 0xF0F0); GL.Begin(BeginMode.Lines); { GL.Color3(Color.LightGreen); // 横線 for (int i = -20; i <= 20; i += 4) { GL.Vertex3(-30, 0, i); GL.Vertex3(30, 0, i); } // 縦線 for (int i = -20; i <= 20; i += 5) { GL.Vertex3(i, 0, -30); GL.Vertex3(i, 0, 20); } GL.Color3(Color.White); } GL.End(); GL.Disable(EnableCap.LineStipple); } } |
Form1クラスではイベントハンドラTimer_Tick内でGameManager.Updateメソッドを呼び出してデータを更新して再描画をさせる処理を追加します。またキーが押されたらGameManager.MoveLeftとGameManager.MoveRightをtrueにして離されたら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 |
public partial class Form1 : Form { protected override void OnLoad(EventArgs e) { InitForm(); GameManager.Init(); base.OnLoad(e); } private void Timer_Tick(object sender, EventArgs e) { // 更新と再描画 GameManager.Update(); glControlEx1.Refresh(); } 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) GameManager.Shot(); base.OnKeyDown(e); } protected override void OnKeyUp(KeyEventArgs e) { if (e.KeyCode == Keys.Left) GameManager.MoveLeft = false; if (e.KeyCode == Keys.Right) GameManager.MoveRight = false; base.OnKeyUp(e); } } |