Jaka jest standardowa pętla gry C # / Windows Forms?

32

Jak pisząc grę w języku C #, która korzysta ze zwykłych formularzy Windows Forms i niektórych interfejsów API graficznych, takich jak SlimDX lub OpenTK , jak powinna wyglądać główna pętla gry?

Kanoniczna aplikacja Windows Forms ma wyglądający punkt wejścia

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

i chociaż można osiągnąć część tego, co jest konieczne, przechwytując różne zdarzenia Formklasy , wydarzenia te nie zapewniają oczywistego miejsca na umieszczenie fragmentów kodu w celu wykonywania okresowych aktualizacji obiektów logiki gry lub rozpoczynania i kończenia renderowania rama.

Jakiej techniki powinna użyć taka gra, aby osiągnąć coś podobnego do kanonicznego

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

pętla gry, a czy zrobić z minimalną wydajnością i wpływem GC?

Josh
źródło

Odpowiedzi:

45

Application.RunWezwanie napędza pompę wiadomość Windows, który jest ostatecznie jakie moce wszystkie zdarzenia można zahaczyć o Formklasie (i innych). Aby utworzyć pętlę gry w tym ekosystemie, chcesz nasłuchiwać, kiedy pompa komunikatów aplikacji jest pusta, a gdy pozostaje pusta, wykonaj typowy „stan wejściowy procesu, zaktualizuj logikę gry, renderuj scenę” etapów prototypowej pętli gry .

W Application.IdleZdarzenie raz za każdym razem kolejka przesłanie aplikacji jest opróżniany, a aplikacja jest przejście do stanu czuwania. Możesz zaczepić zdarzenie w konstruktorze swojej głównej formy:

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

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

Następnie musisz ustalić, czy aplikacja jest nadal bezczynna. IdleZdarzenie wyzwala tylko raz, gdy aplikacja staje się bezczynny. Nie zostanie ponownie zwolniony, dopóki wiadomość nie dostanie się do kolejki, a następnie kolejka opróżni się ponownie. Windows Forms nie ujawnia metody zapytania o stan kolejki komunikatów, ale można użyć usług wywoływania platformy do delegowania zapytania do natywnej funkcji Win32, która może odpowiedzieć na to pytanie . Deklaracja importowa PeekMessagei jej typy pomocnicze wyglądają następująco:

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

PeekMessagew zasadzie pozwala spojrzeć na następną wiadomość w kolejce; zwraca true, jeśli istnieje, false w przeciwnym razie. Na potrzeby tego problemu żaden z parametrów nie jest szczególnie istotny: liczy się tylko wartość zwracana. Pozwala to napisać funkcję, która informuje, czy aplikacja jest nadal bezczynna (to znaczy, że nadal nie ma żadnych komunikatów w kolejce):

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

Teraz masz wszystko, czego potrzebujesz, aby napisać całą pętlę gry:

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

Co więcej, to podejście pasuje jak najbliżej (przy minimalnym uzależnieniu od P / Invoke) do kanonicznej natywnej pętli gier Windows, która wygląda następująco:

while (!done) {
    if (PeekMessage(&message, window, 0, 0, PM_REMOVE)){
        TranslateMessage(&message);
        DispatchMessage(&message);
    }
    else {
        Update();
        Render();
    }
}
Josh
źródło
Jaka jest potrzeba radzenia sobie z takimi funkcjami Windows apis? Wykonanie bloku chwilowego pod rządami precyzyjnego stopera (do kontroli fps) nie byłoby wystarczające?
Emir Lima,
3
W pewnym momencie musisz wrócić z modułu obsługi aplikacji. W przeciwnym razie aplikacja zawiesi się (ponieważ nigdy nie pozwala na pojawienie się kolejnych komunikatów Win32). Zamiast tego możesz spróbować utworzyć pętlę na podstawie komunikatów WM_TIMER, ale WM_TIMER nie jest tak precyzyjny, jak byś tego naprawdę chciał, a nawet gdyby tak był, wymusiłby wszystko do tego poziomu aktualizacji o najniższym wspólnym mianowniku. Wiele gier wymaga lub chce mieć niezależne szybkości renderowania i aktualizacji logiki, z których niektóre (jak fizyka) pozostają stałe, a inne nie.
Josh
Natywne pętle gier systemu Windows używają tej samej techniki (poprawiłem swoją odpowiedź, dodając prostą do porównania. Zegary wymuszające stałą częstotliwość aktualizacji są mniej elastyczne i zawsze możesz wdrożyć swoją stałą częstotliwość aktualizacji w szerszym kontekście PeekMessage -pętla (wykorzystująca timery z większą precyzją i uderzeniem GC niż WM_TIMERoparte na nich)
Josh
@JoshPetrie Aby wyjaśnić, powyższe sprawdzanie bezczynności korzysta z funkcji SlimDX. Czy byłoby idealnie włączyć to do odpowiedzi? A może przypadkiem zredagowałeś kod, aby odczytać „IsApplicationIdle”, który jest odpowiednikiem SlimDX?
Vaughan Hilts
** Proszę, zignoruj ​​mnie, właśnie zdałem sobie sprawę, że to zdefiniujesz dalej ... :)
Vaughan Hilts
2

Zgadzam się z odpowiedzią Josha, chcę tylko dodać moje 5 centów. Domyślna pętla komunikatów WinForm (Application.Run) może zostać zastąpiona następującą (bez 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
        }
    }
}

Również jeśli chcesz wstrzyknąć kod do pompy komunikatów, użyj tego:

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
źródło
2
Trzeba zdawać sobie sprawę z DoEvents () śmieci generacji napowietrznych w przypadku wybrania tego podejścia, jednak.
Josh
0

Rozumiem, że to stary wątek, ale chciałbym przedstawić dwie alternatywy dla technik sugerowanych powyżej. Zanim przejdę do nich, oto niektóre z pułapek związanych z dotychczasowymi propozycjami:

  1. PeekMessage niesie ze sobą znaczne koszty, podobnie jak metody biblioteczne, które go nazywają (SlimDX IsApplicationIdle).

  2. Jeśli chcesz zastosować buforowane RawInput, musisz sondować pompę komunikatów za pomocą PeekMessage w innym wątku innym niż wątek interfejsu użytkownika, więc nie chcesz wywoływać tego dwukrotnie.

  3. Application.DoEvents nie jest zaprojektowany do wywoływania w ciasnej pętli, problemy GC szybko się pojawią.

  4. Podczas korzystania z Application.Idle lub PeekMessage, ponieważ wykonujesz pracę tylko w stanie Bezczynności, twoja gra lub aplikacja nie będzie działać podczas przenoszenia lub zmiany rozmiaru okna, bez zapachu kodu.

Aby obejść te problemy (z wyjątkiem 2, jeśli idziesz drogą RawInput), możesz:

  1. Stwórz wątek. Wątek i uruchom tam swoją pętlę gry.

  2. Utwórz Threading.Tasks.Task z flagą IsLongRunning i uruchom ją tam. Microsoft zaleca używanie Zadań zamiast Wątków i nietrudno zrozumieć, dlaczego.

Obie te techniki izolują interfejs graficzny API od wątku interfejsu użytkownika i pompy komunikatów, co jest zalecanym podejściem. Obsługa niszczenia zasobów / stanu i odtwarzania podczas zmiany rozmiaru okna jest również uproszczona i jest znacznie bardziej profesjonalna, gdy jest wykonywana (wykonując należytą ostrożność, aby uniknąć zakleszczeń przy pompie komunikatów) spoza wątku interfejsu użytkownika.

ROGRat
źródło