Cos'è il loop di gioco standard C # / Windows Form?


32

Quando si scrive un gioco in C # che utilizza Windows Forms semplice e alcuni wrapper di API grafiche come SlimDX o OpenTK , come dovrebbe essere strutturato il ciclo di gioco principale?

Un'applicazione canonica di Windows Form ha un punto di ingresso simile

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

e mentre si può realizzare un po ' di ciò che è necessario agganciando i vari eventi della Formclasse , quegli eventi non forniscono un posto ovvio per mettere i bit di codice per eseguire aggiornamenti periodici costanti agli oggetti della logica di gioco o per iniziare e terminare un rendering telaio.

Quale tecnica dovrebbe usare un gioco del genere per ottenere qualcosa di simile al canonico

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

game loop, e con prestazioni minime e impatto GC?

Risposte:


45

La Application.Runchiamata guida il tuo pump dei messaggi di Windows, che è in definitiva ciò che alimenta tutti gli eventi che puoi agganciareForm classe (e altri). Per creare un loop di gioco in questo ecosistema, si desidera ascoltare quando il pump dei messaggi dell'applicazione è vuoto e, mentre rimane vuoto, eseguire i passaggi tipici "processo input input, aggiornamento logica di gioco, rendering della scena" del prototipo di loop di gioco .

L' Application.Idleevento viene generato una volta ogni volta che la coda dei messaggi dell'applicazione viene svuotata e l'applicazione passa a uno stato inattivo. Puoi agganciare l'evento nel costruttore del tuo modulo principale:

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

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

Successivamente, devi essere in grado di determinare se l'applicazione è ancora inattiva. L' Idleevento viene generato solo una volta, quando l'applicazione diventa inattiva. Non viene licenziato di nuovo fino a quando un messaggio non entra nella coda e quindi la coda si svuota di nuovo. Windows Forms non espone un metodo per eseguire una query sullo stato della coda dei messaggi, ma è possibile utilizzare i servizi di chiamata della piattaforma per delegare la query a una funzione Win32 nativa in grado di rispondere a tale domanda . La dichiarazione di importazione per PeekMessagee i suoi tipi di supporto è simile a:

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

PeekMessagesostanzialmente ti permette di guardare il prossimo messaggio in coda; ritorna vero se ne esiste uno, falso altrimenti. Ai fini di questo problema, nessuno dei parametri è particolarmente rilevante: è solo il valore di ritorno che conta. Ciò ti consente di scrivere una funzione che ti dice se l'applicazione è ancora inattiva (ovvero, non ci sono ancora messaggi nella coda):

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

Ora hai tutto il necessario per scrivere il tuo ciclo di gioco completo:

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

Inoltre, questo approccio si avvicina il più vicino possibile (con la minima dipendenza da P / Invoke) al canone di gioco nativo canonico , che assomiglia a:

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

Qual è la necessità di gestire tali funzionalità di Windows Apis? Fare un blocco di tempo governato da un preciso cronometro (per il controllo fps), non sarebbe sufficiente?
Emir Lima,

3
È necessario tornare dal gestore Application.Idle a un certo punto, altrimenti l'applicazione si bloccherà (poiché non consente mai che si verifichino altri messaggi Win32). Potresti invece provare a creare un ciclo basato sui messaggi WM_TIMER, ma WM_TIMER non è così preciso come vorresti davvero, e anche se lo fosse, forzerebbe tutto alla frequenza di aggiornamento del minimo comune denominatore. Molti giochi necessitano o vogliono avere frequenze di aggiornamento logiche e di rendering indipendenti, alcune delle quali (come la fisica) rimangono fisse mentre altre no.
Josh,

I cicli di gioco nativi di Windows usano la stessa tecnica (ho modificato la mia risposta per includerne una semplice per il confronto. I timer per forzare una frequenza di aggiornamento fissa sono meno flessibili e puoi sempre implementare la tua frequenza di aggiornamento fissa nel contesto più ampio di PeekMessage in stile (usando i timer con una migliore precisione e impatto GC rispetto a WM_TIMERquelli basati).
Josh

@JoshPetrie Per essere chiari, il controllo di cui sopra inattivo utilizza una funzione per SlimDX. Sarebbe ideale includerlo nella risposta? O è solo per caso che hai modificato il codice per leggere "IsApplicationIdle" che è la controparte SlimDX?
Vaughan Hilts,

** Per favore, ignorami, ho appena capito che lo definisci più in basso ... :)
Vaughan Hilts

2

D'accordo con la risposta di Josh, voglio solo aggiungere i miei 5 centesimi. Il ciclo di messaggi predefinito di WinForms (Application.Run) può essere sostituito con il seguente (senza 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
        }
    }
}

Inoltre, se si desidera iniettare del codice nel pump dei messaggi, utilizzare questo:

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
Tuttavia, è necessario essere consapevoli del sovraccarico di generazione di immondizia DoEvents () se si sceglie questo approccio.
Josh,

0

Capisco che questo è un vecchio thread, ma mi piacerebbe fornire due alternative alle tecniche suggerite sopra. Prima di entrare in loro, ecco alcune delle insidie ​​con le proposte fatte finora:

  1. PeekMessage comporta un notevole sovraccarico, così come i metodi di libreria che lo chiamano (SlimDX IsApplicationIdle).

  2. Se si desidera utilizzare RawInput bufferizzato, è necessario eseguire il polling del pump dei messaggi con PeekMessage su un altro thread diverso dal thread dell'interfaccia utente, quindi non si desidera chiamarlo due volte.

  3. Application.DoEvents non è progettato per essere chiamato in un ciclo stretto, emergeranno rapidamente problemi GC.

  4. Quando si utilizza Application.Idle o PeekMessage, perché si esegue il lavoro solo quando è inattivo, il gioco o l'applicazione non verranno eseguiti durante lo spostamento o il ridimensionamento della finestra, senza odori di codice.

Per aggirare questi (tranne 2 se stai percorrendo la strada RawInput) puoi:

  1. Crea un threading. Filtra ed esegui il tuo loop di gioco lì.

  2. Crea un Threading.Tasks.Task con il flag IsLongRunning ed eseguilo lì. Microsoft consiglia di utilizzare Task al posto dei thread in questi giorni e non è difficile capire perché.

Entrambe queste tecniche isolano l'API grafica dal thread dell'interfaccia utente e dal pump dei messaggi, come l'approccio consigliato. Anche la gestione della distruzione e della ricreazione di risorse / stati durante il ridimensionamento di Window è semplificata ed è esteticamente molto più professionale se eseguita (eseguendo le dovute precauzioni per evitare deadlock con la pompa dei messaggi) dall'esterno del thread dell'interfaccia utente.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.