Commit f507a0f2 authored by berumuron's avatar berumuron
Browse files

Ajoute l'article et jeu "une histoire de pizzas etape 2"

parent c6a2cbfe
---
title: Une histoire de pizzas : les objets
date: 2019-10-28 17:30
---
J’ai commencé la semaine dernière une série d’articles pour expliquer comment
j’ai développé un jeu pour [les Ergogames](https://ergogames.com). 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](une-histoire-de-pizzas-1.html) 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](https://en.wikipedia.org/wiki/Entity%E2%80%93component%E2%80%93system) ».
J’avais découvert ce paradigme dans [une série d’articles sur LinuxFR](https://linuxfr.org/news/je-cree-mon-jeu-video-e01-les-systemes-a-entites)
et j’avais même commencé à développé une bibliothèque Python, [Pytity](https://github.com/marienfressinaud/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 :
```javascript
// 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
<abbr>HTML</abbr> et de <abbr>JS</abbr> (pour positionner les entités au bon
endroit) :
```html
<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 <abbr>HTML</abbr> 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 <abbr>HTML</abbr>. 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 <abbr>CSS</abbr> pour rendre nos entités
correctement.
```css
/* ... */
/*
Ç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 <abbr>CSS</abbr> 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](une-histoire-de-pizzas/etape2.html)
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.
---
title: Une histoire de pizzas, étape 2
template: game
---
<a href="../une-histoire-de-pizzas-2.html">Lire l’article associé</a>
<div id="app">
<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>
</div>
<script>
const BOARD_SIZE = 6;
// Cette valeur correspond à la variable --cell-size en CSS.
const CSS_CELL_SIZE = 128;
// 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,
},
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 (et on n'oublie
// pas de préciser l'unité sinon ça marche pas !!)
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>
<style type="text/css">
#app {
--cell-size: 128px;
--color-floor: #fff;
--color-floor-border: #ccc;
}
/*
Ç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;
}
.board-line {
display: flex;
}
.board-cell {
width: var(--cell-size);
height: var(--cell-size);
background-color: var(--color-floor);
border: 1px solid var(--color-floor-border);
}
/*
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;
}
</style>
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