C# OpenTKでスクランブルもどきをつくります。スクロール型のゲームはOpenTKを使ったほうがやりやすいと思います。こんな感じのゲームをつくります。
まずはいつものテンプレから。
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 |
public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void glControl1_Load(object sender, EventArgs e) { GL.ClearColor(glControl1.BackColor); // Projection の設定 SetProjection(); // 視界の設定 SetSight(); // デプスバッファの使用 GL.Enable(EnableCap.DepthTest); // 再描画 glControl1.Refresh(); } private void glControl1_Resize(object sender, EventArgs e) { // Projection の設定 SetProjection(); // 再描画 glControl1.Refresh(); } private void glControl1_Paint(object sender, PaintEventArgs e) { GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); glControl1.SwapBuffers(); } } |
それからビューポートの設定、視体積の設定、どこから見たものを描画するのかの設定も必要ですね。
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 |
public partial class Form1 : Form { // 視体積の高さ static public float ProjectionHeight = 15.0f; // 視体積の幅(SetProjection()メソッドを実行すればこの値が決まる) static public float ProjectionWidth = 0.0f; private void SetProjection() { // ビューポートの設定 GL.Viewport(0, 0, glControl1.Width, glControl1.Height); // 視体積の設定 GL.MatrixMode(MatrixMode.Projection); ProjectionWidth = ProjectionHeight * glControl1.AspectRatio; Matrix4 proj = Matrix4.CreateOrthographic(ProjectionWidth, ProjectionHeight, 0.01f, 4.0f); GL.LoadMatrix(ref proj); // MatrixMode を元に戻す GL.MatrixMode(MatrixMode.Modelview); } void SetSight() { // 視界の設定 Vector3 eye = new Vector3((float)EyeX, (float)EyeY, (float)EyeZ); Vector3 target = new Vector3((float)EyeX, (float)EyeY, (float)0); Matrix4 look = Matrix4.LookAt(eye, target, Vector3.UnitY); GL.LoadMatrix(ref look); } // EyeX、EyeYの値を変更するときは、同時に視界の設定もしてしまう。 static double _eyeX = 0; double EyeX { get { return _eyeX; } set { _eyeX = value; SetSight(); } } static double _eyeY = 0; double EyeY { get { return _eyeY; } set { _eyeY = value; SetSight(); } } double EyeZ { get { return 1; } } } |
スクランブルらしいゲームにするには山やミサイル、燃料タンクが置かれている場所を作らなければなりません。これを管理するためのクラスStageをつくります。Stageクラスでは自機、敵、山などの地形を扱います。
コンストラクタ内でとくにすることはありません。フィールド変数としてBlockクラスのリストがあります。Blockには平地、上り坂、下り坂などの地形を格納します。Missilesには地上に配置または飛行中のミサイルを格納します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class Stage { // 各ブロックの縦横の比 縦1.0:横0.5とする float BlockAspect = 0.5f; // 地形 List<Block> Type1s = new List<Block>(); List<Block> Type2s = new List<Block>(); List<Block> Type3s = new List<Block>(); List<Block> Type4s = new List<Block>(); List<Block> Type5s = new List<Block>(); List<Block> Type6s = new List<Block>(); // ミサイルと自機 List<Missile> Missiles = new List<Missile>(); public Jiki Jiki = new Jiki(); public Stage() { } } |
Init()メソッドはテキストファイルを読み取り、その文字列から地形をつくります。
そのテキストファイルですが、このようなものをつくりました。
文字コードはUTF-8です。そのままリンクをクリックすると文字化けして意味不明な文字列が表示されるかもしれません。
これならどうでしょうか?
テキストファイルのなかの■▲△が地形の状態を表しています。●はビルです。
Mはミサイルの位置、Fは燃料タンクの位置です。数字は各ステージの境界線です。ではInit()メソッドをみてみましょう。
ここでやっていることはテキストファイルを読み出して、そのなかにある文字から地形や初期状態の敵の位置を取得しています。
ビットマップイメージを用意して、これで敵を描画します。
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 |
public class Stage { // 各ステージの開始位置を取得して保存できるようにしておく public float FirstBegin = 0; public float SecondBegin = 0; public float ThirdBegin = 0; public float ForthBegin = 0; public float FifthBegin = 0; public float FinalBegin = 0; public bool Init() { // マップデータをクリアする ClearMap(); // マップデータが保存されているファイルを読み込む List<string> Blocks = LoadMapFile(); if(Blocks == null) return false; int countY = 0; float x = Form1.ProjectionWidth / 2; float y = Form1.ProjectionHeight / 2; foreach(string str in Blocks) { Char[] vs = str.ToArray(); int countX = 0; foreach(Char char1 in vs) { // ステージの境界線を見つけたら保存する if(char1 == '1') FirstBegin = BlockAspect * countX - x; if(char1 == '2') SecondBegin = BlockAspect * countX - x; if(char1 == '3') ThirdBegin = BlockAspect * countX - x; if(char1 == '4') ForthBegin = BlockAspect * countX - x; if(char1 == '5') FifthBegin = BlockAspect * countX - x; if(char1 == '6') FinalBegin = BlockAspect * countX - x; // 地形に関する情報 if(char1 == '■') Type1s.Add(new Block(BlockAspect * countX - x, countY - y)); if(char1 == '▲') Type2s.Add(new Block(BlockAspect * countX - x, countY - y)); if(char1 == '△') Type3s.Add(new Block(BlockAspect * countX - x, countY - y)); if(char1 == '▼') Type4s.Add(new Block(BlockAspect * countX - x, countY - y)); if(char1 == '▽') Type5s.Add(new Block(BlockAspect * countX - x, countY - y)); if(char1 == '●') Type6s.Add(new Block(BlockAspect * countX - x, countY - y)); // ミサイルの初期位置 if(char1 == 'M') Missiles.Add(new Missile(BlockAspect * countX - x, countY - y)); // 燃料タンク // if(char1 == 'F') // 後で考える // UFOの初期位置 // if(char1 == 'U') // 後で考える // 敵の司令基地の位置 // if(char1 == 'B') // 後で考える countX++; } countY++; } return true; } void ClearMap() { Type1s = new List<Block>(); Type2s = new List<Block>(); Type3s = new List<Block>(); Type4s = new List<Block>(); Type5s = new List<Block>(); Type6s = new List<Block>(); Missiles = new List<Missile>(); } public List<string> LoadMapFile() { List<string> Blocks = new List<string>(); string mapFilePath = Application.StartupPath + "\\map.txt"; if(File.Exists(mapFilePath)) { StreamReader sr = new StreamReader(mapFilePath, Encoding.UTF8); while(true) { string str = sr.ReadLine(); Blocks.Add(str); if(str == null) break; } sr.Close(); } else { return null; } Blocks = Blocks.Take(15).ToList(); Blocks.Reverse(); return Blocks; } } |
Draw()メソッドはStageクラスのデータをもとに描画をするためのものです。
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 |
public class Stage { public void Draw() { // 地形を描画する DrawGround(); // ミサイルを描画する foreach(Missile missile in Missiles) missile.Draw(); // 自機(生きていれば・・・) Jiki.Draw(); } void DrawGround() { foreach(Block block in Type1s) DrawType1(block.X, block.Y); foreach(Block block in Type2s) DrawType2(block.X, block.Y); foreach(Block block in Type3s) DrawType3(block.X, block.Y); foreach(Block block in Type4s) DrawType4(block.X, block.Y); foreach(Block block in Type5s) DrawType5(block.X, block.Y); foreach(Block block in Type6s) DrawType6(block.X, block.Y); } } |
Draw()メソッド内で呼び出されているDrawTypeXメソッドを示します。
各ブロックは縦横の比が1:1にはなっていません(描画された山が山らしくないので)。フィールド変数 BlockAspectを使って調整しています。
それからDrawType6(float x, float 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 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 |
public class Stage { void DrawType1(float x, float y) { GL.Color3(Color.Brown); GL.PushMatrix(); { GL.Translate(x, y, 0); GL.Begin(BeginMode.Quads); { GL.Vertex3(0.5 * BlockAspect, 0.5, 0.0); GL.Vertex3(0.5 * BlockAspect, -0.5, 0.0); GL.Vertex3(-0.5 * BlockAspect, -0.5, 0.0); GL.Vertex3(-0.5 * BlockAspect, 0.5, 0.0); } GL.End(); } GL.PopMatrix(); } void DrawType2(float x, float y) { GL.Color3(Color.Brown); GL.PushMatrix(); { GL.Translate(x, y, 0); GL.Begin(BeginMode.Triangles); { GL.Vertex3(0.5 * BlockAspect, 0.5, 0.0); GL.Vertex3(0.5 * BlockAspect, -0.5, 0.0); GL.Vertex3(-0.5 * BlockAspect, -0.5, 0.0); } GL.End(); } GL.PopMatrix(); } void DrawType3(float x, float y) { GL.Color3(Color.Brown); GL.PushMatrix(); { GL.Translate(x, y, 0); GL.Begin(BeginMode.Triangles); { GL.Vertex3(-0.5 * BlockAspect, 0.5, 0.0); GL.Vertex3(-0.5 * BlockAspect, -0.5, 0.0); GL.Vertex3(0.5 * BlockAspect, -0.5, 0.0); } GL.End(); } GL.PopMatrix(); } void DrawType4(float x, float y) { GL.Color3(Color.Brown); GL.PushMatrix(); { GL.Translate(x, y, 0); GL.Begin(BeginMode.Triangles); { GL.Vertex3(-0.5 * BlockAspect, 0.5, 0.0); GL.Vertex3(0.5 * BlockAspect, 0.5, 0.0); GL.Vertex3(0.5 * BlockAspect, -0.5, 0.0); } GL.End(); } GL.PopMatrix(); } void DrawType5(float x, float y) { GL.Color3(Color.Brown); GL.PushMatrix(); { GL.Translate(x, y, 0); GL.Begin(BeginMode.Triangles); { GL.Vertex3(-0.5 * BlockAspect, 0.5, 0.0); GL.Vertex3(-0.5 * BlockAspect, -0.5, 0.0); GL.Vertex3(0.5 * BlockAspect, 0.5, 0.0); } GL.End(); } GL.PopMatrix(); } void DrawType6(float x, float y) { GL.Color3(Color.White); GL.BindTexture(TextureTarget.Texture2D, Form1.BuildingTexture); GL.PushMatrix(); { GL.Translate(x, y, 0); GL.Begin(BeginMode.Quads); { GL.TexCoord2(1, 1); GL.Vertex3(0.5 * BlockAspect, 0.5, 0.0); GL.TexCoord2(1, 0); GL.Vertex3(0.5 * BlockAspect, -0.5, -0.0); GL.TexCoord2(0, 0); GL.Vertex3(-0.5 * BlockAspect, -0.5, -0.0); GL.TexCoord2(0, 1); GL.Vertex3(-0.5 * BlockAspect, 0.5, -0.0); } GL.End(); } GL.PopMatrix(); GL.BindTexture(TextureTarget.Texture2D, 0); } } |
これは地形を描画するためのブロックの座標を管理するためのクラスです。
1 2 3 4 5 6 7 8 9 10 11 |
public class Block { public Block(float x, float y) { X = x; Y = y; } public float X = 0f; public float Y = 0f; } |
またテクスチャを使った描画ができるように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 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 |
public partial class Form1 : Form { private void glControl1_Load(object sender, EventArgs e) { GL.ClearColor(glControl1.BackColor); // Projection の設定 SetProjection(); // 視界の設定 SetSight(); // デプスバッファの使用 GL.Enable(EnableCap.DepthTest); CreateTextures(); // 再描画 glControl1.Refresh(); } static public int JikiTexture = 0; static public int MissileTexture = 0; static public int FuelTankTexture = 0; static public int UfoTexture = 0; static public int BaseTexture = 0; static public int FireBallTexture = 0; static public int BuildingTexture = 0; static public int ExplosionTexture1 = 0; static public int ExplosionTexture2 = 0; void CreateTextures() { // テクスチャ有効化 GL.Enable(EnableCap.Texture2D); JikiTexture = CreateTexture(Properties.Resources.jiki); MissileTexture = CreateTexture(Properties.Resources.missile); FuelTankTexture = CreateTexture(Properties.Resources.fuel); UfoTexture = CreateTexture(Properties.Resources.ufo); FireBallTexture = CreateTexture(Properties.Resources.fireball); BaseTexture = CreateTexture(Properties.Resources.finalbase); BuildingTexture = CreateTexture(Properties.Resources.building); ExplosionTexture1 = CreateTexture(Properties.Resources.explosion1); ExplosionTexture2 = CreateTexture(Properties.Resources.explosion2); } int CreateTexture(Bitmap bmp) { // Y座標は上下が逆になるので・・・ bmp.RotateFlip(RotateFlipType.RotateNoneFlipY); // 領域確保 int texture; GL.GenTextures(1, out texture); // アクティブ設定 GL.BindTexture(TextureTarget.Texture2D, texture); System.Drawing.Imaging.BitmapData data = bmp.LockBits( new Rectangle(0, 0, bmp.Width, bmp.Height), // 画像全体をロック System.Drawing.Imaging.ImageLockMode.ReadOnly, // 読み取り専用 System.Drawing.Imaging.PixelFormat.Format24bppRgb); // ロックをしたら必ずアンロックする bmp.UnlockBits(data); // テクスチャ領域にピクセルデータを貼り付ける GL.TexImage2D( TextureTarget.Texture2D, 0, PixelInternalFormat.Rgb, data.Width, data.Height, 0, PixelFormat.Bgr, // Rgbaにすると赤と青が反転するので注意 PixelType.UnsignedByte, data.Scan0); GL.GenerateMipmap(GenerateMipmapTarget.Texture2D); GL.BindTexture(TextureTarget.Texture2D, 0); return texture; } } |
次にミサイルを描画するためのクラスを示します。
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 class Missile { public float X = 0f; public float Y = 0f; public bool isMove = false; public bool isDead = false; static public float Speed = 0.3f; static Random Random = new Random(); public Missile(float x, float y) { X = x; Y = y; } public void Move() { if(!isMove) return; Y += Speed; } public void Draw() { if(isDead) return; GL.PushMatrix(); { GL.Translate(X, Y, 0); GL.Color3(Color.White); GL.BindTexture(TextureTarget.Texture2D, Form1.MissileTexture); GL.Begin(BeginMode.Quads); { GL.TexCoord2(1, 1); GL.Vertex3(0.25, 0.5, 0.1); GL.TexCoord2(1, 0); GL.Vertex3(0.25, -0.5, 0.1); GL.TexCoord2(0, 0); GL.Vertex3(-0.25, -0.5, 0.1); GL.TexCoord2(0, 1); GL.Vertex3(-0.25, 0.5, 0.1); } GL.End(); GL.BindTexture(TextureTarget.Texture2D, 0); } GL.PopMatrix(); } } |
これでゲームを開始すれば地形とミサイルは描画されます。ただしミサイルは飛ばないし、肝心の自機も表示されません。続きは次回。
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 |
public partial class Form1 : Form { Timer timer = new Timer(); Stage Stage = new Stage(); static public float ScrollSpeed = 0.15f; public Form1() { InitializeComponent(); timer.Tick += Timer_Tick; TimerIntervalReset(); } void GameStart() { TimerIntervalReset(); EyeX = 0; EyeY = 0; Stage.Init(); timer.Start(); } private void Timer_Tick(object sender, EventArgs e) { EyeX += ScrollSpeed; glControl1.Refresh(); } private void glControl1_Paint(object sender, PaintEventArgs e) { GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); Stage.Draw(); glControl1.SwapBuffers(); } } |