こんな感じのゲームをつくります。
前回のC# OpenTKでスクランブルもどきをつくるでは肝心の自機がありませんでした。そこで今回は自機を表示させます。また自機めがけてミサイルを発射させます。
そのまえに下の2つの画像をみると、フォームの大きさを変更するとマップの見える部分が変わっていることに気づきます。これではよくないのでフォームの大きさは一定(640×480ピクセル)にします。


| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public partial class Form1 : Form {     public Form1()     {         InitializeComponent();         FormSizeFix();         timer.Tick += Timer_Tick;         TimerIntervalReset();         this.BackColor = Color.Black;     }     void FormSizeFix()     {         this.Size = new Size(640, 480);         this.FormBorderStyle = FormBorderStyle.FixedSingle;         this.MaximizeBox = false;     } } | 
自機の位置を管理するクラスを作成します。
コンストラクタ内でSetStartPosition()メソッドを実行しています。自機の位置はフィールド変数 XとYで表しますが、SetStartPosition()メソッドでXとYの値を最初に表示されるべき位置にセットしているわけです。そのときにForm1.ProjectionWidthの値を利用しています。またフィールド変数 startXにこのとき得られた値を保存しています(あとで使う)。
Move(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 | public class Jiki {     public float X = 0f;     public float Y = 0f;     public bool IsShow = false;     public float startX = 0f;     public Jiki()     {         SetStartPosition();     }     public void SetStartPosition()     {         X = - Form1.ProjectionWidth / 2 + 3;         Y = 3;         startX = X;     }     public void Move(float x, float y)     {         X += x;         Y += y;     }     public void Draw()     {         if(!IsShow)             return;         GL.PushMatrix();         {             GL.Translate(X, Y, 0);             GL.Color3(Color.White);             GL.BindTexture(TextureTarget.Texture2D, Form1.JikiTexture);             GL.Begin(BeginMode.Quads);             {                 GL.TexCoord2(1, 1);                 GL.Vertex3( 1,  0.5, 0.1);                 GL.TexCoord2(1, 0);                 GL.Vertex3( 1, -0.5, 0.1);                 GL.TexCoord2(0, 0);                 GL.Vertex3(-2, -0.5, 0.1);                 GL.TexCoord2(0, 1);                 GL.Vertex3(-2,  0.5, 0.1);             }             GL.End();             GL.BindTexture(TextureTarget.Texture2D, 0);         }         GL.PopMatrix();     } } | 
次に自機を操縦できるようにします。
これは自機が移動する方向を示す列挙体ですが、「後ろ」がありません。戦闘機なので加速はできてもバックすることはできないのです。
| 1 2 3 4 5 6 7 | public enum Direct {     Up,     Down,     Accelerate,     None, } | 
まずゲームがスタートしたら自機を表示させます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public partial class Form1 : Form {     void GameStart()     {         TimerIntervalReset();         EyeX = 0;         EyeY = 0;         // 自機を表示させる         Stage.Jiki.IsShow = true;         Stage.Jiki.SetStartPosition();         Fuel = FuelMax;         // 最初の自機の移動方向は Direct.None         CurDirect = Direct.None;         Stage.Init();         timer.Start();     } } | 
CurDirectプロパティに移動したい方向をセットして、Timer.Tickイベントを待ちます。イベントが発生したらStageクラスの自作メソッド 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 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 | public partial class Form1 : Form {     static public Direct CurDirect     {         get;         private set;     }     = Direct.None;     protected override void OnKeyDown(KeyEventArgs e)     {         if(e.KeyCode == Keys.Up)         {             CurDirect = Direct.Up;         }         if(e.KeyCode == Keys.Down)         {             CurDirect = Direct.Down;         }         if(e.KeyCode == Keys.Right)         {             CurDirect = Direct.Front;         }         if(e.KeyCode == Keys.Z)         {             Shot();         }         if(e.KeyCode == Keys.X)         {             Bomb();         }         base.OnKeyDown(e);     }     protected override void OnKeyUp(KeyEventArgs e)     {         if(e.KeyCode == Keys.Up || e.KeyCode == Keys.Down || e.KeyCode == Keys.Right)         {             CurDirect = Direct.None;         }         base.OnKeyDown(e);     }     private void Timer_Tick(object sender, EventArgs e)     {         EyeX += ScrollSpeed;         Stage.Update();         glControl1.Refresh();     }     void Shot()     {         // あとで考える     }     void Bomb()     {         // あとで考える     } } | 
StageクラスのUpdateメソッドで自機を動かします(ここでは座標を設定しているだけ)。それから動かさなくても自機は前方に移動しています(通常の飛行)。ただし加速後の減速時には通常の飛行はしません。この場合、自機は後方に移動します。これを利用すると垂直に移動させることができます。この技が使えないとステージクリアは難しいです。
加速後の状態かどうかは、自機のX座標と(カメラのX座標+Jiki.startX)を比較して判別しています。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public class Stage {     public void Update()     {         Direct direct = Form1.CurDirect;         if(direct == Direct.Up)             Jiki.Move(0, 0.5f);         if(direct == Direct.Down)             Jiki.Move(0, -0.5f);         if(direct == Direct.Accelerate)             Jiki.Move(0.5f, 0);         // 通常の飛行         if(Jiki.X <= Form1.GetEyeX() + Jiki.startX)             Jiki.Move(Form1.ScrollSpeed, 0);         // ミサイルは発射されるか?         MissilesLaunch();         MissilesMove();     } } | 
次に自機めがけてミサイルを発射させます。発射すれば命中するタイミングで発射するようにしないといけないのですが、全部のミサイルをそのようにしてしまうとよけやすくなるので早めのタイミングで発射したり、あえて発射しないという工夫が必要です。
Missileクラスのコンストラクタを少し変更しました。コンストラクタのなかで発射のタイミングを決めてしまいます。(生成した乱数-3)が0であれば最適のタイミング、正数であれば早め、負数であれば遅めのタイミングで発射されます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class Missile {     static public float Speed = 0.3f;     static Random Random = new Random();     // 発射のタイミング     // 最適 = 0、0以上は早め     public float Timing = 0;     public Missile(float x, float y)     {         X = x;         Y = y;         Timing = (float)((Random.Next(6)) - 3);     } } | 
StageクラスのMissilesLaunch()メソッドは自機と各ミサイルの座標からどれだけX座標が違っていれば発射のタイミングが最適かを調べます。これと各ミサイルに設定されているタイミングを比較して、本当にミサイルを発射するかどうかを決めます。
発射するときはミサイルクラスのフィールド変数 isMoveをtrueにします。MissilesMove()メソッドが実行されると撃墜されていないミサイルでisMoveがtrueのものだけ上昇していきます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class Stage {     public void MissilesLaunch()     {         Missile missile = Missiles.FirstOrDefault(x => !x.isDead && !x.isMove && (x.Timing > (x.X - Jiki.X) - (Jiki.Y - x.Y) * Form1.ScrollSpeed / Missile.Speed));         if(missile != null)         {             missile.isMove = true;         }     }     public void MissilesMove()     {         List<Missile> missiles = Missiles.Where(x => !x.isDead && x.isMove).ToList();         foreach(Missile missile in missiles)             missile.Move();     } } | 
