ここではこんなゲームをつくります。
『ラリーX』は、1980年11月にナムコ (後のバンダイナムコアミューズメント) から稼働されたアーケード用固定画面アクションゲームです。青い車(マイカー)を操作し、追ってくる敵(レッドカー)やランダムに置かれた岩を避けながら、迷路状のステージ上にある旗(フラッグ)で示された10箇所のチェックポイントを通過するとステージクリアになります。
以下は本物のラリーX(ニュー ラリーX)です。
Rally-Xのようなゲームを作るためにここではC#とOpenTKを使います。まずは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 |
public partial class Form1 : Form { public Form1() { InitializeComponent(); glControl1.Load += GlControlEx1_Load; glControl1.Paint += GlControlEx1_Paint; glControl1.Resize += GlControl1_Resize; timer.Interval = 60; timer.Tick += Timer_Tick; timer.Start(); } Timer timer = new Timer(); private void GlControlEx1_Load(object sender, EventArgs e) { GL.ClearColor(glControl1.BackColor); // Projection の設定 SetProjection(); // 視界の設定 SetSight(); // デプスバッファの使用 GL.Enable(EnableCap.DepthTest); } private void GlControl1_Resize(object sender, EventArgs e) { // Projection の設定 SetProjection(); // 再描画 glControl1.Refresh(); } // Projection の設定 private void SetProjection() { // ビューポートの設定 GL.Viewport(0, 0, glControl1.Width, glControl1.Height); // 視体積の設定 GL.MatrixMode(MatrixMode.Projection); float h = 6.0f, w = h * glControl1.AspectRatio; Matrix4 proj = Matrix4.CreateOrthographic(w, h, 0.01f, 3.0f); GL.LoadMatrix(ref proj); // MatrixMode を元に戻す GL.MatrixMode(MatrixMode.Modelview); } } |
まず道路をつくります。マップがどうなっているかわからないので適当です。
ブロックをつなげて道路をつくります。これはブロックの位置情報を管理するためのクラスです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class Block { public Block(int indexX, int indexY) { IndexX = indexX; IndexY = indexY; } public int IndexX { private set; get; } public int IndexY { private set; get; } } |
これを複数あつめて道路をつくります。GetRoadBlocks()メソッドは道路をつくるブロックを取得するメソッドです。1回呼び出して結果を保存しておきます。RoadBlocksプロパティをつかえばいつでも取得できるようにします。
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 |
public partial class Form1 : Form { List<Block> _roadBlocks = null; List<Block> RoadBlocks { get { if(_roadBlocks == null) _roadBlocks = GetRoadBlocks(); return _roadBlocks; } } List<Block> GetRoadBlocks() { List<Block> blocks = new List<Block>(); for(int i = 0; i <= 24; i++) { blocks.Add(new Block(0, i)); blocks.Add(new Block(8, i)); blocks.Add(new Block(16, i)); blocks.Add(new Block(24, i)); blocks.Add(new Block(i, 0)); blocks.Add(new Block(i, 24)); if(i != 9 && i != 15) blocks.Add(new Block(i, 8)); if(i != 9 && i != 15) blocks.Add(new Block(i, 16)); } for(int i = 0; i <= 8; i++) { blocks.Add(new Block(i, 2)); blocks.Add(new Block(i, 4)); blocks.Add(new Block(i, 6)); blocks.Add(new Block(i, 18)); blocks.Add(new Block(i, 20)); blocks.Add(new Block(i, 22)); } for(int i = 8; i <= 16; i++) { blocks.Add(new Block(i, 10)); blocks.Add(new Block(i, 12)); blocks.Add(new Block(i, 14)); } for(int i = 16; i <= 24; i++) { blocks.Add(new Block(i, 2)); blocks.Add(new Block(i, 4)); blocks.Add(new Block(i, 6)); blocks.Add(new Block(i, 18)); blocks.Add(new Block(i, 20)); blocks.Add(new Block(i, 22)); } for(int i = 0; i <= 8; i++) { blocks.Add(new Block(10, i)); blocks.Add(new Block(12, i)); blocks.Add(new Block(14, i)); } for(int i = 8; i <= 16; i++) { blocks.Add(new Block(2, i)); blocks.Add(new Block(4, i)); blocks.Add(new Block(6, i)); blocks.Add(new Block(18, i)); blocks.Add(new Block(20, i)); blocks.Add(new Block(22, i)); } for(int i = 16; i <= 24; i++) { blocks.Add(new Block(10, i)); blocks.Add(new Block(12, i)); blocks.Add(new Block(14, i)); } blocks.Add(new Block(10, 9)); blocks.Add(new Block(10, 15)); blocks.Add(new Block(14, 9)); blocks.Add(new Block(14, 15)); return blocks; } } |
RoadBlocksプロパティでブロックのリストを取得して描画します。DrawBlockメソッドはブロックの位置から描画すべき座標を求め、実際に描画するメソッドです。
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 |
public partial class Form1 : Form { void DrawBlock(int indexX, int indexY) { double x = indexX; double y = indexY; double side = 1.0; GL.Begin(BeginMode.Quads); { GL.Vertex3(x - side / 2, y - side / 2, 0); GL.Vertex3(x + side / 2, y - side / 2, 0); GL.Vertex3(x + side / 2, y + side / 2, 0); GL.Vertex3(x - side / 2, y + side / 2, 0); } GL.End(); } void DrawRoad() { GL.Color3(Color.Tan); foreach(Block block in RoadBlocks) DrawBlock(block.IndexX, block.IndexY); } } |
描画するためには視界の設定をする必要があります。視界はマイカーの座標と同じです。そのためマイカーは常に中央に描画されることになります。
カメラの位置(=マイカーの位置)はプロパティで管理します。そして変更されたら視界の設定も変更します。
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 { double _eyeX = 12; double EyeX { get { return _eyeX; } set { _eyeX = value; SetSight(); } } double _eyeY = 0; double EyeY { get { return _eyeY; } set { _eyeY = value; SetSight(); } } double EyeZ { get { return 1; } } 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); } } |
キーが押されたら移動する方向を変えます。ただし方向転換ができる場合でなければ変更できないようにします(車は道路しか走れないので)。
まず現在どの方向を向いているのかを管理するフィールド変数をつくります。
1 2 3 4 5 6 7 8 9 10 11 12 |
public partial class Form1 : Form { public enum Direct { North, East, South, West, } Direct CurDirect = Direct.North; Direct NextDirect = Direct.North; } |
方向転換をしたとして次に進める場所がないのであれば方向転換することはできません。以下は各方向に方向転換できるかどうかを調べるメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public partial class Form1 : Form { bool CanNorth(double x1, double y1) { return RoadBlocks.Any(x => (x.IndexY == Math.Ceiling(y1 + 0.001)) && x.IndexX == Math.Round(x1, 4)); } bool CanSouth(double x1, double y1) { return RoadBlocks.Any(x => x.IndexY == Math.Floor(y1 - 0.001) && x.IndexX == Math.Round(x1, 4)); } bool CanEast(double x1, double y1) { return (RoadBlocks.Any(x => x.IndexX == Math.Ceiling(x1 + 0.001) && x.IndexY == Math.Round(y1, 4))); } bool CanWest(double x1, double y1) { return RoadBlocks.Any(x => x.IndexX == Math.Floor(x1 - 0.001) && x.IndexY == Math.Round(y1, 4)); } } |
これは方向キーが押されたときに呼び出されるメソッドです。もし方向転換できないのであれば現在の方向は変更しないでフィールド変数 NextDirectを変更します。そして実際に変更できるようになってから方向転換するようにします。
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 |
public partial class Form1 : Form { [System.Security.Permissions.UIPermission( System.Security.Permissions.SecurityAction.Demand, Window = System.Security.Permissions.UIPermissionWindow.AllWindows)] protected override bool ProcessDialogKey(Keys keyData) { if(keyData == Keys.Up) { NextDirect = Direct.North; if(CanNorth(EyeX, EyeY)) CurDirect = Direct.North; } if(keyData == Keys.Down) { NextDirect = Direct.South; if(CanSouth(EyeX, EyeY)) CurDirect = Direct.South; } if(keyData == Keys.Left) { NextDirect = Direct.West; if(CanWest(EyeX, EyeY)) CurDirect = Direct.West; } if(keyData == Keys.Right) { NextDirect = Direct.East; if(CanEast(EyeX, EyeY)) CurDirect = Direct.East; } return base.ProcessDialogKey(keyData); } } |
タイマーをつかってTimer.Tickイベントが発生したら現在設定されている方向または予約されている方向で進むことができる方向にマイカーを移動させます。そして最後にglControl1.Refresh()メソッドを呼び出して画面を再描画します。
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 |
public partial class Form1 : Form { private void Timer_Tick(object sender, EventArgs e) { if( (CurDirect == Direct.North || NextDirect == Direct.North) && CanNorth(EyeX, EyeY) ) { CurDirect = Direct.North; EyeY += 0.20; } if( (CurDirect == Direct.East || NextDirect == Direct.East) && CanEast(EyeX, EyeY) ) { CurDirect = Direct.East; EyeX += 0.20; } if( (CurDirect == Direct.South || NextDirect == Direct.South) && CanSouth(EyeX, EyeY) ) { CurDirect = Direct.South; EyeY -= 0.20; } if( (CurDirect == Direct.West || NextDirect == Direct.West) && CanWest(EyeX, EyeY) ) { CurDirect = Direct.West; EyeX -= 0.20; } glControl1.Refresh(); } } |
再描画されるときの処理を示します。道路を描画してそのあとマイカーを描画します。最後にglControl1.SwapBuffers()メソッドを実行します。
1 2 3 4 5 6 7 8 9 10 11 |
public partial class Form1 : Form { private void GlControlEx1_Paint(object sender, PaintEventArgs e) { GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); DrawRoad(); DrawMyCar(); glControl1.SwapBuffers(); } } |
DrawMyCar()メソッドはマイカーを描画するメソッドです。位置と進行方向に合わせて適切に描画する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public partial class Form1 : Form { void DrawMyCar() { GL.PushMatrix(); GL.Translate(EyeX, EyeY, 0); if(CurDirect == Direct.East) GL.Rotate(-90, 0, 0, 1); if(CurDirect == Direct.South) GL.Rotate(180, 0, 0, 1); if(CurDirect == Direct.West) GL.Rotate(90, 0, 0, 1); DrawCarZero(Color.Blue); GL.PopMatrix(); } } |
DrawCarZero(Color color)メソッドは、色を指定し、原点を中心に車を描画するメソッドです。
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 |
public partial class Form1 : Form { void DrawCarZero(Color color) { double side = 1.0; GL.PushMatrix(); { // 原点を中心とする GL.Translate(-side / 2, -side / 2, 0); GL.Color3(Color.Black); GL.Begin(BeginMode.Quads); { // 前輪 左 GL.Vertex3(0.2, 0.9, 0.01); GL.Vertex3(0.2, 0.7, 0.01); GL.Vertex3(0.35, 0.7, 0.01); GL.Vertex3(0.35, 0.9, 0.01); // 前輪 右 GL.Vertex3(0.65, 0.9, 0.01); GL.Vertex3(0.65, 0.7, 0.01); GL.Vertex3(0.8, 0.7, 0.01); GL.Vertex3(0.8, 0.9, 0.01); // 前輪 車軸 GL.Vertex3(0.35, 0.82, 0.01); GL.Vertex3(0.35, 0.78, 0.01); GL.Vertex3(0.65, 0.78, 0.01); GL.Vertex3(0.65, 0.82, 0.01); // 後輪 左 GL.Vertex3(0.1, 0.4, 0.01); GL.Vertex3(0.1, 0.1, 0.01); GL.Vertex3(0.25, 0.1, 0.01); GL.Vertex3(0.25, 0.4, 0.01); // 後輪 右 GL.Vertex3(0.75, 0.4, 0.01); GL.Vertex3(0.75, 0.1, 0.01); GL.Vertex3(0.9, 0.1, 0.01); GL.Vertex3(0.9, 0.4, 0.01); // 後輪 車軸 GL.Vertex3(0.25, 0.27, 0.01); GL.Vertex3(0.25, 0.23, 0.01); GL.Vertex3(0.75, 0.23, 0.01); GL.Vertex3(0.75, 0.27, 0.01); } GL.End(); GL.Color3(color); GL.Begin(BeginMode.Quads); { // 車体 前部 GL.Vertex3(0.4, 0.95, 0.02); GL.Vertex3(0.4, 0.7, 0.02); GL.Vertex3(0.6, 0.7, 0.02); GL.Vertex3(0.6, 0.95, 0.02); // 車体 後部 GL.Vertex3(0.3, 0.45, 0.02); GL.Vertex3(0.3, 0.1, 0.02); GL.Vertex3(0.7, 0.1, 0.02); GL.Vertex3(0.7, 0.45, 0.02); } GL.End(); GL.Begin(BeginMode.Triangles); { // 車体 中部 GL.Vertex3(0.5, 0.8, 0.02); GL.Vertex3(0.2, 0.45, 0.02); GL.Vertex3(0.8, 0.45, 0.02); } GL.End(); GL.Color3(Color.Yellow); GL.Begin(BeginMode.TriangleFan); { // 車体 中部 GL.Vertex3(0.5, 0.6, 0.03); GL.Vertex3(0.4, 0.5, 0.03); GL.Vertex3(0.4, 0.25, 0.03); GL.Vertex3(0.6, 0.25, 0.03); GL.Vertex3(0.6, 0.5, 0.03); } GL.End(); GL.Color3(Color.Black); GL.Begin(BeginMode.Quads); { GL.Vertex3(0.35, 0.15, 0.03); GL.Vertex3(0.35, 0.0, 0.03); GL.Vertex3(0.45, 0.0, 0.03); GL.Vertex3(0.45, 0.15, 0.03); GL.Vertex3(0.55, 0.15, 0.03); GL.Vertex3(0.55, 0.0, 0.03); GL.Vertex3(0.65, 0.0, 0.03); GL.Vertex3(0.65, 0.15, 0.03); } GL.End(); } GL.PopMatrix(); } } |