標準のC#/ Windows Formsゲームループとは何ですか?


32

昔ながらのWindowsフォームとSlimDXOpenTKなどのグラフィックAPIラッパーを使用するC#でゲームを作成する場合、メインゲームループはどのように構成する必要がありますか?

正規のWindowsフォームアプリケーションには、次のようなエントリポイントがあります

public static void Main () {
  Application.Run(new MainForm());
}

そしてクラスのさまざまなイベントをフックすることで必要なものを達成することはできますがForm、それらのイベントは、ゲームロジックオブジェクトへの一定の定期的な更新を実行したり、レンダリングを開始および終了するためのコードのビットを置く明確な場所を提供しませんフレーム。

そのようなゲームは、標準に似た何かを達成するためにどのようなテクニックを使用する必要がありますか

while(!done) {
  update();
  render();
}

ゲームループ、そしてパフォーマンスとGCへの影響を最小限に抑えますか?

回答:


45

Application.Run呼び出しは、最終的には何の力、すべてあなたが上フックできるイベントであるWindowsのメッセージポンプ駆動するFormクラス(およびその他)。このエコシステムでゲームループを作成するには、アプリケーションのメッセージポンプが空になったときにリッスンし、空のままで、典型的なゲームループの典型的な「入力状態の処理、ゲームロジックの更新、シーンのレンダリング」手順を実行します。

Application.Idleイベントが発生するたびに一度アプリケーションのメッセージキューが空にされ、アプリケーションがアイドル状態に遷移しています。メインフォームのコンストラクターでイベントをフックできます。

class MainForm : Form {
  public MainForm () {
    Application.Idle += HandleApplicationIdle;
  }

  void HandleApplicationIdle (object sender, EventArgs e) {
    //TODO: Implement me.
  }
}

次に、アプリケーションがまだアイドル状態であるかどうかを判断できるようにする必要があります。Idleアプリケーションは、ときにイベントは、一度発射なっアイドル。メッセージがキューに入ってからキューが再び空になるまで、再び起動されません。Windows Formsは、メッセージキューの状態を照会するメソッドを公開しませんが、プラットフォーム呼び出しサービスを使用して、その質問に答えることができるネイティブWin32関数に照会を委任できます。のインポート宣言PeekMessageとそのサポート型は次のようになります。

[StructLayout(LayoutKind.Sequential)]
public struct NativeMessage
{
    public IntPtr Handle;
    public uint Message;
    public IntPtr WParameter;
    public IntPtr LParameter;
    public uint Time;
    public Point Location;
}

[DllImport("user32.dll")]
public static extern int PeekMessage(out NativeMessage message, IntPtr window, uint filterMin, uint filterMax, uint remove);

PeekMessage基本的に、キュー内の次のメッセージを見ることができます。存在する場合はtrue、存在しない場合はfalseを返します。この問題の目的上、特に関連するパラメーターはありません。重要なのは戻り値のみです。これにより、アプリケーションがまだアイドル状態である(つまり、キューにメッセージが残っていない)かどうかを通知する関数を作成できます。

bool IsApplicationIdle () {
    NativeMessage result;
    return PeekMessage(out result, IntPtr.Zero, (uint)0, (uint)0, (uint)0) == 0;
}

これで、完全なゲームループを作成するために必要なものがすべて揃いました。

class MainForm : Form {
  public MainForm () {
    Application.Idle += HandleApplicationIdle;
  }

  void HandleApplicationIdle (object sender, EventArgs e) {
    while(IsApplicationIdle()) {
      Update();
      Render();
    }
  }

  void Update () {
    // ...
  }

  void Render () {
    // ...
  }

  [StructLayout(LayoutKind.Sequential)]
  public struct NativeMessage
  {
      public IntPtr Handle;
      public uint Message;
      public IntPtr WParameter;
      public IntPtr LParameter;
      public uint Time;
      public Point Location;
  }

  [DllImport("user32.dll")]
  public static extern int PeekMessage(out NativeMessage message, IntPtr window, uint filterMin, uint filterMax, uint remove);
}

さらに、このアプローチは、次のような標準的なネイティブ Windowsゲームループに(P / Invokeへの依存を最小限に抑えて)可能な限り近く一致します。

while (!done) {
    if (PeekMessage(&message, window, 0, 0, PM_REMOVE)){
        TranslateMessage(&message);
        DispatchMessage(&message);
    }
    else {
        Update();
        Render();
    }
}

このようなWindows API機能を扱う必要性は何ですか?正確なストップウォッチ(fpsコントロール用)で支配されるwhileブロックを作成するだけでは十分ではないでしょうか?
エミールリマ

3
ある時点でApplication.Idleハンドラーから戻る必要があります。そうしないと、アプリケーションがフリーズします(これ以上Win32メッセージが発生することはないため)。代わりに、WM_TIMERメッセージに基づいてループを作成しようとすることもできますが、WM_TIMERは実際に必要なほど正確ではありません。多くのゲームでは、独立したレンダリングとロジックの更新レートが必要または必要であり、その一部(物理学など)は固定されたままですが、そうでないものもあります。
ジョシュ

ネイティブのWindowsゲームループは同じテクニックを使用します(比較のために単純なものを含むように回答を修正しました。固定更新レートを強制するタイマーは柔軟性が低く、PeekMessageのより広いコンテキスト内で常に固定更新レートを実装できます。スタイルループ(WM_TIMERベースのループよりも精度が高く、GCに影響するタイマーを使用)
ジョシュ

@JoshPetrie明確にするために、上記のアイドルのチェックでは、SlimDXの関数を使用します。これを答えに含めるのが理想でしょうか?それとも、たまたまコードを編集して、SlimDXの対応物である「IsApplicationIdle」を読み取るようになっていますか?
ヴォーンヒルツ14

**私を無視してください、あなたはそれをさらに下に定義することに気付きました... :)
ヴォーン・ヒルツ14

2

ジョシュからの回答に同意し、5セントを追加したいだけです。WinFormsのデフォルトのメッセージループ(Application.Run)は、次のものに置き換えられます(p / invokeなし):

[STAThread]
static void Main()
{
    using (Form1 f = new Form1())
    {
        f.Show();
        while (true) // here should be some nice exit condition
        {
            Application.DoEvents(); // default message pump
        }
    }
}

また、メッセージポンプにコードを挿入する場合は、次を使用します。

public partial class Form1 : Form
{
    protected override void WndProc(ref Message m)
    {
        // this code is invoked inside default message pump
        base.WndProc(ref m);
    }
}

2
ただし、この方法を選択する場合は、DoEvents()のガベージ生成の オーバーヘッドに注意する必要があります。
ジョシュ

0

私はこれが古いスレッドであることを理解していますが、上記の手法の2つの代替手段を提供したいと思います。それらに進む前に、これまでに提案されたいくつかの落とし穴を以下に示します。

  1. PeekMessageは、それを呼び出すライブラリメソッド(SlimDX IsApplicationIdle)と同様に、かなりのオーバーヘッドを伴います。

  2. バッファリングされたRawInputを使用する場合は、UIスレッド以外の別のスレッドでPeekMessageを使用してメッセージポンプをポーリングする必要があるため、2回呼び出す必要はありません。

  3. Application.DoEventsはタイトループで呼び出されるように設計されていないため、GCの問題はすぐに発生します。

  4. Application.IdleまたはPeekMessageを使用する場合、Idleのときだけ作業を行うため、ウィンドウを移動またはサイズ変更するときにゲームやアプリケーションは実行されず、コードの臭いもありません。

これらを回避するには(RawInputを使用する場合は2を除く)、次のいずれかを実行できます。

  1. Threading.Threadを作成し、そこでゲームループを実行します。

  2. IsLongRunningフラグを使用してThreading.Tasks.Taskを作成し、そこで実行します。マイクロソフトは最近、タスクをスレッドの代わりに使用することを推奨していますが、その理由を確認するのは難しくありません。

これらの手法はどちらも、推奨されるアプローチであるように、グラフィックスAPIをUIスレッドおよびメッセージポンプから分離します。ウィンドウのサイズ変更中のリソース/状態の破壊と再作成の処理も簡素化され、UIスレッドの外部から完了(メッセージポンプによるデッドロックを回避するために十分な注意を払う)を行うと、審美的にはるかにプロフェッショナルになります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.