I. Qu'est-ce que la boucle de jeu ?▲
La boucle de jeu (gameloop en anglais) est la boucle principale d'un jeu vidéo. Nous parlons ici, de boucle, en tant qu'élément de programmation (une boucle while pour être précis).
Contrairement aux programmes simples (scripts) qui ne font qu'un calcul et s'arrêtent, un jeu est un programme qui tourne à l'infini. On pourrait penser aux programmes ayant une interface utilisateur qui eux aussi, tournent de manière infinie et qui possèdent bien souvent, une boucle principale. Mais la boucle principale d'un tel programme est en attente des événements utilisateur et ne va enclencher son travail que suivant les actions de l'utilisateur. Les jeux rentrent dans une autre catégorie : ils ont une boucle infinie, mais qui n'attend pas nécessairement l'utilisateur. En effet, même si vous posez votre manette, le jeu affichera l'image, le monde virtuel continuera de vivre et les ennemis vous tireront toujours dessus.
Voilà ce qu'est la boucle du jeu. C'est la boucle au cœur du programme, dans laquelle nous faisons un certain nombre d'actions élémentaires pour que le jeu fonctionne.
II. Constitution▲
Un jeu vidéo doit en permanence :
- mettre à jour l'état du jeu (update) ;
- afficher une image (draw).
Rien qu'avec ces deux actions, vous pouvez au moins avoir une application affichant une animation, par exemple, un personnage qui marche.
Toutefois, pour que votre application ne se ferme pas dès la première image, ces deux actions doivent être dans une boucle :
while(1)
{
update()
draw()
}
Ici, update() et draw() sont des fonctions généralistes :
- update() s'occupera de plusieurs tâches, notamment bouger les ennemis (intelligence artificielle), gérer les collisions, compter les points du joueur et ainsi de suite. C'est grâce à cette fonction que le jeu est animé, vivant ;
- draw() s'occupera de l'affichage de tous les éléments du jeu, peu importe le comment.
III. Implémentation▲
La boucle de jeu présentée ci-dessus n'est que la première étape de sa conception. Très rapidement et cela, pour obtenir un jeu jouable, nous allons devoir l'améliorer.
III-A. Quitter la boucle de jeu▲
La première chose que l'on pourrait remarquer dans le premier exemple est que nous ne pouvons pas quitter le jeu.
Le joueur souhaitera très certainement quitter le jeu. Pour cela, il y a plusieurs possibilités :
- le joueur utilise le menu du jeu et quitte le jeu ;
- le joueur appuie sur la touche « Échap » ;
- le joueur clique sur la croix.
Ce sont trois questions que nous devons nous poser à chaque itération dans la boucle afin de déterminer si le joueur souhaite arrêter le jeu. Plus précisément, le jeu doit continuer tant que ces actions n'arrivent pas. Cela donne :
while(
veuxQuitter == false ET
aAppuyeToucheEchap() == false ET
aCliqueSurLaCroixRouge() == false
)
{
update()
draw()
}
La variable veuxQuitter sera définie à vrai lorsque l'utilisateur aura choisi le menu « Quitter » du jeu.
La fonction aAppuyeTouchEchap() est une fonction permettant de savoir si le joueur a appuyé sur la touche « Échap ». Dans les bibliothèques de jeux, il existe généralement une fonction permettant de savoir si le joueur appuie sur une touche.
La fonction aCliqueSurLaCroixRouge() est une fonction permettant de savoir si le joueur a cliqué sur la croix rouge pour quitter l'application. Les bibliothèques de jeux (et en réalité, toute bibliothèque permettant d'ouvrir une fenêtre) proposent une telle fonction, ou une méthode pour détecter le clic sur la croix.
III-B. Ralentir la boucle de jeu▲
Un ordinateur va très, très vite. Si vous gardez la boucle ainsi, votre jeu risque d'aller très vite, car l'ordinateur est capable d'exécuter ces fonctions des centaines et même des milliers de fois par seconde. Du coup, nous sommes obligés de la ralentir (au moins pour alléger la charge sur le processeur et augmenter la durée de vie de la batterie).
Dans notre jeu, nous souhaitons avoir 60 images par seconde. Cela nous suffit amplement même si nous pourrions viser plus haut. Cela veut dire que chaque passage dans notre boucle doit durer 16 ms.
kitxmlcodelatexdvptemps = \frac{secondes}{nombre\ images} = \frac{1}{60} = 0.16666...finkitxmlcodelatexdvpPour y arriver, la première idée serait d'attendre 16 ms à la fin de la boucle. Toutefois, cela ne donnera pas un résultat correct, car nous ne savons pas si la fonction draw() et update() prennent 0 ms, ou 5 ou 10… Il faut donc chronométrer la boucle.
while(
veuxQuitter == false ET
aAppuyeToucheEchap() == false ET
aCliqueSurLaCroixRouge() == false
)
{
tempsDebut = time()
update()
draw()
tempsFin = time()
delay(16 - (tempsFin - tempsDebut))
}
La fonction time() permet de récupérer le temps (en millisecondes) depuis le début de l'exécution du programme.
La fonction delay() permet d'attendre N millisecondes.
Encore une fois, ces deux fonctions sont généralement présentes dans les bibliothèques de jeux.
III-C. Interagir avec le joueur▲
Actuellement, le joueur ne peut toujours pas agir sur le jeu. Il faut donc rajouter un moyen de vérifier les touches qu'il appuie (ou s'il bouge la souris, ou s'il utilise la manette…).
while(
veuxQuitter == false ET
aAppuyeToucheEchap() == false ET
aCliqueSurLaCroixRouge() == false
)
{
tempsDebut = time()
verifieActionUtilisateur()
update()
draw()
tempsFin = time()
delay(16 - (tempsFin - tempsDebut))
}
La fonction verifieActionUtilisateur() se contentera de vérifier l'état du clavier/souris/manettes (possiblement en scannant les événements reçus par l'application). Encore une fois, c'est grâce aux fonctions de la bibliothèque de jeux utilisée que vous pourrez savoir ce que fait l'utilisateur.
Une fois ces actions identifiées, vous devez les utiliser dans la fonction update(), notamment pour bouger votre personnage.
En réalité, il est totalement possible d'avoir cette partie de programme vérifiant les touches dans la fonction update(), toutefois, on préfère la placer dans la boucle, sachant que nous avons besoin de savoir quand le joueur va appuyer sur la touche Échap.
IV. Particularités des jeux vidéo▲
Nous avons vu la boucle de jeu basique. Il existe des boucles plus complexes, mais celle-ci répondra à la majorité des besoins. Toutefois, il reste quelques détails à connaître.
IV-A. Éviter les allocations de mémoire dans la boucle de jeu▲
Une allocation mémoire (appel à malloc/new ou toute instanciation d'objets à la volée (pour les langages qui n'ont pas d'allocation de mémoire à proprement parler)) peut apporter des soucis :
- vous devez faire très attention à votre utilisation de la mémoire. Les fuites de mémoire (non libération de la mémoire inutilisée) sont des erreurs toujours d'actualité. Dans la boucle de jeu, qui s'exécute 60 fois par seconde, même une allocation de 10 ko (une petite image) fait perdre 600 ko par seconde, soit 36 Mo par minute ;
- même si vous avez un ramasse-miettes, celui-ci peut s'activer au milieu de votre boucle, afin de libérer la mémoire. Son exécution est non prédictible et dure généralement plus de 16 ms. Celui-ci risque donc de provoquer une saccade dans votre jeu.
IV-B. Ne pas faire de chargement dans la boucle de jeu▲
Le chargement d'une ressource (sprite, musique…) est pire qu'une allocation.
Premièrement, au cours du chargement d'une ressource, le programme devra allouer de la mémoire pour stocker les données de la ressource en mémoire.
Le chargement d'une ressource est généralement lié à des accès au disque (ou au réseau) à partir duquel vous allez charger la ressource. Ceux-ci sont très lents (par rapport aux besoins d'un jeu vidéo). Le disque dur est la mémoire la plus lente du PC.
En suivant ces deux conseils, votre programme prendra la forme suivante :
Ouverture de la fenêtre
Chargement des ressources du jeu (et allocation de la mémoire nécessaire)
while(
veuxQuitter == false ET
aAppuyeToucheEchap() == false ET
aCliqueSurLaCroixRouge() == false
)
{
tempsDebut = time()
verifieActionUtilisateur()
update()
draw()
tempsFin = time()
delay(16 - (tempsFin - tempsDebut))
}
Libération des ressources pour les langages en ayant besoin.
IV-C. Avoir la même vitesse de jeu sur toutes les machines▲
Actuellement, notre fonction update() n'a aucune notion du temps. Cela veut dire, que pour déplacer un élément du jeu, elle va certainement faire :
positionEnX = positionEnX + vitesse
Si tout va bien, notre boucle de jeu s'exécute 60 fois par seconde. Donc la position en X de l'élément se sera déplacée de 60 * vitesse en une seconde. Cela est totalement convenable lorsque « tout va bien ». Mais si, pour une raison ou une autre, le programme ralentit, nous ne sommes plus sûrs de toujours avoir 60 images par seconde. Si on tombe à 30 images par seconde, en une seconde notre élément se sera déplacé de 30 * vitesse. Le déplacement aura été deux fois plus lent.
Cela est plutôt problématique. En effet, suivant la vitesse du PC, notre jeu n'est pas exactement le même. Dans des compétitions, une telle chose serait horrible, car tout le monde ne serait pas sur le même pied d'égalité. Il faut donc que la vitesse dépende du temps.
Pour ce faire, nous allons chronométrer le temps entre chaque appel à la fonction update(). Chaque mouvement et animation dans notre jeu dépendra de ce temps. En effet, lorsque le jeu affiche 60 images par seconde, le temps entre deux appels est de 16,6 ms et lorsque le jeu fonctionne à 30 images par seconde, le temps sera de 33,2 ms.
Avec la formule suivante :
positionEnX = positionEnX + vitesse * deltaTime
Si la vitesse est de 1 et que le jeu fonctionne à 60 images par seconde :
positionEnX = positionEnX + 1 * 16.6
Effectué 60 fois, donc le mouvement sera de 996 au bout d'une seconde. Pour le même jeu, fonctionnant à 30 images par seconde :
positionEnX = positionEnX + 1 * 33.2
Effectué 30 fois, le mouvement en une seconde sera donc de 996. Ce qui est équivalent au cas précédent et donc, le jeu aura la même vitesse sur toutes les machines.
V. Aller plus loin▲
Cet article n'apporte que la base sur les boucles de jeu vidéo. Tous les éléments vus ici suffiront pour obtenir un jeu fonctionnel. Toutefois, il est possible d'aller plus loin, notamment en explorant les pistes suivantes :
- l'implémentation de différentes fréquences pour l'appel aux fonctions update() et draw() ;
- l'amélioration de la gestion des différentes vitesses des machines ;
- le parallélisme et l'utilisation de tous les cœurs des processeurs.
VI. Ressources▲
Au cours de cet article, j'ai fait référence à des bibliothèques de jeux. Sous cette appellation, j'avais notamment en tête :
VII. Conclusion▲
Si vous parlez de la boucle de jeu à n'importe quel programmeur, il comprendra immédiatement en quoi elle consiste et les actions obligatoires la composant. Elle est la base de tout jeu et elle existe obligatoirement. Son implémentation peut changer, mais son concept est toujours le même.
VIII. Remerciements▲
Merci à archMqx. et yahiko pour leur précieuse relecture.
Merci à Claude Leloup et milkoseck pour leur relecture orthographique.