Quelle est la boucle de jeu standard C # / Windows Forms?

32

Lors de l'écriture d'un jeu en C # utilisant des Windows Forms classiques et certains encapsuleurs d'API graphiques tels que SlimDX ou OpenTK , comment structurer la boucle de jeu principale?

Une application canonique Windows Forms a un point d’entrée ressemblant à

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

et bien que l'on puisse accomplir une partie de ce qui est nécessaire en accrochant les divers événements de la Formclasse , ceux-ci ne fournissent pas un endroit évident pour mettre les bouts de code afin d'effectuer des mises à jour périodiques constantes d'objets de logique de jeu ou pour commencer et terminer un rendu. Cadre.

Quelle technique un tel jeu devrait-il utiliser pour obtenir quelque chose qui ressemble au canonique?

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

boucle de jeu et à faire avec des performances minimales et un impact GC?

Josh
la source

Réponses:

45

L’ Application.Runappel entraîne votre pompe de messages Windows, qui alimente en définitive tous les événements que vous pouvez raccrocher auForm classe (et à d'autres). Pour créer une boucle de jeu dans cet écosystème, vous souhaitez écouter lorsque la pompe à messages de l'application est vide et pendant qu'il reste vide, effectuez les étapes typiques "État d'entrée du processus, mise à jour de la logique de jeu, rendu de la scène" de la boucle de jeu prototypique. .

L' Application.Idleévénement est déclenché une fois à chaque fois que la file de messages de l'application est vidée et que l'application passe à un état inactif. Vous pouvez accrocher l'événement au constructeur de votre formulaire principal:

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

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

Ensuite, vous devez être capable de déterminer si l'application est toujours inactive. L' Idleévénement ne se déclenche qu'une seule fois, lorsque l'application devient inactive. Il ne se déclenche plus jusqu'à ce qu'un message arrive dans la file d'attente, puis la file se vide à nouveau. Windows Forms n'expose pas de méthode pour interroger l'état de la file d'attente de messages, mais vous pouvez utiliser les services d'appel de plate-forme pour déléguer la requête à une fonction Win32 native qui peut répondre à cette question . La déclaration d'importation de PeekMessageet ses types de support se présentent comme suit:

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

PeekMessagevous permet essentiellement de regarder le message suivant dans la file d'attente; il retourne vrai s'il en existe un, faux sinon. Pour les besoins de ce problème, aucun paramètre n'est particulièrement pertinent: seule la valeur de retour compte. Cela vous permet d'écrire une fonction qui vous indique si l'application est toujours inactive (c'est-à-dire qu'il n'y a toujours aucun message dans la file d'attente):

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

Vous avez maintenant tout ce dont vous avez besoin pour écrire votre boucle de jeu complète:

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

De plus, cette approche correspond le plus possible (avec une dépendance minimale à P / Invoke) à la boucle de jeu canonique native de Windows, qui ressemble à ceci :

while (!done) {
    if (PeekMessage(&message, window, 0, 0, PM_REMOVE)){
        TranslateMessage(&message);
        DispatchMessage(&message);
    }
    else {
        Update();
        Render();
    }
}
Josh
la source
Quelle est la nécessité de traiter avec de telles fonctionnalités Windows apis? Faire un bloc while gouverné par un chronomètre précis (pour le contrôle en fps) ne serait pas suffisant?
Emir Lima
3
Vous devez quitter le gestionnaire Application.Idle à un moment donné, sinon votre application sera gelée (car elle ne permet jamais à d'autres messages Win32 de se produire). Vous pouvez plutôt essayer de créer une boucle basée sur les messages WM_TIMER, mais WM_TIMER n’est pas aussi précis que vous le voudriez vraiment, et même s’il le faisait, tout serait forcé à cette fréquence de mise à jour avec le plus petit commun dénominateur. De nombreux jeux ont besoin ou souhaitent disposer de vitesses de rendu et de mises à jour logiques indépendantes, dont certaines (comme la physique) restent fixes alors que d'autres ne le sont pas.
Josh
Les boucles de jeu Windows natives utilisent la même technique (j'ai modifié ma réponse pour en inclure une simple à des fins de comparaison. Les minuteries pour imposer un taux de mise à jour fixe sont moins flexibles et vous pouvez toujours implémenter votre taux de mise à jour fixe dans le contexte plus large de PeekMessage. boucle de style (utilisant des minuteries avec une meilleure précision et un impact GC que WM_TIMERceux basés sur)
Josh
@JoshPetrie Pour être clair, la vérification ci-dessus en veille utilise une fonction pour SlimDX. Serait-il idéal d'inclure cela dans la réponse? Ou est-ce simplement par hasard que vous avez édité le code pour lire 'IsApplicationIdle' qui est l'homologue de SlimDX?
Vaughan Hilts
** S'il vous plaît ignorez-moi, je viens de me rendre compte que vous le définissez plus bas ... :)
Vaughan Hilts
2

D'accord avec la réponse de Josh, je veux juste ajouter mes 5 centimes. La boucle de message par défaut de WinForms (Application.Run) peut être remplacée par la suivante (sans 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
        }
    }
}

Aussi, si vous souhaitez injecter du code dans Message Pump, utilisez ceci:

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
la source
2
Cependant, vous devez être conscient de la surcharge générée par la génération de déchets DoEvents () si vous choisissez cette approche.
Josh
0

Je comprends qu’il s’agit d’un vieux fil conducteur, mais j’aimerais proposer deux alternatives aux techniques suggérées ci-dessus. Avant d’y revenir, voici quelques-uns des pièges avec les propositions faites jusqu’à présent:

  1. PeekMessage entraîne une surcharge considérable, tout comme les méthodes de bibliothèque qui l'appellent (SlimDX IsApplicationIdle).

  2. Si vous souhaitez utiliser RawInput en mémoire tampon, vous devez interroger la pompe de messages avec PeekMessage sur un autre thread autre que le thread d'interface utilisateur. Vous ne souhaitez donc pas l'appeler deux fois.

  3. Application.DoEvents n'est pas conçu pour être appelé dans une boucle serrée, des problèmes de GC vont rapidement apparaître.

  4. Lorsque vous utilisez Application.Idle ou PeekMessage, étant donné que vous travaillez uniquement lorsque vous êtes inactif, votre jeu ou votre application ne s'exécutera pas lors du déplacement ou du redimensionnement de votre fenêtre, sans odeur de code.

Pour contourner ces problèmes (sauf 2 si vous suivez la route RawInput), vous pouvez soit:

  1. Créez un Threading.Thread et lancez votre boucle de jeu là-bas.

  2. Créez une tâche Threading.Tasks.Task avec l'indicateur IsLongRunning et exécutez-la ici. Microsoft recommande que les tâches soient utilisées à la place de Threads ces temps-ci et il n’est pas difficile de comprendre pourquoi.

Ces deux techniques isolent votre API graphique du thread d'interface utilisateur et de la pompe de message, comme le recommande l'approche. Le traitement de la destruction des ressources / de l’état et des activités récréatives lors du redimensionnement de la fenêtre est également simplifié et est beaucoup plus professionnel (c’est-à-dire qu’il est très professionnel de le faire (en exerçant la prudence nécessaire pour éviter les blocages avec la pompe de message) depuis l’extérieur du fil de l’interface utilisateur.

ROGRat
la source