前回、ゲームの描画について解説しました。その際に、ゲームのメインループに関しては Timer で暫定的な実装をしています。今回は暫定的ではなくて本格的な実装例を紹介していきます。内容に関しては前回の続きとなりますので、直接こちらに来てしまった場合は、前回の記事を参照してからお読みください。
--
C# 2Dゲームの画面描画


  • Program.cs にメインループを作る
前回はフレートレートの調整にタイマーイベントを利用していました。ゲームのメイン処理もタイマーイベントに乗せていました。実はフォームの各イベントは出来る限り軽く実装することが要求されています。そのため、フォームに対してはあくまでも結果を表示するだけとスべきです。では、ゲームはどこに記述するのかと言えば、Program.cs となります。この Program.cs はソリューションエクスプローラーを見ると確認できます。
起動直後のエントリであるMainがここにある
C言語で見慣れた Main() がここにあります。アプリケーションが起動した直後のエントリである Main() で、アプリケーションの初期化が最初に行われ、続いて Form1 オブジェクトが生成されて実行されています。デフォルトではただそれだけの非常にシンプルな構造となっています。つまりフォームが生きているうちは、ここは何も使われていないのです。これはもったいないです。

少し書き換えて動作の確認をしてみます。
ShowDialogに変更してみた。
Application.Run によるフォームの実行を、ダイアログの表示と実行を行う ShowDialog() 呼び出しに変えています。これでも以前と全く変わらない動作をします。メインフォームだろうがダイアログだろうが、フォームに代わりはないのでこのような記述ができます。さて、ShowDialog() メソッドはブロッキングメソッドです。ブロッキングメソッドとは、呼び出した先の処理が終了するまで、呼び出し元は待つ動作をします。そして、ダイアログには同期型と非同期型がありました。ということは、メインフォームでも同じことが出来るはずです。試しましょう。
ノンブロッキング呼び出しに変えてみる。
一瞬、フォームが表示されますがすぐに終了してしまいます。つまりはフォームの終了を待たずに処理が抜けてるので、親である Main が終了して、呼び出し先のフォームも強制的に終了しているというわけです。抜けないようにするには、フォームが終了したかどうかを判定して、動作中であればループをさせる必要があります。フォームの動作判定ですが、私は IsDisposed という読み取り専用プロパティを利用しました。これでどうでしょうか。
フォームが生きているなら待つように
確かに、終了しなくはなりましたが…
何も表示されないフォーム
フォームは背景色も何も表示されず、何も操作が効かなくて…
CPU負荷が高いまま
CPU負荷が高いままになり、私のPC冷却ファンが唸りを上げてぶん回るようになってしまいました。これは非常にマズイですね。実は、このメインループのままでは、少なくともCPUのコア一つを専有して最高速でぶん回るという、ある意味大変危険な動作をしている事になるのです。また、アプリケーションに対するメッセージもフォームまで処理が飛ばないので、フォーム全体が固まってしまったというわけです。

まず、CPU負荷を下げるには、適度にCPUを休ませる必要があります。そのための命令が Thread.Sleep(1); です。これをループに入れると CPU 負荷が下がります。また、メッセージ処理を行うには、ループ内に Application.DoEvents(); を入れる必要があります。
ノンブロッキングでフォームが正常起動する
これで問題なくフォームが表示されるようになりました。そして、この while(){} こそがゲームのメインループに利用できるスコープとなります。
【第2類医薬品】アレジオン20 48錠
エスエス製薬
2022-01-04
※ 私は花粉症でもありアレルギー性鼻炎でもあるので、こういうお薬は常備必須。最近は秋花粉にも反応し始めていて、堤防沿いを歩くのが苦行になり始めています…

  • メインループにゲームの動作を書く
ゲームのメインループの元は出来ましたが、このままでは超高速でぶん回っていることに変わりはありません。まずはフレームレートの調整を実装します。時間を計測して 1フレームの更新に必要な時間待ちをします。時間待ちにはいろんな方法がありますが、私は今回、Stopwatch を使ってみたいと思います。せっかく便利なクラスが用意されているのですから、使わないともったいないですw

直前で Stopwatch オブジェクトをインスタンスします。そして、ループの先頭で Restart() します。これでゼロスタートしますので、ループの最後に FPS カウントまで待てばフレームレートの固定が完成します。
メインループの最小形が出来た
Application.DoEvents() が二箇所に記述されているのは、どんなに処理が重くても、最低でも毎フレームに一度は Application.DoEvents() を実行させるためです。二度目の記述がないと、ゲームのメイン処理が重い場合は、Windows アプリとしてはよろしくない状態になるので注意が必要です。

さて、これでフレームレートの固定が出来ましたが、そういえばゲームのプログラムはどこに記述すればよいのでしょうか。もうなんとなく察しはついているかと思いますが、そうです、sw.Restart(); の直後から、while で時間待ちをする直前までの間に記述するのです。コメントを整理してみたコードがこちら。
ゲームのメイン処理を記述する場所
最後の疑問として、メインを Program で記述して、最後にフォームに表示を反映させるにはどうするのか。以前、タイマーでメインを記述していたときのことを思い出してください。ダブルバッファカウンタを更新して Invalidate() を呼び出していたかと思います。全く同じ事をメインからフォームに対して指示してやればよいのです。
画面の更新
さて、あとは今までフォームに乗っけていたゲーム関連のプロパティやメソッドをどんどん Program.cs 側に下ろしてしまいましょう。まずはプロパティをドンッ!
メインにゲーム関連のプロパティを移動
フォームから参照する必要がある Screen 画像と各種定数だけ public にしています。続いて、フォームのコンストラクタで行っていた各種プロパティの初期化も、新しく初期化メソッドを作ってそちらに移動します。
メインにゲームの初期化メソッドを移動
タイマーで処理していたプレイヤーの移動と表示もメインに移動します。
メインにプレイヤー処理も全て移動
最後にフォームの Dispose に記載していたリソースの開放関連処理も、新たに Dispose メソッドを作ってそちらに移動します。
メインにリソース開放メソッドを移動する
全部移動したので、フォームの中身はすっからかんです。唯一、Paint だけ、表示用の Screen をクライアント領域に表示するように内容を修正します。
フォームのプログラムはたったこれだけになりました。
メインに移動した各種メソッドは、メインから呼び出さないと意味がないので書き換えます。
ゲームのメイン処理を実装した
この最終的な構造が C# でゲームを記述する際の基本となります。ゲームの場合は、フォームに機能を割り当てることは殆どありませんから、この後は Windows フォームのことを考える必要はありません。

MainLoopTest20231015.zip
2023.10.15.1600

【指定第2類医薬品】新セデス錠 20錠
シオノギヘルスケア
2009-04-01
※ワクチン接種前の薬確保にはこちらを使っています。私はだいたいいつも翌日には発熱してるので…