Blog
Action !
Voilà ce qui sera sans doute le dernier making-of de l'année avant la sortie de la démo jouable du jeu vidéo Superflu prévue pour la fin de l'année.
On avait déjà parlé, dans l'ordre :
- algorithmes (localisation et déplacements)
- graphismes (du caractère aux personnages)
- musique (les thèmes musicaux)
On va aborder le dernier gros morceau nécessaire au fonctionnement du jeu : l'écriture et le stockage des « niveaux » (qu'on devrait plutôt appeler des « pièces » vu que le jeu est séparé en différentes pièces reliées par des portes et des couloirs).
Séparation du moteur et du contenu
Un petit point technique d'abord : les niveaux de sont pas « hard-codés », c'est-à-dire que dans le code source du jeu (langage C++), vous ne trouverez pas de description de pièce, de dialogues, d'énigmes, de chemin vers des images, etc. Les niveaux sont stockés dans des fichiers textes qui sont lus et interprétés par le programme à l'exécution. En pratique, ça veut dire que je n'ai pas besoin de recompiler le jeu et de fournir un logiciel (le fichier EXE sur Windows, APK sur Android, etc.) différent si j'ajoute des niveaux.
Un choix technique classique est de coder les niveaux dans un langage de script d'extension comme le langage Lua. Ça a été ma première tentative, avant de me rendre compte que c'était une solution inutilement complexe pour mes besoins : toutes les actions possibles seront fixées par mon moteur, j'ai surtout besoin d'un langage de description et non d'un langage de script. En gros, je me suis tourné vers un choix technique avec moins de flexibilité mais plus de facilité à mettre en place.
Les niveaux vont consister en une liste d'éléments, avec un certains nombres de caractéristiques. Un élément « objet » décrira par exemple l'image à charger, ses coordonnées XY à l'écran, ainsi qu'une coordonnée Z pour la profondeur, ses différents états (par exemple, une porte peut avoir un état fermé et un état ouvert, que la personne qui joue va pouvoir modifier), etc. Ensuite, il va être possible d'interagir avec la plupart de ces objets et de déclencher une suite d'actions selon le verbe (ouvrir, déplacer, prendre, etc.) choisi.
On aura aussi un élément pour chaque son, chaque musique, chaque animation, chaque personnage, pour les dialogues, pour les changement de niveau, etc. La liste est longue, mais une fois le principe général implémenté, c'est facile d'étendre à d'autres choses.
Quel format ?
Plusieurs options de format d'échange de données se sont rapidement dégagées :
J'ai rapidement évacué l'option « format maison » : bien que ce soit tentant (car par définition, j'y fais ce que je veux), ça implique la création d'un parseur (l'algorithme qui va lire les différents éléments du fichier sans se planter), ce qui est loin d'être trivial si on veut faire un truc un tant soit peu robuste. Autant dire s'ajouter une charge de code pour un truc pas franchement fun à coder.
J'ai commencé par utiliser XML, mais j'ai rapidement trouvé ça trop verbeux, encombré de caractères spéciaux, et sans doute beaucoup plus compliqué que nécessaire pour mon usage : je compte écrire les niveaux « à la main » et le XML a une syntaxe assez pénible pour ça.
J'ai donc regardé du côté de JSON et YAML et j'ai rapidement adopté YAML car ça restait le plus agréable à taper à la main (moins encombré) et à lire : – pour caricaturer, j'avais l'impression que JSON, c'était YAML mais pollué par des accolades partout – presque tout est représenté par des strings (chaînes de caractères) en JSON, alors que YAML accepte différents types (et laisse le choix d'encadrer ses strings par des guillemets ou non, ce qui peut être pratique pour distinguer différents types de strings visuellement). Ça ne change pas grand chose d'un point de vue parseur, mais ça permet d'avoir une coloration syntaxique bien plus lisible en YAML.
Comparaison visuelle d'un morceau de mon niveau dans les trois langages (XML, JSON, et YAML qui a été mon choix)
En termes de code, je me repose sur la libyaml (bibliothèque C) pour parser les fichiers, à laquelle j'ai ajouté ma petite couche perso pour faire le lien avec mon propre code. Il y avait bien une bibliothèque C++ plus haut niveau, mais elle me semblait moins universellement présente et supporté que la libyaml (qui est là depuis un bon moment et présente à peu près partout). Sachant que je comptais, dès le départ, supporter un certain nombre de systèmes d'exploitations assez hétérogènes, j'ai préféré partir sur la bibliothèque la plus commune.
Un exemple
Voici un exemple concret (qui fait d'ailleurs partie du vrai premier niveau du jeu), où on a une bouteille que l'on peut prendre et utiliser pour arroser une plante. Je sais, c'est un peu un spoiler, mais plutôt inoffensif puisque arroser la plante n'a pas d'utilité dans la résolution des énigmes (et la prise de la bouteille était spoilé dès la bande-annonce).
On a donc deux objets, un objet plante, dont le nom affiché à l'écran
est « plante verte », avec ses coordonnées. La propriété «view
»
représente l'endroit où doit se positionner le personnage s'il doit
interagir avec l'objet. « box_collision
» est un détail technique
qui permet de choisir si pour cliquer sur l'objet, le curseur doit
être pile dessus ou simplement sur une boîte englobante (ça permet de
rendre la sélection de petits objets moins fastidieuse, comme c'est le
cas pour la bouteille). Ensuite, on a les différents états avec le
« skin » (l'image PNG à charger) associé. Et enfin, les actions qu'il
est possible d'effectuer dessus. Si une action n'existe pas (par
exemple, si j'essaie de faire « parler à » sur la bouteille), alors
une action par défaut sera déclenchée (typiquement, un commentaire du
genre « non » ou « je ne peux pas faire ça »).
Ainsi, si j'essaie de regarder la plante, Sophie va se tourner vers la
plante (« look []
») et faire le commentaire « C'est un
yucca… ». Un cas plus compliqué va être d'utiliser la bouteille sur
la plante : Sophie va d'abord se rendre au bon endroit (le «view
»
de la plante) avec la fonction « goto []
». Ensuite, on a des
instructions un peu spéciales : « system [wait]
» précise qu'il faut
attendre que toutes les instructions précédentes soient effectuées
avant de passer à la suite (en gros, on attend que Sophie ait atteint
la plante pour faire le reste). Ensuite, « system [lock]
» et
« system [unlock]
» permettent de verrouiller et déverrouiller
l'interface : entre ces deux instructions, le joueur ou la joueuse n'a
plus la main sur le jeu pour être sûr que l'action ne soit pas
interrompue (oui, ça veut dire que tant que Sophie n'a pas atteint la
plante, on peut cliquer ailleurs pour annuler l'action et faire autre
chose). Enfin, l'instruction « play []
» permet de choisir une
animation, en l'occurrence l'animation « action » du personnage (qui
consiste juste à tendre la main) pendant 1 seconde.
Il y a encore un certain nombre d'instructions possibles, pour jouer
des sons, changer l'état d'un objet (le « set [state]
» plus haut),
changer des coordonnées, etc. Ensuite, il faut implémenter les
mécanismes correspondant côté moteur et les appeler au bon moment. En
combinant tout ça, on peut facilement créer un environnement dynamique
avec lequel le personnage peut interagir.
Alors bien sûr, lorsque les niveaux ont beaucoup d'objets et d'interactions possibles, ça fait des gros fichiers YAML : à titre d'exemple, les 2 niveaux qui seront fournis avec la démo jouable de la fin de l'année comportent respectivement 654 et 1172 lignes.
À terme, il n'est pas impossible que je planche sur un éditeur de niveau pour éviter de devoir les taper entièrement à la main. Ça faciliterait notamment les choses pour le placement des objets : pour l'instant, je les place dans Inkscape et je recopie les coordonnées à la main.
Et voilà…
C'était le dernier making-of de l'année, et probablement le dernier de ce type. Après la sortie de la démo jouable, je ferai peut-être d'autres articles, mais plus techniques : par exemple, comment gérer les « assets » (les données du jeu, images, fichiers YAML, etc.) et leur distribution, comment gérer proprement la compilation sur des plateformes hétérogènes, comment porter sur Android, sur Emscripten, cross-compiler de Gnunux vers Windows, etc. (spoiler : oui, j'ai fait tout ça). Bref, tous ces trucs annexes mais importants, compliqués, et qu'on ne vous explique pas dans les cours de programmation. À bientôt pour la démo jouable !