Unverified Commit 23a97087 authored by berumuron's avatar berumuron
Browse files

Ajoute l'article Une histoire de pizzas 7

parent 81033476
---
title: Une histoire de pizzas : les actions
date: 2021-02-14 18:00
---
Hey salut, ça fait un bail hein ? Hein, quoi… plus d’un an ? Le temps passe
vite 😅 Je dois vous avouer que l’écriture de cet article m’angoissait
légèrement parce qu’il s’agissait d’un gros morceau. J’ai donc laissé trainer
quelques semaines… puis mois… puis une année entière. Mais nous y voilà !
Pour redonner le contexte, j’ai donné un coup de main en 2019 à deux anciennes
collègue pour développer un mini-jeu. **Le projet global est un site qui invite
à découvrir différents principes d’ergonomie à travers des jeux illustrant les
concepts : [les Ergogames](https://www.ergogames.fr/).** J’ai accepté à
condition que le code soit ouvert ([il l’est donc](https://gitlab.com/sogilis/ergogames.fr)).
Le jeu sur lequel j’ai bossé consiste à déplacer un personnage (Meiko) dans une
cuisine en cliquant de case en case. L’objectif est de lui faire fabriquer une
pizza. Le principe illustré est celui des [actions minimales](https://www.ergogames.fr/principles/minimum-actions).
**J’ai commencé [une série d’articles](serie/ergogames.html) pour expliquer
comment j’avais conçu ce jeu alors que je n’y connaissais pas grand-chose
auparavant.** J’y explique les étapes pas à pas en tentant d’être aussi
pédagogique que possible. Vous êtes invité‧es à lire les articles précédents
pour vous y retrouver.
Durant les épisodes précédents, on s’est surtout concentré sur le visuel et les
déplacements. On a donc désormais un personnage qui peut aller de case en case,
tout en étant arrêté par des éléments « bloquants » (le frigo, le four, etc.)
Dans le dernier article, on avait ajouté la liste des étapes du jeu. Il nous
faut maintenant la rendre dynamique, en fonction des actions de Meiko !
[Le résultat du code de cet article se trouve ici.](une-histoire-de-pizzas/etape7.html)
Je vous invite à en lire le code source, les parties ajoutées par cet article
sont commentées.
---
Cette fois-ci, on ne va modifier que le JavaScript. Il nous faut commencer par
adapter quelques éléments.
Premièrement, en fonction des étapes du jeu, Meiko et la pizza vont changer de
« look ». Par exemple, la pizza se trouve à l’état de boule au début du jeu,
puis Meiko va l’étaler, donc il faut adapter le visuel. Du côté de Meiko, son
look s’adapte en fonction des ingrédients qu’il porte (ou non). Ce qu’on va
faire, c’est ajouter un composant `look`. Sa valeur sera modifiée en fonction
des actions effectuées par le joueur. On va également modifier le composant
`assets` en l’indexant par les valeurs possibles du composant `look`.
```javascript
let gameStore = [
{
id: 'meiko',
// Meiko va changer de "look" en fonction des actions, on ajoute
// donc un nouveau composant pour le mémoriser
look: 'normal',
assets: {
// On modifie notre composant "assets" pour indexer d’abord par
// le look et on conserve le système de "direction"
normal: {
bottom: 'assets/meiko-face.svg',
right: 'assets/meiko-droite.svg',
left: 'assets/meiko-gauche.svg',
top: 'assets/meiko-dos.svg',
},
mozza: {
bottom: 'assets/meiko-face-mozza.svg',
right: 'assets/meiko-droite-mozza.svg',
left: 'assets/meiko-gauche-mozza.svg',
top: 'assets/meiko-dos.svg',
},
sauce: {
bottom: 'assets/meiko-face-sauce.svg',
right: 'assets/meiko-droite-sauce.svg',
left: 'assets/meiko-gauche-sauce.svg',
top: 'assets/meiko-dos.svg',
},
pizza: {
bottom: 'assets/meiko-face-pizza.svg',
right: 'assets/meiko-droite-pizza.svg',
left: 'assets/meiko-gauche-pizza.svg',
top: 'assets/meiko-dos.svg',
},
},
// ...
},
// Puis on fait la même chose avec la pizza (note : dans les articles
// précédents, l'id était `dough`, j'ai modifié en `pizza` mais c'est
// un détail)
{
id: 'pizza',
// la pizza aussi change de look au fur et à mesure de la partie,
// mais pas de direction pour elle !
look: 'doughBall',
assets: {
doughBall: 'assets/pate-pizza-boule.png',
dough: 'assets/pate-pizza.svg',
tomato: 'assets/pate-tomate.svg',
mozza: 'assets/pizza.svg',
profile: 'assets/pizza-plat.svg',
},
/// ...
},
// ...
};
```
Maintenant qu’on a modifié le fonctionnement du composant `assets`, il faut
prendre ça en compte dans le code qui l’utilise. Heureusement pour nous il
s’agit d’une seule petite fonction simple : `entityAssetImage`. On la retrouve
dans nos méthodes VueJS :
```javascript
const app = new Vue({
methods: {
// On modifie légèrement le rendu ici puisqu’on a fait évoluer le
// composant "assets" pour fonctionner avec le nouveau composant
// "look".
entityAssetImage(entity) {
if (entity.assets == null || entity.look == null) {
return '';
}
// on récupère l'asset correspondant au "look" actuel
const asset = entity.assets[entity.look];
if (entity.direction) {
// si l'entité a un composant direction, il faut encore
// piocher la bonne image
return asset[entity.direction];
} else {
// sinon, on retourne la valeur telle quelle
return asset;
}
},
// ...
},
// ...
});
```
Désormais, lorsque la valeur de `look` changera, VueJS changera pour nous
l’image affichée à l’écran. Idéalement il faudrait faire quelques vérifications
supplémentaires pour rendre le code plus robuste, mais on est sur un mini-jeu
pas critique, je privilégie la lisibilité en allant droit au but.
Deuxième élément important à ajouter à notre jeu : le système qui va exécuter
les actions. Il va s’agir de fonctions exécutées quand on cliquera sur le bon
élément du jeu et qui modifieront l’état interne du jeu (`gameStore`). Ces
fonctions vont donc être rattachées aux différentes entités via un nouveau
composant : `exec`. Elles seront exécutées lorsqu’on cliquera sur l’entité
correspondante (au sein de la fonction `onClickSystem`). Une facilité du jeu,
c’est qu’il n’y a toujours qu’une seule chose à faire à la fois, soit un seul
composant `exec` à la fois. Ça nous évite quelques nœuds au cerveau. La
première action consiste à étaler la pâte, il faut donc rajouter le composant
`exec` à la pizza-pâte :
```javascript
let gameStore = [
{
id: 'pizza',
// Le nouveau composant exec qui permet de faire avancer le jeu !
// La première action consiste à étaler la pâte, on a donc un seul
// composant exec qui pointe vers l'action `rollOutDough`.
exec: rollOutDough,
/// ...
},
// ...
};
```
La fonction elle-même sera écrite un peu plus loin. En attendant on va ajouter
l’exécution de la fonction au sein de `onClickSystem`, exécuté lorsqu’on clique
sur une case :
```javascript
function onClickSystem(e, store) {
// ...
const meiko = getEntity(store, 'meiko');
if (!positionIsAccessibleFor(meiko, position)) {
return store;
}
// Avant de bouger Meiko, on recherche les entités situées sur la case
// cliquée : s'il en existe avec le composant "exec", alors on appelle
// la fonction associée en lui passant le store et la position. La
// fonction doit retourner un nouveau store mis à jour.
let updatedStore = store;
const entitiesAtPosition = searchEntitiesAt(updatedStore, position);
entitiesAtPosition.forEach((entity) => {
if (typeof entity.exec === 'function') {
updatedStore = entity.exec(updatedStore, position);
}
});
// ...
}
```
Rien de sorcier ici, on se contente du fait que JavaScript nous permet de
référencer une fonction par son nom et de l’exécuter via cette référence en
ajoutant les parenthèses et les paramètres.
On pourrait passer tout de suite à l’écriture des actions, mais il va nous
manquer une dernière mini-fonction. On va en effet avoir besoin de supprimer
des entités du `gameStore` (ex. la sauce tomate va disparaître une fois que
Meiko l’aura attrapée).
```javascript
// On a désormais besoin de supprimer des entités du jeu. On ne garde
// (`filter`) que les entités dont l'id est différent de celui passé en
// argument.
function removeEntity(store, id) {
return store.filter((entity) => entity.id !== id);
}
```
Nous avons désormais tous les éléments nécessaires pour faire avancer le jeu,
il n’y a plus qu’à écrire les actions ! Une action est en fait très simple :
1. elle commence par mettre à jour l’étape courante (l’entité `currentStep`) ;
1. elle ajoute un composant `exec`, pointant vers la prochaine action, sur
l’entité correspondante ;
1. et supprime le composant `exec` de l’entité actuelle ;
1. elle peut également changer le look des entités, en supprimer, etc.
Commençons par notre première action : `rollOutDough`.
```javascript
function rollOutDough(store) {
let updatedStore = store;
// on commence par mettre à jour l’étape actuelle
updatedStore = setEntityComponents(updatedStore, 'currentStep', { value: 'STEP_GRAB_TOMATO' });
// puis on "installe" un composant exec sur l'entité "tomato" puisque
// c’est la prochaine destination de Meiko
updatedStore = setEntityComponents(updatedStore, 'tomato', { exec: grabTomato });
updatedStore = setEntityComponents(updatedStore, 'pizza', {
// Meiko vient d’étaler la pâte, il faut changer le "look" de la
// pizza
look: 'dough',
// comme on n’a plus rien à faire pour le moment sur la pizza, on
// pense à enlever son composant "exec" (sinon Meiko pourra étaler
// la pizza plusieurs fois de suite).
exec: null,
});
// et finalement, on renvoie le nouveau store
return updatedStore;
}
```
On le voit, il s’agit uniquement de modifier le `store` passé en paramètre à
l’aide de la fonction `setEntityComponents` qu’on avait écrite dans un article
précédent. Si vous avez réussi à suivre jusqu’ici, ça devrait être vraiment
simple à comprendre ! Le fait de changer le `currentStep` permet de changer
visuellement l’étape courante : c’est le code écrit dans l’article précédent.
Nous avons maintenant une nouvelle action à écrire…
```javascript
function grabTomato(store) {
let updatedStore = store;
// idem, on met à jour l’étape actuelle
updatedStore = setEntityComponents(updatedStore, 'currentStep', { value: 'STEP_SPREAD_TOMATO' });
// puis on ajoute un "exec" sur la pizza car l'action suivante porte
// sur elle
updatedStore = setEntityComponents(updatedStore, 'pizza', { exec: spreadTomato });
// on change le look de Meiko qui porte maintenant le pot de tomate
updatedStore = setEntityComponents(updatedStore, 'meiko', { look: 'sauce' });
// et on enlève l'entité "tomato" car elle n'est plus présente sur le
// plan de travail. On est d'accord que le pot de tomate est toujours
// visible, mais on triche en l’intégrant visuellement à l'asset de
// Meiko.
updatedStore = removeEntity(updatedStore, 'tomato');
return updatedStore;
}
```
En supprimant l’entité `tomato`, on a supprimé par la même occasion son `exec`.
Mais… oh non, on a encore une action à écrire ! Vous l’aurez compris, on va
continuer exactement de la même manière pour chaque action, car elles
s’enchaînent les unes après les autres. Je vous donne tout le code d’un coup.
Il y a quelques passages où j’ai triché par rapport à la version initiale du
jeu pour simplifier les explications. Ces passages sont signalés en
commentaire.
```javascript
function spreadTomato(store) {
let updatedStore = store;
updatedStore = setEntityComponents(updatedStore, 'currentStep', { value: 'STEP_GRAB_MOZZA' });
updatedStore = setEntityComponents(updatedStore, 'mozza', { exec: grabMozza });
updatedStore = setEntityComponents(updatedStore, 'pizza', {
look: 'tomato',
exec: null,
});
updatedStore = setEntityComponents(updatedStore, 'meiko', { look: 'normal' });
return updatedStore;
}
function grabMozza(store) {
let updatedStore = store;
updatedStore = setEntityComponents(updatedStore, 'currentStep', { value: 'STEP_SPREAD_MOZZA' });
updatedStore = setEntityComponents(updatedStore, 'pizza', { exec: spreadMozza });
updatedStore = removeEntity(updatedStore, 'mozza');
updatedStore = setEntityComponents(updatedStore, 'meiko', { look: 'mozza' });
return updatedStore;
}
function spreadMozza(store) {
let updatedStore = store;
updatedStore = setEntityComponents(updatedStore, 'currentStep', { value: 'STEP_GRAB_PIZZA_1' });
updatedStore = setEntityComponents(updatedStore, 'pizza', {
look: 'mozza',
exec: grabUncookedPizza,
});
updatedStore = setEntityComponents(updatedStore, 'meiko', { look: 'normal' });
return updatedStore;
}
function grabUncookedPizza(store) {
let updatedStore = store;
updatedStore = setEntityComponents(updatedStore, 'currentStep', { value: 'STEP_PUT_PIZZA_IN_OVEN' });
updatedStore = setEntityComponents(updatedStore, 'oven', { exec: putPizzaInOven });
// Ici on triche un peu. On ne veut plus afficher la pizza dans le jeu.
// Dans la version initiale, je supprimais le composant et le recréait
// plus tard. Ça se basait cependant sur un mécanisme que je n'ai pas
// expliqué dans ma série d'articles, donc je dois faire différemment.
// Ici, je vire simplement le "look" pour cacher la pizza.
updatedStore = setEntityComponents(updatedStore, 'pizza', {
look: null,
exec: null,
});
updatedStore = setEntityComponents(updatedStore, 'meiko', { look: 'pizza' });
return updatedStore;
}
function putPizzaInOven(store, position) {
let updatedStore = store;
updatedStore = setEntityComponents(updatedStore, 'currentStep', { value: 'STEP_GRAB_PIZZA_2' });
updatedStore = setEntityComponents(updatedStore, 'oven', {
exec: null,
});
// la suite de l'action précédente : on redonne un look à la pizza, et
// on change sa position à l'emplacement qui vient d'être cliqué.
updatedStore = setEntityComponents(updatedStore, 'pizza', {
position,
look: 'profile',
// je triche encore ! Dans la version initiale on est sensé
// attendre la cuisson de la pizza (5 secondes). Ça se base sur un
// autre système que je n'ai pas expliqué dans la série d'articles,
// donc je saute l'étape de la cuisson (STEP_BAKE_PIZZA). Peut-être
// que j'en parlerai dans un article bonus parce que le mécanisme
// est intéressant.
exec: grabCookedPizza,
});
updatedStore = setEntityComponents(updatedStore, 'meiko', { look: 'normal' });
return updatedStore;
}
function grabCookedPizza(store) {
let updatedStore = store;
updatedStore = setEntityComponents(updatedStore, 'currentStep', { value: 'STEP_DELIVER_PIZZA' });
updatedStore = setEntityComponents(updatedStore, 'hatch', { exec: deliverPizza });
updatedStore = setEntityComponents(updatedStore, 'pizza', {
look: null,
exec: null,
});
updatedStore = setEntityComponents(updatedStore, 'meiko', { look: 'pizza' });
return updatedStore;
}
function deliverPizza(store, position) {
let updatedStore = store;
// et c'est la fin ! Remarquez qu'on vire le dernier "exec" sans en
// rajouter un autre.
updatedStore = setEntityComponents(updatedStore, 'currentStep', { value: 'STEP_FINISH' });
updatedStore = setEntityComponents(updatedStore, 'hatch', { exec: null });
updatedStore = setEntityComponents(updatedStore, 'pizza', {
position,
look: 'profile',
});
updatedStore = setEntityComponents(updatedStore, 'meiko', { look: 'normal' });
return updatedStore;
}
```
Les actions se ressemblent vraiment toutes, la difficulté réside dans le fait
de ne pas se tromper dans l’enchaînement des étapes et des choses à faire. Un
avantage à la simplicité de ces fonctions, c’est qu’on peut écrire des tests
pour vérifier leur comportement extrêmement facilement.
---
Voici donc pour le dernier article « officiel » de cette série ! Celui-ci était
peut-être un peu plus dense que les autres, bien qu’au final pas excessivement
compliqué. Pour rappel, vous pouvez [retrouver la version de cet article
ici](une-histoire-de-pizzas/etape7.html).
J’ai été surpris d’arriver à me replonger dans le code aussi facilement après
plus d’un an de pause dans l’écriture de la série, ce qui me conforte dans la
qualité de ce que j’ai développé (malgré les facilités et « hacks » utilisés
par endroits). **J’aurais encore bien des choses à raconter :** la mise en
cache des images, les mécanismes asynchrones (dont la gestion du chronomètre),
les écrans de début et fin de jeu, etc. Certaines de ces choses m’intéressent à
écrire, d’autres moins. **Dans tous les cas je ne veux pas m’engager à les
écrire, donc si je le fais ce sera à travers des articles « bonus ».**
J’ai pris beaucoup de plaisir à expliquer les choses de manière didactiques, en
découpant les étapes autant que possible et en commentant un maximum de code.
Je ne sais pas si beaucoup de monde m’aura lu en intégralité, mais je serais
ravi si ça a pu être utile (ou même simplement intéresser quelqu’un).
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment