Framerate affecte la vitesse de l'objet

9

J'expérimente avec la construction d'un moteur de jeu à partir de zéro en Java, et j'ai quelques questions. Ma boucle de jeu principale ressemble à ceci:

        int FPS = 60;
        while(isRunning){
            /* Current time, before frame update */
            long time = System.currentTimeMillis();
            update();
            draw();
            /* How long each frame should last - time it took for one frame */
            long delay = (1000 / FPS) - (System.currentTimeMillis() - time);
            if(delay > 0){
                try{
                    Thread.sleep(delay);
                }catch(Exception e){};
            }
        }

Comme vous pouvez le voir, j'ai réglé le framerate à 60FPS, qui est utilisé dans le delaycalcul. Le délai garantit que chaque image prend le même temps avant le rendu de la suivante. Dans ma update()fonction, je fais x++ce qui augmente la valeur horizontale d'un objet graphique que je dessine avec ce qui suit:

bbg.drawOval(x,40,20,20);

Ce qui me déroute, c'est la vitesse. lorsque je suis réglé FPSsur 150, le cercle rendu passe à travers la vitesse très rapidement, tandis que le réglage FPSsur 30 se déplace sur l'écran à la moitié de la vitesse. Le framerate n'affecte-t-il pas simplement la "fluidité" du rendu et non la vitesse des objets en cours de rendu? Je pense que je manque une grande partie, j'aimerais avoir des éclaircissements.

Carpetfizz
la source
4
Voici un bon article sur la boucle de jeu: corrigez votre pas de temps
Kostya Regent
2
En remarque, nous essayons généralement de mettre des choses qui ne doivent pas être effectuées à chaque boucle en dehors des boucles. Dans votre code, votre 1000 / FPSdivision pourrait être effectuée et le résultat attribué à une variable avant votre while(isRunning)boucle. Cela permet d'économiser quelques instructions CPU pour faire quelque chose de plus d'une fois inutilement.
Vaillancourt

Réponses:

21

Vous déplacez le cercle d'un pixel par image. Cela ne devrait pas être une grande surprise que si votre boucle de rendu tourne à 30 FPS, votre cercle se déplacera de 30 pixels par seconde.

Vous avez essentiellement trois façons possibles de résoudre ce problème:

  1. Choisissez simplement une fréquence d'images et respectez-la. C'est ce que faisaient beaucoup de jeux à l'ancienne - ils fonctionnaient à un taux fixe de 50 ou 60 FPS, généralement synchronisés avec le taux de rafraîchissement de l'écran, et concevaient simplement leur logique de jeu pour faire tout le nécessaire dans cet intervalle de temps fixe. Si, pour une raison quelconque, cela ne se produisait pas, le jeu aurait juste à sauter une image (ou éventuellement planter), ralentissant efficacement le dessin et la physique du jeu à la moitié de la vitesse.

    En particulier, les jeux qui utilisaient des fonctionnalités telles que la détection de collision de sprites matériels devaient à peu près fonctionner comme cela, car leur logique de jeu était inextricablement liée au rendu, qui était effectué dans le matériel à un taux fixe.

  2. Utilisez un pas de temps variable pour votre physique de jeu. Fondamentalement, cela signifie réécrire votre boucle de jeu pour ressembler à ceci:

    long lastTime = System.currentTimeMillis();
    while (isRunning) {
        long time = System.currentTimeMillis();
        float timestep = 0.001 * (time - lastTime);  // in seconds
        if (timestep <= 0 || timestep > 1.0) {
            timestep = 0.001;  // avoid absurd time steps
        }
        update(timestep);
        draw();
        // ... sleep until next frame ...
        lastTime = time;
    }
    

    et, à l'intérieur update(), ajuster les formules physiques pour tenir compte du pas de temps variable, par exemple comme ceci:

    speed += timestep * acceleration;
    position += timestep * (speed - 0.5 * timestep * acceleration);
    

    Un problème avec cette méthode est qu'il peut être difficile de garder la physique (principalement) indépendante du pas de temps ; vous ne voulez vraiment pas que la distance que les joueurs peuvent sauter dépend de leur fréquence d'images. La formule que j'ai montrée ci-dessus fonctionne bien pour une accélération constante, par exemple sous gravité (et celle du poste lié fonctionne assez bien même si l'accélération varie dans le temps), mais même avec les formules physiques les plus parfaites possibles, travailler avec des flotteurs est susceptible de produire un peu de "bruit numérique" qui, en particulier, peut rendre impossible des relectures exactes. Si c'est quelque chose que vous pensez que vous pourriez vouloir, vous pouvez préférer les autres méthodes.

  3. Découplez la mise à jour et dessinez les étapes. Ici, l'idée est de mettre à jour l'état de votre jeu en utilisant un pas de temps fixe, mais d'exécuter un nombre variable de mises à jour entre chaque image. Autrement dit, votre boucle de jeu pourrait ressembler à ceci:

    long lastTime = System.currentTimeMillis();
    while (isRunning) {
        long time = System.currentTimeMillis();
        if (time - lastTime > 1000) {
            lastTime = time;  // we're too far behind, catch up
        }
        int updatesNeeded = (time - lastTime) / updateInterval;
        for (int i = 0; i < updatesNeeded; i++) {
            update();
            lastTime += updateInterval;
        }
        draw();
        // ... sleep until next frame ...
    }
    

    Pour rendre le mouvement perçu plus fluide, vous pouvez également souhaiter que votre draw()méthode interpole des éléments tels que la position des objets en douceur entre les états de jeu précédent et suivant. Cela signifie que vous devez transmettre le décalage d'interpolation correct à la draw()méthode, par exemple comme ceci:

        int remainder = (time - lastTime) % updateInterval;
        draw( (float)remainder / updateInterval );  // scale to 0.0 - 1.0
    

    Vous devrez également faire en sorte que votre update()méthode calcule l'état du jeu une longueur d'avance (ou éventuellement plusieurs, si vous souhaitez effectuer une interpolation de spline d'ordre supérieur), et lui faire enregistrer les positions précédentes des objets avant de les mettre à jour, afin que la draw()méthode puisse interpoler entre eux. (Il est également possible d'extrapoler simplement les positions prédites en fonction des vitesses et des accélérations des objets, mais cela peut sembler saccadé, surtout si les objets se déplacent de manière compliquée, entraînant souvent l'échec des prédictions.)

    L'interpolation a pour avantage que, pour certains types de jeux, elle peut vous permettre de réduire considérablement le taux de mise à jour de la logique de jeu, tout en conservant une illusion de mouvement fluide. Par exemple, vous pourriez être en mesure de mettre à jour l'état de votre jeu uniquement, disons, 5 fois par seconde, tout en dessinant de 30 à 60 images interpolées par seconde. Dans ce cas, vous pouvez également envisager d'entrelacer votre logique de jeu avec le dessin (c.-à-d. Avoir un paramètre pour votre update()méthode qui lui dit de ne lancer x % d'une mise à jour complète avant de revenir), et / ou exécuter la physique du jeu / la logique et le code de rendu dans des threads séparés (attention aux pépins de synchronisation!).

Bien sûr, il est également possible de combiner ces méthodes de différentes manières. Par exemple, dans un jeu multijoueur client-serveur, vous pouvez demander au serveur (qui n'a pas besoin de dessiner quoi que ce soit) d'exécuter ses mises à jour à un pas de temps fixe (pour une physique cohérente et une rejouabilité exacte), tout en demandant au client d'effectuer des mises à jour prédictives (pour être annulé par le serveur, en cas de désaccord) à un pas de temps variable pour de meilleures performances. Il est également possible de mélanger utilement l'interpolation et les mises à jour à pas de temps variable; par exemple, dans le scénario client-serveur qui vient d'être décrit, il est vraiment inutile que le client utilise des pas de mise à jour plus courts que le serveur, vous pouvez donc définir une limite inférieure sur le pas du client et interpoler dans la phase de dessin pour permettre FPS.

(Edit: Ajout de code pour éviter des intervalles / comptages de mise à jour absurdes, au cas où, par exemple, l'ordinateur est temporairement suspendu ou gelé pendant plus d'une seconde pendant que la boucle de jeu est en cours d'exécution. Merci à Mooing Duck de m'avoir rappelé la nécessité de cela .)

Ilmari Karonen
la source
1
Merci beaucoup d'avoir pris le temps de répondre à ma question, je l'apprécie vraiment. J'aime vraiment l'approche de # 3, cela a le plus de sens pour moi. Deux questions, qu'est-ce que le updateInterval défini et pourquoi divisez-vous par celui-ci?
Carpetfizz
1
@Carpetfizz: updateIntervalcorrespond au nombre de millisecondes souhaité entre les mises à jour de l'état du jeu. Pour, disons, 10 mises à jour par seconde, vous définiriez updateInterval = (1000 / 10) = 100.
Ilmari Karonen
1
currentTimeMillisn'est pas une horloge monotone. Utilisez-le à la nanoTimeplace, à moins que vous ne vouliez que la synchronisation de l'heure réseau perturbe la vitesse des choses dans votre jeu.
user253751
@MooingDuck: bien repéré. Je l'ai corrigé maintenant, je pense. Merci!
Ilmari Karonen
@IlmariKaronen: En fait, en regardant le code, il pourrait être plus simple de le faire while(lastTime+=updateInterval <= time). C'est juste une pensée cependant, pas une correction.
Mooing Duck
7

Votre code s'exécute actuellement chaque fois qu'une image s'affiche. Si la fréquence d'images est supérieure ou inférieure à votre fréquence d'images spécifiée, vos résultats changeront car les mises à jour n'ont pas le même timing.

Pour résoudre ce problème, vous devez vous référer à Delta Timing .

Le but de Delta Timing est d'éliminer les effets du décalage sur les ordinateurs qui tentent de gérer des graphiques complexes ou beaucoup de code, en augmentant la vitesse des objets afin qu'ils finissent par se déplacer à la même vitesse, quel que soit le décalage.

Pour faire ça:

Cela se fait en appelant un temporisateur chaque trame par seconde qui contient le temps entre maintenant et le dernier appel en millisecondes.

Vous devrez ensuite multiplier le temps delta par la valeur que vous souhaitez modifier par le temps. Par exemple:

distanceTravelledSinceLastFrame = Speed * DeltaTime
Statique
la source
3
Mettez également des plafonds sur les deltatimes minimum et maximum. Si l'ordinateur passe en veille prolongée puis reprend, vous ne voulez pas que les choses se lancent hors écran. Si un miracle apparaît et time()renvoie le même deux fois, vous ne voulez pas d'erreurs div / 0 et de traitement inutile.
Mooing Duck
@MooingDuck: C'est un très bon point. J'ai édité ma propre réponse pour la refléter. (Habituellement, vous ne devez pas diviser quoi que ce soit par le pas de temps dans une mise à jour d'état de jeu typique, donc un pas de temps zéro devrait être sûr, mais le permettre ajoute une source supplémentaire d'erreurs potentielles pour un gain faible ou nul, et devrait donc être évité.)
Ilmari Karonen
5

C'est parce que vous limitez votre fréquence d'images, mais vous ne faites qu'une seule mise à jour par image. Supposons donc que le jeu fonctionne à la cible 60 fps, vous obtenez 60 mises à jour logiques par seconde. Si la fréquence d'images tombe à 15 ips, vous n'auriez que 15 mises à jour logiques par seconde.

Au lieu de cela, essayez d'accumuler le temps de trame passé jusqu'à présent, puis mettez à jour votre logique de jeu une fois pour chaque intervalle de temps donné, par exemple, pour exécuter votre logique à 100 images par seconde, vous exécuterez la mise à jour une fois toutes les 10 ms accumulées (et soustrayez celles du compteur).

Ajoutez une alternative (meilleure pour les visuels) mettez à jour votre logique en fonction du temps écoulé.

Mario
la source
1
ie mise à jour (elapsedSeconds);
Jon
2
Et à l'intérieur, position + = velocity * elapsedSeconds;
Jon