Une histoire de pizzas : les objets

J’ai commencé la semaine dernière une série d’articles pour expliquer comment j’ai développé un jeu pour les Ergogames. Il me semble qu’il n’y a rien d’exceptionnel dans ce que j’explique, je vise plutôt un public amateur qui chercherait à comprendre comment fonctionne un jeu. Mon but est aussi de montrer ma démarche alors que je n’y connaissais rien au développement de jeux vidéos (enfin « presque rien », j’y reviens plus loin).

Dans l’article précédent nous avons commencé par afficher une grille de 6 par 6 cases. Nous allons maintenant afficher les éléments du jeu (le frigo, le four, Meiko, etc.) Ces éléments vont interagir entre eux, il faut donc les représenter dans notre code JavaScript pour gérer ces aspects « dynamiques » et l’« état » du jeu. Cette représentation est importante et ne doit pas être prise à la légère puisqu’elle va impliquer notre capacité à ajouter de nouvelles fonctionnalités au jeu.

C’est le moment où je révèle que j’ai légèrement triché dans mon précédent article quand je disais que je n’y connaissais rien : je m’étais déjà un peu amusé par le passé avec quelques concepts, et notamment l’architecture « entité-composant-système ». J’avais découvert ce paradigme dans une série d’articles sur LinuxFR et j’avais même commencé à développé une bibliothèque Python, Pytity.

Le principe est assez simple :

  • les entités représentent les objets du jeu (ex. un frigo) mais ne possèdent pas de données ; on représente une entité par un identifiant
  • à ces entités, on associe des composants qui représentent l’état de l’entité (ex. une couleur, une position) ; il s’agit d’un ensemble de données
  • les systèmes contiennent le code pour interagir avec notre tas de données comme, par exemple, pour afficher les entités, écouter les actions du joueur ou modifier les composants

Alors comment on s’y prend pour représenter tout ça ? Ici on va faire simple et même légèrement différent de ce que j’ai fait pour les Ergogames afin d’être plus lisible. Il n’existe de toute façon pas de manière unique pour représenter une architecture « entité-composant-système ».

Commençons par lister les différents objets de notre jeu à l’état initial, nous en avons 9 :

// L'ensemble de nos entités avec leurs composants est stocké dans un gros
// objet JavaScript.
let gameStore = {
    // L'identifiant de l'entité est une clé (meiko) de l'objet JS. On lui
    // associe un ensemble de composants (label, size et position)
    meiko: {
        label: 'Meiko',
        size: { width: 1, height: 1 },
        position: { x: 3, y: 2 },
    },

    // Et on fait la même chose avec toutes les entités de notre jeu
    mozza: {
        label: 'Mozzarella',
        size: { width: 1, height: 1 },
        position: { x: 1, y: 3 },
    },
    tomato: {
        label: 'Sauce tomate',
        size: { width: 1, height: 1 },
        position: { x: 2, y: 5 },
    },
    dough: {
        label: 'Pâte à pizza',
        size: { width: 1, height: 1 },
        position: { x: 5, y: 3 },
    },

    oven: {
        label: 'Four',
        size: { width: 2, height: 1 },
        position: { x: 1, y: 0 },
    },
    hatch: {
        label: 'Passe-plat',
        size: { width: 1, height: 1 },
        position: { x: 5, y: 0 },
    },
    fridge: {
        label: 'Frigo',
        size: { width: 2, height: 2 },
        position: { x: 0, y: 2 },
    },
    workplan: {
        label: 'Plan de travail',
        size: { width: 1, height: 2 },
        position: { x: 5, y: 2 },
    },
    shelf: {
        label: 'Étagère',
        size: { width: 4, height: 1 },
        position: { x: 2, y: 5 },
    },
};

const app = new Vue({
    el: '#app',
    data: {
        size: BOARD_SIZE,
        // on ajoute le store à "data" pour pouvoir y accéder dans le html
        store: gameStore,
    },
});

Ceci étant fait, nous pouvons créer notre premier « système » : celui qui va afficher les objets. Dans notre cas, le rendu est géré par le navigateur ce qui nous simplifie grandement la vie par rapport à un jeu « natif ». Ça n’a d’ailleurs pas grand-chose à voir à mon avis et je ne sais pas si on peut vraiment parler de « système ». On va se contenter ici d’écrire un peu de HTML et de JS (pour positionner les entités au bon endroit) :

<div class="board">
    <!-- Ça, ça ne change pas -->
    <div v-for="y in size" :key="y" class="board-line">
        <div v-for="x in size" :key="x" class="board-cell">
        </div>
    </div>

    <!--
        On itère sur les entités (objets) du store pour les ajouter dans le
        HTML. Le truc important ici est la fonction "entityPosition" qui va
        retourner les dimensions et la position en CSS de l'entité pour que
        le navigateur l'affiche au bon endroit.
    -->
    <div
      v-for="(entity, id) in store"
      :key="id"
      :class="['board-cell-entity', id]"
      :style="entityPosition(entity)"
    >
        <!-- Si l'entité a un composant "label", on l’ajoute au HTML. -->
        <div v-if="entity.label" class="entity-label">
            {{ entity.label }}
        </div>
    </div>
</div>

<script>
    // ...

    // Cette valeur correspond à la variable --cell-size en CSS.
    const CSS_CELL_SIZE = 128;

    const app = new Vue({
        // ...

        methods: {
            // entityPosition retourne la position de l'entité en CSS.
            entityPosition(entity) {
                if (entity.size == null || entity.position == null) {
                    // Si l'entité ne possède pas de taille ou de position,
                    // cela signifie qu'on ne peut pas l'afficher : on ne
                    // retourne donc rien.
                    return {};
                }

                return {
                    // on multiplie la taille de l'entité par la taille d'une
                    // cellule pour obtenir sa taille en CSS.
                    width: (entity.size.width * CSS_CELL_SIZE) + 'px',
                    height: (entity.size.height * CSS_CELL_SIZE) + 'px',

                    // et pareil pour obtenir sa position
                    top: (entity.position.y * CSS_CELL_SIZE) + 'px',
                    left: (entity.position.x * CSS_CELL_SIZE) + 'px',
                };
            },
        },
    });
</script>

Une solution plus élégante serait de ne générer le HTML que pour les entités avec une taille et une position : en effet, quel intérêt y a-t-il à ajouter une entité qui n’a pas d’existence visuelle ? J’ai choisi cette façon de faire afin de garder le code plus simple et parce que je n’avais pas pensé à cela initialement (je reste ainsi plus proche de ce que j’ai fait initialement).

Vous noterez aussi que je ne tente pas de placer les entités dans les cellules du tableau que l’on a généré lors de l’étape précédente. En effet, une entité peut se trouver dans plusieurs cellules en même temps et ce serait galère à gérer correctement en termes de HTML. Je préfère donc dissocier les entités des cellules et les superposer. Ça n’a pas beaucoup d’importance ici puisqu’il s’agit uniquement de rendu visuel.

À ce niveau on est pas mal, mais ça reste toujours moche et on ne voit pas grand-chose. On va donc ajouter du CSS pour rendre nos entités correctement.

/* ... */

/*
   Ça c'est important pour pas se prendre la tête sur les dimensions mais
   je ne rentre pas dans le détail du pourquoi du comment.
*/
* {
    box-sizing: border-box;
}

/*
   Les entités ont une position absolue *au sein* du tableau, donc on
   précise le nécessaire pour éviter qu'elles ne volent n'importe où à
   l'écran, et surtout pas en dehors du cadre de jeu.
*/
.board {
    position: relative;
}
.board-cell-entity {
    position: absolute;
}

/* Puis on colorie nos entités pour les rendre plus visibles à l'écran */
.board-cell-entity.oven { background-color: #333; }
.board-cell-entity.fridge { background-color: #666; }
.board-cell-entity.workplan { background-color: #783f04; }
.board-cell-entity.shelf { background-color: #000; }
.board-cell-entity.hatch { background-color: #cf2a27; }

.board-cell-entity .entity-label { color: #fff; }
.board-cell-entity.shelf .entity-label { text-align: right; }

/*
   Certaines entités peuvent être superposées à d'autres (la pâte, la mozza
   et la sauce tomate). On leur met un z-index pour être sûr qu'elles se
   trouveront au-dessus.
*/
.board-cell-entity.dough,
.board-cell-entity.mozza,
.board-cell-entity.tomato {
    z-index: 10;

    background-color: green;
}

/*
   Meiko a droit à un style un peu différent mais ça change pas grand-chose
   à la logique : on le colorie aussi !
*/
.board-cell-entity.meiko {
    border: 2px solid green;
}
.board-cell-entity.meiko .entity-label {
    color: initial;
}

Notez ici que la majorité du code CSS n’est pas très important puisqu’on remplacera nos blocs colorés par des images plus tard.

On arrive toutefois à la fin de cet article puisqu’on a vu comment représenter les objets du jeu et comment les afficher. Le résultat final est visible ici et, comme la dernière fois, n’hésitez pas à regarder le code source de la page.

Nous verrons dans le prochain article comment déplacer Meiko de case en case tout en faisant attention à ce qu’il ne puisse pas traverser les objets tel que le frigo.

Revenir à la série