La Boucle d’Animation (partie 2)

On l’a vu, la boucle d’animation nous permet d’animer nos scènes et d’exploiter nos entités en mettant à jours leurs données.
Evidemment, dans un soucis d’organisation on est obligé d’intégrer tout ça dans une classe qui nous permettra, entre autre, de gérer la boucle ne fut-ce que pour lui adjoindre des méthodes de lancement et d’arrêt (vous verrez que pendant le dev d’un jeu, avoir ces deux méthodes c’est assez utile pour tracker les bugs).

Cela va impliquer pas mal changement dans nos différentes parties de code, en effet notre moteur actuellement ne gère que les entités de type rectangle et franchement ce serait pas mal qu’on puisse faire évoluer tout ça. Sans oublier évidemment que pour l’instant, le déplacement d’une entité par exemple on doit le gérer à la main. On va donc reprendre tout ce qu’on a fait jusqu’à présent, du Resource System à la Boucle d’Animation. Si vous n’avez pas encore lu les articles précédent, je vous invite à le faire.

Ne perdons pas de temps et attaquons nous à la création d’une classe LoopManager. La structure est simple il nous faut :

  • Un attribut d’état de boucle.
  • Une méthode de boucle qui exécutera les procédures (mise à jour, dessin des scènes etc..).
  • Une méthode de lancement de la boucle.
  • Une méthode d’arrêt de la boucle.

Codons un peu

/**
 * LoopManager
 * @description Gère la boucle d'animation et son état
 */
class LoopManager {
    constructor(SceneManager) {
        this.sm    = SceneManager;
        this.state = null;
        this.frame = null;
    }

    /**
     * start
     * @description Execute la boucle et défini son état à LOOP_STARTED
     */
    start() {
        this.state = 'LOOP_STARTED';
        this.loop();
    }

    /**
     * stop
     * @description Annule la dernière frame et change l'état de la boucle
     *              à LOOP_STOPPED
     */
    stop() {
        this.state = 'LOOP_STOPPED';
        cancelAnimationFrame(this.frame);
    }

    /**
     * loop
     * @description Méthode d'exécution de boucle, exécutera toutes les 
     *              procédure en fonction de son état.
     */
    loop() {
        // Si la boucle est lancée on exécute les procédure
        if(this.state === 'LOOP_STARTED') {
            // Dessin des Scènes
            this.sm.drawScenes();
        }

        // On appel requestAnimationFrame pour bouclé et
        // refaire un tour de boucle.
        this.frame = requestAnimationFrame(() => {
            this.loop();
        });
    }
}

Vous remarquerez rapidement que j’ai rajouté un attribut « frame », ce dernier doit recevoir le timestamp renvoyé par la fonction requestAnimationFrame. Ce time est une sorte d’ID unique permettant d’identifier la frame en cours et donc de l’annuler si besoin.
On annule une frame en appelant la fonction cancelAnimationFrame et en lui envoyant le timestamp créé par requestAnimationFrame en paramètre.

Maintenant que c’est fait, exploitons un peu cette nouvelle classe, vous remarquerez que j’ai déjà écrit quelques ligne sur l’exploitation des ressources (voir l’article sur la gestion des ressources dans le moteur 2D), car prochainement, on va pouvoir exploiter tout ça comme il faut.

// Définition des constantes de taille du canvas
const SCREEN_WIDTH  = 500;
const SCREEN_HEIGHT = 500;

// Création du Système de Ressources
const resource = new ResourceSystem();
// On créé le système de dessin
const draw     = new DrawSystem("game", SCREEN_WIDTH, SCREEN_HEIGHT, '2d');
// On créé le Scene Manager
const sManager = new SceneManager(draw);
// On créé le Loop Manager
const lManager = new LoopManager(sManager);
// Définition du tableau des resources
const assets = [];
// Définition des données d'entité dans la scène
let sceneData = [
    { x: 25, y: 25, width: 150, height: 25, fill: { color: 'red'},   stroke: { width: 5, color: 'blue' } },
    { x: 50, y: 50, width: 25, height: 150, fill: { color: 'green'}, stroke: { width: 5, color: 'black' } }
];

// Injection des resources
resource.loadResources(assets).then(() => {
    // Création de la scène
    sManager.create('GAME_LAYER', sceneData);
    // Lancement de la boucle d'animation
    lManager.start();
}).catch((e) => console.error(e));

Et nous revoilà avec cette bonne vieille scène toute moche, remplie par un rectangle rouge et un rectangle vert totalement immobiles.
Apportons un peu de mouvement dans tout ça !

Tout d’abord centrons nous sur l’Entity Manager des scènes, car c’est lui qui contient les entités donc c’est lui qui les mettra à jours et disons que pour l’instant on ne mettra à jours que la vitesse de déplacement sur les axes X et Y. Créons donc une méthode update() dans EntityManager

    /**
     * update
     * @description Parcours toutes les entités et les met à jours
     */
    update() {
        for(let i in this.entities) {
            this.entities[i].position.x += this.entities[i].speed.x;
            this.entities[i].position.y += this.entities[i].speed.y;
        }
    }

Il faudra créer une méthode updateScene() pour appeler l’update des entités dans la classe Scene :

    /**
     * updateScene
     * @description Appel la fonction update de l'EntityManager pour mettre à jours toutes
     *              les entités
     */
    updateScene() {
        this.em.update();
    }

Et de même dans le SceneManager pour appeler la mise à jours de toutes les scènes bien entendu :

/**
     * updateScenes
     * @description Parcours
     */
    updateScenes() {
        for(let i in this.scenes) {
            this.scenes[i].updateScene();
        }
    }

On pourra appeler ça comme nouvelle procédure dans la boucle d’animation, juste avant que les scènes ne soient dessinées.
N’oubliez pas de rajouter le DrawSystem dans la classe LoopManager pour pouvoir appeler la méthode clearScreen(), sinon l’animation aura quelques problèmes.

    /**
     * loop
     * @description Méthode d'exécution de boucle, exécutera toutes les procédure
     *              en fonction de son état.
     */
    loop() {
        // Si la boucle est lancée on exécute les procédure
        if(this.state === 'LOOP_STARTED') {
            // On reset le canvas avec clearScreen de DrawSystem
            this.draw.clearScreen(SCREEN_WIDTH, SCREEN_HEIGHT);
            // On met à jours les scènes
            this.sm.updateScenes();
            // Dessin des Scènes
            this.sm.drawScenes();
        }

        // On appel requestAnimationFrame pour bouclé et
        // refaire un tour de boucle.
        this.frame = requestAnimationFrame(() => {
            this.loop();
        });
    }

Maintenant tout ce qu’il nous reste à faire c’est de faire prendre ça en compte par les entités en ajoutant cette structure et ces données, d’abord dans la classe Entity :

class Entity {
    constructor(x, y, w, h, fill = {}, stroke = {}, speed = {}) {
        this.position = {
            x: x || 0,
            y: y || 0
        };

        this.size = {
            width:  w || 0,
            height: h || 0
        };

        this.fill = {
            color: fill.color || 'black'
        };

        this.stroke = {
            width: stroke.width || 0,
            color: stroke.color || 'black'
        };

        this.speed = {
            x: speed.x || 0,
            y: speed.y || 0
        }
    }
}

On modifie la méthode create() de l’EntityManager

    /**
     * create
     * @description Créer une instance d'entité selon une configuration donnée
     * @param data
     */
    create(data) {
        this.entities.push(new Entity(data.x, data.y, data.width, data.height, data.fill, data.stroke, data.speed));
    }

Et ENFIN, on peut mettre les données à jours dans notre programme pour avoir une ou des entités qui bougent :

// Définition des données d'entité dans la scène
let sceneData = [
    { x: 25, y: 25, width: 150, height: 25, fill: { color: 'red'},   stroke: { width: 5, color: 'blue' }, speed: { x: 2, y: 3 } },
    { x: 50, y: 50, width: 25, height: 150, fill: { color: 'green'}, stroke: { width: 5, color: 'black' }, speed: { x: 1, y: 0 } }
];

Voilà, ça devrait nous faire deux rectangles moches en mouvement.

C’est très joli tout ça mais on sent qu’on est à une grosse limite quand même, on a du modifier pas mal de chose pour uniquement prendre en compte le déplacement des deux rectangles. D’ailleurs on ne gère QUE des rectangles, moi je veux des images, des personnages que je puisse habiller, que je puisse mouvoir, faire réagir en fonction d’un combat, le bloquer contre un mur, etc.., etc..

He ben tout ça pourra se faire grâce aux saint Composants (ou Component), oui oui oui et d’ailleurs tout prendra sens bientôt.

Dans le prochain article j’essayerai d’expliquer au mieux le fonctionnement et l’avantage des composants.
Pour résumer, un composant est une mini partie d’entité. Chaque composant doit correspondre à une fraction d’une entité par exemple, sa taille ou encore sa position. Si vous observez l’entité qu’on a pour l’instant, elle est principalement composée de petites données, hé bien on va reprendre ces petites données et les transformer en composant afin qu’a chaque fois qu’on veut créer une entité, plutôt que de le faire en dur, on le fera avec une configuration de composant. Mais ce n’est pas tout ! Vous verrez que les Systèmes sont étroitement lié à ce fonctionnement.

Bref, j’en dis pas plus et on se revoit dans le prochain article 😉

J’espère que la lecture vous aura plu, n’hésitez pas à mettre un commentaire et partager ce contenu sur vos réseaux sociaux 🙂

Code en entier

/**
 * ResourceSystem
 */
class ResourceSystem {
    constructor() {}
    /**
     * loadResources
     * @description Parcours un tableau d'adresse de ressources pour les charger une à une et renvoyer une promise
     * @param resources
     * @returns {Promise<unknown[]>}
     */
    loadResources(resources) {
        // On prépare le tableau qui contiendra nos promises
        let arrayPromise = [];
        // On parcours le tableau
        for(let i in resources) {
            // On vérifie à quel type de ressource est proposé
            if(resources[i].type === 'image') arrayPromise.push(this.addImage(resources[i].namespace, resources[i].src));
        }
        // Et on retourne un Promise.all qui exécutera toutes les
        // promises une par une.
        return Promise.all(arrayPromise);
    }
    /**
     * addImage
     * @description Ajoute une image aux tableau des ressources
     * @param name
     * @param src
     * @returns {Promise<unknown>}
     */
    addImage(namespace, src) {
        // On utilise une promise pour pouvoir attendre le chargement
        // complet de l'image.
        return new Promise((resolve, reject) => {
            // On créé un nouvel élément image
            let image = new Image();
            // On défini l'adresse source (url, url:data)
            image.src = src;
            // On défini la fonction callback onload
            image.onload = () => {
                // On résolve la promise seulement quand l'image est
                // complètement chargée
                resolve();
            };
            this.define(namespace, image);
        });
    }
    /**
     * define
     * @description Défini une ressource selon un namespace précis
     * @param namespace
     * @param resource
     */
    define(namespace, resource) {
        // On split le namespace à chaque point
        let parts = namespace.split('.');
        // On défini la racine de notre namespace comme étant le contexte
        // global de l'instance de notre ResourceSystem
        let root = this;
        // On parcours le namespace
        for(let i = 0; i < parts.length; ++i) {
            // Tans qu'on est pas dans le dernier nom on redéfini
            // la racine.
            if(parts.length - 1 > i) {
                // Si le nom d'espace suivant n'existe pas on le créé
                if(!root[parts[i]]) root[parts[i]] = {};
                root = root[parts[i]]; // Puis on redéfini la racine
            } else {
                // sinon on défini la resource
                root[parts[i]] = resource;
            }
        }
    }
}

/**
 * Une classe DrawSystem permet d'initialiser le canvas quand on veut
 * et rend le code réutilisable
 */
class DrawSystem {
    constructor(id, width, height, context) {
        // Création de l'élément Canvas
        this.canvas = document.createElement("canvas");
        // Création du contexte
        this.ctx = this.canvas.getContext(context || '2d');
        // Initialisation de l'élément Canvas
        this.canvas.id     = id     || "game"; // On lui donne un ID
        this.canvas.width  = width  || 500;    // Une largeur
        this.canvas.height = height || 500;    // Une hauteur
        // Injection de l'élément Canvas
        document.body.appendChild(this.canvas);
    }

    /**
     * clearScreen
     * @description nettoie l'écran entièrement avant la prochaine frame
     * @param w
     * @param h
     */
    clearScreen(w, h) {
        this.ctx.clearRect(0, 0, w, h);
    }

    /**
     * image
     * @description Dessine une image donnée
     * @param image
     * @param x
     * @param y
     * @param w
     * @param h
     */
    image(image, x, y, w, h) {
        this.ctx.drawImage(image, x, y, w, h);
    }

    /**
     * rectangle
     * @description Méthode permettant de dessiner des carrés ou rectangles.
     */
    rectangle(x, y, w, h, options = {}) {
        if(options.fill) {
            this.ctx.fillStyle = options.fill.color;
        }

        if(options.stroke && options.stroke.width) {
            this.ctx.lineWidth = options.stroke.width;
        }

        if(options.stroke && options.stroke.color) {
            this.ctx.strokeStyle = options.stroke.color;
        }

        if(options.fill)   this.ctx.fillRect(x, y, w, h);
        if(options.stroke) this.ctx.strokeRect(x, y, w, h);
    }
}

/**
 * Entity
 * @description Classe d'entité, permet de créer et configurer une entité
 */
class Entity {
    constructor(x, y, w, h, fill = {}, stroke = {}, speed = {}) {
        this.position = {
            x: x || 0,
            y: y || 0
        };

        this.size = {
            width:  w || 0,
            height: h || 0
        };

        this.fill = {
            color: fill.color || 'black'
        };

        this.stroke = {
            width: stroke.width || 0,
            color: stroke.color || 'black'
        };

        this.speed = {
            x: speed.x || 0,
            y: speed.y || 0
        }
    }
}

/**
 * EntityManager
 * @description Apporte les outils nécéssaire à la gestion des entités dans une scène.
 */
class EntityManager {
    constructor(DrawSystem) {
        this.ds       = DrawSystem;
        this.entities = [];
    }

    /**
     * loadEntities
     * @description Reçoit un tableau de données et créé les entités
     * @param data
     */
    loadEntities(data) {
        // Création des instances d'entités
        for(let i in data) {
            this.create(data[i]);
        }
    }

    /**
     * create
     * @description Créer une instance d'entité selon une configuration donnée
     * @param data
     */
    create(data) {
        this.entities.push(new Entity(data.x, data.y, data.width, data.height, data.fill, data.stroke, data.speed));
    }

    /**
     * update
     * @description Parcours toutes les entités et les met à jours
     */
    update() {
        for(let i in this.entities) {
            this.entities[i].position.x += this.entities[i].speed.x;
            this.entities[i].position.y += this.entities[i].speed.y;
        }
    }

    /**
     * draw
     * @description Parcours le tableau des entités et les dessine une à une
     */
    draw() {
        // Dessin des entités dans la Scène
        for(let i in this.entities) {
            this.ds.rectangle(this.entities[i].position.x, this.entities[i].position.y,this.entities[i].size.width, this.entities[i].size.height, {
                fill: this.entities[i].fill,
                stroke: this.entities[i].stroke
            });
        }
    }
}

/**
 * Scene
 */
class Scene {
    constructor(DrawSystem) {
        this.state = null;
        this.em = new EntityManager(DrawSystem);
    }

    /**
     * initScene
     * @description Fourni un tableau de données de configuration d'entités à l'EntityManager
     *              et configure un état de départ
     * @param data
     * @param state
     */
    initScene(data, state = 'DRAWABLE_SCENE') {
        this.em.loadEntities(data);
        this.state = state;
    }

    /**
     * setVisible
     * @description Change l'état de la scene à DRAWABLE_SCENE, rend donc la scene visible
     */
    setVisible() {
        this.state = 'DRAWABLE_SCENE';
    }

    /**
     * setHidden
     * @description Change l'état de la scene à HIDDEN_SCENE, rend donc la scene invisible
     */
    setHidden() {
        this.state = 'HIDDEN_SCENE';
    }

    /**
     * addEntity
     * @description Fourni des données de configuration pour la création d'une nouvelle entité
     * @param data
     */
    addEntity(data) {
        this.em.create(data);
    }

    /**
     * updateScene
     * @description Appel la fonction update de l'EntityManager pour mettre à jours toutes
     *              les entités
     */
    updateScene() {
        this.em.update();
    }

    /**
     * drawScene
     * @description Ordonne à l'EntityManager de dessiner les Entités
     */
    drawScene() {
        if(this.state === 'DRAWABLE_SCENE') {
            this.em.draw();
        }
    }
}

/**
 * SceneManager
 * @description
 */
class SceneManager {
    constructor(DrawSystem) {
        this.draw   = DrawSystem;
        this.scenes = [];
    }

    /**
     * create
     * @description Créé une nouvelle instance de Scene et l'initialise avec des données de configuration
     * @param name
     * @param data
     */
    create(name, data) {
        // Si aucune scene sous ce nom n'est déjà présente dans le tableau
        if(!this.scenes[name]) {
            // On créé une nouvelle instance
            this.scenes[name] = new Scene(this.draw);
            // Et on l'initialise.
            this.scenes[name].initScene(data);
        }

        return this;
    }

    /**
     * updateScenes
     * @description Parcours
     */
    updateScenes() {
        for(let i in this.scenes) {
            this.scenes[i].updateScene();
        }
    }

    /**
     * drawScenes
     * @description Parcours le tableau des scenes et exécute leur fonction de dessin.
     */
    drawScenes() {
        for(let i in this.scenes) {
            this.scenes[i].drawScene();
        }
    }
}

/**
 * LoopManager
 * @description Gère la boucle d'animation et son état
 */
class LoopManager {
    constructor(DrawSystem, SceneManager) {
        this.draw  = DrawSystem;
        this.sm    = SceneManager;
        this.state = null;
        this.frame = null;
    }

    /**
     * start
     * @description Execute la boucle et défini son état à LOOP_STARTED
     */
    start() {
        this.state = 'LOOP_STARTED';
        this.loop();
    }

    /**
     * stop
     * @description Annule la dernière frame et change l'état de la boucle
     *              à LOOP_STOPPED
     */
    stop() {
        this.state = 'LOOP_STOPPED';
        cancelAnimationFrame(this.frame);
    }

    /**
     * loop
     * @description Méthode d'exécution de boucle, exécutera toutes les procédure
     *              en fonction de son état.
     */
    loop() {
        // Si la boucle est lancée on exécute les procédure
        if(this.state === 'LOOP_STARTED') {
            // On reset le canvas avec clearScreen de DrawSystem
            this.draw.clearScreen(SCREEN_WIDTH, SCREEN_HEIGHT);
            // On met à jours les scènes
            this.sm.updateScenes();
            // Dessin des Scènes
            this.sm.drawScenes();
        }

        // On appel requestAnimationFrame pour bouclé et
        // refaire un tour de boucle.
        this.frame = requestAnimationFrame(() => {
            this.loop();
        });
    }
}

// Définition des constantes de taille du canvas
const SCREEN_WIDTH  = 500;
const SCREEN_HEIGHT = 500;

// Création du Système de Ressources
const resource = new ResourceSystem();
// On créé le système de dessin
const draw     = new DrawSystem("game", SCREEN_WIDTH, SCREEN_HEIGHT, '2d');
// On créé le Scene Manager
const sManager = new SceneManager(draw);
// On créé le Loop Manager
const lManager = new LoopManager(draw, sManager);
// Définition du tableau des resources
const assets = [];
// Définition des données d'entité dans la scène
let sceneData = [
    { x: 25, y: 25, width: 150, height: 25, fill: { color: 'red'},   stroke: { width: 5, color: 'blue' }, speed: { x: 2, y: 3 } },
    { x: 50, y: 50, width: 25, height: 150, fill: { color: 'green'}, stroke: { width: 5, color: 'black' }, speed: { x: 1, y: 0 } }
];

// Injection des resources
resource.loadResources(assets).then(() => {
    // Création de la scène
    sManager.create('GAME_LAYER', sceneData);
    // Lancement de la boucle d'animation
    lManager.start();
}).catch((e) => console.error(e));

Ecrire un commentaire