¿Cuál es el bucle de juego estándar de C # / Windows Forms?

32

Al escribir un juego en C # que usa Windows Forms simples y algunos contenedores de API de gráficos como SlimDX u OpenTK , ¿cómo debe estructurarse el bucle principal del juego?

Una aplicación de Windows Forms canónica tiene un punto de entrada que se parece a

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

y si bien uno puede lograr algo de lo que es necesario enganchando los diversos eventos de la Formclase , esos eventos no proporcionan un lugar obvio para colocar los bits de código para realizar actualizaciones periódicas constantes de los objetos lógicos del juego o para comenzar y finalizar un render cuadro.

¿Qué técnica debería usar un juego así para lograr algo similar a lo canónico?

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

game loop, y que ver con un rendimiento mínimo y un impacto GC

Josh
fuente

Respuestas:

45

La Application.Runllamada impulsa su bomba de mensajes de Windows, que en última instancia es lo que alimenta todos los eventos que puede conectarForm clase (y otros). Para crear un bucle de juego en este ecosistema, desea escuchar cuándo la bomba de mensajes de la aplicación está vacía y, mientras permanece vacía, realice los pasos típicos de "estado de entrada del proceso, actualice la lógica del juego, renderice la escena" del bucle de juego prototípico .

El Application.Idleevento se dispara una vez cada vez que se vacía la cola de mensajes de la aplicación y la aplicación está pasando a un estado inactivo. Puede enganchar el evento en el constructor de su formulario principal:

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

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

A continuación, debe poder determinar si la aplicación aún está inactiva. El Idleevento solo se dispara una vez, cuando la aplicación queda inactiva. No se dispara nuevamente hasta que un mensaje ingresa a la cola y luego la cola se vacía nuevamente. Windows Forms no expone un método para consultar el estado de la cola de mensajes, pero puede usar los servicios de invocación de plataforma para delegar la consulta a una función nativa de Win32 que pueda responder esa pregunta . La declaración de importación PeekMessagey sus tipos de soporte se ven así:

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

PeekMessagebásicamente le permite mirar el siguiente mensaje en la cola; devuelve verdadero si existe, falso de lo contrario. Para los propósitos de este problema, ninguno de los parámetros es particularmente relevante: lo que importa es solo el valor de retorno. Esto le permite escribir una función que le indica si la aplicación aún está inactiva (es decir, todavía no hay mensajes en la cola):

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

Ahora tienes todo lo que necesitas para escribir tu ciclo de juego 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);
}

Además, este enfoque coincide lo más cerca posible (con una dependencia mínima de P / Invoke) con el bucle canónico del juego nativo de Windows, que se ve así:

while (!done) {
    if (PeekMessage(&message, window, 0, 0, PM_REMOVE)){
        TranslateMessage(&message);
        DispatchMessage(&message);
    }
    else {
        Update();
        Render();
    }
}
Josh
fuente
¿Cuál es la necesidad de lidiar con tales funciones de apis de Windows? Hacer un bloque de tiempo regido por un cronómetro preciso (para el control de fps), ¿no sería suficiente?
Emir Lima
3
Debe regresar de la aplicación. Manejador inactivo en algún momento, de lo contrario su aplicación se congelará (ya que nunca permite que sucedan más mensajes Win32). En su lugar, podría intentar hacer un bucle basado en los mensajes WM_TIMER, pero WM_TIMER no es tan preciso como realmente desearía, e incluso si lo fuera, forzaría todo a la tasa de actualización de denominador común más bajo. Muchos juegos necesitan o quieren tener tasas de actualización de renderización y lógica independientes, algunas de las cuales (como la física) permanecen fijas mientras que otras no.
Josh
Los bucles de juegos nativos de Windows usan la misma técnica (modifiqué mi respuesta para incluir una simple para comparar. Los temporizadores para forzar una tasa de actualización fija son menos flexibles, y siempre puedes implementar tu tasa de actualización fija dentro del contexto más amplio del mensaje PeekMessage -style loop (usando temporizadores con mejor precisión e impacto GC que los WM_TIMERbasados ​​en).
Josh
@JoshPetrie Para ser claros, la comprobación anterior de inactividad utiliza una función para SlimDX. ¿Sería ideal incluir esto en la respuesta? ¿O es por casualidad que editó el código para leer 'IsApplicationIdle', que es la contraparte de SlimDX?
Vaughan Hilts
** Por favor ignórenme, me acabo de dar cuenta de que lo definen más abajo ... :)
Vaughan Hilts
2

De acuerdo con la respuesta de Josh, solo quiero agregar mis 5 centavos. El bucle de mensajes predeterminado de WinForms (Application.Run) podría reemplazarse con lo siguiente (sin 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
        }
    }
}

Además, si desea inyectar algún código en la bomba de mensajes, use esto:

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
fuente
2
Es necesario ser consciente de los DoEvents () de basura generación sobrecarga si elige este método, sin embargo.
Josh
0

Entiendo que este es un hilo viejo, pero me gustaría ofrecer dos alternativas a las técnicas sugeridas anteriormente. Antes de entrar en ellos, aquí están algunas de las trampas con las propuestas hechas hasta ahora:

  1. PeekMessage lleva una sobrecarga considerable, al igual que los métodos de biblioteca que lo llaman (SlimDX IsApplicationIdle).

  2. Si desea emplear RawInput almacenado en el búfer, deberá sondear la bomba de mensajes con PeekMessage en otro subproceso que no sea el subproceso de la interfaz de usuario, por lo que no desea llamarlo dos veces.

  3. Application.DoEvents no está diseñado para ser llamado en un ciclo cerrado, los problemas de GC surgirán rápidamente.

  4. Al usar Application.Idle o PeekMessage, debido a que solo está trabajando cuando está inactivo, su juego o aplicación no se ejecutará al mover o cambiar el tamaño de su ventana, sin olores de código.

Para solucionar estos problemas (excepto 2 si va por el camino de RawInput) puede:

  1. Crea un Threading.Thread y ejecuta tu ciclo de juego allí.

  2. Cree un Threading.Tasks.Task con el indicador IsLongRunning y ejecútelo allí. Microsoft recomienda que se usen tareas en lugar de subprocesos en estos días y no es difícil ver por qué.

Ambas técnicas aíslan su API de gráficos del hilo de la interfaz de usuario y la bomba de mensajes, como es el enfoque recomendado. El manejo de la destrucción de recursos / estado y la recreación durante el cambio de tamaño de la ventana también se simplifica y es estéticamente mucho más profesional cuando se realiza desde el final (ejerciendo la debida precaución para evitar puntos muertos con la bomba de mensajes) desde fuera del hilo de la interfaz de usuario.

ROGRat
fuente