Qual é o loop do jogo C # / Windows Forms padrão?

32

Ao escrever um jogo em C # que usa Windows Forms simples e alguns wrappers de API de gráficos como SlimDX ou OpenTK , como o loop do jogo principal deve ser estruturado?

Um aplicativo canônico do Windows Forms possui um ponto de entrada que se parece com

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

e embora seja possível realizar parte do necessário, conectando os vários eventos da Formclasse , esses eventos não fornecem um local óbvio para colocar os bits de código para executar atualizações periódicas constantes nos objetos da lógica do jogo ou para iniciar e terminar uma renderização quadro, Armação.

Que técnica esse jogo deve usar para alcançar algo semelhante ao canônico

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

loop de jogo e a ver com desempenho mínimo e impacto no GC?

Josh
fonte

Respostas:

45

A Application.Runchamada aciona sua bomba de mensagens do Windows, que é o que alimenta todos os eventos que você pode conectar à Formclasse (e outros). Para criar um loop de jogo nesse ecossistema, você deseja escutar quando a bomba de mensagens do aplicativo estiver vazia e, enquanto estiver vazia, execute as etapas típicas do "estado de entrada do processo, atualize a lógica do jogo, renderize a cena" do loop de protótipo do jogo .

O Application.Idleevento é acionado uma vez toda vez que a fila de mensagens do aplicativo é esvaziada e o aplicativo está em transição para um estado inativo. Você pode conectar o evento no construtor do seu formulário principal:

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

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

Em seguida, você precisa determinar se o aplicativo ainda está ocioso. O Idleevento é acionado apenas uma vez, quando o aplicativo fica inativo. Ele não é acionado novamente até que uma mensagem entre na fila e, em seguida, a fila esvazia novamente. O Windows Forms não expõe um método para consultar o estado da fila de mensagens, mas você pode usar os serviços de chamada de plataforma para delegar a consulta a uma função nativa do Win32 que possa responder a essa pergunta . A declaração de importação PeekMessagee seus tipos de suporte são semelhantes 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);

PeekMessagebasicamente permite que você veja a próxima mensagem na fila; retorna true se houver, false caso contrário. Para os propósitos desse problema, nenhum dos parâmetros é particularmente relevante: é apenas o valor de retorno que importa. Isso permite que você escreva uma função que informa se o aplicativo ainda está ocioso (ou seja, ainda não há mensagens na fila):

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

Agora você tem tudo o que precisa para escrever seu ciclo completo do jogo:

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

Além disso, essa abordagem se aproxima o mais próximo possível (com dependência mínima de P / Invoke) do loop de jogo nativo canônico do Windows, que se parece com:

while (!done) {
    if (PeekMessage(&message, window, 0, 0, PM_REMOVE)){
        TranslateMessage(&message);
        DispatchMessage(&message);
    }
    else {
        Update();
        Render();
    }
}
Josh
fonte
Qual é a necessidade de lidar com esses recursos de API do Windows? Criar um bloco de tempo governado por um cronômetro preciso (para controle de fps) não seria suficiente?
Emir Lima
3
Você precisa retornar do manipulador Application.Idle em algum momento, caso contrário, seu aplicativo congelará (pois nunca permite que outras mensagens do Win32 aconteçam). Em vez disso, você pode tentar fazer um loop com base nas mensagens WM_TIMER, mas o WM_TIMER não é tão preciso quanto você realmente deseja e, mesmo que fosse, forçaria tudo para a menor taxa de atualização do denominador comum. Muitos jogos precisam ou desejam ter taxas independentes de atualização e atualização lógica, algumas das quais (como a física) permanecem fixas enquanto outras não.
Josh
Os loops de jogos nativos do Windows usam a mesma técnica (alterei minha resposta para incluir uma simples para comparação. Os cronômetros para forçar uma taxa de atualização fixa são menos flexíveis e você sempre pode implementar sua taxa de atualização fixa no contexto mais amplo do PeekMessage loop de estilo (usando cronômetros com melhor precisão e impacto no GC do que os WM_TIMERbaseados).
Josh
@ JoshPetrie Para ficar claro, a verificação acima para inativo usa uma função para SlimDX. Seria ideal incluir isso na resposta? Ou é por acaso que você editou o código para ler 'IsApplicationIdle', que é o equivalente do SlimDX?
Vaughan Empunhaduras
** Por favor, ignore me, eu só percebi que você defini-lo ainda mais para baixo ... :)
Vaughan Hilts
2

Concordo com a resposta de Josh, só quero adicionar meus 5 centavos. O loop de mensagem padrão do WinForms (Application.Run) pode ser substituído pelo seguinte (sem 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
        }
    }
}

Além disso, se você deseja injetar algum código na bomba de mensagens, use o seguinte:

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
fonte
2
Você precisa estar ciente da sobrecarga de geração de lixo do DoEvents (), se escolher essa abordagem, no entanto.
Josh
0

Entendo que esse é um tópico antigo, mas gostaria de fornecer duas alternativas para as técnicas sugeridas acima. Antes de entrar nelas, aqui estão algumas das armadilhas das propostas feitas até agora:

  1. PeekMessage carrega uma sobrecarga considerável, assim como os métodos da biblioteca que o chamam (SlimDX IsApplicationIdle).

  2. Se você deseja empregar RawInput em buffer, precisará pesquisar a bomba de mensagens com PeekMessage em outro thread que não seja o thread da UI, para que não queira chamá-lo duas vezes.

  3. O Application.DoEvents não foi projetado para ser chamado em um circuito fechado, os problemas do GC surgirão rapidamente.

  4. Ao usar Application.Idle ou PeekMessage, porque você só está trabalhando quando Idle, seu jogo ou aplicativo não será executado ao mover ou redimensionar sua janela, sem odores de código.

Para contornar esses problemas (exceto 2, se você estiver indo pela estrada RawInput), você pode:

  1. Crie um Threading.Thread e execute o loop do jogo lá.

  2. Crie um Threading.Tasks.Task com o sinalizador IsLongRunning e execute-o lá. A Microsoft recomenda que as tarefas sejam usadas no lugar dos threads hoje em dia e não é difícil entender o porquê.

Ambas as técnicas isolam sua API de gráficos do thread da interface do usuário e da bomba de mensagens, como é a abordagem recomendada. O manuseio da destruição e recreação de recursos / estado durante o redimensionamento do Windows também é simplificado e é esteticamente muito mais profissional quando concluído (exercitando a devida cautela para evitar bloqueios com a bomba de mensagens) de fora do thread da interface do usuário.

ROGRat
fonte