Une histoire de pizzas : coup de pinceau

Vous aimez les pizzas ? Vous aimez les ratons laveurs ? Alors voici déjà le cinquième épisode de ma série d’articles retraçant la conception d’un mini-jeu vidéo !

Dans le deuxième article, on ne s’était pas trop embêté pour les graphismes : on avait fait de gros blocs de couleurs moches. Il est toutefois temps de corriger ça en passant un coup de peinture sur le jeu. Notre raton va bientôt ressembler à un raton !

Le premier ingrédient qu’il nous faut, c’est un graphiste ; et surprise, je ne le suis pas. Mes anciennes collègues de uxShadow ont fait appel à Goulven Barron pour réaliser les graphismes du jeu (merci à lui !), je n’avais ainsi plus qu’à les intégrer.

Pour cet article, je me suis donc contenté de récupérer des images du jeu d’origine et de les déposer dans un répertoire assets (qu’on peut traduire par « ressources » en bon françois).

Ensuite, il ne nous reste plus qu’à attacher chaque entité à une image et à l’afficher. Pour cela, on va tout simplement créer un nouveau composant au sein des entités (asset) qui contiendra l’URL vers l’image et ajouter cette dernière au HTML à l’aide d’une balise <img />.

<div id="app">
    <div class="board">
        <!-- ... -->

        <div
          v-for="entity in store"
          :key="entity.id"
          :class="['board-cell-entity', entity.id]"
          :style="entityPosition(entity)"
        >
            <!-- On affiche une image si l'entité possède un composant "asset" -->
            <img
                v-if="entity.asset"
                :src="entity.asset"
                alt=""
                class="entity-asset"
            />

            <!-- Sinon, on continue d'afficher le label -->
            <div v-else-if="entity.label" class="entity-label">
                {{ entity.label }}
            </div>
        </div>

        <!-- ... -->
    </div>
</div>

<script>
    // ...

    let gameStore = [
        // On se contente d'ajouter un composant "asset" à chacune de nos
        // entités. Il s’agit juste d'une URL vers une image.
        {
            id: 'meiko',
            asset: 'assets/meiko-face.svg',
            // ...
        },

        {
            id: 'mozza',
            asset: 'assets/pot-mozzarella.svg',
            // ...
        },
        {
            id: 'tomato',
            asset: 'assets/sauce-tomate.svg',
            // ...
        },
        {
            id: 'dough',
            asset: 'assets/pate-pizza-boule.png',
            // ...
        },

        {
            id: 'oven',
            asset: 'assets/four.svg',
            // ...
        },
        {
            id: 'hatch',
            asset: 'assets/passeplat.svg',
            // ...
        },
        {
            id: 'fridge',
            asset: 'assets/refrigirateur.svg',
            // ...
        },
        {
            id: 'workplan',
            asset: 'assets/plan-travail.svg',
            // ...
        },
        {
            id: 'shelf',
            asset: 'assets/etagere-horizontale.svg',
            // ...
        },
    ];

    // ...
</script>

<style type="text/css">
    /* ... */

    /*
        On vire quasiment tout le CSS concernant les entités (sauf les z-index)
        et on le remplace par... seulement ce max-width pour éviter que les
        images débordent des cases. Vous vous attendiez à plus de CSS ? :)
    */
    .board-cell-entity img {
        max-width: 100%;
    }

    /* Bon OK, on en ajoute un pour que la mozza ne déborde pas trop du frigo */
    .board-cell-entity.mozza img {
        max-width: 85%;
    }

    /* ... */
</style>

Et voilà, en seulement quelques lignes, on a largement transformé notre jeu. On est désormais visuellement très proches du jeu d’origine. Un petit détail toutefois reste à régler : on aimerait bien que Meiko se tourne dans la direction vers laquelle on vient de cliquer. Pour cela on va avoir besoin de 4 images représentant Meiko : une pour chaque direction. La question qui se pose est : « comment changer d’image ? »

Je vais aborder ici deux solutions possibles pour faire cela. Le choix que j’ai fait n’est pas forcément le meilleur, mais il sera justifié. Libre à vous de penser que c’est vraiment n’importe quoi et de préférer l’autre solution.

La première façon de faire est de regrouper toutes les images au sein d’un seul fichier : un « sprite » (ou lutin, c’est Wikipédia qui le dit, j’ai toujours entendu parler que de sprite). Ensuite, il faut afficher seulement une partie de l’image, en fonction d’une classe CSS par exemple. Je ne rentre pas dans le détail de l’implémentation, les méthodes sont de plus sensiblement différentes si vous avez un PNG ou un SVG. Cette technique me posait plusieurs problèmes :

  • générer un sprite est un poil plus pénible (soit on complique la tâche de la personne qui génère l’image, soit on complique la chaîne de build du projet)
  • je ne savais pas faire avec des SVG et n’avais pas envie de passer trop de temps là-dessus, ayant des choses plus intéressantes à développer par ailleurs (c’est important d’utiliser son temps à bon escient 😉)
  • je pensais que ça me poserait des problèmes de responsive design (problèmes grandement exagérés par le fait que je ne savais pas faire, tout est lié !)

Bref, je suis parti sur une seconde solution plus naïve, moins performante et moins élégante, mais qui me permettait au moins d’avancer sans me poser trop de questions.

La technique est simple. Chaque image se trouve dans un fichier à part, et on les rattache via un composant assets (notez le « s » final, c’est un autre composant !) qui va regrouper ces images, indexées par des directions. Lors de l’affichage, on va calculer dynamiquement l’image à afficher en fonction de la direction vers laquelle Meiko est dirigé. Ça ressemble à ça :

<div id="app">
    <div class="board">
        <!-- ... -->

        <div
          v-for="entity in store"
          :key="entity.id"
          :class="['board-cell-entity', entity.id]"
          :style="entityPosition(entity)"
        >
            <!-- On affiche une image si l'entité possède un composant "asset" -->
            <img
                v-if="entity.asset"
                :src="entity.asset"
                alt=""
                class="entity-asset"
            />

            <!--
                Ou bien un composant "assets", il faut alors calculer
                dynamiquement l'image à afficher.
            -->
            <img
                v-else-if="entity.assets"
                :src="entityAssetImage(entity)"
                alt=""
                class="entity-asset"
            />

            <!-- Sinon, on continue d'afficher le label -->
            <div v-else-if="entity.label" class="entity-label">
                {{ entity.label }}
            </div>
        </div>

        <!-- ... -->
    </div>
</div>

<script>
    // ...

    let gameStore = [
        {
            id: 'meiko',

            // Pour Meiko, on remplace le composant "asset" par "assets". Cela
            // nous permettra d'associer plusieurs images, la bonne sera
            // choisie dynamiquement lors de l'affichage.
            assets: {
                bottom: 'assets/meiko-face.svg',
                right: 'assets/meiko-droite.svg',
                left: 'assets/meiko-gauche.svg',
                top: 'assets/meiko-dos.svg',
            },

            // On ajoute également un composant "direction" à Meiko
            direction: 'bottom',

            // ...
        },

        // ...
    ];

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

        methods: {
            // ...

            // Cette méthode JS calcule dynamiquement l'image à afficher pour
            // les entités possédant un composant "assets"
            entityAssetImage(entity) {
                if (entity.assets == null || entity.direction == null) {
                    return '';
                }

                // Comme le composant "assets" est indexé par les directions,
                // récupérer la bonne image se fait très facilement. Ce code
                // n’est pas très robuste mais comme le jeu est très simple
                // avec peu de contributeurices, on peut s’en contenter pour
                // l'instant.
                return entity.assets[entity.direction];
            },
        },
    });

    // ...
</script>

La dernière étape consiste à changer effectivement la direction de Meiko en fonction d’où le clic a été effectué. Vous l’aurez compris : on va modifier l’état du store, cela se passe donc au sein de notre système onClickSystem.

function onClickSystem(e, store) {
    // ...

    // Notre code doit s'exécuter avoir vérifié que Meiko peut accéder à la
    // case ciblée, sinon il ne doit pas bouger du tout.
    const meiko = getEntity(store, 'meiko');
    if (!positionIsAccessibleFor(meiko, position)) {
        return store;
    }

    // On veut désormais changer la direction de Meiko : il suffit de
    // comparer sa position à la position de la case ciblée pour savoir
    // vers où Meiko doit se tourner.
    let direction = meiko.direction;
    if (position.x > meiko.position.x) { direction = 'right'; }
    else if (position.x < meiko.position.x) { direction = 'left'; }
    else if (position.y < meiko.position.y) { direction = 'top'; }
    else if (position.y > meiko.position.y) { direction = 'bottom'; }

    let updatedStore = setEntityComponents(store, meiko.id, { direction });

    // On fait bien attention à utiliser le updatedStore à partir d’ici au
    // lieu du store sinon Meiko ne se tournera pas.
    const entitiesAtPosition = searchEntitiesAt(updatedStore, position);
    if (entitiesAtPosition.some((entity) => entity.obstruct)) {
        return updatedStore;
    }

    updatedStore = setEntityComponents(updatedStore, meiko.id, { position });

    return updatedStore;
}

Avec ça on a fini notre coup de peinture sur le jeu. D’ailleurs, on a même terminé de toucher au HTML concernant la zone de jeu elle-même : même pas 50 lignes de code. Vous remarquerez sans doute en testant le jeu (le résultat est ici) que Meiko ne change pas immédiatement de direction : il s’agit du temps de charger l’image la première fois. On corrigera ça plus tard dans un article bonus. Pour l’instant, on a quelque chose de bien plus important à faire : ajouter les étapes ainsi que les actions pour avancer dans le jeu. Ce sera l’objet des deux prochains articles (au moins !)

Revenir à la série