Un bon moyen de créer une boucle de jeu en OpenGL


31

Je commence actuellement à apprendre OpenGL à l'école et j'ai commencé à faire un jeu simple l'autre jour (seul, pas pour l'école). J'utilise freeglut et je le construis en C, donc pour ma boucle de jeu, je venais vraiment d'utiliser une fonction à laquelle j'ai fait passer glutIdleFuncpour mettre à jour tous les dessins et la physique en un seul passage. C'était bien pour des animations simples que je ne me souciais pas trop de la fréquence d'images, mais comme le jeu est principalement basé sur la physique, je veux vraiment (avoir besoin) de déterminer la vitesse de mise à jour.

Donc, ma première tentative a été de passer ma fonction à glutIdleFunc( myIdle()) pour garder une trace du temps écoulé depuis l'appel précédent et mettre à jour la physique (et actuellement les graphiques) toutes les millisecondes. j'ai utilisétimeGetTime() de le faire (en utilisant <windows.h>). Et cela m'a fait penser, est-ce que l'utilisation de la fonction de veille est vraiment une bonne façon de parcourir la boucle du jeu?

Ma question est, quelle est une meilleure façon d'implémenter la boucle de jeu dans OpenGL? Dois-je éviter d'utiliser la fonction de veille?



Je pense que cette question est plus spécifique à OpenGL et s'il est conseillé d'utiliser GLUT pour la boucle
Jeff

1
Ici mec , n'utilise pas GLUT pour des projets plus importants . C'est bien pour les plus petits, cependant.
bobobobo

Réponses:


29

La réponse simple est non, vous ne voulez pas utiliser le rappel glutIdleFunc dans un jeu qui a une sorte de simulation. La raison en est que cette fonction divorce l'animation et tire le code de la gestion des événements de fenêtre mais pas de manière asynchrone. En d'autres termes, la réception et la remise des événements de fenêtre bloquent le code (ou tout ce que vous mettez dans ce rappel), cela convient parfaitement pour une application interactive (interagir, puis répondre), mais pas pour un jeu où la physique ou l'état du jeu doit progresser indépendamment de l'interaction ou du temps de rendu.

Vous voulez découpler complètement la gestion des entrées, l'état du jeu et dessiner le code. Il existe une solution simple et propre à cela qui n'implique pas directement la bibliothèque graphique (c'est-à-dire qu'elle est portable et facile à visualiser); vous voulez que la boucle de jeu entière produise du temps et que la simulation consomme le temps produit (en morceaux). La clé est cependant d'intégrer ensuite la quantité de temps que votre simulation a consommée dans votre animation.

La meilleure explication et tutoriel que j'ai trouvé à ce sujet est Fix Your Timestep de Glenn Fiedler

Ce tutoriel a le traitement complet, cependant si vous n'avez pas de simulation physique réelle , vous pouvez ignorer la véritable intégration mais la boucle de base se résume toujours à (en pseudo-code verbeux):

// The amount of time we want to simulate each step, in milliseconds
// (written as implicit frame-rate)
timeDelta = 1000/30
timeAccumulator = 0
while ( game should run )
{
  timeSimulatedThisIteration = 0
  startTime = currentTime()

  while ( timeAccumulator >= timeDelta )
  {
    stepGameState( timeDelta )
    timeAccumulator -= timeDelta
    timeSimulatedThisIteration += timeDelta
  }

  stepAnimation( timeSimulatedThisIteration )

  renderFrame() // OpenGL frame drawing code goes here

  handleUserInput()

  timeAccumulator += currentTime() - startTime 
}

En procédant ainsi, les blocages dans votre code de rendu, la gestion des entrées ou le système d'exploitation n'entraînent pas de retard dans l'état de votre jeu. Cette méthode est également portable et indépendante de la bibliothèque graphique.

GLUT est une belle bibliothèque mais elle est strictement pilotée par les événements. Vous enregistrez des rappels et lancez la boucle principale. Vous passez toujours le contrôle de votre boucle principale à l'aide de GLUT. Il existe des hacks pour le contourner , vous pouvez également simuler une boucle externe à l'aide de minuteries et autres, mais une autre bibliothèque est probablement une meilleure façon (plus facile) de s'y rendre. Il existe de nombreuses alternatives, en voici quelques-unes (avec une bonne documentation et des tutoriels rapides):

  • GLFW qui vous donne la possibilité d'obtenir des événements d'entrée en ligne (dans votre propre boucle principale).
  • SDL , cependant, son accent n'est pas spécifiquement OpenGL.

Bon article. Donc, pour ce faire dans OpenGL, je n'utiliserais pas le glutMainLoop, mais plutôt le mien? Si je le faisais, serais-je en mesure d'utiliser le glutMouseFunc et d'autres fonctions? J'espère que cela a du sens, je suis encore assez nouveau dans tout cela
Jeff

Malheureusement non. Tous les rappels enregistrés dans GLUT sont déclenchés depuis glutMainLoop à mesure que la file d'attente des événements est épuisée et qu'elle itère. J'ai ajouté quelques liens vers des alternatives dans le corps de la réponse.
charstar

1
+1 pour abandonner GLUT pour autre chose. Pour autant que je sache, GLUT n'a jamais été conçu que pour des applications de test.
Jari Komppa le

Vous devez quand même gérer les événements système, non? Ma compréhension était qu'en utilisant glutIdleFunc, il gère simplement la boucle (sous Windows) GetMessage-TranslateMessage-DispatchMessage et appelle votre fonction chaque fois qu'il n'y a pas de message à obtenir. Vous le feriez vous-même de toute façon, sinon vous souffririez de l'impatience du système d'exploitation à signaler votre application comme ne répondant pas, non?
Ricket

@Ricket Techniquement, glutMainLoopEvent () gère les événements entrants en appelant les rappels enregistrés. glutMainLoop () est effectivement {glutMainLoopEvent (); IdleCallback (); } Une considération clé ici est que le redessin est également un rappel géré à partir de la boucle d'événement. Vous pouvez faire en sorte qu'une bibliothèque événementielle se comporte comme une bibliothèque temporelle, mais Maslow's Hammer et tout ça ...
charstar

4

Je pense que glutIdleFuncc'est très bien; c'est essentiellement ce que vous finiriez par faire à la main de toute façon, si vous ne l'utilisiez pas. Peu importe ce que vous allez avoir une boucle serrée qui n'est pas appelée dans un modèle fixe, et vous devez faire le choix si vous voulez le convertir en boucle à temps fixe ou le garder une boucle à temps variable et faire sûr que tous vos comptes mathématiques pour l'interpolation du temps. Ou même un mélange de ceux-ci, en quelque sorte.

Vous pouvez certainement avoir une myIdlefonction à laquelle vous passez glutIdleFunc, et dans ce cadre, mesurer le temps écoulé, la diviser par votre pas de temps fixe et appeler une autre fonction autant de fois.

Voici ce qu'il ne faut pas faire:

void myFunc() {
    // (calculate timeDifference here)
    int numOfTimes = timeDifference / 10;
    for(int i=0; i<numOfTimes; i++) {
        fixedTimestep();
    }
}

fixedTimestep ne serait pas appelé toutes les 10 millisecondes. En fait, cela fonctionnerait plus lentement qu'en temps réel, et vous pourriez finir par truquer les nombres pour compenser quand vraiment la racine du problème réside dans la perte du reste lorsque vous divisez timeDifferencepar 10.

Au lieu de cela, faites quelque chose comme ceci:

long queuedMilliseconds; // static or whatever, this must persist outside of your loop

void myFunc() {
    // (calculate timeDifference here)
    queuedMilliseconds += timeDifference;
    while(queuedMilliseconds >= 10) {
        fixedTimestep();
        queuedMilliseconds -= 10;
    }
}

Maintenant, cette structure de boucle garantit que votre fixedTimestepfonction sera appelée une fois toutes les 10 millisecondes, sans perdre de millisecondes comme reste ou quoi que ce soit du genre.

En raison de la nature multitâche des systèmes d'exploitation, vous ne pouvez pas compter sur une fonction appelée exactement toutes les 10 millisecondes; mais la boucle ci-dessus se produirait assez rapidement pour être, espérons-le, suffisamment proche, et vous pouvez supposer que chaque appel de fixedTimestepincrémenterait votre simulation d'une valeur de 10 millisecondes.

Veuillez également lire cette réponse et en particulier les liens fournis dans la réponse.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.