Des Entités à la Scène (Partie 2)

Dans la partie précédente, nous avons vu comment créer et gérer des Entités, maintenant, dans cette deuxième partie nous allons voir comment intégrer tout ça dans une scène et gérer plusieurs scènes en même temps.

Une Scene est donc un objet possédant un manager d’entités, son rôle est d’être un accès à ce manager et doit pouvoir également être mis en différentes couche avec d’autres scène via un SceneManager (qu’on codera plus bas).

La Scene étant utilisée comme couche de dessin, on doit pouvoir la contrôler en lui définissant un état, qu’elle soit visible ou non selon nos besoin. Prenons l’exemple d’un menu de jeu. Quand vous êtes en jeu et que vous appuyez sur la touche échappe en général un menu apparaît.
Parfois même on peut voir le jeu continuer à tourner en arrière plan, c’est ce que la scène permet, entre autre, de faire.

Codons un peu

Le code ci-dessous créé une scène de base, avec un état, un entity manager et des méthodes pour gérer l’état et accéder à l’entity manager. On verra plus bas pour son utilisation avec une unique scène et ensuite on attaquera le SceneManager.

/**
 * 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);
    }

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

En intégrant directement la Scene dans le code précédent cela donne ceci :

const SCREEN_WIDTH  = 500;
const SCREEN_HEIGHT = 500;

// Données au format JSON, pour simuler un chargement distant
let data = [
    { 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' } }
];

// On créé le système de dessins
const draw = new DrawSystem("game", SCREEN_WIDTH, SCREEN_HEIGHT, '2d');
// Création de l'EntityManager, on lui fourni le DrawSystem
const myScene = new Scene(draw);
// Initialisation de la scène avec les données, pas besoin de spécifier
// son état, elle se dessinera directement.
myScene.initScene(data);

Rien de bien compliqué. Maintenant créons et ajoutons un SceneManager à tout ça, puis testons :

/**
 * 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;
    }

    /**
     * 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();
        }
    }
}

En toute simplicité on peut donc maintenant gérer plusieurs scène avec chacune leur propre entity manager et leur état.

Aller on teste, notre code devrait ressembler à ça :

const SCREEN_WIDTH  = 500;
const SCREEN_HEIGHT = 500;

// Données au format JSON, pour simuler un chargement distant
let dataSceneOne = [
    { 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' } }
];

let dataSceneTwo = [
    { x: 30, y: 150, width: 75, height: 200, fill: { color: 'black'},   stroke: { width: 5, color: 'black' } },
    { x: 200, y: 50, width: 125, height: 150, fill: { color: 'orange'}, stroke: { width: 5, color: 'black' } }
];

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

// On créé les scene à partir des données.
sm.create('one', dataSceneOne);
sm.create('two', dataSceneTwo);

// méthodes à tester pour afficher ou non une scène
// sm.scenes['one'].setHidden();
// sm.scenes['two'].setHidden();

// Et on dessine les scenes
sm.drawScenes();

Il devient super aisé de gérer nos scènes et leurs entités. On pourrait imaginer plus tard une méthode resetScene, qui permettrait de charger des nouvelles données d’entité, par exemple pour créer une nouvelle partie ou charger une nouvelle map.
On pourra également inclure une méthode updateScenes qui ira mettre à jour toutes les entités de toutes les scènes qu’on veut mettre à jours.

A ce stade du code, on ne peut que créer des scènes statiques, forcément, on a pas encore aborder la boucle d’animation. Je préfère aborder un concept statique car lorsque la boucle sera lancée, je pense qu’il est préférable d’avoir aborder tout ce qu’on vient de voir, sinon en cas d’erreur il sera surement plus complexe d’en trouver l’origine, surtout si on ne comprend pas le déroulement des étapes.

La suite des articles devrait porter sur :

  • La boucle d’animation
  • Les Components
  • Les événements hardware (souris, clavier, manette, …)

J’espère que cet article vous aura plu, n’hésitez pas à mettre un petit commentaire si vous avez des idées, des suggestions ou des critiques (constructives siouplait quand même) 😉

Le code en entier

/**
 * 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 = {}) {
        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'
        };
    }
}

/**
 * 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));
    }

    /**
     * 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);
    }

    /**
     * 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;
    }

    /**
     * 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();
        }
    }
}

const SCREEN_WIDTH  = 500;
const SCREEN_HEIGHT = 500;

// Données au format JSON, pour simuler un chargement distant
let dataSceneOne = [
    { 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' } }
];

let dataSceneTwo = [
    { x: 30, y: 150, width: 75, height: 200, fill: { color: 'black'},   stroke: { width: 5, color: 'black' } },
    { x: 200, y: 50, width: 125, height: 150, fill: { color: 'orange'}, stroke: { width: 5, color: 'black' } }
];

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

// On créé les scene à partir des données.
sm.create('one', dataSceneOne);
sm.create('two', dataSceneTwo);

// méthodes à tester pour afficher ou non une scène
// sm.scenes['one'].setHidden();
// sm.scenes['two'].setHidden();

// Et on dessine les scenes
sm.drawScenes();