3Dっぽい縦シューティングゲームをつくるに対してちょっと変わったコメントをいただきました。3Dっぽい縦シューティングゲームとはこれのことです。
今まで作成したFrom1の他に新しいFromでタイトル画面と操作画面をそれぞれ作成しそれをタイトル→操作方法→ゲーム画面の順で呼びだしたいのですが、Fromの呼び出し順序のところが上手くいきませんでした。(From3,2,1の順で呼び出したい。)
Form1に加えてForm2、Form3クラスを作成して最初にForm3クラスで生成されるフォームを作成する、あとはForm3のメニューをクリックしたらForm2が出現するようにしてForm2のメニューがクリックされたらForm1を出現させる。簡単なことではないか!
そうおもって実際にコードを書いてFrom3,2,1の順で呼び出すことができることを確認。これで作業完了♪
と思ったのですが、実はそんな簡単な問題ではなかったのです。
Contents
あとから実装したForm2を先に表示させるだけなら簡単
これは一般論なのですが、Form1クラスを作成して、あとになって最初に別のフォームを表示させてそれからForm1を表示させたいと思ったら、この方法で解決します。
まずFrom2クラスをつくる。
From2にFrom1を表示させるためのメニューまたはボタンをつくる
メニューまたはボタンがクリックされたら以下を実行する
1 2 |
Form1 form1 = new Form1(); form1.ShowDialog(); |
これでいいのですが、このゲームで同じ事をしようとすると失敗します。
静的フィールド変数がトラブルメーカーに
実は3Dっぽい縦シューティングゲームをつくるで作成したForm1クラスは「同じプログラムから複数のインスタンスが生成される」ということを想定していません。Form1のインスタンスは常にひとつしか存在しないという前提で作られています。
そのためForm1のなかには静的なフィールド変数が存在します。この場合、Form1クラスを複数生成したり、一度閉じてしまったForm1を再び表示させようとするとForm1のなかに存在する静的なフィールド変数が問題をおこします。静的な変数は値を保持し続けるからです。
ではこれらの静的なフィールド変数をリセットすればいいじゃんと思うかもしれませんが、もうひとつ問題が存在します。このプログラムはOpenTkを使用していますが、これが使用するテクスチャがうまく機能してくれないのです。この説明でなにを言っているのかわからないかもしれませんが、実際にやってみるとテクスチャがおかしなことになっていることに気づきます。
ではどうすればいいのか?
Form1クラスのインスタンスはプログラムが終了するまで破棄されず、つねにひとつだけしか存在しないことを保証するプログラムを書けばよいのです。
フォームを閉じたように見せかけて非表示にする
ではコードを示します。
質問者様にとって最初に表示させたいフォームはForm3です。これはタイトル画面のようなものにします。デザインに関しては各自に任せます。Form3が生成されたらすぐにForm2をつくります。そしてForm2が閉じられ、新たにForm2を表示させたいときにはForm2を作り直すのではなく、最初に作成したForm2を再度使用します。
Form2を閉じたときにForm2クラスのインスタンスは破棄されてしまうのではなく、非表示にしてそのまま残すのです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Form3 最初に表示させたいフォーム public partial class Form3 : Form { public Form3() { InitializeComponent(); } Form2 form2 = new Form2(); private void HowToGameMenuItem_Click(object sender, EventArgs e) { form2.Show(); } } |
Programクラスの変更
それからプログラムが開始されたら最初に表示させたいのはForm3なのでProgramクラスを書き換えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
static class Program { /// <summary> /// アプリケーションのメイン エントリ ポイントです。 /// </summary> [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); //Application.Run(new Form1()); Application.Run(new Form3()); // Form1()ではなくForm3()に変更する } } |
次にForm2クラスを示します。Form2は操作方法を表示させるフォームになります。このフォームのメニューをクリックすると最初に作成したForm1が表示されます。
ここでもForm1が閉じられようとしたときにForm1のインスタンスを破棄してはなりません。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 |
// Form2 その次に表示させたいフォーム public partial class Form2 : Form { public Form2() { InitializeComponent(); } Form1 form1 = new Form1(); private void GameStartMenuItem_Click(object sender, EventArgs e) { form1.Show(); // form1.Show();にすること。form1.ShowDialog();だとうまくいかない form1.TimerStart(); } // フォームが閉じられようとしたら非表示にする // ユーザーから見るとフォームが閉じたように見えるが、実は非表示になっただけ protected override void OnClosing(CancelEventArgs e) { this.Visible = false; // 非表示にする e.Cancel = true; // フォームを閉じるという通常の動作をさせない base.OnClosing(e); } } |
最後にForm1クラスに対する追加部分を示します。
原作ではコンストラクタのなかでタイマーをスタートさせていましたが、今回はコンストラクタが実行された段階ですぐにゲームが始まるわけではありません。そこでForm1がロードされたタイミングでタイマーをスタートさせます。
フォームが閉じられようとしたら非表示にする部分はForm2クラスと同じですが、そのほかにタイマーやBGMを止めたり、フィールド変数をリセットする処理もおこないます。
またOnLoadメソッドでタイマーをスタートさせていますが、2回目にForm1が表示されるときはOnLoadメソッドは実行されません。そうなると一度止めてしまったタイマーを再スタートさせる手段が存在しないため、TimerStartメソッドを新しく作成しています。
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 |
public partial class Form1 : Form { public Form1() { InitializeComponent(); Timer.Interval = 30; Timer.Tick += Timer_Tick; } public void TimerStart() { Timer.Start(); } protected override void OnLoad(EventArgs e) { Timer.Start(); base.OnLoad(e); } protected override void OnClosing(CancelEventArgs e) { // タイマーとBGMを止める mediaPlayerBGM.controls.stop(); Timer.Stop(); // 次にフォームが表示されたときゲームが進行している状態で表示させないために // フィールド変数をリセットする Enemies.Clear(); EnemyBurrets.Clear(); JikiBurrets.Clear(); Explosions.Clear(); IsBattleAntiBoss = false; BaseY = 0; Jiki = null; Score = 0; // 非表示にする this.Visible = false; e.Cancel = true; base.OnClosing(e); } } |
新たに発覚した問題点
それからもうひとつ問題点があることがわかりました。ゲームを開始したあと別のウィンドウをクリックしてまたゲームのフォームをアクティブにしたときになぜか自機を操作することができません(普通はゲームがはじまったらゲームが終わるまで他のウィンドウに触ることはないので気がつかなかった)。
一度、別のところにいってしまったフォーカスを取り戻すことができなくなっているのが原因のように思われます。この問題は以下で解決できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public partial class Form1 : Form { public Form1() { InitializeComponent(); Timer.Interval = 30; Timer.Tick += Timer_Tick; KeyPreview = true; // これを追加 } // ProcessDialogKeyメソッドをオーバーライドする protected override bool ProcessDialogKey(Keys keyData) { return false; // 必ずfalseを返すこと } } |
どうしてもForm1を複数表示したい場合
さて、一応、これで質問に対する回答としたいのですが、この方法ではForm1を複数表示させることはできません。これをやろうとすると設計からやり直しとなります。
もしForm1を複数表示させたいのであれば(そんなことをしたいと思うかな?)、別のプログラムから起動させるという方法があります。これだと別々のプログラムでForm1が生成されるので、問題はおきません。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 別のプロジェクトでつくったForm1 public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void GameStartMenuItem_Click(object sender, EventArgs e) { System.Diagnostics.Process.Start("ゲームの実行ファイルがあるパスを指定する"); } } |