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 delay
calcul. 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é FPS
sur 150, le cercle rendu passe à travers la vitesse très rapidement, tandis que le réglage FPS
sur 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.
la source
1000 / FPS
division pourrait être effectuée et le résultat attribué à une variable avant votrewhile(isRunning)
boucle. Cela permet d'économiser quelques instructions CPU pour faire quelque chose de plus d'une fois inutilement.Réponses:
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:
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.
Utilisez un pas de temps variable pour votre physique de jeu. Fondamentalement, cela signifie réécrire votre boucle de jeu pour ressembler à ceci:
et, à l'intérieur
update()
, ajuster les formules physiques pour tenir compte du pas de temps variable, par exemple comme ceci: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.
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:
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 à ladraw()
méthode, par exemple comme ceci: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 ladraw()
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 .)
la source
updateInterval
correspond au nombre de millisecondes souhaité entre les mises à jour de l'état du jeu. Pour, disons, 10 mises à jour par seconde, vous définiriezupdateInterval = (1000 / 10) = 100
.currentTimeMillis
n'est pas une horloge monotone. Utilisez-le à lananoTime
place, à moins que vous ne vouliez que la synchronisation de l'heure réseau perturbe la vitesse des choses dans votre jeu.while(lastTime+=updateInterval <= time)
. C'est juste une pensée cependant, pas une correction.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 .
Pour faire ça:
Vous devrez ensuite multiplier le temps delta par la valeur que vous souhaitez modifier par le temps. Par exemple:
la source
time()
renvoie le même deux fois, vous ne voulez pas d'erreurs div / 0 et de traitement inutile.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é.
la source