ARTICLE
Tick Tick Boom
par CBL,
email @CBL_Factor
The Evil Within est encore un exemple de jeu dont le framerate est bloqué à 30 FPS sur PC. Bethesda a publié la liste des commandes debug permettant de faire sauter cette limite tout en précisant que cela peut donner des résultats inattendus.
Pour comprendre l'origine du problème, il est nécessaire de donner un petit cours de programmation sur les moteurs graphiques.
En général, ils fonctionnent grosso modo de la manière suivante. On commence par faire les calculs liés au gameplay, à l'IA, à la physique, aux collisions, aux animations... afin d'obtenir la position des différents objets 3D. Puis on va balancer le tout à la carte graphique qui va rendre le résultat et afficher une belle image.Pour comprendre l'origine du problème, il est nécessaire de donner un petit cours de programmation sur les moteurs graphiques.
Selon la complexité de ce qu'il y a à afficher et la puissance de la carte graphique, le rendu va prendre plus ou moins de temps. Accessoirement les calculs mentionnés plus haut vont aussi prendre du temps. La conséquence est qu'entre deux images, il se passe quelques dizaines de millisecondes. C'est la période entre deux images et si vous vous rappelez vos cours de collège, la fréquence équivaut à l'inverse de la période. F = 1/T. Donc si votre période est de 33 ms ou 0.033 s, votre fréquence est de 30 Hz (et des poussières). C'est votre frame rate. On l'exprime plutôt en frames par seconde (FPS).
Pour la majorité des calculs liés au gameplay comme les déplacements des personnages, on les effectue au sein de fonctions Tick qui sont gentiment fournies par le moteur et qui s'exécutent à chaque nouvelle image. Cette fonction Tick va aussi vous fournir le temps qui s'est écoulé depuis la dernière image, la fameuse période. Appelons-la dt (pour delta temps). Du coup il y a deux manières de faire ces calculs : en fonction du framerate ou en fonction du temps.
Quand vous fondez vos calculs sur le frame rate, vous espérez toujours atteindre un certain frame rate. Mettons 30 images par seconde. Imaginons maintenant qu'un ennemi court à 10 mètres par seconde en ligne droite le long de l'axe X. Comme une frame correspond à 1/30 de seconde, le calcul de la position de l'ennemi va ressembler à cela dans sa fonction Tick :
void Tick(float dt)
{
x = x + 10/30;
}
Si vous souhaitez plutôt fonder vos calculs sur le temps, le calcul sera plutôt du genre :
void Tick(float dt)
{
x = x + 10 * dt;
}
Si votre jeu tourne vraiment à 30 FPS, le résultat sera le même. L'ennemi bougera de 10 mètres en une seconde. Bloquer un jeu à 30 FPS est facile mais vous ne pouvez pas empêcher qu'il tombe en-dessous. Supposons qu'en même temps que l'ennemi court, votre carte graphique doive rendre une explosion atomique et arrive péniblement à cracher du 20 FPS. Si vous avez fondé vos calculs sur le frame rate, à la place d'avancer 30 fois en une seconde de 10/30 mètres, l'ennemi ne le fera que 20 fois. Il ira plus lentement.
En clair, tout le jeu sera ralenti. Si au contraire vous avez utilisé le temps, l'ennemi aura parcouru la même distance qu'à 30 FPS. Mais comme le rendu n'a été rafraîchi que 20 fois, son mouvement n'a pas dû paraitre très fluide. Si à la place d'un ennemi c'est une balle de Metal Slug qui se dirige vers vous, il est beaucoup plus juste de ralentir l'ensemble du jeu que de vous laisser moins de chance d'esquiver le projectile. Accessoirement quand le delta temps entre deux frames devient trop grand car le jeu rame à plein tubes, utiliser ce dt dans certains calculs type physique peut provoquer des effets indésirables vu qu'on perd beaucoup en précision.
Metal Slug 2, un bon exemple de jeu qui rame à fond les ballons
Ceci étant dit, coder en fonction d'un framerate fixe n'est pas propre pour deux raisons. Tout d'abord si des petits malins débloquent la limite que vous avez fixée, votre jeu va aller beaucoup plus vite que prévu. C'est l'effet Benny Hill. Mais aussi certains aspects du jeu bénéficient d'un framerate élevé. C'est le cas des animations de personnage. Mettons que vous levez un bras. Les animateurs vont définir l'orientation de départ et l'orientation d'arrivée et le moteur va se charger d'interpoller entre les deux. Plus vous avez d'images par seconde pour réaliser cette interpollation et plus ce mouvement paraitra fluide.
Un des solutions consiste à mélanger les deux systèmes à l'aide d'un accumulateur. L'idée est de débrider le framerate mais de ne faire certains calculs qu'après un certain temps écoulé. La fonction Tick va alors ressembler à quelque chose dans ce genre :
const float PERIODE = 1 / 60;
float accumulateur = 0;
void Tick(float dt)
{
accumulateur = accumulateur + dt;
while(accumulateur >= PERIODE)
{
MonTick(PERIODE);
accumulateur = accumulateur - PERIODE;
}
}
void MonTick(float dt)
{
x = x + 10 * dt;
{
En clair, si le delta temps est supérieur à celui voulu (donc si le frame rate est trop bas), on va effectuer les calculs plusieurs fois à chaque Tick histoire de compenser le fait qu'il y a moins de Tick. S'il est inférieur (donc si le frame rate est trop haut), on ne va pas faire les calculs à chaque Tick. Mais comme vous pouvez le constater, le code est bien plus lourd que si on se fondait uniquement sur le frame rate en priant pour qu'il atteigne les 30/60 FPS voulus. C'est du temps de dev en plus.
Need For Speed Rivals, l'exemple typique de la version PC codée avec les pieds
Du coup quand l'équipe qui est chargée de réaliser la version PC d'un jeu pensé avant tout pour consoles se retrouve avec ce genre de code sur les bras, ils ont deux solutions : soit ils en réécrivent une partie, soit ils conservent la même limite de frame rate que sur consoles.