Des Entités à la Scène (partie 1)

Scène de théâtre, par Pietro Longhi (vers 1780) – Wikipédia.

Quand on regarde une image, ou plus précisément un dessin, une photo, une peinture, on y voit toujours l’expression de quelque chose. Un sentiment, une explication logique, un moment, des émotions, etc…
En général l’artiste place ses éléments sur sa scène et cherche à montrer et exprimer quelque chose. Le peintre ou le dessinateur créeront des personnages dans une posture précises, employant des couleurs et/ou des formes choisies précisément, un photographe attendra le bon moment, la bonne expression ou choisira un angle de vue bien précis pour laisser apparaître ce qu’il veut montrer ou exprimer.

Placer et montrer des éléments, montrer des expressions, des couleurs, des formes, parler au spectateur pour transmettre une information, voilà ce qu’est une scène pour moi.

Dans notre cas, notre toile c’est Canvas, notre scène sera une classe Scene et ses éléments seront des entités rassemblées dans un tableau.

Qu’est-ce qu’une entité ?

Une entité est une classe avec laquelle on construit des objets précis, un personnage, un mur, un arbre, un élément qui permet au joueur d’apparaître dans le jeu, etc…
L’idée de l’entité, c’est de pouvoir partir sur des bases communes et que son utilisation soit standardisée. Par exemple, faire un rendu du personnage doit pouvoir se faire de la même manière que le rendu d’un arbre, ou d’un ennemi, ou d’un bloc de béton.

En gros chaque élément de notre scène est une entité modelée et façonnée comme on le veut, respectant un certains standard pour pouvoir être gérée facilement.

Codons un peu

On l’a vu dans le précédant article, on peut charger des ressources, une fois le chargement fini on peut commencer à dessiner.
Hé bien le code après le chargement, pour l’instant c’est notre scène.

Partons d’un exemple simple : Dessiner un rectangle rouge

draw.rectangle(25, 25, 150, 25, { fill: { color: 'red' }, stroke: { width: 5, color: black });

Chaque méthode utilisée pour dessiner doit être paramétrée pour avoir un résultat attendu. On ne peut clairement pas s’amuser à tapoter les informations en brut dans le code, il faut que cela puisse être paramétrable de manière simple et efficace, on va donc créer une Classe Entité

class Entity {
    constructor() {
        this.position = {
            x: 0,
            y: 0
        };

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

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

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

// On créé une instance rectangle
let rectangle = new Entity(25, 25, 150, {color: 'red'}, {width: 5, color: 'blue' });

// Et on dessine
draw.rectangle(
        rectangle.position.x,
        rectangle.position.y,
        rectangle.size.width,
        rectangle.size.height,
        {fill: rectangle.fill, stroke: rectangle.stroke}
    );

La différence ici c’est qu’on va pouvoir charger une configuration, par exemple en JSON, d’entité avec chacune leur propriétés individuelles.
Voilà à quoi ressemblerait notre app pour tester ça :

/**
 * 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) {
            this.ctx.lineWidth   = options.stroke.width;
            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);
    }
}

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'
        };
    }
}

const SCREEN_WIDTH  = 500;
const SCREEN_HEIGHT = 500;

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

// 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' } }
];

// Tableau qui contiendra les entités
let entities = [];

// Création des instances d'entités
for(let i in data) {
    entities.push(new Entity(data[i].x, data[i].y, data[i].width, data[i].height, data[i].fill, data[i].stroke));
}

// Dessin des entités dans la Scène
for(let i in entities) {
    draw.rectangle(entities[i].position.x, entities[i].position.y,entities[i].size.width, entities[i].size.height, {
        fill: entities[i].fill,
        stroke: entities[i].stroke
    });
}

Avec ça, on peut désormais charger un fichier de configuration d’entités et nourrir la scène avec. Fichier de configuration qu’on pourra mettre dans nos ressources ! Cela permettra entre autre de configurer plusieurs Scènes, par exemple des maps, des niveaux, bref, tout un tas de truc sympa.
Ça veut également dire que plus tard on pourra créer un fichier de configuration d’entités facilement avec un éditeur qu’on pourrait créer !

La classe EntityManager

Tout ce code me parait parfait pour commencer une nouvelle classe qui nous permettra de gérer facilement les entités, l’EntityManager.

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

Le code est relativement simple et permet de créer des Manager d’entité à la volée.

  • On dispose d’une fonction qui peut recevoir et parcourir les données pour construire les entités.
  • Une fonction qui créé et injecte les entités dans le tableau.
  • Et une fonction qui dessine les entités.

Le code qui exploite toutes nos classes devient relativement simple :

const SCREEN_WIDTH  = 500;
const SCREEN_HEIGHT = 500;

// 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 em   = new EntityManager(draw);

// 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 charge les entités
em.loadEntities(data);
// Et on les dessine
em.draw();

J’ai survolé le concept d’entité dans cette partie. En fait je n’ai abordé que le coté gestion, et démontré comment rendre notre future scène configurable. Mais sachez que les entités dans un moteur 2D ne se résume pas à cela, bien au contraire, si l’on dispose d’un moyen de configuration cela veut dire que nous pourrons aborder un moyen de construction d’entité plutôt organique qui nous permettra de faire des entités bien différentes les unes des autres en utilisant les « Components ».

Dans la deuxième partie de cet article nous verrons rapidement la création d’une classe Scene et de son manager. On verra également comment utiliser les Scène en différentes couches de dessin qui nous permettront d’avoir des scène superposée pour par exemple créer un HUD et un Menu.

Je vous met le code en entier et vous propose également de chercher par vous même la création de scène 😉

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

const SCREEN_WIDTH  = 500;
const SCREEN_HEIGHT = 500;

// 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 em   = new EntityManager(draw);

// 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 charge les entités
em.loadEntities(data);
// Et on les dessine
em.draw();