Was ist die Standard-C # / Windows Forms-Spieleschleife?

32

Wie sollte die Hauptspielschleife strukturiert sein, wenn Sie ein Spiel in C # schreiben, das normale Windows Forms und einige Grafik-API-Wrapper wie SlimDX oder OpenTK verwendet?

Eine kanonische Windows Forms-Anwendung verfügt über einen Einstiegspunkt, der aussieht

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

und während man durch Verknüpfen der verschiedenen Ereignisse der Klasse einiges von dem erreichen kann, was erforderlich ist, bieten diese Ereignisse keinen offensichtlichen Ort, um die Codebits zu platzieren, um konstante periodische Aktualisierungen an Spiellogikobjekten durchzuführen oder ein Rendern zu beginnen und zu beenden Rahmen.Form

Welche Technik sollte ein solches Spiel verwenden, um etwas zu erreichen, das dem Kanonischen ähnlich ist?

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

Spieleschleife und mit minimaler Leistung und GC-Auswirkung zu tun?

Josh
quelle

Antworten:

45

Der Application.RunAufruf steuert Ihre Windows-Nachrichtenpumpe. Dies ist letztendlich die Grundlage für alle Ereignisse, die Sie für die FormKlasse (und für andere) festlegen können . Um eine Spieleschleife in diesem Ökosystem zu erstellen, möchten Sie abhören, wenn die Nachrichtenpumpe der Anwendung leer ist. Führen Sie die typischen Schritte der prototypischen Spieleschleife aus, während sie leer bleibt .

Das Application.IdleEreignis wird jedes Mal ausgelöst, wenn die Nachrichtenwarteschlange der Anwendung geleert wird und die Anwendung in einen Ruhezustand übergeht. Sie können das Ereignis in den Konstruktor Ihres Hauptformulars einbinden:

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

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

Als Nächstes müssen Sie feststellen können, ob die Anwendung noch inaktiv ist. Das IdleEreignis löst nur einmal, wenn die Anwendung wird im Leerlauf. Es wird nicht erneut ausgelöst, bis eine Nachricht in der Warteschlange eingeht und die Warteschlange dann wieder geleert wird. Windows Forms stellt keine Methode zum Abfragen des Status der Nachrichtenwarteschlange zur Verfügung. Sie können jedoch Plattformaufrufdienste verwenden, um die Abfrage an eine systemeigene Win32-Funktion zu delegieren, die diese Frage beantworten kann . Die Einfuhranmeldung für PeekMessageund ihre unterstützenden Typen sieht folgendermaßen aus:

[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);

PeekMessageIm Prinzip können Sie die nächste Nachricht in der Warteschlange anzeigen. es gibt true zurück, falls vorhanden, andernfalls false. Für die Zwecke dieses Problems ist keiner der Parameter besonders relevant: Nur der Rückgabewert ist von Bedeutung. Auf diese Weise können Sie eine Funktion schreiben, die angibt, ob die Anwendung noch inaktiv ist (dh, die Warteschlange enthält noch keine Nachrichten):

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

Jetzt hast du alles, was du brauchst, um deine komplette Spieleschleife zu schreiben:

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);
}

Darüber hinaus entspricht dieser Ansatz so genau wie möglich (mit minimaler Abhängigkeit von P / Invoke) der kanonischen systemeigenen Windows-Spieleschleife, die wie folgt aussieht:

while (!done) {
    if (PeekMessage(&message, window, 0, 0, PM_REMOVE)){
        TranslateMessage(&message);
        DispatchMessage(&message);
    }
    else {
        Update();
        Render();
    }
}
Josh
quelle
Was ist die Notwendigkeit, mit solchen Windows-API-Funktionen umzugehen? Es würde nicht ausreichen, eine Weile zu blockieren, die von einer präzisen Stoppuhr (zur Steuerung der Bildrate) gesteuert wird.
Emir Lima
3
Sie müssen irgendwann vom Application.Idle-Handler zurückkehren, da Ihre Anwendung sonst einfriert (da keine weiteren Win32-Nachrichten mehr möglich sind). Sie könnten stattdessen versuchen, eine Schleife basierend auf WM_TIMER-Nachrichten zu erstellen, aber WM_TIMER ist bei weitem nicht so genau, wie Sie es wirklich möchten, und selbst wenn dies der Fall wäre, würde dies alles auf die niedrigste Aktualisierungsrate für gemeinsame Nenner zwingen. Viele Spiele benötigen oder möchten unabhängige Render- und Logik-Aktualisierungsraten, von denen einige (wie die Physik) unverändert bleiben, während andere dies nicht tun.
Josh
Native Windows-Game-Loops verwenden die gleiche Technik (ich habe meine Antwort so geändert, dass sie zum Vergleich eine einfache enthält. Die Zeitgeber für die Erzwingung einer festen Aktualisierungsrate sind weniger flexibel, und Sie können Ihre feste Aktualisierungsrate immer im breiteren Kontext von PeekMessage implementieren -Style Loop (mit Timern mit besserer Präzision und GC-Wirkung als WM_TIMER-basierte)
Josh
@JoshPetrie Bei der obigen Prüfung auf Leerlauf wird eine Funktion für SlimDX verwendet. Wäre es ideal, dies in die Antwort aufzunehmen? Oder haben Sie den Code zufällig so bearbeitet, dass er "IsApplicationIdle" lautet, das Gegenstück zu SlimDX?
Vaughan Hilts
** Bitte ignoriere mich, ich habe gerade gemerkt, dass du es weiter unten definierst ... :)
Vaughan Hilts
2

Mit der Antwort von Josh einverstanden, möchte nur meine 5 Cent hinzufügen. Die WinForms-Standardnachrichtenschleife (Application.Run) könnte durch die folgende ersetzt werden (ohne 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
        }
    }
}

Auch wenn Sie Code in Message Pump einfügen möchten, verwenden Sie diesen Befehl:

public partial class Form1 : Form
{
    protected override void WndProc(ref Message m)
    {
        // this code is invoked inside default message pump
        base.WndProc(ref m);
    }
}
infra
quelle
2
Sie müssen sich bewusst sein , die DoEvents () Müll-Generation - Overhead , wenn Sie diesen Ansatz wählen, aber.
Josh
0

Ich verstehe, dass dies ein alter Thread ist, möchte aber zwei Alternativen zu den oben vorgeschlagenen Techniken anbieten. Bevor ich darauf eingehe, hier einige Fallstricke mit den bisher gemachten Vorschlägen:

  1. PeekMessage und die aufgerufenen Bibliotheksmethoden (SlimDX IsApplicationIdle) verursachen einen erheblichen Overhead.

  2. Wenn Sie gepufferten RawInput verwenden möchten, müssen Sie die Nachrichtenpumpe mit PeekMessage in einem anderen Thread als dem UI-Thread abfragen, damit Sie sie nicht zweimal aufrufen müssen.

  3. Application.DoEvents ist nicht dafür ausgelegt, in einer engen Schleife aufgerufen zu werden. GC-Probleme treten schnell auf.

  4. Wenn Sie Application.Idle oder PeekMessage verwenden, wird Ihr Spiel oder Ihre Anwendung beim Verschieben oder Ändern der Fenstergröße ohne Codegerüche nicht ausgeführt, da Sie nur im Leerlauf arbeiten.

Um dies zu umgehen (mit Ausnahme von 2, wenn Sie die RawInput-Straße entlang fahren), haben Sie folgende Möglichkeiten:

  1. Erstellen Sie einen Threading.Thread und führen Sie dort Ihre Spieleschleife aus.

  2. Erstellen Sie eine Threading.Tasks.Task mit dem IsLongRunning-Flag und führen Sie sie dort aus. Microsoft empfiehlt, dass Aufgaben heutzutage anstelle von Threads verwendet werden, und es ist nicht schwer zu verstehen, warum.

Beide Techniken isolieren Ihre Grafik-API vom Benutzeroberflächenthread und der Nachrichtenpumpe, wie es der empfohlene Ansatz ist. Der Umgang mit der Zerstörung von Ressourcen / Zuständen und der Wiederherstellung während der Größenänderung von Fenstern wird ebenfalls vereinfacht und ist ästhetisch weitaus professioneller, wenn dies von außerhalb des UI-Threads erledigt wird.

ROGRat
quelle