La Boucle d’Animation

Jusqu’ici nous n’avons vu que des scènes statiques et pour cause nous n’avons pas encore intégrer la gestion de la boucle d’animation.

La boucle d’animation permettra d’enchaîner les mises à jours de nos scène et leurs entités. Par exemple mettre à jour la position d’une entité sur le plan pourra donner l’illusion qu’elle se déplace.

Comment la définir ? Comment va-t-on faire pour faire tourner une boucle qui permettra d’exécuter la procédure de mise à jours de notre scène ? Question qui mènera à d’autre réflexion, par exemple quelle est la procédure de mise à jour de notre scène ? Quelles sont les opérations prioritaires ? Quelles sont ces opérations à mener d’ailleurs ?

Au départ on pourrait simplement penser à une boucle while ou une boucle for, c’est assez simple à mettre en place donc allons-y, testons voir ce que ça donne :

Codons un peu

const SCREEN_WIDTH  = 500;
const SCREEN_HEIGHT = 500;

// On créé le système de dessin
const draw = new DrawSystem("game", SCREEN_WIDTH, SCREEN_HEIGHT, '2d');

// Boucle de 250 tours
for(let i = 0; i < 250; ++i) {
    // On dessine le rectangle à chaque nouvelle boucle,
    // sa position X prendra +1 pixel à chaque tour
    draw.rectangle(i, 50, 50, 50, { fill: { color: 'red' }});
}

Le code ci-dessus est simple, on fait tourner une boucle for, sa variable d’incrémentation est utilisée comme variable X du rectangle. Le rectangle devrait bouger de gauche à droite. Sauf que… On a pas vraiment le résultat escompté.

C’est pas vraiment ce qu’on attendait

En effet, plutôt qu’un rectangle de 50×50 on se retrouve avec un gros trait rouge de 250x50px. L’explication est simple, à chaque nouveau tour, ou chaque nouvelle « frame », le rectangle est dessiné par dessus la dernière frame. Il nous faut donc une méthode qui permette de nettoyer le canvas avant de re-dessiner dessus, et ça tombe bien parce que DrawSystem intègre déjà cette fonction, clearScreen(), c’est une simple méthode qui fait appel à la fonction clearRect() de Canvas. Modifions donc:

const SCREEN_WIDTH  = 500;
const SCREEN_HEIGHT = 500;

// On créé le système de dessin
const draw = new DrawSystem("game", SCREEN_WIDTH, SCREEN_HEIGHT, '2d');

// Boucle de 250 tours
for(let i = 0; i < 250; ++i) {
    // On nettoie le canvas avant de dessiner quoi que ce soit
    draw.clearScreen(SCREEN_WIDTH, SCREEN_HEIGHT);
    // On dessine le rectangle à chaque nouvelle boucle,
    // sa position X prendra +1 pixel à chaque tour
    draw.rectangle(i, 50, 50, 50, { fill: { color: 'red' }});
}

Et là, clearScreen() fait bien sont boulot, mais l’animation semble ne pas se faire, on ne voit rien bouger, il n’y a que le rectangle rouge à 250×50.
La raison est simple, la boucle for est beaucoup trop rapide, en fait avec n’importe quelle boucle cela aurait été trop rapide. Il faudrait donc intégrer une notion de temps entre chaque frame. Donc, on s’embête pas et on se dirige vers setInterval, là on contrôle complètement la durée des frames, modifions encore donc :

const SCREEN_WIDTH  = 500;
const SCREEN_HEIGHT = 500;

// On créé le système de dessin
const draw = new DrawSystem("game", SCREEN_WIDTH, SCREEN_HEIGHT, '2d');
// Variable d'incrémentation
let i = 0;
// Intervalle d'exécution
setInterval(() => {
    // On nettoie l'écran
    draw.clearScreen(SCREEN_WIDTH, SCREEN_HEIGHT);
    // On dessine le rectangle
    draw.rectangle(i, 150, 50, 50, { fill: { color: 'red' }});
    // On incrémente la variable
    ++i;
}, 1000 / 30);

Et là, Ô magie, le rectangle prend bien son temps pour bouger de pixel en pixel. J’ai défini une intervalle de 1 seconde divisée par le nombre d’image que je veux par seconde.

Tout ceci étant dit, nous avons tout de même un petit soucis. En effet, 1000 divisé par 30, cela nous fait 33.33 millisecondes. Chaque frame devrait donc être calculée en moins de 33.33 millisecondes et ça, ben ça va pas être une mince affaire. On travail avec juste un seul rectangle, avec le temps beaucoup de choses doivent être implémentée, la physique, les collisions, les déplacements, la logique des événements, etc., etc. . Et tout ça pour potentiellement plusieurs centaine voir millier d’entités.

Le soucis qui arrivera très vite, c’est que le calcul de l’ensemble des exécution prenne plus que 33.33 milliseconde et donc que les frames s’empiètent les unes sur les autres.

Request Animation Frame

Heureusement ! Il y a une solution facile ! C’est une méthode native aux navigateurs récents, appelée requestAnimationFrame().

Grosso merdo cette méthode est nourrie par une callback, requestAnimationFrame appellera la callback en moyenne 60 fois par secondes. Si la callback est plus longue que prévue, le framerate descend, c’est tout.
Le navigateur attendra que chaque appel de la callback soit fini pour refaire un appel. Plus de soucis d’empiétement donc. Aller, testons ça :

const SCREEN_WIDTH  = 500;
const SCREEN_HEIGHT = 500;

// On créé le système de dessin
const draw = new DrawSystem("game", SCREEN_WIDTH, SCREEN_HEIGHT, '2d');
// Variable d'incrémentation
let i = 0;

/**
 * loop
 * @description Fonction callback pour requestAnimationFrame,
 *              intègre nos procédure de dessin.
 */
function loop() {
    // On nettoie l'écran
    draw.clearScreen(SCREEN_WIDTH, SCREEN_HEIGHT);
    // On dessine le rectangle
    draw.rectangle(i, 150, 50, 50, { fill: { color: 'red' }});
    // On incrémente la variable
    ++i;
    // On rappel la fonction requestAnimationFrame en lui redonnant
    // la fonction loop, la boucle est donc bouclée.
    requestAnimationFrame(loop)
}

// On appel requestAnimationFrame une première fois pour son lancement.
requestAnimationFrame(loop);

C’est une façon propre de faire, mais je vous invite sérieusement à voir la doc de Canvas sur requestAnimationFrame pour compléter ces explications.
La boucle d’animation est clairement un concept à comprendre et à tester, même si de prime abord il parait simple.

Cet article sur la boucle d’animation est une première explication pour faire découvrir la boucle et montrer comment elle fonctionne.
La prochaine fois on verra comment créer une classe autour de tout ça, comment intégrer des priorités d’exécution et comment ça influencera le reste de l’application.

J’espère que cet article vous aura plus, n’hésitez pas à mettre un commentaire et partager le lien avec vos connaissances 😉
Bonne journée