EXTRÊMEMENT confus sur la boucle de jeu "Vitesse de jeu constante FPS maximum"

12

J'ai récemment lu cet article sur les boucles de jeu: http://www.koonsolo.com/news/dewitters-gameloop/

Et la dernière mise en œuvre recommandée me déroute profondément. Je ne comprends pas comment cela fonctionne, et cela ressemble à un gâchis complet.

Je comprends le principe: mettre à jour le jeu à une vitesse constante, avec ce qui reste rendre le jeu autant de fois que possible.

Je suppose que vous ne pouvez pas utiliser:

  • Obtenez une entrée pour 25 ticks
  • Jeu de rendu pour 975 ticks

Approche puisque vous obtiendriez une entrée pour la première partie de la seconde et que cela vous semblerait bizarre? Ou est-ce ce qui se passe dans l'article?


Essentiellement:

while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)

Comment est-ce même valable?

Assumons ses valeurs.

MAX_FRAMESKIP = 5

Supposons que next_game_tick, qui a été affecté quelques instants après l'initialisation, avant que la boucle de jeu principale ne dise ... 500.

Enfin, comme j'utilise SDL et OpenGL pour mon jeu, avec OpenGL utilisé uniquement pour le rendu, supposons que GetTickCount()renvoie le temps écoulé depuis l'appel de SDL_Init, ce qu'il fait.

SDL_GetTicks -- Get the number of milliseconds since the SDL library initialization.

Source: http://www.libsdl.org/docs/html/sdlgetticks.html

L'auteur suppose également ceci:

DWORD next_game_tick = GetTickCount();
// GetTickCount() returns the current number of milliseconds
// that have elapsed since the system was started

Si nous développons la whiledéclaration, nous obtenons:

while( ( 750 > 500 ) && ( 0 < 5 ) )

750 car le temps s'est écoulé depuis qu'il a next_game_tickété attribué. loopsest nul comme vous pouvez le voir dans l'article.

Nous avons donc entré la boucle while, faisons un peu de logique et acceptons une entrée.

Yada yada yada.

À la fin de la boucle while, je vous rappelle que notre boucle de jeu principale est la suivante:

next_game_tick += SKIP_TICKS;
loops++;

Mettons à jour à quoi ressemble la prochaine itération du code while

while( ( 1000 > 540 ) && ( 1 < 5 ) )

1000 car le temps s'est écoulé pour obtenir des données et faire des choses avant d'atteindre la prochaine inétération de la boucle, où GetTickCount () est rappelé.

540 car, dans le code 1000/25 = 40, donc, 500 + 40 = 540

1 parce que notre boucle a répété une fois

5 , vous savez pourquoi.


Donc, puisque cette boucle While est CLAIREMENT dépendante MAX_FRAMESKIPet non pas TICKS_PER_SECOND = 25;comment le jeu est-il censé fonctionner correctement?

Ce n'était pas une surprise pour moi que lorsque j'ai implémenté cela dans mon code, je pourrais ajouter correctement car j'ai simplement renommé mes fonctions pour gérer les entrées utilisateur et dessiner le jeu selon ce que l'auteur de l'article a dans son exemple de code, le jeu n'a rien fait .

J'ai placé un fprintf( stderr, "Test\n" );à l'intérieur de la boucle while qui n'est pas imprimé jusqu'à la fin du jeu.

Comment cette boucle de jeu fonctionne-t-elle 25 fois par seconde, garantie, tout en rendant le plus rapidement possible?

Pour moi, à moins que je manque quelque chose d'énorme, cela ressemble à ... rien.

Et cette structure, de cette boucle while, n'est-elle pas censée fonctionner 25 fois par seconde puis mettre à jour le jeu exactement ce que j'ai mentionné au début de l'article?

Si tel est le cas, pourquoi ne pourrions-nous pas faire quelque chose de simple comme:

while( loops < 25 )
{
    getInput();
    performLogic();

    loops++;
}

drawGame();

Et comptez pour l'interpolation d'une autre manière.

Pardonnez ma question extrêmement longue, mais cet article m'a fait plus de mal que de bien. Je suis gravement confus maintenant - et je n'ai aucune idée de comment implémenter une boucle de jeu appropriée en raison de toutes ces questions.

tsujp
la source
1
Les diatribes sont mieux dirigées vers l'auteur de l'article. Quelle est votre question objective ?
Anko
3
Cette boucle de jeu est-elle même valable, explique quelqu'un. D'après mes tests, il n'a pas la bonne structure pour fonctionner 25 fois par seconde. Expliquez à mon pourquoi il le fait. Ce n'est pas non plus une diatribe, c'est une série de questions. Dois-je utiliser des émoticônes, ai-je l'air en colère?
tsujp
2
Puisque votre question se résume à "Qu'est-ce que je ne comprends pas à propos de cette boucle de jeu", et que vous avez beaucoup de mots en gras, cela semble au moins exaspéré.
Kirbinator
@Kirbinator Je peux l'apprécier, mais j'essayais de mettre à la terre tout ce que je trouve inhabituel dans cet article, donc ce n'est pas une question superficielle et vide. Je ne pense pas que ce soit la quintessence de toute façon, j'aimerais penser que j'ai des points valables - après tout, c'est un article essayant d'enseigner, mais ne faisant pas un si bon travail.
tsujp
7
Ne vous méprenez pas, c'est une bonne question, elle pourrait être 80% plus courte.
Kirbinator

Réponses:

8

Je pense que l'auteur a fait une petite erreur:

while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)

devrait être

while( GetTickCount() < next_game_tick && loops < MAX_FRAMESKIP)

C'est-à-dire: tant qu'il n'est pas encore temps de dessiner notre prochaine image et bien que nous n'ayons pas ignoré autant d'images que MAX_FRAMESKIP, nous devons attendre.

Je ne comprends pas non plus pourquoi il se met next_game_tickà jour dans la boucle et je suppose que c'est une autre erreur. Puisqu'au début d'une trame, vous pouvez déterminer quand la trame suivante doit être (lors de l'utilisation d'une fréquence d'images fixe). Le next game tickne dépend pas de combien de temps nous reste plus après la mise à jour et le rendu.

L'auteur fait également une autre erreur courante

avec tout ce qui reste rendre le jeu autant de fois que possible.

Cela signifie rendre le même cadre plusieurs fois. L'auteur en est même conscient:

Le jeu sera mis à jour régulièrement 50 fois par seconde, et le rendu se fera le plus rapidement possible. Remarquez que lorsque le rendu est effectué plus de 50 fois par seconde, certaines images suivantes seront identiques, de sorte que les images visuelles réelles seront affichées à un maximum de 50 images par seconde.

Cela ne fait que faire du GPU un travail inutile et si le rendu prend plus de temps que prévu, cela pourrait vous faire commencer à travailler sur votre prochaine trame plus tard que prévu, il est donc préférable de céder au système d'exploitation et d'attendre.

Roy T.
la source
+ Votez. Mmmm. Cela me laisse en effet perplexe sur quoi faire alors. Je vous remercie pour votre perspicacité. Je vais probablement avoir un jeu, le problème est vraiment la connaissance de la limitation des FPS ou de la définition dynamique des FPS, et comment faire cela. Dans mon esprit, une gestion d'entrée fixe est nécessaire pour que le jeu fonctionne au même rythme pour tout le monde. Ce n'est qu'un simple MMO de plateforme 2D (à très long terme)
tsujp
Alors que la boucle semble correcte et incrémentée, next_game_tick est là pour ça. Il est là pour faire fonctionner la simulation à vitesse constante sur un matériel plus lent et plus rapide. L'exécution du code de rendu "aussi vite que possible" (ou plus rapide que la physique de toute façon) n'a de sens que lorsqu'il y a une interpolation dans le code de rendu pour le rendre plus fluide pour les objets rapides, etc. (fin de l'article), mais c'est juste de l'énergie gaspillée si il rend plus que ce que n'importe quel périphérique de sortie (écran) peut afficher.
Dotti
Son code est donc une implémentation valide, maintenant. Et nous devons juste vivre avec ces déchets? Ou existe-t-il des méthodes que je peux rechercher pour cela.
tsujp
Céder au système d'exploitation et attendre n'est une bonne idée que si vous avez une bonne idée du moment où le système d'exploitation vous rendra le contrôle - ce qui peut ne pas être dès que vous le souhaitez.
Kylotan
Je ne peux pas voter pour cette réponse. Il manque complètement les éléments d'interpolation, ce qui rend plutôt invalide toute la seconde moitié de la réponse.
AlbeyAmakiir
4

Peut-être mieux si je simplifie un peu:

while( game_is_running ) {

    current = GetTickCount();
    while(current > next_game_tick) {
        update_game();

        next_game_tick += SKIP_TICKS;
    }
    display_game();
}

whileloop inside mainloop est utilisé pour exécuter des étapes de simulation de l'endroit où elles se trouvaient jusqu'à l'endroit où elles devraient être maintenant. update_game()La fonction doit toujours supposer que seul le SKIP_TICKStemps s'est écoulé depuis le dernier appel. Cela permettra à la physique du jeu de fonctionner à vitesse constante sur un matériel lent et rapide.

L'incrémentation next_game_tickpar quantité de SKIP_TICKSmouvements le rapproche de l'heure actuelle. Lorsque cela devient plus grand que l'heure actuelle, il se casse ( current > next_game_tick) et la boucle principale continue de rendre l'image actuelle.

Après le rendu, le prochain appel à GetTickCount()retournerait une nouvelle heure actuelle. Si ce temps est supérieur à next_game_tickcela, cela signifie que nous sommes déjà derrière les étapes 1-N de la simulation et que nous devons rattraper le retard, en exécutant chaque étape de la simulation à la même vitesse constante. Dans ce cas, s'il est inférieur, il restituera simplement la même image (sauf s'il y a interpolation).

Le code d'origine avait limité le nombre de boucles si on nous laisse trop loin ( MAX_FRAMESKIP). Cela ne fait que montrer quelque chose et ne ressemble pas à être verrouillé si, par exemple, la suspension ou le jeu est suspendu pendant longtemps dans le débogueur (en supposant GetTickCount()qu'il ne s'arrête pas pendant ce temps) jusqu'à ce qu'il ait rattrapé le temps.

Pour se débarrasser du même rendu d'image inutile si vous n'utilisez pas d'interpolation à l'intérieur display_game(), vous pouvez l'envelopper à l'intérieur si une instruction comme:

while (game_is_running) {
    current = GetTickCount();
    if (current > next_game_tick) {
        while(current > next_game_tick) {
            update_game();

            next_game_tick += SKIP_TICKS;
        }
    display_game();
    }
    else {
    // could even sleep here
    }
}

Ceci est également un bon article à ce sujet: http://gafferongames.com/game-physics/fix-your-timestep/

De plus, la raison pour laquelle vos fprintfsorties à la fin de votre jeu est peut-être simplement qu'elle n'a pas été éliminée.

Désolé de mon anglais.

Dotti
la source
4

Son code semble entièrement valide.

Considérez la whileboucle du dernier ensemble:

// JS / pseudocode
var current_time = function () { return Date.now(); }, // in ms
    framerate = 1000/30, // 30fps
    next_frame = current_time(),

    max_updates_per_draw = 5,

    iterations;

while (game_running) {

    iterations = 0;

    while (current_time() > next_frame && iterations < max_updates_per_draw) {
        update_game(); // input, physics, audio, etc

        next_frame += framerate;
        iterations += 1;
    }

    draw();
}

J'ai un système en place qui dit "pendant que le jeu est en cours, vérifiez l'heure actuelle - si elle est supérieure à notre nombre d'images en cours d'exécution, et nous avons ignoré le dessin de moins de 5 images, puis sautez le dessin et mettez simplement à jour l'entrée et physique: sinon dessinez la scène et lancez la prochaine itération de mise à jour "

À chaque mise à jour, vous incrémentez le temps "next_frame" de votre taux de rafraîchissement idéal. Ensuite, vous vérifiez à nouveau votre temps. Si votre heure actuelle est maintenant inférieure à la date à laquelle le next_frame doit être mis à jour, vous ignorez la mise à jour et dessinez ce que vous avez.

Si votre courant_heure est supérieur (imaginez que le dernier processus de dessin a pris très longtemps, car il y avait un hoquet quelque part, ou un tas de garbage-collection dans un langage géré, ou une implémentation de mémoire gérée en C ++ ou autre), alors draw est ignoré et next_frameest mis à jour avec une autre image supplémentaire, jusqu'à ce que les mises à jour rattrapent l'endroit où nous devrions être sur l'horloge, ou que nous ayons ignoré suffisamment d'images pour que nous devions absolument en dessiner une, afin que le joueur puisse voir ce qu'ils font.

Si votre machine est super rapide ou que votre jeu est super simple, cela current_timepeut être moins que next_framefréquent, ce qui signifie que vous ne mettez pas à jour pendant ces points.

C'est là que l'interpolation entre en jeu. De même, vous pouvez avoir un booléen distinct, déclaré en dehors des boucles, pour déclarer un espace "sale".

À l'intérieur de la boucle de mise à jour, vous définissez dirty = true, ce qui signifie que vous avez réellement effectué une mise à jour.

Ensuite, au lieu de simplement appeler draw(), vous diriez:

if (is_dirty) {
    draw(); 
    is_dirty = false;
}

Ensuite, vous manquez toute interpolation pour un mouvement fluide, mais vous vous assurez que vous ne mettez à jour que lorsque des mises à jour se produisent réellement (plutôt que des interpolations entre les états).

Si vous êtes courageux, il y a un article intitulé "Fix Your Timestep!" par GafferOnGames.
Il aborde le problème un peu différemment, mais je le considère comme une solution plus jolie qui fait essentiellement la même chose (en fonction des fonctionnalités de votre langage et de l'importance que vous accordez aux calculs physiques de votre jeu).

Norguard
la source