🏠 Accueil

Une histoire de pizzas : coup de pinceau

(lecture : 19 minutes) — sĂ©rie : Ergogames, une histoire de pizzas

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