Je crée mon jeu vidéo E01 : les systèmes à entités

Posté par . Édité par 4 contributeurs. Modéré par Ontologia. Licence CC by-sa
100
16
sept.
2013
Jeu

«Je crée mon jeu vidéo» est une série d'articles sur la création d'un jeu vidéo, depuis la feuille blanche jusqu'au résultat final. On y parlera de tout : de la technique, du contenu, de la joie de voir bouger des sprites, de la lassitude du développement solitaire, etc. Vous pourrez suivre cette série grâce au tag gamedev.

Cet article est le premier de la série. Avant de vous dévoiler l'idée de jeu que j'ai (et qui n'est pas révolutionnaire, rassurez-vous) dans un prochain article, on va commencer par un peu de technique et parler des systèmes à entités. C'est un nouveau paradigme de programmation assez intéressant, en particulier dans le cadre des jeux vidéos où il est beaucoup utilisé depuis quelques années, en particulier dans le moteur de jeu (propriétaire) Unity.

Sommaire

Introduction

Le problème de la programmation orientée objet

Les systèmes à entités (entity systems) sont nés des limites du paradigme orienté objet appliqué aux jeux vidéos. Tout programmeur qui a pratiqué l'orienté objet suffisamment a déjà rencontré ce cas où il ne sait pas où placer une classe dans l'arbre d'héritage parce qu'elle irait bien à deux endroits différents. Petit exemple :

            /---+ Volant +---+ Avion
|            |
Vehicule ---+            +---+ Soucoupe
|
\---+ Roulant +---+ Voiture
|
+---+ Moto

Voici une belle hiérarchie comme on en rencontre souvent. Mais dans cette hiérarchie, où placer la classe VoitureVolante ? C'est une voiture, donc elle devrait être une fille de la classe Voiture. Mais elle vole donc elle devrait être une fille de Volant. Certains langages permettent l'héritage multiple, mais comme tout le monde le sait, ça pose des problèmes. Que faire ?

Les non-solutions objets

Surtout que ce genre de cas arrive assez fréquemment dans les jeux vidéos où il y a une foultitude d'objets avec une chiée de propriétés.

Du coup, si on reste avec de l'orienté objet, on se retrouve avec deux solutions qui n'en sont pas vraiment :

  1. Remonter les propriétés dans l'arbre des classes. Mais du coup, on se retrouve avec des classes de base énormes qui ont des tonnes de propriétés inutiles pour de nombreuses sous-classes. Dans notre exemple, on pourrait très bien mettre une propriété nombreDeRoues dans la classe Vehicule et mettre cette propriété à 0 pour les objets volants non-roulants. Notre voiture volante serait alors une fille de Volant mais avec un nombre de roues strictement positif. Et on sent bien que c'est crade.
  2. Dupliquer le code entre les classes. L'inconvénient classique est qu'il faut maintenir deux copies du code et que c'est à coup sûr un échec programmé. Notre voiture volante sera alors une fille de Voiture et on dupliquera le code d'une méthode vole() tiré de la classe Volant. Bref, saimal.

C'est là qu'interviennent les systèmes à entités.

La programmation orientée donnée

Avant de rentrer dans le vif du sujet, disons tout de suite ce que les systèmes à entités ne sont pas. Ce n'est pas une évolution de la programmation orientée objet, c'est même une approche complètement orthogonale. Ce n'est pas de la programmation orientée composants, même si par certains côtés, ça se ressemble, de loin, dans le brouillard, la nuit.

Les systèmes à entités sont ce qu'on pourrait appeler de la programmation orientée donnée dans le sens où ce qui est au centre, ce sont les données et pas les fonctions, contrairement à l'orienté objet où le code (les méthodes) sont au cœur même du paradigme. La programmation orientée donnée partage quelques concepts avec les bases de données, et on verra qu'un système à entités pourrait être décrit comme une base de données.

Quoi qu'il en soit, si vous découvrez les systèmes à entités, vous devez totalement oublier tout ce qu'on vous a appris et notamment la programmation orientée objet. Dès que vous essaierez de rapprocher les concepts des systèmes à entités de ceux de la programmation orientée objet, vous aurez perdu et vous ne pourrez pas profiter de la puissance des systèmes à entités.

Après, qu'on s'entende bien, les systèmes à entités ne signent pas la mort de la programmation orientée objet. Chaque paradigme a ses avantages et ses inconvénients et il convient de choisir le meilleur pour chaque usage. Il se trouve que dans les jeux vidéos, les systèmes à entités trouvent une application assez naturelle.

Les concepts des systèmes à entités

Il y a dans les systèmes à entités quelques concepts qu'on retrouve dans toutes les descriptions. Ces concepts portent parfois des noms différents mais globalement, ils ont la même fonction.

Entité

Une entité (entity) représente un objet dans le jeu (game object), c'est-à-dire n'importe quel élément d'un jeu. Une entité ne possède pas de données propres, ni de méthodes propres. Une entité est une sorte d'identifiant de l'objet du jeu, rien de plus.

Mais alors, où sont les données ? Elles sont dans les composants.

Composants

Un composant (component) représente un aspect d'un objet ou d'un ensemble d'objet. Par exemple, la couleur est un composant, la position est un composant, la valeur en pièce d'or est un composant, etc. Les données inscrites dans le composant lui permettent de faire fonctionner l'aspect en question. Une entité va donc être caractérisée par un ensemble de composant, pas forcément constant d'ailleurs. Prenez une Voiture, ajoutez lui un composant Ailes et hop, vous avez une VoitureVolante.

Donc, si les composants sont un tas de données, comment met-on tout cela en route ? Dans les systèmes.

Systèmes

Un système (system) contient tout le code pour mettre à jour les composants. Tout le code se trouve dans les systèmes et pas ailleurs (notamment pas dans les composants). Un système va donc avoir besoin d'un ensemble de composants qu'il va lire et/ou écrire. Un système va être exécuté en permanence sur toutes les entités qui possèdent les composants adéquats. Par exemple, un système Rendu va prendre toutes les entités qui possèdent le composant Representation, va lire les informations du composant et rendre l'entité sur l'écran.

Voilà, on pourrait en rester là mais on va aborder un dernier concept, les archétypes.

Archétypes

Un archétype est un type d'entité, c'est-à-dire une liste de composants qu'on va utiliser pour représenter un type d'objet. Techniquement, on pourrait se passer des archétypes mais ils se révèlent bien pratique pour gérer l'initialisation des entités et en particulier des différents composants des entités.

Comment peut-on implémenter ce bouzin ?

Généralités

Vient alors la question de comment on peut implémenter ce machin. Il y a en gros deux manières de faire : la mauvaise et la bonne. Enfin, question de point de vue. Disons que si on ne veut pas trop s'écarter du paradigme, il vaut mieux utiliser la seconde. Dans la vraie vie, on trouve un peu de tout, même de l'orienté objet !.

La mauvaise manière de faire est d'implémenter les entités comme des listes de composants. Ça semble assez naturel, mais ça pose des problèmes. Notamment quand il faut accéder à la mémoire. En effet, un système va mettre à jour tout un tas de composants auquel il va accéder de manière linéaire. Si tous les composants d'un même type sont alloués les uns à côté des autres (genre dans un tableau), ça va aider le cache et donc améliorer les performances. Si les composants sont stockés dans l'entité, on va perdre cet avantage.

La bonne manière de faire, c'est donc d'implémenter les entités comme des entiers. En fait, si on pousse l'orienté donnée jusqu'au bout, on peut imaginer les systèmes à entités comme des bases de données où les entités seraient des identifiants qui serviront de clefs primaires pour les différentes tables. Chaque composant aurait sa table avec les données. Ensuite, on aurait quelques tables pour faire le lien entre les entités et les composants. On pourrait ajouter également quelques tables pour définir des archétypes.

Cette dernière manière de faire a été plus ou moins normalisée dans une API qui s'appelle ES alpha. C'est à mon avis la bonne vision à avoir. Même si après, on va être obligé de s'écarter un peu du dogme pour des raisons pratiques parmi lesquelles le besoin de communiquer entre entités qui est généralement géré à part.

Un des énormes avantages des systèmes à entités, c'est que la sauvegarde d'un jeu devient triviale : comme toutes les données sont dans les composants, il suffit de sauvegarder les composants. L'ensemble des composants forment l'état du jeu à l'instant t. En les sérialisant sur le disque, on crée une photo instantanée du jeu qu'on pourra recharger pour reprendre le jeu exactement dans le même état.

libes, une implémentation d'un système à entité

Je vous propose donc mon implémentation d'un système à entité, qui est ma première contribution pour cette série d'articles (licence ISC). libes, c'est son nom, est une bibliothèque qui essaie de suivre au mieux l'API ES alpha en l'adaptant à C++. Le code n'est pas très long et reste relativement lisible et compréhensible.

J'ai produit un tutoriel pour montrer comment on peut utiliser la bibliothèque sur un exemple tout bête : des balles rebondissantes. Le code complet de ce tutoriel est bien entendu disponible.

Et bien entendu, j'utiliserai cette bibliothèque et les concepts des systèmes à entités pour mon jeu.

D'autres implémentations

Vous pouvez également aller voir d'autres implémentations existantes (souvent plus complexe à mon sens) :

  • Artemis, Arni Arent, 2012, Java
  • EntityX, Alec Thomas, 2012, C++
  • anax, Miguel Martin, 2013, C++

Pour aller plus loin…

Pour écrire cet article, je me suis largement inspiré des articles mis en lien dans cette dépêche et qui sont de très bonnes sources d'information complémentaires.

N'hésitez pas à dire si ce genre d'article vous plaît. Si c'est le cas, j'en ferai d'autres.

Je crée mon jeu vidéo E02 : le jeu et ses challenges

Posté par . Édité par Nils Ratusznik et palm123. Modéré par Ontologia. Licence CC by-sa
38
26
sept.
2013
Jeu

«Je crée mon jeu vidéo» est une série d'articles sur la création d'un jeu vidéo, depuis la feuille blanche jusqu'au résultat final. On y parlera de tout : de la technique, du contenu, de la joie de voir bouger des sprites, de la lassitude du développement solitaire, etc. Vous pourrez suivre cette série grâce au tag gamedev.

Dans l'épisode 01, on a parlé d'un nouveau paradigme utilisé dans les jeux vidéo et appelé système à entités. Ce deuxième opus sera consacré à la description du jeu que j'aimerais faire, et aux divers challenges associés. On ne parlera pas encore des technologies et bibliothèques diverses choisies pour implémenter tout ça (il faut bien garder quelques sujets sous le coude pour les prochains épisodes).

D'ailleurs, depuis le premier épisode, on a vu plein de projets liés au jeux vidéos sur LinuxFr :

Sommaire

Le concept du jeu

Type du jeu

Rentrons tout de suite dans le vif du sujet : le jeu que je compte faire est un RPG en vue de haut dans un monde ouvert.

Et là, j'en vois déjà plusieurs qui me prennent pour un fou. Ils ont sans doute raison. Il est vrai que je cumule trois difficultés d'un seul coup :

  • les RPG sont sans doute les jeux les plus casse-gueule à faire, en particulier quand on est tout seul, en particulier quand c'est le premier jeu ;
  • la vue de haut (la vraie, celle à la verticale) est une des vues les plus difficiles parce que ce n'est pas très habituel d'avoir ce point de vue ;
  • le monde ouvert implique des dimensions hors norme qui dépassent mes capacités horaires de production de contenu.

Mais ces difficultés ne me font pas peur ! Je vais expliquer les raisons qui me poussent dans cette direction.

Les raisons

Première raison : c'est un genre de jeu qui me plaît. Ancien rôliste, j'ai toujours aimé les RPG (même avec leurs limites), j'adore m'évader dans ces univers à part. Je peux passer des heures juste à explorer des endroits qui me sont encore inconnus sur une carte. Ce sont les jeux auxquels j'ai le plus joué en nombre d'heures : Final Fantasy, Xenoblade Chronicles, The Elder Scrolls (il paraît que je suis un realife) sont mes références du genre. Donc à un moment donné, ça devait arriver, je devais me confronter au genre.

Deuxième raison : je ne suis pas pressé. J'ai le temps, j'aime bien prendre le temps de faire les choses, de m'intéresser, d'approfondir (et c'est la raison même de ces articles, partager ce que je vais trouver). Donc me lancer dans une telle aventure, longue et semée d'embûches, ça me va bien. J'ai bien conscience de tout ce qui m'attend, j'ai bien conscience que ce ne sera pas toujours facile. On s'en fout, on avance !

Troisième raison : finalement, la conception du jeu n'est qu'un prétexte pour apprendre et s'amuser. Si j'arrive à un résultat à la fin (et c'est quand même le but), c'est parfait. Mais le chemin pour y parvenir compte autant que le point d'arrivée. Partager cette expérience est aussi un moyen de «rentabiliser» l'affaire : si je ne parviens pas au bout, il restera tout de même cette série d'articles qui aura permis à d'autres de se lancer.

Thème du jeu

Ceci étant posé, il est temps de parler du thème du jeu. Mon idée est de changer un peu des habitudes et de ne pas faire jouer un héros propre sur lui mais de faire jouer un méchant, une sorcière plus précisément, le tout avec une grosse dose d'humour (noir forcément) et d'auto-dérision. Le pitch, c'est une sorcière qui sort de sa geôle après tellement d'années que tout le monde l'a oubliée, elle a perdu tous ses pouvoirs et part dans une quête pour les retrouver. L'idée, c'est donc de détourner les codes du genre pour jouer les méchants. Par exemple, il n'y aura pas de mission pour sauver un chaton, mais plutôt pour empoisonner un chaton. Je vais rester dans un univers médiéval fantastique avec son bestiaire, ses items et ses personnages classiques, mais détournés un peu pour que ce soit drôle.

Les challenges

Les challenges que je vais lister ne sont sans doute pas exhaustifs (je compte sur vos commentaires pour compléter au cas où) mais ce sont les sujets que j'ai déjà identifié et pour lesquels j'ai un intérêt certain (et qui feront sans doute l'objet d'un ou plusieurs articles chacun).

Dimension

Premier challenge de taille (huhu), les dimensions du jeu. Dans un monde ouvert, on part forcément sur une carte énorme. Sans aller jusqu'à créer une des plus grandes cartes du jeu vidéo, il va falloir un territoire assez vaste. Si on prend Skyrim comme référence, l'univers fait entre 21km² et 37km² (la version officielle dit 41km², plus précisément 16 square miles). C'est grand, très grand. Ça va poser des problèmes techniques à n'en pas douter, juste par ces dimensions. Si je devais me fixer un ordre de grandeur dès maintenant, je dirais que je serais content avec une carte entre 10 et 20km².

Parce que, qui dit grande carte, dit nombreuses entités de toutes sortes sur la carte : végétation, animaux, habitations, routes, etc. Et donc autant de sprites à créer. Oui, parce que la difficulté avec le jeu vu de haut, c'est qu'il n'y a pas beaucoup de sprites prêts à l'emploi, contrairement à beaucoup d'autres types de vues.

Une solution dans ce genre de cas, c'est la génération procédurale, c'est-à-dire qu'on génère du contenu à l'aide d'une procédure et de l'aléa. C'était autrefois utilisé à cause du manque de mémoire des ordinateurs, c'est encore utilisé pour générer tout un tas d'objet, l'exemple le plus célèbre étant SpeedTree pour générer des arbres. Bref, ça ne s'applique pas à tout mais ça peut aider dans certains cas.

Ombre et lumière

La vue de haut a un énorme inconvénient, elle ne permet pas de bien distinguer les objets. Ajouter des ombres sur les objets peut aider à se rendre compte de leur taille ou de leur forme. C'est donc un deuxième challenge, jouer sur les ombres et les lumières pour, d'une part, mieux rendre l'univers de jeu, d'autre part, créer une vraie ambiance pour le jeu et contribuer à l'immersion.

Dans les commentaires du premier épisode, devnewton a donné quelques liens sur la lumière dynamique dans les jeux 2D. Ils offrent des pistes de réflexion très intéressantes, même si je doute arriver à ce niveau de détail et de rendu. Déjà, si je pouvais avoir des cycles jour/nuit qui soient bien rendus, je serais très heureux. C'est une fonctionnalité que je considère comme une obligation pour l'aspect «réaliste» du jeu. Sans compter qu'un cycle jour/nuit offre tout un tas de possibilités amusantes au-delà du rendu.

Et la technique alors ?

Oui, il va y avoir plein de problèmes techniques. Mais ce n'est que de la technique ! Le principal dans un jeu, c'est le contenu. La technique n'est là que pour soutenir le contenu. Il y aura des articles sur la technique (le premier de la série est un bon exemple), mais souvent, la technique ne sera là que pour régler un problème de contenu.

Conclusion

Voilà, ce n'est que le début de l'aventure et les idées fusent (c'est bien normal). Certaines disparaîtront sans doute en cours de route, d'autres apparaîtront.

Je crée mon jeu vidéo E03 : la version zéro !

Posté par . Édité par 5 contributeurs. Modéré par patrick_g. Licence CC by-sa
49
14
oct.
2013
Jeu

«Je crée mon jeu vidéo» est une série d'articles sur la création d'un jeu vidéo, depuis la feuille blanche jusqu'au résultat final. On y parlera de tout : de la technique, du contenu, de la joie de voir bouger des sprites, de la lassitude du développement solitaire, etc. Vous pourrez suivre cette série grâce au tag gamedev.

Dans l'épisode 02, on a vu le principe du jeu et ses principaux challenges. Beaucoup de commentaires fort pertinents sont venus agrémenter cette première description. Je vais en reprendre une partie avec quelques liens fort utiles à lire quand on commence un jeu (et que c'est le premier).

Sommaire

Premier jeu

Quand on commence un premier jeu, il y a quelques conseils à retenir. J'ai lu beaucoup de choses avant de me lancer et ça a été très instructif, j'ai retrouvé ces conseils dans les commentaires du dernier épisode et il me paraît intéressant d'en faire un résumé. Je vous conseille donc l'excellent site GamedevTuts+ qui contient beaucoup de choses intéressantes, dont deux articles indispensables.

Making Your First Game: A Walkthrough for Game Developers

Dans l'article Making Your First Game: A Walkthrough for Game Developers, Steven Lambert explique quelles sont les quatre étapes nécessaires pour créer un jeu.

Premièrement, il faut planifier.

Il faut écrire tout ce qui passe par la tête et se poser des questions et y répondre. Et il faut évaluer la quantité de travail global nécessaire au développement complet du jeu avant de commencer à coder. L'auteur cite d'ailleurs une phrase assez marrante mais tellement réaliste (et qui pourrait s'appliquer à d'autres projets) : «Souvenez-vous, les premiers 90% de votre jeu prennent 90% de votre temps ; les derniers 10% prennent les 90% restant de votre temps. Planifier en conséquence.»

Deuxièmement, il faut prototyper.

Il est nécessaire de savoir si les mécaniques du jeu vont fonctionner avant de se lancer définitivement dans le code. Et donc, il faut avoir des bouts de code (crades) pour tester des fonctionnalités vite fait et se rendre compte si elles seront utiles et amusantes ou pas. Cette phase est importante car une fois terminée, tout ce qui sera dans le jeu sera déterminé, avec un bout de code qui marche à peu près à intégrer.

Troisièmement, il faut développer.

Et c'est long, mais si on a bien réussi les deux premières étapes, ça va. Il faut passer les coups de mou mais pour ça, il y a des techniques dont on va parler après. Et puis il ne faut pas hésiter à ré-utiliser tout ce qui existe déjà : bibliothèques, sprites, sons, musiques ! Et il ne faut pas hésiter à tailler dans le lard : «Avant de commencer à coder, enlevez 90% des fonctionnalités attendues»

Quatrièmement, il faut sortir le jeu.

À un moment, il faut montrer ce qu'on a fait et affronter la critique. Le jeu ne sera pas parfait, il faut l'admettre, mais il sera fini.

How to Succeed at Making One Game a Month

Dans l'article How to Succeed at Making One Game a Month, Christer Kaitila donne quelques astuces pour réussir à finir un jeu, après avoir réalisé douze jeux en douze mois en 2012.

  1. Le brainstorm. C'est la phase pendant laquelle on couche sur papier toutes les idées saugrenues qu'on a. Il s'agit de faire dans la quantité, pas dans la qualité. Et ensuite, il faut en enlever 99%.
  2. Faire deux listes : ce dont on a besoin et ce qu'on veut. Il s'agit de réduire la liste pour parvenir au produit viable minimum, celui duquel on ne peut plus rien enlever. Le reste doit disparaître pour le moment.
  3. Écrire le pitch. Il faut pouvoir décrire le jeu en deux phrases, comme si c'était la description derrière la boîte dans le magasin.
  4. Dessiner un brouillon du jeu en action. Une douzaine de dessins grossiers doivent permettre de voir à quoi le jeu ressemblera à la fin. Ça permet de savoir où placer quoi une fois le code venu.
  5. Faire une première version jouable sans art. C'est le premier point de sauvegarde. Faire une version sans art permet de se concentrer sur l'essence du jeu.
  6. Commencer à rendre le produit viable minimum joli. À partir de ce moment, on peut améliorer le jeu petit à petit, tout en faisant attention aux performances.
  7. Première version. C'est le deuxième point de sauvegarde, il n'y a qu'un seul niveau, mais c'est à peu près jouable et testable.
  8. Travailler sur une fonctionnalité à la fois. Il ne faut pas commencer 36 choses à la fois, il faut prendre les tâches les unes après les autres pour toujours avoir une version qui marche.
  9. Continuer à atteindre des points de sauvegarde. En travaillant de manière incrémentale, on a en permanence une version jouable qu'on peut éventuellement abandonner telle quelle.
  10. Atteindre la ligne d'arrivée. À un moment, il faut décider que le jeu est terminé. Le reste des fonctionnalités de départ sera dans une version 2.0 !

Conclusion

Finir un jeu, c'est une qualité ! Et même s'il y a des techniques, ça reste difficile. Là, j'en suis plutôt au début, donc ça ne pose pas encore de problèmes. Mais je me dis qu'il faudra que je relise tout ça quand je serai en panne.

Voici quelques autres sites dignes d'intérêt (pour des débutants ou des confirmés) :

Version 0 !

La version 0 du jeu est donc sortie. Pour l'instant, on peut bouger un sprite représentant un personnage sur un fond de carte, rien de plus. C'est donc bien une version 0.

Ha si, le jeu a désormais un nom : « Akagoria, la revanche de Kalista ». Et un dépôt sur gitorious ! Et voici une toute première capture d'écran.

Akagoria version 0.0

Maintenant, place au making-of the cette version 0.

La définition du jeu

Comme conseillé, notamment par Julien Jorge, j'ai écrit une sorte de guide du jeu qui contient le scénario assez détaillé, des indications sur l'univers du jeu, quelques quêtes annexes, la description de l'héroïne, les premiers personnages secondaires, les systèmes d'évolution et de progression, la liste des sorts, des objets et des créatures. Le tout en français pour me faciliter le travail.

C'est un travail en cours, mais je pense qu'il est essentiel car il permet de se fixer des objectifs, de se rendre compte du nombre de choses à développer, et donc de pouvoir planifier. Du coup, j'ai commencé à écrire une petite roadmap avec cette liste. Il n'y a pas encore de dates, j'attends d'avoir fini d'écrire à peu près la description complète pour fixer les itérations successives, en essayant de mêler à chaque fois du code et du graphisme.

Le sprite militaire

Puisqu'on en parle, il faut parler de ce sprite. Le sprite militaire n'est évidemment pas le sprite de l'héroïne, vous l'aurez remarqué. C'est mon premier essai pour fabriquer moi-même mes sprites. Et pour ceux qui se poseraient des questions, je ne suis pas doué, j'ai juste suivi pas à pas un excellent tutoriel sur la création d'un sprite en vue de haut avec le logiciel Inkscape. D'ailleurs, mon militaire est beaucoup moins joli que dans le tutoriel mais c'est bien suffisant pour l'instant.

Du coup, j'en ai profité pour essayer de créer un sprite plus simple, un puits vu de haut (qu'on distingue sur la capture d'écran). Et je dois dire que je suis plutôt satisfait du résultat. Je ne maîtrise pas Inkscape, mais je me dis qu'à l'usage, je vais m'améliorer. Et puis, vu la quantité de sprites à créer, j'ai plutôt intérêt à m'améliorer.

J'en profite pour refaire de la pub pour le blog 2D Game Art For Programmers qui est une mine d'or. Il ne faut pas hésiter à remonter assez loin pour voir quelques techniques de base.

La carte

Pour réaliser la carte, j'ai utilisé l'excellent Tiled qui permet de créer des cartes à base de tuiles. J'ai utilisé un tileset de base que j'ai bricolé vite fait. Cette petite île sera mon terrain expérimental pour tester les fonctionnalités dans les premières versions du jeu.

Pour lire le format TMX (qui est le dialecte XML utilisé par Tiled), j'ai concocté une petite bibliothèque de lecture de fichier TMX. Il en existait plusieurs mais aucune qui ne me plaisait et aucune maintenue. Pour tester ma bibliothèque, j'ai écrit un petit programme de rendu de carte avec SFML et j'ai utilisé les cartes de Newton Adventure qui utilise une version Java de Tiled, légèrement différente. En tout cas, c'est un peu lent mais ça fonctionne ! Une des limites est que le rendu se fait dans une texture et donc, ne peut pas dépasser 8192x8192 pixels (soit 256x256 tuiles de 32x32). Je chercherai plus tard une autre bibliothèque de rendu pour enlever cette limite.

Comme j'aimerais avoir un très grand terrain de jeu, la carte finale sera certainement générée procéduralement. Du moins le fond de carte. Ensuite, j'y placerai les lieux et les items dont j'ai besoin. J'ai actuellement deux étudiants qui travaillent sur cet aspect de génération procédurale de carte, je pense qu'ils sont capables de produire un bon résultat.

Le système à entités

Évidemment, j'ai utilisé le système à entités présenté lors de l'épisode 01. En plus, j'ai écrit une petite gem Ruby pour générer automatiquement tout un tas de fichiers à partir de simples descriptions en YAML. Ce n'est pas encore parfait, il manque une validation des fichiers (genre ne pas référencer un composant qui n'existe pas) mais ça fait le boulot de base.

J'ai inclus les fichiers générés dans le dépôt pour éviter d'avoir Ruby comme prérequis pour contruire le jeu. En revanche, pour un développeur, c'est obligatoire. Ces fichiers sont clairement marqués comme générés.

Pour ceux qui veulent tester

Pour ceux qui parviennent à compiler et qui vont tester le jeu en l'état, voici quelques conseils. Il faut utiliser les flèches directionnelles pour bouger. Par défaut, la vue est fixe, c'est-à-dire que le personnage pivote et le cadre reste avec la même orientation. En appuyant sur la touche V, on change de vue et on passe en vue mobile, le personnage est fixe et tout pivote autour de lui. Je ne sais pas quelle vue est la meilleure, j'aime bien les deux.

Si vous voulez zoomer et dézoomer, vous pouvez utiliser PageUp et PageDown. Cette fonctionnalité sera supprimée dans la version finale, mais elle permet actuellement de voir la carte en entier.

La touche Echap permet d'arrêter le jeu.

Moyen de communication

Comme certains l'ont demandé au dernier épisode, il existe désormais un canal IRC #akagoria sur le réseau freenode.

La prochaine fois…

Pour le prochain épisode, on parlera de collisions et normalement, le héros ne rentrera plus dans le puits. J'essaierai aussi d'ajouter quelques sprites de différentes formes et différentes tailles pour tester les collisions.

Si vous voulez que j'approfondisse un des aspects évoqués dans cet épisode, n'hésitez pas à le demander dans les commentaires.

Je crée mon jeu vidéo E04 : Paf ! les collisions

Posté par . Édité par palm123 et Benoît Sibaud. Modéré par Xavier Claude. Licence CC by-sa
Tags :
46
29
oct.
2013
Jeu

«Je crée mon jeu vidéo» est une série d'articles sur la création d'un jeu vidéo, depuis la feuille blanche jusqu'au résultat final. On y parlera de tout : de la technique, du contenu, de la joie de voir bouger des sprites, de la lassitude du développement solitaire, etc. Vous pourrez suivre cette série grâce au tag gamedev.

Dans l'épisode 03, on a vu la version zéro du jeu : un sprite qui bouge sur une carte. Les commentaires se sont focalisés sur la carte (la manière de l'afficher notamment) et sur le scénario/guide/design du jeu. Certains lecteurs ont même tenté la compilation (et y sont parvenus pour la plupart, malgré les difficultés). Aujourd'hui, on va parler de moteur physique, et plus particulièrement de Box2D, qui devient de plus en plus une référence en la matière dans les jeux 2D.

Sommaire

Un moteur physique est une bibliothèque chargée de gérer les interactions physiques entre les entités du jeu. Techniquement, ça consiste à émuler des forces qui s'appliquent à des solides. Par conséquent, un moteur physique ne s'occupe absolument pas de la manière dont s'affichent les entités. On peut donc choisir son moteur physique presque indépendamment de son moteur graphique.

Le moteur physique Box2D

Box2D est un moteur physique. C'est une référence en 2D, le moteur est utilisé dans plusieurs jeux, dont le très célèbre Angry Birds. La dernière version (2.2.1) est un peu vieille (septembre 2011) mais elle est assez complète. Voici donc un petit aperçu de comment marche Box2D. Ce n'est pas vraiment un tutoriel, c'est plutôt un point d'entrée pour aller voir plus loin.

Bouge ton corps !

Un corps (body) est un élément solide, c'est-à-dire qu'il ne se déforme pas. Il est constitué de une ou plusieurs formes (shape), qui sont ajoutées au corps via des fixtures auxquelles on associe des propriétés matérielles (densité, friction, restitution). Plusieurs corps peuvent être unis par des liaisons mécaniques (joints) qui vont les priver de degré de liberté. Box2D permet de représenter toutes ces notions, voici quelques détails.

Les formes peuvent être très variées, depuis le cercle jusqu'à n'importe quel polygone convexe. On peut aussi créer des chaînes (chain) qui sont des suites de segments, ouvertes ou fermées, qui seront infranchissables par les autres solides (et on imagine assez bien l'utilisation qu'on va pouvoir en faire dans un jeu).

Comme dit précédemment, les fixtures contiennent des propriétés matérielles. La densité permet de calculer la masse du corps. La friction (ou frottement) permet de faire glisser deux corps l'un contre l'autre (pas d'allusion sexuelle dans cette phrase). La restitution permet de gérer les rebonds entre deux corps. Une fixture peut également devenir un capteur (sensor), c'est-à-dire qu'il n'y aura pas d'interaction avec les autres corps mais qu'on pourra tester si un corps est en contact ou pas.

Les corps peuvent être de trois sortes. Les corps statiques ne bougent pas, ils servent pour représenter des éléments fixes du décor et ne sont soumis à aucune force. Les corps dynamiques bougent, sont soumis à des forces et peuvent entrer en collision avec n'importe quel autre corps. Ils servent pour représenter les entités tels que les personnages. Une troisième catégorie de corps existe pour les corps qui ne doivent être soumis à aucune force mais qui doivent pouvoir bouger, comme une plateforme mobile par exemple. Ces corps sont des corps «cinématiques» (selon la terminologie de Box2D). Les corps statiques ou cinématiques ne peuvent pas entrer en collision les uns avec les autres.

Un corps a une position dans le monde et un angle. On peut également endormir le corps si jamais on veut qu'il ne participe pas à la simulation (quand on a beaucoup de corps par exemple). On peut indiquer qu'un corps va avoir une grande vitesse (comme un projectile par exemple) et qu'il faudra faire attention lors de la simulation. Dans ces cas là, Box2D utilise une détection de collision continue de manière à éviter les effets tunnels, c'est-à-dire un corps qui passerait par dessus un autre sans le toucher.

Enfin, il existe différents types de liaison mécaniques pour contraindre deux corps : liaison pivot, liaison glissière, etc. Elles permettent de modéliser des mécanismes tels que des poulies ou des tapis roulants ou plein d'autres choses de ce genre.

Que la force soit avec vous !

Tous ces objets sont définis dans un objet monde (world) qui s'occupe de la gestion mémoire et de la simulation proprement dite. Cet objet monde est défini avec un vecteur gravité qu'on définira à 0 si jamais on veut simuler un plan horizontal, comme dans un jeu en vue de haut, et à un vecteur descendant si on simule un plan vertical, comme dans un jeu en vue de côté. Ou à un vecteur bizarre dans un jeu où on s'amuse avec la gravité.

Hormis la gravité, il est également possible d'appliquer des forces, soit linéaires, soit angulaires. Ou alors une quantité de mouvement (impulse), soit linéaire, soit angulaire. Le corps se met alors en mouvement. C'est de cette manière qu'on peut faire bouger le héros ou les différents éléments mobiles d'un jeu.

Pour aller plus loin

Sur se former complètement sur Box2D, il y a d'abord la documentation officielle :

  • la FAQ, à lire avant de commencer ;
  • le manuel en PDF, qui décrit les concepts de manière simple et concise.

Une fois le manuel lu, vous aurez envie d'aller plus loin et là, il existe également de la documentation très intéressante :

  • Box2D tutorials : une mine d'or, il y a les rappels de base, mais aussi des cas concrets qui sont décrits et implémentés dans l'outil de test de Box2D ;
  • The nature of code : des tutoriels en vidéos, idéal pour ceux qui ne comprennent strictement rien aux paragraphes précédents.

Et pour ceux qui veulent en savoir plus sur les moteurs physiques :

Comment utiliser Box2D en pratique

Une échelle adaptée

Box2D requiert que les tailles des objets soient de l'ordre du mètre, la bibliothèque ne gère pas bien les très petits objets ou les très gros. Disons qu'entre une gemme et un dragon, ça passe. Il est donc nécessaire d'avoir une bonne échelle pour l'univers et de ne surtout pas utiliser des pixels comme unités. Une phase de conversion sera souvent nécessaire entre les coordonnées du monde Box2D et les coordonnées du monde du jeu ou celles de l'écran.

Intégrer Box2D dans un système à entités

J'ai repris l'exemple de balles rebondissantes que j'avais présenté dans l'épisode 01 pour ajouter la collision entre les balles à l'aide de Box2D. Le résultat est disponible sur le dépôt github de libes. Par rapport au tutoriel, voici les principaux changements liés à Box2D.

Au niveau des composants, les Position et Speed ont été remplacés par un composant Body qui contient simplement un pointeur vers un b2Body de Box2D (tout ce qui commence par b2, c'est de l'API Box2D).

struct Body : public es::Component {
b2Body *body;
static const es::ComponentType type = 1;
};

Ensuite, il faut définir les murs et le sol. Pour cela, on utilise un corps statique (les constantes WORLD_WIDTH et WORLD_HEIGHT représentent la taille de la boîte dans laquelle les balles rebondissent). Ici, on dit que notre boîte fait trois mètres de haut pour 4 de large et on adapte l'échelle à la taille de la fenêtre.

// ground
b2BodyDef groundBodyDef;
groundBodyDef.position.Set(WORLD_WIDTH / 2.0f, -THICKNESS);
b2Body* groundBody = m_world->CreateBody(&groundBodyDef);
b2PolygonShape groundBox;
groundBox.SetAsBox(WORLD_WIDTH / 2.0f + THICKNESS, THICKNESS);
groundBody->CreateFixture(&groundBox, 0.0f);
// right wall
b2PolygonShape rightBox;
b2Vec2 rightPos(WORLD_WIDTH / 2 + THICKNESS, 2 * WORLD_HEIGHT);
rightBox.SetAsBox(THICKNESS, 2 * WORLD_HEIGHT + THICKNESS, rightPos, 0.0f);
groundBody->CreateFixture(&rightBox, 0.0f);
// left wall
b2PolygonShape leftBox;
b2Vec2 leftPos(- WORLD_WIDTH / 2 - THICKNESS, 2 * WORLD_HEIGHT);
leftBox.SetAsBox(THICKNESS, 2 * WORLD_HEIGHT + THICKNESS, leftPos, 0.0f);
groundBody->CreateFixture(&leftBox, 0.0f);

Reste ensuite à définir des balles. Une balle a un corps dynamique de sorte que les balles vont s'entrechoquer et vont rebondir contre les murs et sur le sol.

b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
bodyDef.position = pos;
b2Body* body = world->CreateBody(&bodyDef);
b2CircleShape circle;
circle.m_radius = RADIUS / SCALE;
b2FixtureDef fixtureDef;
fixtureDef.shape = &circle;
fixtureDef.density = 1.0f;
fixtureDef.friction = 0.001f;
fixtureDef.restitution = 0.98f;
body->CreateFixture(&fixtureDef);

Et voilà, il n'y a plus qu'à lancer tout ça. Et les collisions se font automatiquement, il n'y a rien à faire, juste à afficher où les balles se situent. Ici, on ne gère pas l'angle des balles. Comme elles sont rondes, pas besoin de faire de rotation. Mais si on faisait la même chose avec des carrés, il faudrait afficher les carrés avec le bon angle. Il n'empêche que les balles tournent bien sur elles-mêmes et que ça a un impact (jeu de mot toussa) sur les collisions.

J'oubliais presque. Pour simuler notre monde physique, il est nécessaire d'appeler la fonction adéquate, dans la mise à jour du système Physics :

int32 velocityIterations = 10;
int32 positionIterations = 8;
m_world->Step(delta, velocityIterations, positionIterations);

On voit qu'on peut régler le nombre d'itérations. Concrètement, pourquoi a-t-on besoin de plusieurs itérations ? Si on considère un pendule de Newton, on se rend compte que la collision de la première bille va entraîner une force sur la seconde qui va elle-même entraîner une force sur la troisième, et ainsi de suite jusqu'à la dernière bille qui sera propulsée. Avec une seule itération, on ne pourrait gérer que la première interaction et non la suite d'interactions. Évidemment, il y a un compromis à trouver entre le nombre d'itérations et la précision : un grand nombre d'itérations donnera une meilleure précision mais prendra plus de temps et inversement.

Les autres nouvelles d'Akagoria

Git branching model

Sur les trois dépôts existants (libes, libtmx et akagoria), j'ai mis en place le fameux successful git branching model, histoire de m'y retrouver moi-même dans ce que je fais. Il existe désormais une branche develop qui reçoit les nouveaux développements, et une branche master qui sera synchronisée quand les développements fonctionneront à peu près.

En fait, ce modèle est très logique et fonctionne assez naturellement. L'essayer, c'est l'adopter !

Mise à jour libes : systèmes locaux

La libes a reçu un nouveau développement qui permet de différencier des systèmes locaux et des systèmes globaux. Un système global est un système qui agit sur l'ensemble des entités qu'il gère, c'est ce qui était fait avant. Un système local est un système qui gère uniquement une partie des entités, celles qui sont autour du focus. Chaque système local traite donc un ensemble d'entités réparties dans une grille rectangulaire propre à chaque système. Au moment de mettre à jour les entités, le système détermine les cases de la grille qui sont concernées, celles où se trouve le focus ainsi que les 8 cases adjacentes, et les transmet à la fonction de mise à jour.

Je ne sais pas si cette fonctionnalité est suffisamment intéressante pour se trouver ici ou si elle devrait être déportée dans le jeu lui-même. Pour l'instant, elle est ici. On pourrait encore l'optimiser. Pour l'instant, les entités concernées sont calculées à chaque fois, mais si le focus ne change pas, on pourrait renvoyer le dernier calcul fait, avec une mémoïsation.

Mise à jour d'Akagoria : carte partielle

Depuis le dernier épisode, je me suis concentré, entre autres, sur l'affichage de la carte (en mode moins bourrin qu'auparavant). J'ai utilisé pour cela les nouvelles fonctionnalités de libes décrites juste avant. J'ai donc créé un système MapRender qui est chargé du rendu de la carte. Il crée les entités correspondant à chacune des tuiles puis les place suivant une grille. La grille partage la carte en carrés de 50 tuiles de côtés, ce qui fait que sur ma carte actuelle, j'ai 25 cases en tout. Désormais, la carte n'est donc plus affichée entièrement. Quand on dézoome, on le constate, et quand on bouge, on le constate encore mieux, certaines zones disparaissent tandis que d'autres apparaissent.

Pas de capture d'écran pour cette fois, mais la prochaine fois, il y aura des surprises !

Et toujours IRC

Vous pouvez toujours passer sur le canal IRC #akagoria du réseau freenode pour poser des questions, avoir de l'aide pour la compilation du bouzin, ou juste pour dire bonjour. Il y a un compte connecté quasi en permanence qui me permettra de voir l'activité même quand je ne suis pas là.

Je crée mon jeu vidéo E05 : de retour de la Paris Games Week

Posté par . Édité par palm123, Benoît Sibaud et Xavier Claude. Modéré par Christophe Guilloux. Licence CC by-sa
Tags :
37
11
nov.
2013
Jeu

«Je crée mon jeu vidéo» est une série d'articles sur la création d'un jeu vidéo, depuis la feuille blanche jusqu'au résultat final. On y parlera de tout : de la technique, du contenu, de la joie de voir bouger des sprites, de la lassitude du développement solitaire, etc. Vous pourrez suivre cette série grâce au tag gamedev.

Dans l'épisode 04, on a parlé des moteurs physiques et de Box2D en particulier. Depuis, je n'ai pas eu beaucoup de temps à consacrer à mon jeu. Donc, pour cet épisode, on va parler d'un truc qui n'a absolument rien à voir : la Paris Games Week.

Sommaire

Le 1er et le 2 novembre dernier, je suis allé à la Paris Games Week à Paris. Qu'est-ce qu'on en retire quand on est comme moi en train de faire un jeu vidéo sur son temps libre ? À la fois beaucoup et pas grand chose.

Confusion

Un autre monde

Autant le dire tout de suite, les jeux présentés à la Paris Games Week n'ont absolument rien à voir avec ce qu'un amateur dans mon genre peut produire. Mais vraiment rien à voir, pas juste par les moyens mis en œuvre, mais aussi par l'esprit. C'est comme comparer la Ligue 1 de football avec une bande de copains qui fait un match le dimanche. Ce n'est pas le même monde, pas les mêmes codes, pas les mêmes contraintes : ce n'est simplement pas la même chose.

Même si on va un peu plus loin et qu'on regarde les développeurs indie, ils vivent également dans un autre monde que celui des jeux vidéos commerciaux des grands studios. Et je pense qu'un développeur indie se rapproche plus d'un amateur que d'un grand studio par l'esprit.

Les métiers du jeu vidéo

Ce qui m'a le plus frappé, c'est une présentation des métiers du jeu vidéo, organisé par le Syndicat National du Jeu Vidéo. En guise d'introduction, on avait droit à une vidéo présentant les métiers du jeu vidéo, avec Marcus en guise de présentateur. Puis une sorte de témoignage avec des créateurs indépendants de jeu vidéo.

Que dit cette vidéo ? Tout d'abord, Marcus nous replonge dans le passé des premiers jeux vidéo, le temps où les jeux étaient faits par des petites équipes, voire des personnes uniques, et n'étaient pas encore vus comme une industrie. Puis on revient au présent, où les tâches ont été découpées, rationalisées et spécialisées. Le SNJV a d'ailleurs produit un référentiel où sont décrits les différents métiers du jeu vidéo. Vient alors la question de l'embauche et des formations, puisqu'étant relativement nouveau, ce domaine n'est pas encore bien délimité et qu'il existe de nouveaux métiers (game designer, level designer, etc). Enfin, les principales qualités nécessaires pour travailler dans le jeu vidéo sont listées : implication, motivation, passion, créativité, curiosité, travail en équipe, flexibilité.

Que veut faire comprendre cette vidéo ? Peu importe le nombre d'heures, peu importe les bas salaires, puisqu'on s'amuse en faisant un jeu vidéo, c'est fun, et on a la passion. Et ne pas hésiter à emmener son travail chez soi, pour continuer à se former et à faire de la veille ! Et commencer bien avant d'être embauché, hein ! Bref, vous vous en doutez, je n'ai pas beaucoup aimé cette manière de présenter les choses. Comme les majors du disque font appel à l'image d'Épinal du musicien seul dans son grenier en train de mourir parce qu'on télécharge ses œuvres, le SNJV joue sur l'image des pionniers qui étaient certes des passionnés mais qui évoluaient dans un monde totalement différent. Si on leur avait dit qu'on investirait des centaines de millions d'euros dans un jeu dont la moitié en marketing, ils nous auraient sans doute pris pour des tarés. Et quand on sait que l'industrie du jeu vidéo est, en France et dans le monde, la première industrie du divertissement (oui, devant le cinéma et la musique), on se dit qu'il y a de l'argent et que ça paraît bizarre que ceux qui font le travail admettent qu'ils sont mal payés.

On en revient à nos deux mondes. J'estime (et vous pouvez ne pas être d'accord) que les salariés des jeux vidéos croient travailler dans le monde des pionniers et des jeux indie alors qu'en fait, ils sont ailleurs et que le SNJV entretient cette confusion. Je ne leur jette pas la pierre, ils sont sans aucun doute de bonne foi en présentant leur métier, on est à mon sens dans un cas typique d'aliénation. Le final est d'ailleurs éclairant, après avoir montré les petites mains, on voit ceux qui sont derrière le fameux référentiel : le président du SNJV et le président de Capital Games, qui se veut être le représentant des entreprises du jeu vidéo en Île-de-France. Ces deux personnes dénotent complètement par rapport au reste. Et il suffit de se rendre sur la page de Capital Games pour se rendre compte de la supercherie, on ne vous propose pas des formations techniques sur le jeu vidéo, on vous propose de vous expliquer comment bénéficier du crédit d'impôt !

Vient alors le témoignage, dans la même veine. Et la question que toute l'assemblée se pose arrive alors naturellement : «quelle est la meilleure formation pour travailler dans le jeu vidéo ?» Et là, nos deux protagonistes de reprendre le couplet sur la passion et le travail à la maison, et ensuite aller voir une des écoles présentes sur le salon.

Les «écoles»

Les écoles m'ont autant plu que la vidéo du SNJV, c'est vous dire. J'avais, il est vrai, un à priori négatif sur ces écoles. Étant enseignant-chercheur en informatique dans une université, je vois toujours d'un très mauvais œil ces formations privées qui sont souvent de très mauvaise qualité. Mais quand bien même, j'ai rangé mon arbalète à trolls et je suis allé à la pêche aux renseignements auprès de ces stands.

Une surprise parmi ces stands, une formation dans une université, à Polytech Nice : le master MAJE (Management de projets en jeux vidéo). Pas vraiment une formation technique (développement ou graphisme) mais bon, je ne m'attendais pas à voir une formation universitaire classique au milieu. Et d'ailleurs, ils mettent bien l'accent sur le fait qu'ils sont une formation d'une université publique.

Le reste, et bien, pas grand chose de vraiment surprenant. Des écoles privées, qui coûtent un bras à l'année, et qui proposent surtout de former des bons petits soldats aux dernières technologies en vogue. Les quelques lignes concernant les maths ne font pas illusion. Toutes ces formations proposent des stages intensifs d'une ou plusieurs semaines à temps plein sur des projets de jeux vidéo. Temps plein, ici, ça veut sans doute dire sans dormir, pour les habituer à leur futur métier. Honnêtement, qui peut faire du bon travail dans ces conditions ? Personne. J'affirme que pour faire du jeu vidéo, même la plus moisie des formations universitaires est bien mieux que n'importe laquelle de ces écoles. Parce qu'à l'université, on n'apprend pas (que) des technologies, on apprend à apprendre, on apprend des concepts réutilisables dans beaucoup de situations, peu importe la technologie. Et donc, on forme des gens qui, sur le long terme, sauront évoluer. Il est d'ailleurs étonnant de voir que la plupart des protagonistes de la vidéo sont jeunes, ce qui indique qu'il y a un gros turnover dans ces boîtes.

Mais l'arnaque la plus grosse, c'est que quasiment aucune de ces écoles ne délivre de diplôme reconnu par l'État. Et oui, c'est bien une arnaque, surtout quand on vient de voir la vidéo du SNJV. Il ne faut pas être naïf, le SNJV sait ce qu'il fait. Son référentiel, ce n'est pas pour orienter les ado geeks du salon qui rêvent de travailler dans le jeu vidéo, c'est avant tout pour créer des grilles de salaires, comme dans n'importe quelle autre branche professionnelle. Et qui dit grille de salaire dit qualification et donc diplôme (reconnu par l'État). Je suis désolé de voir tant de jeunes se précipiter dans ces stands avec plein d'illusions. J'avais vraiment envie de leur dire : n'y allez pas, allez à l'université, ça vaudra mieux pour vous. Ces écoles vendent du rêve mais certainement pas un avenir.

Une belle vitrine

Bon, allez, je vais arrêter d'être aigri. J'ai quand même passé un bon moment là-bas. Parce qu'au delà de ces choses très agaçantes, il y a quand même de beaux joujoux technologiques qui font plaisir à tous les joueurs.

PS4

Une des attractions de ce salon était bien évidemment la PS4 avec un grand espace dédié et tout un tas de démos. Il faut le dire tout de suite, on voit la différence à l'œil nu au niveau graphique. La profondeur de champ est vraiment spectaculaire, les effets visuels sont bluffants, le réalisme des scènes commencent à se rapprocher des cinématiques.

J'ai tout d'abord joué à Assasin's Creed 4, qui est déjà sorti sur PS3 et qui sortira également sur PS4. Niveau technique, il ne doit donc pas y avoir beaucoup de différence, et pourtant, la version PS4 rend beaucoup mieux, grâce à de nombreux petits détails visuels inexistants sur la version PS3. C'est tout simplement beau et fluide. Je crois que je me suis laissé convaincre d'investir dans une PS4 grâce à ce jeu mais peut-être pas dès sa sortie. Ensuite, j'ai joué à Lego Marvel, et là encore une sacré claque visuelle. On pourrait se dire qu'un jeu Lego, ça ne demande pas beaucoup de resources, et c'est sans doute vrai. Mais quand on voit le décor se refléter parfaitement dans la tête brillante d'un personnage, on se dit que la puissance est quand même utilisée. J'ai aussi pu tester Drive Club qui a l'air d'être un mauvais clone de Need For Speed, je n'ai pas grand chose à en dire de plus.

Kinect

Je ne voulais pas mourir idiot et je me suis donc prêté au jeu de me ridiculiser avec Kinect, plus particulièrement avec Kinect Star Wars. Dur, dur. Impossible d'avoir une maîtrise de ce qu'on fait. Que ce soit avec un sabre laser ou au volant d'un pod, on a vraiment l'impression de ne rien contrôler. On s'agite, on se bouge mais au final, rien ne se passe comme prévu et les choses arrivent plus par hasard que par une réelle volonté du joueur.

Je vais repasser en mode aigri quelques instants, mais je trouve vraiment que le Kinect est une régression en matière d'interface de jeu vidéo. Certes, c'est techniquement plus avancé, mais ça n'a pas la richesse de ce qu'on peut faire avec un joystick, ou une manette. La WiiMote avait déjà pris cette voie, mais il restait quand même quelques boutons sur la WiiMote qui permettait un minimum de précision dans les commandes. Là, on n'a plus rien, les mouvements détectés sont grossiers. Ce qui fonctionne à peu près pour un jeu de danse ne fonctionne plus du tout pour un jeu de combat ou un jeu de course.

Retrogaming

Enfin, je vais finir par le stand qui m'a sans doute le plus amusé : le stand retrogaming. Au centre, il y avait une version à dix joueurs simultanés de Bomberman, sorti à l'époque sur Saturn. La file d'attente ne désemplissait pas. J'ai pu rejouer au mythique Sega Rally, sorti également sur Saturn et sur borne d'arcade (il existe encore dans certaines salles de jeux vidéo). Il y avait également des NeoGeo, des Nes, toute en parfait état de fonctionnement. Bref, une retombée en enfance qui fait du bien au milieu des autres milliards de polygones par seconde.

Conclusion

Quelle conclusion tirer de cette incursion ? Pour mon jeu, quasiment aucune, si ce n'est qu'un jeu n'est pas amusant parce qu'il est technique, et qu'un bon concept, même avec des graphismes datés, peut encore procurer du plaisir au joueur. Il va donc falloir s'y remettre maintenant.

Je crée mon jeu vidéo E06 : génération procédurale de végétation

Posté par . Édité par palm123 et Benoît Sibaud. Modéré par Benoît Sibaud. Licence CC by-sa
Tags :
51
24
nov.
2013
Jeu

«Je crée mon jeu vidéo» est une série d'articles sur la création d'un jeu vidéo, depuis la feuille blanche jusqu'au résultat final. On y parlera de tout : de la technique, du contenu, de la joie de voir bouger des sprites, de la lassitude du développement solitaire, etc. Vous pourrez suivre cette série grâce au tag gamedev.

Dans l'épisode 05, on a discuté d'un sujet qui n'avait rien à voir et qui a généré quelques trolls, ce qui est assez cocasse pour un jeu vidéo dans un univers med-fan. Cette fois, on va parler de génération procédurale de végétation et aussi un peu d'animation de sprite. Avec quelques captures d'écran pour montrer le résultat de tout ça intégré dans le jeu !

Sommaire

Vous avez peut-être déjà entendu parler de SpeedTree. Non ? C'est un générateur de végétation en 3D qui permet de générer quantités d'arbres, d'arbrisseaux ou de plantes marines qu'on retrouve après dans des jeux vidéos ou dans des films. La particularité de cet outil est qu'il permet de générer aléatoirement et de manière procédurale toute cette végétation à partir de quelques paramètres de départ. Un outil dans Unity permet grosso-modo de faire le même genre de chose. Je vous présente donc SlowTree qui fait la même chose en 2D et en beaucoup moins impressionant.

SlowTree, un générateur de sprites 2D de végétation

Déjà, avant de commencer, précisons pourquoi avoir créé cet outil. La carte d'Akagoria va sans doute être très grande et va nécessiter de la meubler et de la décorer avec une bonne quantité de sprites en tout genre. Il paraît alors impossible de devoir créer à la main tous ces sprites qui, pour beaucoup, devront se ressembler plus ou moins, notamment tout ce qui relève de l'environnement naturel. Donc, l'idée vient alors naturellement de générer ces sprites à l'aide d'un programme.

Comment générer des sprites d'arbres

Générer des sprites n'est pas chose aisée, surtout en vue de haut. La première question qui vient est : comment faire pour que ce qu'on va dessiner ait l'air d'un arbre, sans que ça ne fasse trop simpliste et/ou moche ? De suite, on se dit qu'on va faire un gros cercle vert, parce que vue du haut, ça doit être à peu près ça. Je n'ai pas conservé d'archive de cette étape mais autant vous dire que non, ça ne ressemble pas à un arbre.

Alors, on essaie d'améliorer. On se dit que, quand on était enfant, on faisait une sorte de nuage vert pour dessiner le feuillage. Donc, deuxième étape, faire un nuage vert. On définit le nombre de «faces» qu'on souhaite, puis on va tracer successivement des courbes de Bézier entre des points tirés au hasard (la distance au centre est au hasard entre un min et un max défini par l'utilisateur, mais les angles par rapport au centre sont réguliers). Et ma foi, ça commence à ressemble à quelque chose.

Première version d'un arbre

On ajoute un peu d'ombre, grâce à un gradient radial pour donner un peu de volume à ce sprite.

Seconde version d'un arbre

Ça ne se voit peut-être pas mais le feuillage est transparent, pour permettre à un objet (ou un personnage) qui serait dessous d'apparaître légèrement. La transparence est évidemment plus prononcée sur les bords qu'au milieu, où le feuillage est censé être plus dense.

En tout cas, ça commence à rendre pas trop mal. Mais on sent qu'il manque quelque chose, que ça manque de dynamisme. Alors m'est venue une idée. Quand on se figure un arbre, il n'est pas homogène, c'est d'ailleurs pour ça qu'il y a cette forme de nuage qui n'est pas régulière. Les branches forment elle-même des sous-arbres qui pourraient apparaître, sous forme de boules un peu plus marquées. J'ai alors généré des boules, dont j'ai marqué le bord extérieur pour les rendre visible.

Troisième version d'un arbre

Là, on a une bien meilleure impression de volume et de fouillis dans ce feuillage. Me voilà donc satisfait du résultat. Reste à en générer plusieurs. À l'aide de l'outil slowtree, il est désormais possible de générer des planches complètes avec de nombreux arbres que l'on pourra placer sur la carte finale. En les tournant, on aura l'impression d'avoir des centaines d'arbres tous différents.

Une planche d'arbres

J'ai généré aussi un tronc d'arbre et des branches, mais je n'en parle pas parce que je n'en suis pas encore satisfait. On peut en distinguer quelques unes sur les bords extérieur. L'idée est de pouvoir aussi générer des arbres morts où il ne reste que les branches.

Et des buissons

Nous savons désormais générer des feuillages d'arbre, la question qui vient est : ne peut-on pas s'en servir pour générer des feuillages de buissons ? Et bien si ! Les différences entre un arbre et un buisson sont assez évidentes : moins de boules, moins de face dans le nuage, et moins de distance entre le rayon minimum et le rayon maximum. On réduit également la taille et on obtient de jolis petits buissons. L'outil slowbushes est donc capable, avec les mêmes base qu'avant de générer des planches de buissons.

Une planche de buissons

Et plein d'autres choses d'autre encore

Ceci n'est que le début de la génération procédurale de sprites. Dans les idées à venir, j'ai commencé à me pencher sur la génération de rocher et de caillou. J'en suis pour l'instant à l'étape où j'ai ma forme de base avec une ombre. Je n'ai pas encore trouvé comment donner du dynamisme pour que les rochers rendent bien. Un autre problème qui se pose est de définir à la génération une forme de collision pour ces rochers. Pour les arbres, la forme de collision a été approximé au cercle du tronc (mais comme on ne le voit pas, ce n'est pas très grave si ce n'est pas exact), ce qui permet donc au personnage d'aller sous l'arbre mais de buter contre le tronc. Pour les arbustes, j'ai fait à peu près pareil en approximant par un cercle. Comme la différence entre le rayon minimum et le rayon maximum est petite, ça marche assez bien.

J'ai aussi dans l'idée de faire varier les arbres générés, en essayant de générer ce qui pourrait ressembler à des conifères. Ou en variant la couleur, du vert foncé au jaune ou au rouge (les couleurs d'automne actuellement visibles dehors sont inspirantes).

Enfin, il faut absolument que j'améliore la ligne de commande en mettant des options plutôt que de devoir recompiler quand on change un paramètre.

Mises à jour d'Akagoria

Un sprite de sorcière !

Naha a fourni un sprite de sorcière pour remplacer le militaire ! Il devient ainsi le premier contributeur extérieur d'Akagoria. Un grand merci à lui. Il a prévu d'améliorer ce sprite mais il faut avouer qu'il a déjà fait du bon boulot.

Kalista

Adieu donc le militaire et bienvenue à Kalista la sorcière qui prend enfin forme.

Ça s'anime

Autre avancée importante, Akagoria permet désormais d'avoir des animations, grâce à l'excellent projet Nanim qui, rappelons-le, a été créé par devnewton.

L'intégration fut assez facile, plus que je ne l'aurais cru. Tout d'abord, le format des nanim est décrit dans un fichier Protocol Buffers, ce qui permet non seulement de connaître sa structure mais également de générer, grâce à protoc, un parseur dans le langage de son choix : Java, C++ ou Python. En C++, on a ainsi deux fichiers à intégrer que j'ai encapsulés dans ma propre classe Nanim.

Pour l'intégration dans le système à entités, j'ai créé un composant Animated qui contient une instance de Nanim et le nom de l'animation courante. Puis j'ai créé deux systèmes. Le premier système, Movement lit un composant Movement qui décrit le mouvement d'une entité (pour l'instant, seule la sorcière possède ce composant) et qui donne le nom de l'animation à utiliser. Les noms des animations, tels qu'ils apparaissent dans le fichier nanim, suivent donc une certaine convention pour être compatibles avec ce système. Le second système, Animation fait avancer l'animation. Il calcule donc l'image courante à utiliser pour afficher l'entité. Ce calcul est fait en grande partie dans la classe Nanim pour cacher les détails liés aux fichiers générés par Protocol Buffers.

Ensuite, il faut créer l'animation. Avec Nanim Studio et les sprites de Naha, c'est alors un jeu d'enfant. En même pas vingt minutes, j'ai obtenu plusieurs animations pour ma sorcière : une animation pour la marche en avant, une animation pour la marche en arrière et une animation pour la position statique.

On compile et hop, ça marche !

Les captures d'écran

Après ça, j'ai donc sorti la version 0.0.2 et j'ai réalisé quelques captures d'écran.

Kalista avec des arbres et des buissons

Sur cette première capture, on distingue Kalista en train de marcher (l'animation rend mal sur une capture, vous avez remarqué ?). On peut également voir deux arbres et des buissons issus de planches générées par SlowTree, ce qui donne une bonne idée de leur taille relative par rapport à Kalista.

Kalista sous un arbre

Cette seconde capture illustre la transparence sur les arbres. Kalista se cache sous l'arbre mais on peut tout de même la voir, ce qui est exactement l'effet que je recherchais. J'imagine déjà le joueur dans une forêt assez dense, en train d'être poursuivi par des créatures, mais étant presque incapable de les distinguer à cause des feuillages. La forêt sera un territoire dangereux dans Akagoria : qui sait ce qui va s'y cacher ?

La suite

Pour la suite, j'ai ordonné à peu près la feuille de route et la prochaine étape sera l'implémentation du multi-niveau sur la carte. J'y reviendrai dans un prochain épisode !

Un site web pour Akagoria

Allez, dernière nouveauté pour la route. Le jeu dispose dorénavant d'un site web : www.akagoria.org/ ! Il est très austère pour l'instant, mais c'est voulu. Je ne voudrais pas qu'on puisse penser que ce jeu est terminé ou presque terminé. Donc le site est moche pour bien faire comprendre que le travail est en cours. J'y mettrai des captures d'écran au fur et à mesure des avancées.

Je crée mon jeu vidéo E07 : cartes, données et systèmes à entités

Posté par . Édité par ZeroHeure et Benoît Sibaud. Modéré par Xavier Claude. Licence CC by-sa
Tags :
29
16
déc.
2013
Jeu

« Je crée mon jeu vidéo » est une série d'articles sur la création d'un jeu vidéo, depuis la feuille blanche jusqu'au résultat final. On y parle de tout : de la technique, du contenu, de la joie de voir bouger des sprites, de la lassitude du développement solitaire, etc. Vous pouvez suivre cette série grâce au tag gamedev.

Dans l'épisode 06, on a vu comment on pouvait simplement générer des arbres procéduralement. Cet épisode invitait à des extensions grâce aux quelques liens donnés dans les commentaires et aussi grâce à d'autres lectures que j'ai pu faire depuis. Mais pas tout de suite. Aujourd'hui, on va s'intéresser à la carte du jeu et je vais partager mes réflexions sur les données du jeu et les systèmes à entités.

Sommaire

La version 0.0.3 d'Akagoria vient de sortir et avec elle, une gestion améliorée de la carte. Ces développements m'ont amené à me poser plein de questions sur les données du jeu et les systèmes à entités.

Gestion de carte

Tout d'abord, je signale que la carte actuellement présente n'est pas du tout la carte définitive. Il s'agit simplement d'une île pour tester les divers éléments qui sont ajoutés. La carte finale sera beaucoup plus grande et aura certainement un autre ensemble de tuiles (tileset). On peut voir cet espace de test comme la maison de Lara Croft dans les premiers opus de la série Tomb Raider (oui, ceux où on pouvait enfermer le majordome dans la chambre froide).

L'île de test

La dernière fois qu'on a parlé de la carte, j'avais dit que j'étais capable de l'afficher morceau par morceau, et pas en entier à chaque frame, ce qui améliorait les performances. Maintenant, la carte ne doit pas avoir qu'un seul niveau : il y aura des grottes, des maisons, des caves, bref tout plein d'endroits qui sont soit en hauteur, soit en profondeur. Il faut donc gérer plusieurs éléments :

  • comment on définit un niveau différent, c'est-à-dire où va-t-on mettre les données de ce niveau ?
  • comment on passe d'un niveau à un autre ?
  • comment on implémente tout ça ?

Les données des niveaux

Il y a deux solutions que j'ai envisagées pour répondre à ce problème : soit mettre les niveaux dans des fichiers complètement différents, soit mettre les niveaux dans le même fichier. Dans le premier cas, il fallait faire une correspondance entre les différents fichiers et niveaux, par exemple savoir que quand on descend à tel endroit, on se retrouve à tel autre endroit. L'avantage, c'est qu'on était plus flexible avec plusieurs cartes, dont certaines plus petites (genre le sixième sous-sol où se trouve le boss de fin et rien d'autre, il peut être assez petit). Dans le second cas, la correspondance entre les niveaux est bien plus aisée. En contrepartie, l'édition des niveaux est plus difficile étant donné qu'il faut jongler entre les calques pour avoir la bonne vue du niveau.

J'ai choisi la seconde solution parce que je préférais avoir la correspondance gratuitement. En effet, étant donné la dimension envisagée de la carte, il me paraît difficile de devoir gérer cette correspondance à la main, je m'y perdrai forcément au bout d'un moment. Et puis les calques ne sont pas si mal gérés dans Tiled, ce qui permet d'éditer assez facilement.

Le passage d'un niveau à l'autre

La question qui vient ensuite est de bien gérer le passage d'un niveau à l'autre. Pour ça, Box2D nous aide. Box2D nous permet de définir des zones de capture (sensor) qui n'ont aucune interaction avec les autres éléments mais qui provoquent des événements de contact qu'on peut écouter et traiter. Il suffit alors de définir une zone de passage au niveau inférieur qui permettra au héros de descendre d'un niveau quand il sera en contact avec celle-ci.

Une difficulté est que le traitement des événements de contact est global dans Box2D, et pas lié à un élément en particulier. Cela force à gérer toutes les zones de changement de niveau de manière globale, mais ce n'est pas trop un problème vu qu'elles vont toutes être définies dans la carte. Car, oui, on va se servir une nouvelle fois de la carte pour définir ces zones. On va utiliser en particulier ce qui est appelé « objets » dans la terminologie de Tiled, c'est-à-dire des formes arbitraires qu'on peut placer n'importe où sur la carte (pas juste aux intersections des tuiles).

L'implémentation

Maintenant qu'on a plusieurs niveaux, il faut être capable de n'afficher que les éléments du niveau courant. Il faut donc pouvoir mettre à jour le niveau courant. J'ai pour cela utilisé le nouveau système d'événements introduit dans la dernière version de ma bibliothèque de systèmes à entités, libes. Pourquoi avoir introduit ça dans cette bibliothèque ? En fait, c'est très complémentaire. J'en parle dans la suite de cet épisode.

Donc, il existe un événement qui va dire quand le héros change de niveau et tous ceux qui doivent mettre à jour leur comportement peuvent alors le faire. Ça concerne tout d'abord l'affichage. Mais pour ça, il faut un composant qui permet de dire à quel niveau se situent les entités concernées. C'est le rôle du composant Altitude qui a été ajouté à pas mal d'entités. Quand le héros passe sur une zone de changement de niveau, il envoie cet événement et tout est bien mis à jour et seules sont affichées les entités du bon niveau.

Reste le dernier problème, comment faire pour éviter les collisions entre objets de niveaux différents ? Heureusement, Box2D vient encore à notre rescousse puisque la bibliothèque prévoit de pouvoir filtrer les collisions en attribuant des masques de bits aux différents éléments. Je ne rentre pas dans les détails de ces filtres qui sont assez évolués et qui permettent de faire des choses très avancées. Le résultat est qu'on peut attribuer des masques suivant le niveau de chaque entité. Et pour le héros, on peut changer son masque dynamiquement suivant le niveau où il se trouve. La limite est qu'on ne peut avoir que seize niveaux mais c'est largement suffisant.

On voit donc que ce petit problème de pouvoir aller d'un niveau à un autre met en œuvre beaucoup de parties différentes qu'il faut coordonner de manière intelligente : la définition dans le fichier TMX, la traduction avec Box2D, le traitement avec libes.

Maintenant qu'on peut lire les zones de changement de niveau dans le fichier TMX, le travail pour lire d'autres informations est déjà bien entamé. Cela a deux conséquences : la première est que la fonctionnalité « lire les données directement dans la carte » qui était prévue un peu plus tard a été avancé dans la roadmap (puisqu'une grosse partie est déjà faite), la seconde est qu'on va aussi lire des données de collision avec des éléments du décor.

Quand je dis « éléments du décor », je ne parle pas des arbres ou des puits qu'on a vu la dernière fois, je parle uniquement des contraintes liées à la géographie de la carte, comme les rivières et les rochers infranchissables, les murs des grottes, le bord de mer. Encore une fois, Box2D permet de définir des formes constituées de segments de droite. Et là, on se dit que le monde est bien fait puisque le fichier TMX et Box2D ont une définition similaires de ces formes : un point de départ et un ensemble de coordonnées relatives à ce point de départ. Les deux font également la différence entre les formes closes (polygon) et les formes ouvertes (polyline). Donc, il suffit de lire le fichier TMX et de traduire les coordonnées du fichier TMX (en pixels) en coordonnées de Box2D (en mètres). Puis on crée le corps qui va bien et on l'ajoute au monde.

Un petit aperçu

Voici un petit aperçu de ce que ça donne sur la carte dans Tiled. Plus précisément, c'est le niveau du sol, on distingue une zone infranchissable en bleuté et une zone de changement de niveau en orangé.

le niveau du sol dans Tiled

Dans cette deuxième capture, on a exactement la même vue mais au premier niveau souterrain. On distingue à nouveau les zones infranchissables en bleuté (ici, les murs de la grotte), et la zone de changement de niveau en orangé (pour pouvoir remonter).

le premier niveau sous le sol dans Tiled

Une fois tout ceci défini, on peut voir ce que ça donne dans une vidéo disponible sur le site (attention, le chargement est assez lent) que vous pouvez télécharger directement si le cœur vous en dit.

Réflexion sur les données

J'aimerais maintenant avoir une réflexion sur les données dans les jeux en général, du point de vue du développeur. L'utilisation d'un système à entités pousse à avoir cette réflexion, pour savoir quoi mettre où et comment faire.

Données statiques et données dynamiques

Tout d'abord, dans un jeu, on peut distinguer deux types de données. Premièrement, les données statiques, c'est-à-dire les données qui ne changeront pas au cours du jeu. Le fond de carte est un bon exemple de donnée statique. Deuxièmement, les données dynamiques, c'est-à-dire les données qui apparaissent et/ou évoluent au cours du jeu. La position du joueur est un bon exemple de données dynamique.

Si on imagine ce qu'est l'état courant du jeu, c'est-à-dire ce qu'on va devoir sauvegarder, on voit bien qu'il s'agit uniquement des données dynamiques. Toutes les données dynamiques ? Non, sinon ça serait trop simple. Si on regarde dans le détail, on voit bien qu'on a plusieurs types de données dynamiques. Par exemple pour une animation, on a un numéro de frame courant qui est une donnée dynamique (on la sauvegardera pour retrouver le même état au prochain chargement), et on a l'image courante, qui est une donnée dynamique induite par le numéro de frame courant. Et celle là, on ne devra pas la sauvegarder, puisqu'on peut la retrouver à partir du reste.

Dernier problème, c'est le mélange entre données statiques et dynamiques. Par exemple, quand on définit le corps d'un objet, on a tout un tas de données statiques (la densité, la friction, etc) et on a quelques données dynamiques (position, angle), le tout réuni dans une seule structure qu'on ne maîtrise pas :b2Body.

Données et systèmes à entités

Viennent alors les systèmes à entités. La théorie nous dit que les entités ont plusieurs composants et que ces composants représentent l'état du système. Et quand on veut sauvegarder l'état, il faut « juste » sauvegarder les composants, ça suffit. Donc, on se dit qu'un composant est une donnée dynamique. D'accord, mais je le met où mon b2Body ? Si je le met dans un composant, déjà je vais avoir un problème vu que je n'aurai qu'un pointeur, et je ne vais certainement pas sauvegarder un pointeur ! Bon, je vais donc sauvegarder la position et l'angle contenu dans le b2Body. Mais au moment du chargement, je fais comment ? Je les récupère comment les données statiques contenu dans le b2Body et qui sont nécessaires à sa construction ?

D'ailleurs, dans la version actuelle d'Akagoria, j'ai fait une grosse erreur. En effet, pour afficher ma carte, j'avais tout un tas de tuile, que je voulais gérer de manière efficace, c'est-à-dire n'afficher que ce qui est nécessaire. J'ai donc définit un composant Tile que j'ai utilisé pour mettre toutes les informations nécessaires (position de la tuile, coordonnées sur le tileset). Puis j'ai fait un système MapRender qui prend toutes les tuiles situés vers le héros et qui les affiche. Ça marche très bien, mais ça ne va pas du tout ! En effet, mes tuiles sont des données statiques, elles ne font pas partie de l'état du jeu. Si je charge une sauvegarde, je n'ai pas besoin d'avoir tout ça dans la sauvegarde puisque c'est commun à toutes les sauvegardes.

Bref, définir des composants, ce n'est jamais aussi simple qu'on le croit. On peut rapidement tomber dans un piège.

Données et traitements

Dernier point dans cette réflexion, l'ajout d'un système d'événements au système à entités. Souvent, dans la littérature, on présente les systèmes à entités et au détour d'un transparent ou d'une phrase, on voit : « ha oui, tiens il y a aussi un système d'événement mais c'est pas important ». Il peut s'appeler « système de messages » également, mais l'idée est la même : qu'il puisse y avoir une communication directe entre deux entités ou entre l'environnement extérieur et une entité. Je pense et j'affirme que cet élément n'est pas qu'un à-côté qu'on doit traiter comme une note de bas de page, mais qu'il est nécessaire à tout système à entité, il en fait partie intégrante !

Comment en suis-je arrivé à cette conclusion ? Déjà, le fait qu'il y ait systématiquement un système d'événements associé à un système à entités m'a mis la puce à l'oreille. Je trouvais bizarre que personne n'en parle vraiment, sans doute parce que la programmation événementielle est un paradigme bien connu, tandis que les systèmes à entités sont relativement nouveau. Puis, quand on implémente un jeu complet, on tombe forcément sur un cas où on en a besoin. Dans mon cas, c'était le changement de niveau, mais j'avais déjà en tête d'autres cas d'utilisation.

Alors, j'ai poussé ma réflexion. En quoi un système d'événements est-il complémentaire d'un système à entités pur ? La réponse est assez évidente : un système à entités met à jour ses données de manière régulière via les systèmes (environ soixante fois par secondes en simplifiant) tandis qu'un système d'événement n'agit que s'il y a un événement. On retrouve la dichotomie bien connue polling/event, sauf qu'ici, on a mis les données au centre de la réflexion. Ma première conclusion, c'est qu'on a besoin des deux. On pourrait sans doute avoir un composant Event et un système EventHandler qui regarde toutes les entités avec un Event et agit en fonction. Mais d'une part, on serait limité par ce qu'on peut mettre comme données dans l'événement (à moins de définir des composants spéciaux pour chaque type d'événement), et d'autre part, on ferait en fait du polling pour implémenter un gestionnaire d'événement, ce qui est un peu contradictoire. Sans compter que ces événements ne font pas partie de l'état du jeu !

La suite, elle est simple. Avec ces nouveaux outils (entités/événements), il va falloir refaire tout ce qui a été fait pour d'autres paradigme, c'est-à-dire trouver des design pattern adaptés à ces outils, de manière à éviter d'être en permanence en train de se demander comment faire telle ou telle chose. Construire un jeu permet d'avoir des cas très concrets d'utilisation et il suffit alors d'en extraire l'essence. Ça sera un des effets de bord de la création de ce jeu.

Version 0.0.3 et la suite

La version 0.0.3 est sortie le 15 décembre avec les nouveautés décrites précédemment. Dans la version suivante, je vais m'attaquer à la gestion des dialogues, ce qui amènera nécessairement à s'intéresser aux traductions. Il y aura également une petite mise à jour du sprite de Kalista que Naha m'a soumise (mon premier patch par courriel !) mais qui n'a pas été intégrée à cette version 0.0.3.

Je crée mon jeu vidéo E08 : fiche de lecture de «L'Art du game design» par Jesse Schell

Posté par . Édité par palm123, Xavier Claude et Benoît Sibaud. Modéré par Xavier Claude. Licence CC by-sa
Tags :
39
19
jan.
2014
Jeu

«Je crée mon jeu vidéo» est une série d'articles sur la création d'un jeu vidéo, depuis la feuille blanche jusqu'au résultat final. On y parlera de tout : de la technique, du contenu, de la joie de voir bouger des sprites, de la lassitude du développement solitaire, etc. Vous pourrez suivre cette série grâce au tag gamedev.

Dans l'épisode 07, on a parlé de cartes, de données et de systèmes à entités. Avec beaucoup de questions et peu de réponses. Et ce n'est pas dans cet épisode qu'on va y répondre. Non, aujourd'hui, cet épisode est consacré à un livre qui m'avait été conseillé dans un des premiers épisodes. Je l'ai acheté, je l'ai lu avec attention et je vous en parle maintenant.

Sommaire

L'Art du game design

«L'Art du game design : 100 objectifs pour mieux concevoir vos jeux» (de son nom complet) est un livre écrit par Jesse Schell. L'auteur a beaucoup travaillé avec Disney, que ce soit pour des jeux vidéos, comme Toontown online ou pour des attractions, comme Pirates des Caraïbes.

Résumé du livre

Tout le livre est fondé sur l'idée que le travail du game designer n'est pas de créer un jeu mais d'offrir une expérience à travers un jeu. Après avoir défini ce qu'était un jeu (notamment par rapport à un jouet), l'auteur définit ce qui fait un jeu. Les quatre éléments fondamentaux sont les mécaniques, l'histoire, l'esthétique et la technologie. À partir de là, on peut définir un thème pour le jeu (de préférence fédérateur), puis partir d'une ou plusieurs idées et tenter de définir différents problèmes à résoudre dans le jeu. Ensuite, on s'intéresse au joueur, c'est-à-dire à qui s'adresse le jeu, notamment son âge et son sexe, la manière dont il peut s'intéresser au jeu et la méthode pour qu'il continue à s'y intéresser, en combinant une montée en compétence et en challenge.

Un jeu doit contenir des mécaniques qui s'inscrivent dans un espace, avec des objets et des actions sur ces objets, des règles du jeu (dont l'objectif final du jeu), le tout faisant appel à la compétence du joueur ou à la chance. Un jeu doit être globalement équilibré : équitable, permettant des prises de risques, avec un bon rapport entre compétition et coopération, une durée de vie correcte, des récompenses (symboliques) adéquates mais aussi des punitions (game over), des mécaniques à la fois simples et complexes, et qui permettent de faire appel à l'imagination du joueur. L'interface du jeu se décompose entre une partie physique (manette, écran) et une partie virtuelle (ce qui s'affiche à l'écran, l'univers du jeu) qu'il est nécessaire de coordonner pour une meilleure immersion. Pour cela, il faut afficher les informations nécessaires et suffisantes via divers canaux et divers modes.

Un jeu doit avoir une courbe d'intérêt globalement croissante avec des pics au début (pour capter l'attention) et à la fin (pour le clou du spectacle). Pour cela on doit combiner l'intérêt inhérent du jeu, la poésie du jeu et la projection que peut avoir le joueur. Un jeu doit avoir une histoire à raconter mêlant des obstacles, des voyages, des univers (qui peuvent dépasser le simple cadre du jeu), des personnages. Ces personnages ont une fonction par rapport au jeu, et un statut par rapport à l'avatar du joueur, Les univers de jeu ont une architecture qui structure l'espace du jeu. Un jeu peut se jouer à plusieurs. Il est alors intéressant de favoriser l'émergence de communauté, tout en évitant les brebis galeuses.

Le game designer (professionnel) travaille en équipe, rédige des documents et organise des séances de tests. Il doit choisir des technologies sans succomber à la mode, et satisfaire son client, le convaincre que le jeu peut réaliser un profit. Le jeu transformant les joueurs, le game designer a également une responsabilité sur son jeu.

Mon avis

Il faut le dire, ce livre m'a plu. Il est très simple d'approche, il est très pédagogique. Il n'y a pas de grandes théories fumeuses mais plutôt du bon sens. Oui, ce livre enfile des perles, il n'y a rien de vraiment nouveau ni surprenant dans son contenu, il est d'une banalité affligeante. Mais c'est justement sa force : mettre en ordre une quantité impressionante de conseils et d'astuces sous forme de fiches pratiques.

Le fait que les exemples soient tirés de divers domaines, pas seulement des jeux vidéos, apporte un plus indéniables. Jeux de plateau, jeux de cartes, attractions dans un parc à thèmes, tout y passe. L'expérience de Jesse Schell au service de Disney aide beaucoup mais il va plus loin. Quand vous arrivez sur une page avec une photo de Swiffer, vous vous demandez bien ce que ça vient faire là, mais ça fonctionne parce qu'il y a une explication tout à fait logique à côté ! De même, quand il cite comme exemple la mouche incrustée au fond des toilettes d'un aéroport, on a du mal à en croire ses yeux.

Chaque chapitre débute avec un morceau d'un diagramme qui se construit au fur et à mesure et qui permet de voir les relations entre les différentes parties. On dirait presque une carte heuristique. Il est organisé autour de pôles qui sont l'expérience, le joueur, le jeu, le processus (de création), et le concepteur. Tout s'emboîte bien jusqu'au diagramme final. On peut donc par la suite revenir sur les différentes parties de manière aléatoire.

Ce livre s'adresse avant tout à des game designers professionnels comme le montre la fin du livre. Mais il est très intéressant pour des gens comme moi qui fabriquent un jeu amateur, parce qu'on ne fait pas un jeu uniquement pour se faire plaisir, mais aussi pour faire plaisir à d'autres. Même si je ne vendrai sans doute jamais mon jeu, il faut quand même penser à sa diffusion et donc avoir une approche professionnelle de sa conception. Quand je dis ça, je ne dis pas que je vais devenir game designer, mais je vais essayer d'appliquer tous les bons conseils de ce livre, du mieux que je peux. Mais bon, je ne suis pas le prochain génie du jeu vidéo.

Je sais que certaines personnes font des jeux. Connaissiez-vous ce livre ? Si oui, comment vous aide-t-il ? Si non, allez-vous l'acheter ?

Dévelopements

Après tout ça, revenons au jeu lui-même qui avance doucement mais sûrement.

La carte du monde

Sur le front du développement du jeu, j'ai revu complètement le chargement de la carte comme indiqué la dernière fois. J'en ai profité pour réfléchir à comment organiser la carte et je pense converger dans les semaines qui viennent. Ce travail est nécessaire pour une des prochaines grandes étapes qui sera de générer procéduralement une grande carte. Il faut que je sache à l'avance ce dont j'ai besoin pour pouvoir générer un maximum de choses. Par exemple, maintenant que j'ai des sprites d'arbres, je peux générer des forêts sans aucun problème, reste à savoir la meilleure manière de générer ces forêts et l'endroit le plus pertinent où les placer.

Par effet de bord, j'en ai profité pour améliorer libtmx qui me sert à charger les cartes au format TMX. Et j'ai réécrit l'utilitaire tmx_render en utilisant Qt5 à la place de SFML, de manière à pouvoir générer l'ensemble de la carte dans une image. Ça me sera très utile pour gérer une minimap dans l'interface, mais on aura l'occasion d'en reparler.

Il me reste à définir un ensemble de tuiles de base que j'utiliserai pour construire la carte (pas forcément le dessin final mais au moins la structure). J'envisage en fait deux ensembles : un ensemble pour le terrain proprement dit, c'est-à-dire le sol ; un ensemble pour ce qui se trouve sur le terrain (rivières, routes, etc). J'ai longtemps hésité à inclure les maisons (plus précisément les toits) dans ce deuxième ensemble (l'autre solution étant de faire des sprites), mais je crois que je vais garder cette solution. L'inconvénient est qu'on aura des maisons dont les murs sont horizontaux et verticaux (on pourra peut-être pousser vers les murs obliques à 45°). Ça donnera sans doute une impression old-school (pour être gentil), à voir donc.

Les dialogues

J'espère finir cette partie assez vite pour m'attaquer aux dialogues. Mais je dois dire que je me pose encore pas mal de questions. Il y a tout d'abord le problème de la traduction, mais pour ça, après avoir hésité et regardé ce qui était possible, j'en reviens systématiquement à Gettext. La foultitude d'outils existants me poussent vers cette direction. Et même Boost.Locale peut utiliser les fichiers au format Gettext (sans avoir besoin de Gettext). Ensuite, il y a le format des chaînes de caractères, et là, je n'ai pas encore fait de choix. J'ai à ma disposition le printf standard des familles, boost::format, ou encore FastFormat qui semble assez bien foutu même si assez complexe (vous aurez remarqué que iostream est disqualifié d'office). Enfin, il y a le format de stockage de tous ces dialogues, je m'oriente plutôt vers du YAML, j'aimerais savoir s'il existe déjà un dialecte YAML permettant de stocker des dialogues, mais je n'ai pas encore réussi à trouver.

Donc, je fais appel à tous ceux qui auraient des idées sur ces différents sujets et qui pourraient éclairer mes lanternes, ou me confirmer que mes choix ne sont pas trop mauvais.

Je crée mon jeu vidéo E09 : Techniques de C++11 appliquées au système à entités

Posté par . Édité par palm123 et Benoît Sibaud. Modéré par ZeroHeure. Licence CC by-sa
Tags :
38
12
fév.
2014
Jeu

«Je crée mon jeu vidéo» est une série d'articles sur la création d'un jeu vidéo, depuis la feuille blanche jusqu'au résultat final. On y parlera de tout : de la technique, du contenu, de la joie de voir bouger des sprites, de la lassitude du développement solitaire, etc. Vous pourrez suivre cette série grâce au tag gamedev.

Dans le dernier épisode, on a parlé du livre de Jesse Schell, «L'Art du Game Design». Mais bon, comme il paraît qu'on ne discute pas assez de technique sur LinuxFR, donc cette fois, on va causer uniquement de technique. Du gros, du lourd, du C++ ! Et dans sa version 2011 pour que ça soit encore plus imbitable pour le commun des mortels. Comme ça, on discutera entre nous, techniciens divins et on laissera la bonne plèbe se vautrer dans la mélasse. (Est-il nécessaire que je rajoute une balise humour ?)

Sommaire

C++11 mon amour

Programmer un jeu en C++11 (connu auparavant comme C++0x) est l'occasion de tester des fonctionnalités de cette nouvelle norme. Et les systèmes à entités donnent l'occasion de faire des choses assez élégantes. Comme Bjarne Stroustrup, inventeur du C++, je ne suis pas loin de penser que C++11 est presque un nouveau langage tellement tout paraît plus facile et naturel. Une fois essayé, difficile de s'en passer. Voici donc deux exemples qui utilisent des fonctionnalités C++11 et qui sont intégrés à libes et utilisé dans mon jeu.

Des gestionnaires d'événements vraiment génériques

Dans le patron de conception Observateur qui est à la base de certains systèmes d'événements, l'observateur est censé hériter d'une classe Observer et implémenter une fonction notify() qui sera appelée au moment adéquat. En C++, cela se traduit souvent par une classe de base avec une fonction virtuelle pure :

class Observer {
public:
virtual void notify() = 0;
}

Cette technique n'est pas très pratique, surtout comparé à Java où on aurait une simple interface (et non une classe, même abstraite). Heureusement, C++11 vient avec un moyen encore plus puissant que Java : std::function !

std::function, c'est les pointeurs de fonction en démultiplié ! Bon d'accord, la comparaison n'est peut-être pas idéale mais disons que c'est l'idée. Les amateurs de langages fonctionnels trouveront sûrement que cette fonctionnalité est triviale, et implémentée de manière verbeuse et inélégante, mais en C++, c'est nouveau et c'est révolutionnaire !

Concrètement, comment peut-on s'en servir ? Et bien je vais prendre l'exemple de libes et de ses gestionnaires d'événements. Dans libes, un gestionnaire d'événement est défini de la manière suivante :

typedef std::function<EventStatus(Entity, EventType, Event*)> EventHandler;

Ce qui veut dire qu'un EventHandler est une «fonction» prenant en paramètre une entité (l'origine de l'événement), un type d'événement et un pointeur vers les données de cet événement et renvoyant un statut (que je ne détaille pas ici). Ça a l'air limité mais en fait, ça ne l'est pas, au contraire. Parce qu'on peut avoir une vraie fonction :

EventStatus monGestionnaire(Entity e, EventType t, Event *ev) {
return EventStatus::KEEP;
}

Mais on peut aussi avoir un lambda :

auto monGestionnaireLambda = [](Entity e, EventType t, Event *ev) {
return EventStatus::KEEP;
}

Mais on peut aussi avoir une méthode d'une classe ! Et c'est là que ça déchire :

class Foo {
EventStatus maMethodeGestionnaire(Entity e, EventType t, Event *ev) {
return EventStatus::KEEP;
}
}

Et dans ces cas-là, on peut l'associer à un objet en particulier via std::bind :

using namespace std::placeholders;
Foo foo;
auto gestionnaire = std::bind(&Foo::maMethodeGestionnaire, &foo, _1, _2, _3)

Ce qui signifie que, quand on appellera gestionnaire avec les trois arguments qui vont bien, en fait, on appellera la méthode maMethodeGestionnaire sur l'objet foo avec les trois arguments. On pourrait faire des choses encore plus drôles en ayant des méthodes qui ont les paramètres dans le désordre, ou dans laquelle il manque des paramètres. Bref, tout est possible avec std::bind !

Maintenant, on n'est donc plus limité par une classe de base, on peut avoir tout ce qu'on veut comme gestionnaire d'événements.

Cerise sur le gâteau, comme ce dernier cas est plutôt courant, libes permet de spécifier un pointeur sur une méthode et un objet et fait le bind automatiquement. Magique !

Comment avoir des identifiants uniques en C++ ?

Dans mon implémentation de libes, j'utilise des identifiants uniques pour les composants et les événements. Ces identifiants doivent être tous différents et différents de zéro (qui est l'identifiant qui représente le composant ou l'événement invalide). Évidemment, cette manière de faire est très utile pour le développeur de libes (moi) qui a un joli entier qu'il peut utiliser pour plein de choses (essentiellement ranger les composants/événements dans une table de hachage) mais pas très pratique pour l'utilisateur de libes.

Et bien désormais, libes permet de ne pas avoir à trop se préoccuper de cet entier (notamment savoir s'il est différent des autres) grâce à la magie de C++. Pour cela, j'ai introduit un littéral définis par l'utilisateur qui, à partir d'une chaîne de caractère, permet d'avoir un entier. En fait, l'entier est obtenu à partir d'un hash de la chaîne, ce qui garantit (presque) que pour deux chaînes différentes, on aura deux identifiants différents (ou alors, c'est vraiment pas de bol !).

Déjà, quel hash utiliser ? Ici, une fonction de hachage non-cryptographique et simple convient. Et même, si elle peut être suffisamment simple pour pouvoir être calculée à la compilation, ce serait parfait. À la compilation ? Oui, C++ permet, grace au mot-clef constexpr de calculer des choses à la compilation. Donc, notre fonction de hachage doit être constexpr, ce qui implique qu'elle doit tenir sur une seule ligne ! Heureusement, tout un tas de fonction de hachage sont comme ça.

J'ai donc choisi une variante d'un hash FNV. Voilà son implémentation en C++ :

constexpr uint64_t Hash(const char *str, std::size_t sz) {
return sz == 0 ? 0xcbf29ce484222325 : (str[0] ^ Hash(str + 1, sz - 1)) * 0x100000001b3;
}

On peut voir que la variante vient du fait qu'ici, à cause de l'appel récursif, on prend les données à l'envers, c'est-à-dire qu'on commence par la fin et on remonte jusqu'au début. Ce n'est pas très grave, ça donne les mêmes résultats en terme de collisions potentielles.

Ensuite, il n'y a plus qu'à définir un nouveau littéral. À noter que les littéraux définis pas les utilisateurs doivent commencer par _, les littéraux commençant par une lettre étant réservés pour un usage futur (comme en C++14 où on aura plusieurs littéraux de ce genre dans la bibliothèque standard). Ici, on choisit _type :

constexpr uint64_t operator"" _type(const char *str, std::size_t sz) {
return Hash(str, sz);
}

Maintenant, pour définir un identifiant d'un composant (par exemple), on peut faire :

struct Foo {
static const es::ComponentType type = "Foo"_type;
}

Et cette constante est calculée à la compilation, pas à l'exécution. On a l'avantage d'avoir un identifiant clair sous forme de chaîne de caractères et un identifiant entier pour le développeur, sans aucun surcoût à l'exécution, bref que des avantages.

Après, on pourrait s'amuser à définir des macros pour encapsuler tout ça, ou le générer automatiquement (ce que je fais dans mon jeu), mais tout ça est laissé à l'utilisateur, la bibliothèque ne fournit que le mécanisme de base et c'est déjà pas mal.

Des nouvelles du front

Pas grand chose de nouveau par rapport à la dernière fois. Je continue ma réflexion sur les dialogues et malgré l'excellent lien qui m'a été fourni à propos du jeu Andor's Trail, il y a encore des zones d'ombre que je souhaite éclaircir avant de me lancer dans un début d'implémentation. Mais j'ai vraiment hâte d'attaquer cette partie.

Par ailleurs, j'ai terminé un gros refactoring (nécessaire) sur le chargement de la carte et j'ai commencé à spécifier un peu proprement la manière dont j'allais construire ma carte graĉe à Tiled.

Je crée mon jeu vidéo E10 : génération procédurale de carte (partie 1)

Posté par . Édité par 4 contributeurs. Modéré par Benoît Sibaud. Licence CC by-sa
111
7
mar.
2014
Jeu

«Je crée mon jeu vidéo» est une série d'articles sur la création d'un jeu vidéo, depuis la feuille blanche jusqu'au résultat final. On y parlera de tout : de la technique, du contenu, de la joie de voir bouger des sprites, de la lassitude du développement solitaire, etc. Vous pourrez suivre cette série grâce au tag gamedev.

Dans l'épisode 09, on a vu comment C++11 procurait des constructions bien pensées qu'on pouvait utiliser dans les systèmes à entités. Cette fois, on attaque dans le dur à travers un double épisode qui va nous permettre de générer une carte pour du RPG. Dans la première partie, on va voir comment générer une «carte d'altitude» (heightmap). On va passer en revue plein de techniques qui permettent d'arriver à ce résultat. Avec tout plein d'images pour illustrer. Attention les yeux !

Sommaire

Exemple de carte finale

Généralités sur les cartes d'altitude

Une carte d'altitude est une image en niveaux de gris qui indique la hauteur d'une surface virtuelle représentant un terrain de jeu. Généralement, le noir indique une altitude basse et le blanc indique une altitude haute. En pratique, on utilise une matrice où chaque case de la matrice représente un pixel de la carte. La matrice contient généralement un flottant, normalisé entre 0 et 1, ou entre -1 et 1. Chez moi, ça sera entre 0 et 1.

Les cartes en niveaux de gris, c'est marrant mais pour vous en mettre vraiment plein les yeux, je vais plutôt générer des cartes en couleur en utilisant le gradient suivant (0 à gauche, 1 à droite, le niveau de la mer étant à 0,5) :

Un gradient pour les cartes d'altitude

Trouver un gradient correct est assez difficile. Et ce n'est pas mon sens du graphisme inné qui m'aide beaucoup. J'ai récupéré ce gradient sur un site, j'aurais pu en choisir d'autres mais je les trouvais moins jolis. Si vous avez des talents pour améliorer ce gradient, n'hésitez pas à apporter votre aide.

L'idéal quand on génère ce genre de carte, c'est de pouvoir l'affiner à l'envie. En pratique, on essaie de générer un terrain fractal, c'est-à-dire un terrain qui présente un comportement fractal. Affiner le terrain (en pratique, avoir une carte de plus grande taille) ne va pas changer la physionomie générale du terrain, juste sa précision.

Ensuite, une fois que le processus de génération est en place, on cherche à avoir des cartes intéressantes, c'est-à-dire avec du relief mais qu'on puisse jouer. Pour cela, on va définir deux scores pour nos cartes, le score d'érosion et le score de jouabilité. Ces deux définitions sont issues de l'article Realtime Procedural Terrain Generation de Jacob Olsen, écrit en 2004. C'est un excellent article qui m'a beaucoup servi, notamment pour les algorithmes d'érosion décrits plus loin.

Comment mesurer l'importance du relief ?

Pour mesurer le relief, on utilise le score d'érosion. Le principe est, pour chaque case, de calculer la pente maximale, c'est-à-dire la différence d'altitude en valeur absolue, par rapport aux voisins. On ne considère que les voisins parallèles aux axes, c'est-à-dire les voisins du haut, de droite, du bas et de gauche. Ensuite, on calcule la moyenne de ces pentes et l'écart-type, puis enfin, le coefficient de variation, c'est-à-dire le rapport entre l'écart-type et la moyenne. C'est ce coefficient de variation qu'on appelle score d'érosion.

Un score d'érosion de 0 indique une carte plate. Plus le score est élevé, plus il y a de relief. En pratique, dès qu'on atteint 1, on constate des cartes avec pas mal de relief. Tout au long de cet article, j'essaierai de donner les scores d'érosion des différentes cartes générées pour vous donner un ordre d'idée.

Par exemple, pour la carte du début, le score d'érosion est de 0,970156. On constate qu'elle présente pas mal de relief, avec de grands plateaux qui permettent de délimiter des zones intéressantes.

Comment mesurer la pertinence d'une carte ?

Pour mesurer la pertinence d'une carte, c'est-à-dire sa jouabilité, l'idée est de regarder si les unités peuvent se déplacer et les bâtiments peuvent être placés sans encombre. En effet, on va considérer qu'une pente trop importante ne permet pas aux unités de passer ou aux bâtiments d'être placés. En plus, on considère qu'une unité a une certaine taille, de même que les bâtiments.

En pratique, on va d'abord calculer une carte binaire qui indique les cases qui ont une pente inférieure à Tu, la pente maximale franchissable par les unités, puis on va enlever de cette carte binaire toutes les cases qui ne peuvent pas contenir un carré de côté Nu, la taille des unités. Enfin, on calcule la plus grande zone connectée dans cette carte ce qui donne une carte d'unités. Pour la suite, on a pris Nu=1.

Ensuite, on fait de même avec les bâtiments, on prend une pente Tb maximum (généralement inférieure à Tu parce qu'un bâtiment supporte moins bien la pente) et une taille Nb (généralement supérieure à Nu parce qu'un bâtiment prend plus de place qu'une unité) et on calcule la carte binaire de la même manière, sauf pour la plus grande zone connectée (les bâtiments ne se déplacent pas). Enfin, on garde dans cette deuxième carte les zones accessibles dans la carte d'unité, ce qui donne la carte de bâtiments. Pour la suite, on a pris Nb=9.

Pour calculer le score de jouabilité, on va calculer la proportion de cases accessibles dans la carte d'unités, ainsi que la proportion de cases disponibles dans la carte de bâtiments. Et on va les multiplier par le score d'érosion pour obtenir le score de jouabilité. On comprend alors qu'une carte plate donnera des cartes d'unités et de bâtiments excellente mais un score d'érosion nul. Inversement, si on a trop de pente partout, on aura des cartes d'unités et de bâtiments mauvaises, voire très mauvaises, mais un score d'érosion excellent. Dans les deux cas, le score de jouabilité est mauvais. Il faut donc trouver un compromis entre les deux.

Voici la carte d'unités associée à la carte de début.

Carte d'unités

Voici la carte de bâtiments associée à la carte du début.

Carte de bâtiments

Le score de jouabilité pour cette carte est de 0,233147. Avec un score d'unité de 0,834806 (ce qui signifie que 83% des terres émergées sont accessibles aux unités (NdM : capables de voler ou d'y accéder par la mer, les terrains n'étant pas forcément tous accessibles uniquement par la terre) et un score de bâtiment de 0,287874 (ce qui signifie qu'on peut placer des bâtiments sur 28% des terres émergées), on a une carte tout à fait jouable.

Comment afficher une carte avec du relief ?

Avant de continuer, il faut expliquer que les cartes que je vais présenter sont représentées avec un relief ombré. Ça a l'air simple mais ça ne l'est pas. Sans relief ombré, la carte ressemblerait à ça.

Exemple de carte finale sans relief

Après beaucoup de recherches, j'ai utilisé l'algorithme simple utilisé dans le tutorial de génération de cartes polygonales d'Amit Patel (Red Blog Games). J'ai juste modifié un peu les couleurs. Plutôt que d'utiliser du gris, j'ai utilisé un jaune très léger pour le côté soleil et un violet très sombre pour le côté ombre. J'ai aussi conservé la convention de la lumière venant du nord-ouest (ce qui est impossible en réalité mais aide à discerner les trous des bosses).

Pourquoi est-ce difficile d'avoir un bon relief ombré ? Parce que sur les cartes réelles, cet ombrage est fait à la main, c'est un art en tant que tel chez les cartographes. Il existe des algorithmes pour le faire automatiquement mais il est difficile de les trouver écrits de manière claire, et leur résultat est souvent moins bon que les tracés à la main. Dans ma longue route à la recherche d'informations, voici quelques liens intéressants sur lesquels je suis tombé.

Tout d'abord, deux sites avec plein d'informations : Shaded Relief et Relief Shading. On y voit plein d'exemples de cartes ombrées à la main. On a également accès à tout un tas d'articles sur les techniques à utiliser, dont beaucoup utilisent le logiciel propriétaire de traitement d'image leader du marché. Pour les allergiques au logiciel propriétaire, Wikipédia francophone propose un tutoriel pour créer un relief ombré avec des outils libres.

Cette petite digression étant finie, passons aux choses sérieuses.

Bruit cohérent

Une carte d'altitude est par nature constituée de bruit cohérent. On peut définir le bruit cohérent comme une fonction (informatique) de Rn dans R avec les propriétés suivantes :

  • les mêmes valeurs d'entrée produisent la même valeur de sortie
  • un petit changement dans les valeurs d'entrée produit un petit changement dans la valeur de sortie
  • un gros changement dans les valeurs d'entrée produit un changement aléatoire dans la valeur de sortie

Dans notre cas, nous voulons produire une carte en deux dimensions (n=2), donc nous nous intéresserons uniquement aux techniques pour produire du bruit en deux dimensions. Beaucoup de ces techniques s'adaptent à des dimensions supérieures.

Parmi les algorithmes de génération de bruit qu'on rencontre souvent, il y a deux grandes classes d'algorithmes :

  • les générateurs à base de bruit de Perlin (au sens large)
  • les générateurs à base de placement de point

Bruit de Perlin

Dans la catégorie bruit de Perlin, je classe toute une série de bruits qui ne sont généralement pas mis sous ce vocable mais qui utilisent globalement une même procédure. Le vrai bruit de Perlin utilise le bruit à base de gradient, comme on le verra par la suite.

La procédure dont je parle est parfois appelée fractional brownian motion, ou fBm pour les intimes. Je l'ai nommée plus simplement fractal.

Elle consiste, à partir d'une fonction de bruit «simple» à combiner plusieurs octaves de différentes amplitudes et de différentes fréquences. Plus précisément, pour chaque octave supplémentaire, on divise par deux l'amplitude, on multiplie par deux la fréquence, et on additionne toutes ces octaves.

On peut appliquer cette technique à plusieurs types de bruit que nous allons détailler.

Bruit à base de valeur (value noise)

Le principe du bruit à base de valeur est simple. On génère une grille dont les coordonnées entières contiennent une valeur aléatoire fixe. Ensuite, pour un point (x,y), on regarde dans quelle case de la grille se trouve le point (éventuellement en répétant la grille de valeurs), puis on détermine les coordonnées (rx,ry) de ce point dans la case (c'est-à-dire qu'on enlève la partie entière de chaque coordonnée).

Grille

Ensuite, on effectue plusieurs interpolations avec les quatre points situés aux coins de la case de la grille correspondante. On fait d'abord une interpolation entre la valeur en A et la valeur en B avec comme coefficient rx, puis entre la valeur en D et la valeur en C avec la valeur rx, puis enfin entre les deux valeurs obtenues avec la valeur ry.

Comment interpoler deux valeurs ? Généralement, on utilise une interpolation linéaire. On utilise une fonction appelée traditionnellement lerp, définie de la manière suivante :

double lerp(double v0, double v1, double t) {
return v0*(1-t)+v1*t;
}

Pour t=0, on aura v0 et pour t=1, on aura v1. Et entre les deux, on aura une valeur intermédaire. Mais dans le cas de bruit, ça ne donne pas de beaux résultats. On va donc lisser la courbe d'interpolation et utiliser une fonction qu'on va appliquer à t :

double lerp(double v0, double v1, double t) {
t = g(t);
return v0*(1-t)+v1*t;
}

On va choisir la fonction g pour qu'elle ait de bonnes propriétés. En particulier, si on ne veut pas d'angles, on va plutôt choisir une fonction dont la dérivée en 0 et en 1 est nulle. Si on prend une fonction polynômiale alors, on tombe sur un polynôme de degré 3 : -2 x3 + 3 x2. Si on veut en plus que la dérivée seconde soit nulle en 0 et en 1, on tombe sur un polynôme de degré 5 : 6 x5 - 15 x4 + 10 x3. On peut aussi choisir une fonction trigonométrique comme (1 - cos(pi * x)) * 0.5 mais cette fonction se rapproche beaucoup de notre polynôme de degré 3. Voici l'ensemble de ces fonctions dessinées sur un même graphe :

Interpolations

Voici le résultat sur les mêmes valeurs :

linear cubic quintic cosine
linear cubic quintic cosine

Généralement, le polynôme de degré 3 donne des résultats satisfaisants. On garde donc celui-ci :

Bruit à base de valeur

Et avec 10 octaves, on obtient une carte tout à fait convenable (score d'érosion : 0,429078) :

Bruit à base de valeur

Voir l'implémentation du bruit à base de valeur.

Bruit à base de gradient (gradient noise)

Le buit à base de gradient est le vrai bruit de Perlin, décrit dans Making Noise, que Ken Perlin a inventé pour le film Tron et qu'il a par la suite décrit en 1985. C'est une amélioration du bruit à base de valeur. L'idée au départ n'est pas de créer des cartes d'altitude mais des textures. Et de manière générale, on peut appliquer beaucoup des techniques vues ici pour créer des textures procédurales assez bluffantes.

Mais revenons à notre bruit à base de gradient. Par rapport au bruit à base de valeur, on ne définit pas des valeurs (aux coordonnées entières de la grille) mais des vecteurs, également appelés gradients. Ensuite, pour déterminer une valeur aux quatre coins de la case, on calcule un produit scalaire. Pour le point A, on calcule le produit scalaire entre le gradient défini au point A et le vecteur PA. Et pareil pour les trois autres. Enfin, on interpole ces quatre valeurs comme pour le bruit à base de valeurs.

Le résultat est meilleur qu'avec le bruit à base de valeur, où on distinguait bien les contributions des quatre coins. Maintenant, on a des formes plus variées :

Bruit à base de gradient

Le résultat avec 10 octaves n'est pas mal du tout (score d'érosion : 0,433705) :

Bruit à base de gradient

Voir l'implémentation du bruit à base de gradient.

Bruit à base de simplexe (simplex noise)

Le bruit à base de simplexe est une évolution du bruit à base de gradient et proposé par le même Ken Perlin. Le problème du bruit à base de gradient est qu'il requiert O(2n) interpolation pour un bruit à n dimensions. Ceci vient du fait qu'on utilise un hypercube qui a 2n sommets. Pour avoir moins de points, on va utiliser un objet à n dimensions qui possède le moins de points possible : c'est ce qu'on appelle un simplexe. En dimension 2, c'est un triangle. En dimension 3, c'est un tétraèdre. Et ainsi de suite. De manière générale, c'est un objet à n+1 points.

Ensuite, l'implémentation proposé par Ken Perlin est assez complexe. La difficulté consiste à calculer le triangle dans lequel on se situe. L'idée est d'appliquer une sorte de transvection qui va transformer nos triangles en demi-carrés. De cette manière, savoir si on est dans le demi-carré du dessus ou du dessous revient à savoir si rx est plus grand ou plus petit que ry. Dernière subtilité, c'est qu'on ne va pas avoir d'interpolations, ni de gradients tirés au hasard. Chaque coin va apporter une contribution qu'on va ajouter les unes aux autres. Ceci est fait pour accélérer la génération.

Au final, le résultat est plutôt convaincant :

Bruit à base de simplexe

Et avec 10 octaves, on observe des artefacts obliques. Je ne sais pas si c'est une erreur dans l'implémentation, mais ça se pourrait parce que ce type de bruit est supposé donner de bons résultats (score d'érosion : 0,452591) :

Bruit à base de simplexe

Voir l'implémentation du bruit à base de simplexe.

Bruit à base de cellule (cell noise)

Dernier bruit de la série, le bruit à base de cellule. Il porte aussi le nom de bruit de Worley (du nom de son inventeur, Steven Worley) ou de bruit de Voronoï (parce que visuellement, on observe un diagramme de Voronoï). L'algorithme général est bien différent de ce qu'on a pu voir jusqu'ici.

L'idée est de générer n points Qi dans l'espace, puis pour un point P, on définit les fonctions Fi qui représentent la distance au i-ème point le plus proche parmi Qi. Ensuite, le bruit est défini par la somme C1 * F1(P) + C2 * F2(P) + … + Cn * Fn(P) où les Ci sont des constantes prédéfinies.

Suivant les constantes qu'on choisit et la fonction utilisée pour la distance, on obtient des résultats assez différents les uns des autres. On a le choix entre prendre la distance euclidienne (distance associée à la norme 2), la distance de Manhattan (distance associée à la norme 1) ou la distance de Tchebychev ou Chebyshev (distance associée à la norme infini).

Examinons d'abord quelques ensembles de constantes classiques. On va utiliser dans ce cas une représentation en niveau de gris, parce que la représentation en couleurs ne rend pas bien. Le choix le plus logique est C1=1 et tout le reste à zéro, c'est là qu'on distingue le mieux le diagramme de Voronoï. Et en fait, on prend plutôt C1=-1 histoire d'avoir des bosses plutôt que des creux. On peut ensuite penser à C2=1 et tout le reste à zéro, ou C3=1 et tout le reste à zéro, et on voit apparaître des formes assez originales. Mais en fait, on obtient de bons résultats avec C1=-1 et C2=1 où on a l'impression d'avoir des collines les unes à côtés des autres. La distance utilisée est la distance euclidienne.

C1=-1 C2=1 C3=1 C1=-1, C2=1
c1=-1 c2=1 c3=1 c1=-1, c2=

Pour la suite, nous prendrons C1=-1 et C2=1. Et nous allons voir l'influence de la distance.

euclidean manhattan chebyshev
euclidean manhattan chebyshev

On constate que les distances de Manhattan et de Tchebychev produisent des formes très géométriques, avec des artefacts horizontaux et verticaux très présents.

Si on représente la combinaison gagnante en grand, ça donne :

Bruit à base de cellule

Et on peut évidemment appliquer une fractale (score d'érosion : 0,386916). À noter que pour ces deux cartes, j'ai placé le niveau de l'eau à 0,1 plutôt que 0,5, et j'ai ajusté l'échelle présentée au début, sinon la majorité de la carte est sous l'eau.

Bruit à base de cellule

Il faut faire attention à l'implémentation pour que la fractale marche bien et ne donne pas des trucs horribles. Il faut en fait répéter nos points à l'infini de manière virtuelle de manière à avoir une continuité dans le bruit, sinon les discontinuités apparaissent et on n'a pas un bruit cohérent. La conséquence, c'est que notre texture peut se répéter également (le haut joint avec le bas et la gauche joint avec la droite) et ça se voit assez clairement.

Voir l'implémentation du bruit à base de cellule.

Méthodes à base de placement de point

Voilà pour les méthodes à base de bruit de Perlin. Passons maintenant aux méthodes à base de placement de point. Il y en a deux, et la seconde est une amélioration de la première. La particularité de ces méthodes est qu'elles génèrent des cartes de tailles 2k+1. Pour avoir des tailles arbitraires, on génère une carte plus grande de la bonne taille et on prend une sous-partie de ce qui a été généré.

Déplacement du point médian (Midpoint displacement)

La première méthode s'appelle le déplacement du point médian. Elle est assez simple à décrire. On part d'un carré pour lequel on a défini quatre valeurs aux coins. Puis, on fait la moyenne des quatre coins, à laquelle on ajoute une petite variation proportionnelle au côté du carré, et cela définit la valeur du centre du carré. Reste alors à compléter les quatre carrés par quatre points, chaque point étant entre deux points du carré initial pour lesquels on va faire la moyenne et ajouter à nouveau une petite variation. Puis on recommence récursivement sur ces quatre carrés jusqu'à arriver à un pixel.

On obtient ce genre de carte (score d'érosion : 0,385764)

midpoint displacement

Le résultat montre des artefacts horizontaux et verticaux bien visibles. C'est la raison d'être de la méthode suivante.

Voir l'implémentation du déplacement du point médian.

Diamant-Carré (Diamond-Square)

La seconde méthode s'appelle l'algorithme du diamant-carré. Elle ressemble à la précédente mais elle est partagée en deux phases : la phase diamant et la phase carré. Pendant la phase diamant, on procède comme précédemment, on fait la moyenne des quatre coins, à laquelle on ajoute une petite variation proportionnelle au côté du carré, et cela définit la valeur du centre du carré. Puis on passe à la phase carré pour définir les quatre derniers points. La différence par rapport à précédemment, c'est qu'on utilise pas seulement les points du carré initial mais aussi les points des centres des carrés adjacents. La phase diamant a créé des diamants et chacun des points restants est donc au centre d'un de ces diamants, donc on utilise les quatre coins du diamant pour recréer des carrés en faisant la moyenne, à laquelle on ajoute une petite variation proportionnelle au côté du carré. Ainsi, on a partagé notre carré initial en quatre carrés et on peut appliquer la même méthode récursivement.

Et voici le résultat (score d'érosion : 0,382071)

diamond square

L'impression visuelle est bien meilleure. Les artefacts ont complètement disparu. Cette carte servira de base pour la section suivante de cet épisode.

Voir l'implémentation de l'algorithme diamant-carré.

Autres méthodes

À côté de toutes les méthodes décrites précédemment et qui sont assez standard, il existe d'autres méthodes qu'on arrive à débusquer au hasard de la navigation. En voici une qui construit une carte à base collines. L'idée est de générer des collines, c'est-à-dire des demi-sphères de manière aléatoire. On les accumule et ça donne des formes assez sympas même s'il y a des artefacts visibles (score d'érosion : 0,480934).

hills

Voir l'implémentation de l'algorithme des collines.

Modification de la carte

Maintenant qu'on a de jolies cartes, on va les modifier. En effet, ces cartes rendent bien mais elles n'ont pas forcément les bonnes caractéristiques. En particulier, aucune des cartes présentées précédemment n'a un score de bâtiments non-nul, ce qui signifie qu'elles ont toutes des pentes beaucoup trop importantes. Si on veut qu'elles s'approchent d'un relief réel ou qu'elles soient plus lisses, on peut appliquer divers filtres que je vais vous présenter.

Érosion

Pour rendre une carte plus réaliste, la première technique est de simuler de l'érosion. Voici trois techniques, présentées dans l'article Realtime Procedural Terrain Generation.

Érosion thermique

La première technique permet de simuler une érosion thermique. L'érosion thermique est celle qui provoque des éboulements. À cause de l'action des températures, le sol va se fissurer, puis s'effriter puis s'effondrer et va glisser si la pente le permet. On simule cette érosion de la manière suivante : pour toutes les cases, on regarde si la pente est supérieure à une limite fixée, puis si c'est le cas, on enlève une fraction de matière de la case qui va s'accumuler sur les cases adjacentes les moins élevées. On répète ce processus un certain nombre de fois et voilà ce qu'on obtient (score d'érosion : 0,475935).

Érosion thermique

On observe le tassement surtout sur les côtes qui ont pris un peu d'embonpoint.

Voir l'implémentation de l'érosion thermique.

Érosion hydraulique

La seconde technique permet de simuler une érosion hydraulique. L'érosion hydraulique est dûe à l'action de la pluie et du phénomène de sédimentation. On le simule avec quatre étapes. Première étape, de l'eau tombe du ciel uniformément sur le terrain. Deuxième étape, une partie du matériel présent sur le terrain se dissout dans l'eau. Troisième étape, l'eau ruisselle sur les pentes. Quatrième étape, l'eau s'évapore et le matériel qu'elle transportait se dépose au sol. De la même manière qu'avant, on répète ce processus un certain nombre de fois et voilà ce qu'on obtient (score d'érosion : 0,446365).

Érosion thermique

Malgré un temps de calcul bien plus élevé que pour l'érosion thermique, les différences sont assez imperceptibles visuellement. L'article montre qu'en fait, l'érosion thermique aplanit les zones à peu près plates et renforce les pentes, ce qui accroît le score d'érosion.

Voir l'implémentation de l'érosion hydraulique.

Érosion rapide

On a donc une technique rapide mais qui aplanit les pentes, et une technique lente qui renforce les pentes. Et on aimerait bien un mélange, c'est-à-dire une technique rapide qui renforce les pentes. Pour ça, un nouvel algorithme, appelé fast erosion, a été développé par l'auteur de l'article. Il reprend le principe de l'érosion thermique mais plutôt que de considérer des éboulements quand la pente est forte, il considère des éboulements quand la pente est faible. Et le résultat est conforme à celui qui était voulu. Voici le résultat (score d'érosion : 1,271748).

Érosion rapide

On constate bien que le résultat diffère vraiment de l'original. On voit bien de grandes zones planes apparaître. Et pour la première fois depuis le début, on a un score de bâtiment non nul sans être démentiel (score de bâtiment : 0,032020).

Voir l'implémentation de l'érosion rapide.

Transformation en île

Une des caractéristiques voulues pour un jeu vidéo est que l'univers de jeu doit être limité. Et pour cela, la méthode la plus courante, en particulier dans les RPG, est de jouer sur une île. Jusqu'à présent, notre terrain n'était pas une île. Nous allons voir deux techniques pour transformer un terrain quelconque en île.

Par les bords

La première technique, que j'ai imaginée moi-même (pour une fois), consiste à replier les bords de la carte. Mais pas n'importe comment. Si on applique un facteur linéaire en fonction de la distance au bord, on obtient des côtes droites. Sans compter que ça peut créer une discontinuité là où on a commencé à appliquer le repliement. J'ai donc essayé plusieurs fonctions.

Bord de cartes

J'ai commencé par la racine carré qui donnait des résultats plutôt satisfaisants mais on observait toujours cette discontinuité à la limite du repliement. Il me fallait donc une fonction qui ait une dérivée nulle en 1 (ce n'est pas obligatoire en 0, puisque c'est le bord de la carte donc la discontinuité ne se voit pas). Et là, on pense de suite à une fonction trigonométrique et en l'occurrence, sinus qui présente le bon profil. Le résultat commençait à devenir intéressant même si on observait des côtes droites. Cela vient du fait que la pente de la courbe sinus est supérieure à 1, ce qui fait que de petits changements sur la carte d'origine sont complètement occultés. L'idéal est donc d'avoir une pente inférieure à 1 mais un truc genre sinus. Et donc, j'ai combiné le sinus et la racine carrée pour obtenir le résultat que je souhaitais.

Îlification

Voir l'implémentation de la transformation en île par les bords.

Par le milieu

La seconde technique, qui m'a été inspirée, consiste à multiplier notre carte par une fonction gaussienne en deux dimensions, la fameuse cloche. Simple, efficace, on peut également régler l'écartement pour ajuster l'île comme on le souhaite. Le principal inconvénient est que ça force quand même les îles à avoir une forme… de cloche.

Gaussification

Voir l'implémentation de la transformation en île par le milieu.

MapMaker

MapMaker est un logiciel que j'ai concocté pour expérimenter toutes ces techniques. Il permet à partir d'un fichier YAML de décrire un pipeline d'algorithmes, en partant d'un générateur puis en appliquant des modificateurs et enfin éventuellement un finaliseur.

Actuellement

Actuellement, toutes les techniques décrites ici ont été implémentées et fonctionnent (enfin, j'espère). MapMaker produit des fichiers au format portable pixmap qui a l'avantage d'être facile à générer même s'il n'est pas très optimal. Ensuite, convert est votre ami. Vous pouvez d'ailleurs voir les fichiers correspondant à tous les exemples présentés dans cet épisode si vous voulez une idée de comment ça se présente en vrai.

Pour la carte du tout début, j'ai commencé par la carte issue du diamant-carré. Puis j'ai appliqué une érosion rapide, puis un léger aplatissement (flatten) qui a tendance à creuser les vallées et que j'ai piqué ailleurs. Ensuite, j'ai appliqué un peu d'érosion thermique, histoire de créer des passages entre les plateaux créé par l'érosion rapide. Puis un petit lissage (smooth) qui est le lissage trivial que l'on fait en traitement d'image. Enfin, j'ai transformé mon terrain en île, en utilisant l'algorithme par les bords. Bon, on peut sans doute faire mieux, et j'ai tâtonné pour arriver à ce résultat, mais ça me convient.

Je n'ai pas cherché à optimiser la vitesse de génération ou la taille des cartes. Quand on manipule des cartes de 512x512, composé de double, ça fait la carte à 2Mio. On peut considérer que c'est beaucoup, ou que c'est peu, suivant le contexte. Pour avoir un ordre d'idée, en utilisant mon laptop de développement (Dell latitude E5530), voici quelques chiffres pour la génération de la carte du début. Pour le temps de génération :

generator: 'diamond-square'
size: 513 x 513
duration: 43 ms
modifier: 'fast-erosion'
duration: 1064 ms
modifier: 'flatten'
duration: 21 ms
modifier: 'thermal-erosion'
duration: 19 ms
modifier: 'smooth'
duration: 6 ms
modifier: 'islandize'
duration: 11 ms

Ce qui est tout à fait raisonnable. Valgrind me dit que j'utilise un peu moins de 158Mio de mémoire (en cumulé). Là en revanche, ça pourrait être mieux.

Sur la même carte mais en version 8193x8193, c'est-à-dire 256 fois plus grande :

generator: 'diamond-square'
size: 8193 x 8193
duration: 8772 ms
modifier: 'fast-erosion'
duration: 324826 ms
modifier: 'flatten'
duration: 5662 ms
modifier: 'thermal-erosion'
duration: 5691 ms
modifier: 'smooth'
duration: 1920 ms
modifier: 'islandize'
duration: 3143 ms

Il faudrait faire plus de tests mais ça a l'air de passer à l'échelle de manière linéaire. Je n'ai pas fait de Valgrind mais je pense que en mémoire ça passe à l'échelle également de manière linéaire, ce qui nous ferait dans les 40Gio (en cumulé). Bon d'accord, pour la prochaine version, je vais m'occuper de cet aspect.

Possibilités

Les possibilités d'extension sont nombreuses. Je ne sais pas si je vais les implémenter, mais je les mets ici pour mémoire.

Tout d'abord, on peut expérimenter des bruits plus récents, tel que le wavelet noise (qui a quand même l'air assez difficile à implémenter). On peut aussi expérimenter des méthodes alternatives de génération ou de modification si on a beaucoup d'idées.

La limite la plus visible, c'est le pipeline. En vrai, on aimerait bien avoir un graphe d'opérateurs qu'on pourrait manipuler et brancher à travers une interface graphique. Et bien, bonne nouvelle, ce genre d'outil existe ! Ça s'appelle World Machine mais malheureusement, c'est propriétaire. Mais ça permet de faire des choses assez complexes. Je dois avouer que je n'ai pas les capacités pour faire ce genre de choses, je suis parfaitement inexpérimenté en interface graphique, mais, il existe des tutoriaux pour Qt5 qui pourraient servir de base.

Bon, mais c'est bien gentil ces cartes d'altitude, mais pour l'instant, ce n'est pas très exploitable en l'état. C'est l'objet de la deuxième partie de cet épisode : voir comment transformer une carte d'altitude en un truc jouable. Et du coup, je pourrai remplacer ma vieille île toute pas belle en quelque chose de plus joli. Mais ça, ça sera la prochaine fois (et ça sera sans doute moins long) !

Pour aller plus loin

Pour finir, voici quelques liens pour ceux qui veulent aller plus loin.

Tout d'abord, je ne saurais trop vous conseiller d'aller voir la libnoise. La bibliothèque en elle-même est un peu vieille et l'implémentation est faite pour du bruit 3D, ce qui n'est pas toujours adapté pour du bruit 2D (ça complexifie et ça alourdit les calculs). Mais les tutoriaux sont très pédagogiques pour bien comprendre ce qu'est le bruit cohérent. De même que l'exemple d'une planète complète construite avec de multiples générateurs et modificateurs.

Pour ceux qui ne maîtrisent pas la langue de la perfide albion, il y a le tutoriel de Jérémy Cochoy qui est très bien fait. Il décortique le bruit de Perlin mais aussi le bruit à base de simplexe. C'est très progressif, il y a beaucoup d'illustrations, bref, un bon point d'entrée.

Notons aussi le projet Fractal Terrain Generation sur Google code qui, à défaut de fournir beaucoup de code, a un excellent wiki avec des explications sur comment implémenter divers types de bruit ainsi que les méthodes à base de placement de point.

Enfin, pour tout savoir sur la notion de bruit, il y a toujours le tutoriel du Red Blog Games qui vous fera voir du bruit de toutes les couleurs. Le même Red Blog Games fournit également un tutoriel pour la création de cartes polygonales dont j'ai déjà parlé mais qui est un véritable délice.

Journal Akagoria devient un jeu indie propriétaire

Posté par . Licence CC by-sa
27
1
avr.
2014

Cher journal,

Tu as suivi les tribulations de la création de ce jeu vidéo depuis le début et il va y avoir un peu de changement. J'ai décidé de quitter le monde amateur du libre pour passer entièrement à un développement indie propriétaire afin de gagner ma vie avec. Le développement va donc continuer dans une structure entièrement professionnelle à laquelle je vais me consacrer à 100%. Je serai accompagné de Naha qui a déjà réalisé quelques graphismes pour le jeu et qui est prêt à sauter le pas.

À terme, le jeu va se transformer en un MMORPG et sera financé d'une part par des goodies interne au jeu qui permettront au joueur de progresser plus vite et d'avoir de plus jolies tenues que celles proposées par défaut, d'autre part par des abonnements permettant d'avoir un accès privilégié aux serveurs de jeu.

Évidemment, la série d'épisodes va s'arrêter de manière à garder l'exclusivité du jeu à ceux qui paieront. Cette décision va permettre un gain de productivité pour l'ensemble de la communauté qui n'aura plus à subir des dépêches longues et techniques et qui pourra retourner à des activités plus saines.

Journal Modèle économique dans les jeux libres

18
15
avr.
2014

Bonjour à tous,

Ce week-end, j'ai fait une petite virée à Lyon pour tenir un stand de Jeux Libres aux JDLL 2014.
Le stand Jeux Libres proposait de découvrir 0AD et Battle of Wesnoth. Geeky Goblin Productions étaient sur le stand juste à côté de moi. Il présentait évidemment Ned et les maki. Devnewton n'était pas présent.

Parmi les sujets abordés tout au long du week-end, nous avons discuté avec Pierre et Alexandre du modèle économique le plus adapté pour se rémunérer au moins un minimum.
Qu'est-il possible de mettre en place en France pour que l'artiste (le développeur, le game designer, …) qui donne de son temps libre (ou pas) sur une œuvre vidéo-ludique obtienne une reconnaissance pécuniaire ?
Je suis tombé sur cet article et j'ai pris quelques informations sur la fondation ici et .
Le sujet me semble suffisant imposant pour en parler ici et lancer une réflexion sur LinuxFr. Si quelque chose se met en place, ce sera avec la communauté. A vos claviers, donc, pour lancer cette réflexion.

Du côté de Toulouse et pour le prochain Capitole du Libre, j'espère que nous pourrons proposer avec Toulibre un track "Jeux Libres" et faire découvrir au public qu'il existe aussi dans le libre une alternative aux jeux vidéos des grands studios de développement.

Dernier jour pour proposer une conférence "Jeux Libres" pour les RMLL de juillet prochain.
N'hésitez pas à vous proposer.

Je crée mon jeu vidéo E11 : génération procédurale de carte (partie 2)

Posté par . Édité par Benoît Sibaud, Xavier Claude et palm123. Modéré par patrick_g. Licence CC by-sa
Tags :
52
28
avr.
2014
Jeu

«Je crée mon jeu vidéo» est une série d'articles sur la création d'un jeu vidéo, depuis la feuille blanche jusqu'au résultat final. On y parlera de tout : de la technique, du contenu, de la joie de voir bouger des sprites, de la lassitude du développement solitaire, etc. Vous pourrez suivre cette série grâce au tag gamedev.

Dans l'épisode 10, on a fabriqué des cartes d'altitude avec diverses méthodes et divers opérateurs qui permettent de rendre la carte plus réaliste. Dans cette deuxième partie de ce double épisode consacré à la génération de carte pour un RPG, on va décorer notre carte.

Sommaire

En retard

Avant toute chose, cet épisode a pris beaucoup plus de temps que prévu. La fatigue de la vie quotidienne est un élément qu'il ne faut jamais sous-estimer, surtout quand elle s'accumule. Elle réduit considérablement la capacité à avancer, malgré une motivation intacte. C'est ce qui m'est arrivé ces dernières semaines, ce qui a provoqué un énorme retard. Pourtant, les tâches étaient simples et claires, il y avait un peu de travail algorithmique, comme nous allons le voir, mais rien de vraiment insurmontable. Juste qu'une fois devant le clavier, je n'arrivais plus à taper les choses efficacement, tout mettait des heures alors que ça aurait dû mettre quelques minutes.

Les vacances actuelles seront sans doute profitables pour me remettre sur les rails. En tout cas, elles m'ont déjà permis de finir cet épisode, même s'il est assez loin de ce que j'avais imaginé au départ. Parce qu'il reste du travail sur ce sujet.

Rappel de l'objectif

L'idée de ce double épisode est d'être capable de générer des cartes pour un RPG. Plus précisément, on utilisera notre carte comme un fond, étant donné que le jeu utilise la vue de dessus. Ça veut donc dire que cette carte sera très présente sur l'écran et qu'il faut la préparer au mieux. Pourquoi la générer ? Il ne s'agit pas ici de générer toute la carte dans ses moindres détails, mais simplement de produire un canevas assez précis qui puisse être manipulé par la suite. Toute la question est dans cette précision.

Le premier épisode permettait de générer une carte d'altitude, ce qui est une première étape essentielle. Cette carte d'altitude avait de bonnes caractéristiques, par exemple en terme de déplacement possible des unités, ce qu'on avait matérialisé par le score de jouabilité. On avait donc à notre disposition une carte d'unité dont on va se resservir ici.

La carte de base

Pour cet épisode, on va voir trois aspects essentiels : la génération des rivières, la génération des biomes, la génération des formes de collision. Puis on discutera de ce qu'on peut apporter de plus.

Les étapes de construction

Génération des rivières

La génération des rivières est sans doute la partie la plus facile. Il s'agit d'ajouter à notre carte des rivières pour la rendre un peu plus réaliste. Et puis le franchissement de rivière peut devenir une activité importante quand on joue à un RPG.

L'algorithme utilisé ici est extrêmement simple, tout en donnant des résultats pas trop mauvais. L'idée est de partir d'un point en altitude (on peut fixer l'altitude minimale) puis de suivre la pente la plus forte jusqu'à arriver à la mer. Ça a l'air simple comme ça mais ça ne l'est pas. Parce qu'il se peut que l'eau se retrouve piégée dans un minimum local, et il faut donc savoir comment sortir de ce minimum local sans effondrer les performances de la génération.

La solution la plus efficace est d'utiliser une file de priorité. Pour chaque case ajoutée à la rivière, on ajoute toutes les cases adjacentes à la file avec comme priorité l'altitude la plus faible. Ainsi, on a toujours en tête de file la case qui permet de descendre le plus. Ainsi, quand on se retrouve dans un minimum local, cet algorithme va permettre de faire monter l'eau petit à petit jusqu'à trouver un endroit par où descendre un peu plus. On crée ainsi des lacs naturels.

Ensuite, on itère un certain nombre de fois pour avoir plusieurs rivières. Si on repart à chaque fois de la carte initiale, certaines rivières vont parcourir les mêmes cases, créant ainsi des affluents.

La carte avec des rivières

Voilà ce que ça peut donner avec vingt rivières. On voit en particulier le lac qui s'est créé au nord-est et comment la rivière longe la colline. Et on observe aussi qu'avec six rivières, on arrive à former un fleuve avec ses affluents à l'ouest.

Génération des biomes

Un biome est, d'après Wikipédia, «un ensemble d'écosystèmes caractéristique d'une aire biogéographique et nommé à partir de la végétation et des espèces animales qui y prédominent et y sont adaptées». Un biome est caractérisé par un climat, lui-même étant associé à des températures et des précipitations. Pour savoir quel biome correspond à quelle fourchette de températures et de précipitations, on utilise généralement le diagramme Whittaker.

Sur notre carte, nous n'avons ni températures, ni précipitations. Mais nous avons des altitudes et des rivières ! On va considérer que plus on monte en altitude, plus il fait froid. En vrai, la température baisse entre 0.6 et 1.0°C tous les 100m, donc l'approximation n'est pas si insensée. Il reste donc à calculer l'humidité, et là, on va considérer que plus on est loin d'un point d'eau (rivière ou mer), moins on est humide. Pour cela, on fait un parcours en largeur de toutes les cases, en commençant aux points d'eau et on décrémente l'humidité petit à petit.

La carte d'humidité

On obtient une carte d'humidité : plus c'est clair, plus c'est humide. Et inversement, les zones sombres indiquent des zones très sèches.

Ensuite, en fonction de l'altitude et de l'humidité, on doit déterminer un biome. On s'inspire pour cela du diagramme de Whittaker qu'on adapte. J'ai utilisé la même adaptation que celle du tutoriel d'Amit Patel. On peut le visualiser de la manière suivante.

Les biomes

En abscisse, on a l'humidité (plus on va à droite, plus c'est humide), et en ordonnée, on a l'altitude (plus on va en haut et… plus on monte logique). Chaque couleur représente un biome différent. Depuis les forêts tropicales jusqu'au sommets enneigés, en passant par les steppes, tout y est à peu près.

Reste ensuite à appliquer les biomes en fonction de la carte d'altitude et de la carte d'humidité qu'on vient de calculer. Et tadam !

La carte des biomes

Ou, si on veut une version avec du relief :

La carte des biomes en relief

On se rend compte que la zone quasi-désertique au sud-est correspond à une dépression où il n'y a pas de rivière. On voit également les montagnes qui se dégagent avec les zones neigeuses.

Générations des formes de collisions

Un des objectifs de cette génération de carte est de pouvoir générer automatiquement les formes de collisions. Parce que les faire à la main peut être assez long et fastidieux. Dans l'épisode précédent, on avait vu comment on générait une carte d'unité, c'est-à-dire un ensemble de cases accessibles à une unité dont on précise à la fois la taille (en nombre de cases) et la capacité à franchir les pentes.

La carte d'unité

La carte d'unité est donc une carte binaire. Il reste à déterminer les polygônes qui forment les contours infranchissables, c'est-à-dire la limite des zones blanches et noires. Je dois avouer que c'est un des algorithmes qui m'a demandé le plus de réflexion et dont je suis assez fier. Voici comment j'ai procédé.

Pour chacune des cases accessibles (les cases blanches), j'ai ajouté quatre arcs orientés qui représentent le contour de cette case à un premier ensemble E1. Toutes les cases sont orientés dans le même sens, ici le sens des aiguilles d'une montre. Ensuite, il faut enlever des arcs pour ne garder que ceux des contours des zones blanches. L'idée est ici d'enlever les arcs (A,B) tels que l'arc (B,A) fait également partie de l'ensemble E1. En effet, si on trouve ces deux arcs, c'est qu'ils ont été ajoutés par deux cases adjacentes et donc, ils font partie de l'intérieur de la zone. Nous obtenons alors un ensemble E2 qui contient tous les contours mais en petits morceaux. Dernière étape, il faut donc reconstituer les contours ! Pour ça, tant que E2 n'est pas vide, on prend un arc, puis on cherche le ou les arcs qui ont comme origine l'extrêmité de cet arc.

Un contour

Je dis «le ou les» parce qu'il se peut qu'il y en ait deux, dans le cas où deux cases sont reliées uniquement par un sommet, comme on peut le voir sur la figure ci-dessus. Il faut alors faire un choix non-aléatoire : un choix aléatoire pourrait nous faire tourner en rond assez longtemps. Pour maximiser la taille du contour, on choisit de toujours tourner à gauche. Comme on parcourt le contour dans le sens des aiguilles d'une montre, on inclut dans chaque zone les cases qui sont reliées uniquement par un sommet et donc on minimise le nombre de contours. On itère tout ça et on obtient nos contours.

Cet algorithme est assez efficace si on prend garde à utiliser les bonnes structures de données et les bons algorithmes. S'il y a n1 cases accessibles, c'est-à-dire 4*n1 arcs dans l'ensemble E1, la construction de E2 peut se faire en O(n1 log n1), grâce à un tri préalable des arcs, puis à une recherche dichotomique pour essayer de trouver l'arc inverse pour chaque arc. Ensuite, s'il y a n2 éléments dans E2, on peut reconstruire les contours en O(n2 log n2) de la même manière, c'est-à-dire en triant les arcs et en faisant une recherche dichotomique pour trouver le ou les prochains arcs. Au final, on n'est jamais quadratique, ce qui est une performance tout à fait honorable à mon sens.

Cette fois, on a tout ce qu'il nous faut en terme de données.

La carte prête à l'emploi

Il faut désormais générer la carte prête à l'emploi, c'est-à-dire au format TMX de Tiled, et plus précisément avec les conventions retenues pour le jeu lui-même.

Le problème fondamental

Tiled est un très bon logiciel et il permet de faire plein de choses. Il a notamment une brosse qui permet de créer des «terrains» de manière très facile, une fois qu'on a indiqué sur le tileset quel morceau de tuile était associé à quel terrain. La brosse est capable de générer des gros morceaux de carte de manière à ce que les cases adjacentes de la carte aient des terrains identiques. Tout ceci est bien expliqué dans un tutoriel pour créer les terrains et utiliser la brosse.

Pour pouvoir bénéficier de cette fonctionnalité le jour où on passera à l'édition manuelle de la carte, il nous faut deux choses. Premièrement, déterminer les terrains. Ça, c'est la partie facile, parce qu'on peut identifier les biomes aux terrains et donc, on a autant de terrains que de biomes. Deuxièmement, il faut avoir un tileset adapté à cette brosse. Et ça, c'est la partie difficile. Parce qu'on voit bien dans le tutoriel que ce qui marche bien, c'est quand la limite entre deux terrains est sur la tuile, puisque c'est ça qui va permettre à l'outil de pouvoir générer des zones complètes.

Or, notre carte, pour l'instant, ce sont uniquement des cases monochromes. Au départ, j'avais pensé à modifier la carte pour étendre certaines zones d'une demi-case. Ça permettait notamment d'élargir les rivières facilement. Le problème, c'est qu'on arrive très vite à des cas particuliers impossibles à démêler. On doit prendre en compte au moins les huit cases adjacentes (directement ou par un coin) et donc, si on prend en compte qu'on peut avoir autant de biomes que de cases, ça fait un nombre de cas beaucoup trop importants. La solution est toute bête, il suffit de décaler notre grille d'une demi-case suivant les deux axes ! Et pour faire simple, on sacrifie une rangée sur le bord. Ainsi, toutes les limites de biomes sont maintenant au milieu des cases.

Notre nouvelle carte est alors composée de tuiles, elles-mêmes découpés en quatre parties qui ont les couleurs de quatre cases de la carte originale. Évidemment, on va trouver toute sorte de tuiles et il va bien falloir déterminer le tileset.

Détermination du tileset

Comme notre carte est générée, il faut également générer le tileset qui sera utilisée pour la carte. Pour ça, il faut déterminer l'ensemble de tuiles qui a été utilisé. Au départ, on place déjà dans le tileset les tuiles pleines qui seront les référents pour chacun des terrains et qui les identifie dans Tiled.

Ensuite, on parcourt toute notre carte et pour chaque tuile nouvelle, on l'ajoute à notre ensemble de tuiles. Puis, une fois qu'on a fini, il ne reste plus qu'à générer l'image avec toutes les tuiles, comme les attends le format TMX. Ici, en l'occurrence, les tuiles font 32x32 à l'affichage mais à cause d'un pseudo-bug, les tuiles font en fait 34x34. Le bug provoquait des petits défauts d'affichage, on voyait notamment la couleur de la tuile voisine à la limite des tuiles, ce qui était assez désagréable. Je crois avoir lu que ce bug vient d'une erreur d'arrondi dans les nombres flottants, voilà pourquoi je parle de pseudo-bug. La solution que j'avais trouvée à l'époque, c'était d'agrandir mes tuiles d'un pixels, ce qui permet de masquer ces petits défauts.

Touche finale, plutôt que d'avoir des limites de biomes uniquement horizontales et verticales, on peut modifier un peu les tuiles de manière à avoir des biseaux. Et ce n'est pas si simple. Il y a notamment le cas merdique où la tuile a deux couleurs mais disposées en diagonales. Dans ce cas, il faut donner la priorité à une des deux couleurs pour les joindre. C'est essentiel quand une de ces deux couleurs représente une rivière, on veut que la rivière soit continue. Je passe les détails qui tiennent parfois de la magie sur comment déterminer si on dessine un biseau ou pas, et je passe aussi sur la taille de ce biseau pour que les diagonales joignent parfaitement sans qu'on ne puisse voir les limites des tuiles.

Au final, on obtient ça :

Le tileset final

Évidemment, une fois ce tileset généré, rien n'empêche un artiste de l'améliorer comme bon lui semble. Mais cette version de base peut tout à fait convenir, dans un premier temps, pour le développement du jeu.

La carte finale dans Tiled

Ce moment où on ouvre la carte générée dans Tiled la toute première fois est un moment assez intense. On se demande si on a tout fait correctement, si ça va marcher, si ça ne va pas être trop gros pour Tiled. Et puis tadam, ça s'affiche et c'est merveilleux. Voici quelques captures d'écran pour montrer le résultat dans Tiled.

Tiled

Sur cette première capture, on voit les contours qu'on a calculés précédemment. On retrouve les formes qu'on avait dans la carte d'unité (ce qui est plutôt une bonne nouvelle). On peut également voir le tileset en bas à droite, avec les limites des tuiles, qui a été ajouté par Tiled, ce qui permet de mieux les distinguer.

Tiled

Sur cette seconde capture, on voit exactement le même endroit mais avec les biomes, c'est-à-dire ici l'ensemble des tuiles. En bas à droite, j'ai mis la vue où on peut voir tous les terrains.

Tiled

Sur cette troisième capture, j'ai zoomé sur une des rivières au nord pour observer les limites biscornues entre les biomes qu'on obtient avec les biseaux. Ce n'est pas si mal que ça, non ?.

La carte finale dans le jeu

Évidemment, la carte marche également dans le jeu. Pour vous convaincre, voici une petite capture d'écran avec Kalista à la plage. L'endroit correspond à la troisième capture dans Tiled, au nord de l'île.

Akagoria

Pour les contours, vous devez me faire confiance, ça marche aussi. Presque trop bien d'ailleurs, et nous allons en parler maintenant.

La suite !

Les zones inaccessibles

Dans le jeu, les zones inaccessibles sont bien inaccessibles, aucun problème pour ça. Malheureusement, ces zones ne sont pas distinguées visuellement des autres zones. Ce qui fait qu'on a l'impression de heurter des murs invisibles en permanence. C'est très désagréable. Je suis à la recherche d'un artefact visuel permettant de distinguer ces zones.

J'ai fait quelques essais qui sont pour l'instant non-convaincants. Le premier essai consistait à mettre par dessus chaque tuile inaccessible une autre tuile, partiellement transparente qui assombrissait la tuile du dessous. Outre le fait que ça ne rendait pas très bien visuellement, on n'avait pas l'impression d'une zone infranchissable mais plutôt d'un biome différent. Bref, solution abandonnée. Au second essai, j'ai essayer de placer des objets qui montrerait que la zone est infranchissable, en l'occurrence des cailloux. J'ai donc fabriqué une tuile avec trois cailloux. L'impression visuelle était nettement meilleure, et on distinguait bien la zone infranchissable. Mais avec une tuile unique, on avait des zones très géométriques avec un même motif répété beaucoup trop de fois.

J'ai encore trois idées. La première, c'est de générer plusieurs tuiles de cailloux de manière à éviter cet aspect géométrique. Seulement, à force de voir des cailloux partout, ça risque d'être un peu rébarbatif à la longue. La seconde idée, c'est d'utiliser non pas des tuiles, mais des sprites complets, un peu comme mes arbres de l'épisode 06. On pourrait par exemple avoir des rochers. La difficulté ici consiste à recouvrir exactement la zone infranchissable avec ces sprites mais pas plus, ni moins. Une troisième idée est de se servir des informations de pentes (puisque ces zones correspondent à des endroits trop pentus pour notre unité) et de trouver des tuiles qui puissent rendre compte de la pente. Avec un rebord, par exemple.

Peut-être que la solution viendra d'un mélange de plusieurs idées.

Les décorations

Une autre manière de rendre la carte moins monotone est d'ajouter des décorations. Et là, je pense immédiatement à mes arbres. Pourquoi ne pas tout simplement créer des forêts complètes ? Générer des forêts aléatoirement n'est pas si difficile. Avec les biomes, je sais où elles sont situées, j'ai déjà des sprites d'arbres, yapluka. Ça permettrait d'avoir une première version de la carte pas trop pauvre.

Dans la même idée, on pourrait aussi ajouter des rochers, et pas seulement sur les zones infranchissables. Dans les zones montagneuses par exemple, ça ferait assez couleur locale. Reste à pouvoir générer des rochers aléatoirement. J'ai un peu avancé sur ce point, ça rend assez bien, dans le même style que les arbres en fait, donc ça pourrait marcher.

En termes d'éléments naturels, ça me paraît déjà une bonne base. Mais je suis sûr que les idées vont venir au fur et à mesure. N'hésitez pas à donner vos idées !

D'autres pistes à explorer

Parmi les autres pistes à explorer, il y également la génération de routes. Et là, il faut vraiment que je vous fasse découvrir quelque chose. J'ai eu l'occasion de discuter sur le canal IRC #akagoria avec un doctorant d'une équipe de recherche française qui étudie, entre autre, la génération procédurale de paysage, l'équipe GeoMod du LIRIS à Lyon. Ils ont notamment une plateforme de développement de mondes virtuels, malheureusement propriétaire. Mais on peut voir des vidéos de démonstration assez époustouflantes. Vraiment, je vous les conseille toutes, ça ne dure pas très longtemps et c'est bluffant.

Parmi tous les travaux de cette équipe, il y a notamment des travaux sur la génération de routes, ainsi que sur la génération de réseaux routiers hiérarchiques. On peut retrouver une partie de ces travaux dans la thèse d'Adrien Peytavie sur la génération procédurale de monde (en français). Ça donne une bonne base pour générer des routes, même si les algorithmes sont décrits très succinctement.

Surtout, les discussions que j'ai eues avec ce doctorant m'ont ouvert des pistes de réflexion intéressantes. Il regrettait (et je ne peux pas l'en blâmer) que la plupart du temps, la génération procédurale de paysage suit le même schéma : celui que j'ai présenté au cours de ces deux épisodes. Je partage son analyse. J'ai choisi cette voie par facilité, parce que je savais qu'elle donnerait des résultats suffisamment convaincants. Mais je pense comme lui qu'on peut faire mieux que le bruit de Perlin et le diagramme de Whittaker. Notons bien que ce couple est utilisé dans de nombreux jeux, dont le célèbre Minecraft.

Il défendait l'idée que c'était une erreur de faire de la génération de paysage à grain fin directement, comme je l'ai fait. Les algorithmes utilisés ne conviennent pas pour traiter à la fois les paysages globaux (des montagnes, des plaines) et les détails (un arbre, un cailloux). Il proposait plutôt de procéder en plusieurs étapes, tout d'abord déterminer un paysage global avec du bruit de Perlin ou autre, ce qui permet de déterminer des zones globales. Puis, dans chaque zone, d'utiliser à nouveau du bruit pour générer les détails. Pour donner un exemple concret, si on considère que chaque case de ma carte initiale représente non pas une tuile au final mais un grand carré de tuiles, on aurait alors la possibilité, pour chaque carré de tuiles d'ajuster les limites de biomes avec du bruit de manière à avoir des formes un peu moins rectilignes (même avec les biseaux). J'ai trouvé cette idée très intéressante même si le temps me manque pour l'expérimenter dans l'immédiat. Et dans son esprit, on pouvait même avoir plusieurs niveaux dans ce genre.

Le deuxième point qui mériterait sans doute un peu de travail, c'est d'oublier le diagramme de Whittaker. En effet, ce diagramme est très pratique mais il donne des paysages hautement irréalistes ! Qui a déjà vu un désert, une taïga, une forêt tropicale et des steppes sur une île aussi petite ? Bon, on est dans un jeu donc on peut faire un peu comme on veut, mais quand même. On pourrait aussi rester dans un climat tempéré, et donc limiter le nombre de biomes, et ça ne serait pas moins intéressant. On pourrait partir sur l'étagement et avoir toute une série de climat et de végétation très intéressante. Je crois que je vais approfondir ce point quand j'aurai un peu de temps parce qu'il me semble qu'il peut donner des résultats plus «réaliste» que le diagramme de Whittaker, si on peut parler de «réalisme» dans un jeu de cette nature.

Conclusion

Voilà donc où nous en sommes rendus : nous avons une carte de base qui demande encore quelques ajustements avant de pouvoir entrer dans la phase d'édition manuelle. Je n'ai pas encore tout pushé sur les dépôts git mais ça sera sans doute fait entre le moment où j'écris ces lignes et le moment où elles seront publiées. En particulier, j'ai mis tout ce que j'ai décrit ici dans un binaire à part dans le projet mapmaker de manière à séparer ce qui relève des algorithmes génériques (ceux de la partie 1) et des algorithmes spécifiques au jeu (ceux de la partie 2).

Pour la suite immédiate, je vais laisser cet aspect en plan et je vais m'intéresser aux inputs. Parce que ma réflexion sur les dialogues (prochain point dans ma feuille de route), ainsi que la bibliothèque jnuit de devnewton, m'ont amené à y réfléchir et de manière plus générale à réfléchir aux interactions entre mon personnage et mon univers. Sur les dialogues, les questions arrivent très vite : qui initie le dialogue ? Est-ce qu'il faut appuyer sur un bouton ? Comment détecter qu'on est bien à côté de quelqu'un qui veut dialoguer ? Comment gérer les dialogues interactifs ? L'exemple, donné dans un commentaire sur un épisode précédent, des dialogues du jeu Andor's Trail donne une piste.

Bref, encore de nombreux épisodes en perspective !

Je crée mon jeu vidéo E12 : interfaces physiques et graphiques

Posté par . Édité par 4 contributeurs. Modéré par Ontologia. Licence CC by-sa
52
20
août
2014
Jeu

«Je crée mon jeu vidéo» est une série d'articles sur la création d'un jeu vidéo, depuis la feuille blanche jusqu'au résultat final. On y parlera de tout : de la technique, du contenu, de la joie de voir bouger des sprites, de la lassitude du développement solitaire, etc. Vous pourrez suivre cette série grâce au tag gamedev.

Dans l'épisode 11, on a décoré notre carte, et même si elle n'est pas encore dans un état jouable, elle constitue une bonne base pour la suite. Pour ce retour de vacances, on va s'intéresser aux interfaces physiques et graphiques du jeu.

Sommaire

Introduction

Contrairement à ce que j'avais espéré, les vacances n'ont pas été très productives pour mon jeu. Je comptais passer du temps dessus de manière à avoir des avancées significatives, mais il n'en a rien été. J'ai seulement pu faire quelques tests dont je parlerai sans doute dans un prochain épisode. C'est très frustrant ce genre de situation.

Ça ne m'empêche pas de continuer mes réflexions (et mes bouts de code) et de les partager. Aujourd'hui on va discuter des interfaces :

  • les interfaces physiques, celles qui permettent de commander les actions dans le jeu (input),
  • et les interfaces graphiques, celles qui rendent compte d'élements du jeu (output).

Et comme d'autres y ont réfléchi avant moi, je me suis largement inspiré de jnuit, créé par devnewton pour ses jeux en Java.

Les interfaces physiques

À la base, je ne savais pas trop quoi utiliser pour l'interface physique. Quand on pense RPG, on imagine que ça se manipule à la souris et/ou au clavier. Mais avec les RPG sortis sur console, ce n'est plus le cas. Et puis bon, il y a devnewton qui veut jouer à la manette. Du coup, allons-y, essayons de contenter tout le monde.

SFML gère les principales interfaces physiques rencontrées dans la nature : clavier, souris, manettes. Donc, de ce côté là, on n'aura pas trop de problème. Le seul problème est de trouver un moyen de gérer tous ces périphériques de manière à peu près commune et de s'éviter de longs switch redondants et désagréables, c'est-à-dire trouver le bon niveau d'abstraction.

Pour les interfaces physiques, on trouve deux notions dans jnuit que j'ai reprises. Celle de contrôleur et celle d'action.

Contrôleur

Un contrôleur, c'est juste l'abstraction d'une interface physique. Dans jnuit, le contrôleur fournit une valeur qui indique son état. Le contrôleur est alors couplé à un détecteur qui va superviser cet état et dire si le contrôleur est actif ou pas.

L'inconvénient (à mon sens) dans jnuit est qu'on est en mode polling, c'est-à-dire qu'on va demander l'état du contrôleur à chaque tour (par exemple : « est-ce que le bouton droit de la souris est appuyé ? »). L'autre mode, c'est le mode événement, c'est-à-dire qu'on regarde les événements qui se sont produits (comme par exemple un appui sur un bouton de souris) et on enregistre le nouvel état à ce moment là. J'ai une nette préférence pour le mode événement que je trouve plus naturel, mais c'est une question de goût. SFML n'est pas casse-pied et propose les deux modes de toute façon.

L'autre chose qui me chagrinait dans l'approche de jnuit, c'est cette distinction entre le contrôleur et son détecteur. La différence est très subtile mais trop subtile pour moi, alors j'ai fusionné les deux notions dans une seule : un contrôleur dit s'il est actif ou pas. Et il met à jour son état en scrutant les événements renvoyés par SFML.

Action

Une action est une abstraction d'une… action qui peut être déclenchée par le joueur — par exemple « sauter ». Une action est provoquée par un ou plusieurs contrôleurs (comme le bouton droit de la souris ou la lettre J du clavier). Comme pour les contrôleurs, il existe aussi un détecteur qui va se charger de vérifier si l'action est active ou pas, suivant l'état des contrôleurs associés. La règle est simple, il suffit d'un seul contrôleur activé pour activer l'action.

Outre le fait que la différence entre l'action et son détecteur soit une fois de plus trop subtile pour moi, j'ai trouvé qu'il manquait un élément important dans cette abstraction. En effet, une action peut être continue ou pas. Prenons deux exemples pour voir la différence. Quand j'appuie sur une flèche, je souhaite que mon personnage avance tant que j'appuie sur la flèche : c'est ce que j'appelle une action continue. En revanche, quand j'appuie sur J, je veux que mon personnage saute une fois, même si je continue d'appuyer sur la touche : c'est ce que j'appelle une action instantanée (non-continue). C'est le même contrôleur (une touche de clavier), mais la manière de le gérer est différente. Dans un cas, je veux que l'action soit active tant que le contrôleur est actif, et dans l'autre cas, je veux que l'action soit active une seule fois même si le contrôleur reste actif.

Et avec tout ça…

Une fois qu'on a des contrôleurs pour tous les périphériques, on peut alors définir des ensembles d'action, dont certains qu'on va retrouver à peu près partout. L'exemple le plus classique est l'ensemble d'actions qui permet de naviguer dans une interface graphique : haut, bas, gauche, droite, accepter.

En tout cas, cette double notion contrôleur/action est très pratique et offre le niveau d'abstraction suffisant pour définir l'interaction entre le joueur et le jeu. Du coup, ajouter la gestion de la manette, c'est juste ajouter un contrôleur à une action existante et rien d'autre ne change. C'est simple et ça répond au besoin initial.

Les interfaces graphiques

Pourquoi les interfaces graphiques de jeux vidéos sont-elles particulières ? Il y a deux raisons :

  1. Premièrement, à cause du mode de fonctionnement de l'affichage. Dans une interface graphique de bureau, l'affichage est mis à jour de temps en temps en fonction d'événements. Dans un jeu vidéo, on affiche une frame tous les soixantièmes de seconde et on la redessine à chaque fois, on doit donc redessiner notre interface complètement.
  2. Deuxièmement, à cause des interfaces physiques. Une interface graphique de bureau classique est prévue pour être utilisée avec une souris. Dans un jeu vidéo, la souris n'est pas obligatoire, il faut donc pouvoir piloter l'interface graphique avec toutes les interfaces physiques possibles — essentiellement le clavier et la manette.

Évidemment, je ne suis pas le premier à avoir réfléchi à tout ça ; il existe donc déjà une tétrachiée de bibliothèques d'interfaces graphiques pour SFML (de qualités inégales, d'ailleurs) :

J'ai décidé de réaliser ma propre bibliothèque d'interface graphique. Pour deux raisons. La première raison, c'est que les bibliothèques existantes intègrent complètement la gestion des événements. Or, sachant que j'ai déjà mes contrôleurs et mes actions, je veux les utiliser comme bon me semble et ne pas dépendre des choix faits par la bibliothèque. La deuxième raison, qui se rapproche de la première, c'est que ces bibliothèques intègrent complètement le dessin des widgets, parfois à l'aide d'un langage du genre CSS. Vous devez vous dire que je suis un peu idiot de refuser qu'une bibliothèque de widgets dessine ses widgets. En fait, le fait de découpler les widgets de leur affichage n'est pas si idiot : il permet de customiser l'affichage pour chaque jeu. Avoir un interpréteur de CSS juste pour afficher quelques widgets, ça fait un peu trop usine à gaz à mon goût.

L'intérêt d'avoir sa propre bibliothèque, c'est qu'on peut piquer une excellente idée de jnuit, à savoir offrir les widgets standard des jeux, et notamment celui qui gère la résolution de l'écran. Ça permet aussi de voir comment on programme ce genre de logiciel assez particulier qu'est une interface graphique. On en vient à se poser les mêmes questions que ses prédécesseurs et au final y apporter les mêmes réponses. Bref, c'est un exercice assez sympathique que tout le monde devrait avoir fait au moins une fois dans sa vie (comme écrire un compilateur), même si ça s'éloigne beaucoup du jeu vidéo au final.

lisuit, SFML User Interface Toolkit

Je vous présente donc SUIT (SFML User Interface Toolkit) (ou lisuit suivant mon humeur) qui est le résultat de toutes ces réflexions. SUIT vient avec la documentation de l'API, une micro documentation générale et quelques exemples (nommés respectivement spade, heart, diamond et club, hahaha) et qui permettent de voir quelques fonctionnalités de la bibliothèque. club notamment montre le widget de configuration de la vidéo.

widget de configuration de la vidéo

Autres nouvelles en vrac

La documentation des API de mes bouts de code

Comme vous avez pu le voir plus haut, j'ai mis en ligne la documentation des API des bibliothèques que j'ai écrites pour ce jeu, sur l'espace mis à ma disposition par github. Ça concerne libes (la bibliothèque pour faire de l'entités-composants-systèmes), libtmx (la bibliothèque pour lire les fichiers TMX produit par Tiled), et libsuit donc.

Un canal IRC pour parler de jeux libres

J'ai rejoint il y a peu le canal IRC #jeuxlibres sur Freenode. Ce canal doit exister depuis un moment mais il n'était pas très connu. Après avoir reçu une invitation, j'ai rejoint ce canal avec quelques autres personnes et au final, ce canal est maintenant assez animé. Nous sommes une grosse dizaine et il y a de temps en temps des débats assez intéressants. Si vous aimez les jeux libres, n'hésitez pas à venir nous rejoindre !

Message personnel

Enfin, je profite de cette nouvelle pour passer un petit message personnel, une fois n'est pas coutume. En fait, cet été, quelqu'un s'est aperçu que j'étais « le rewind de linuxfr ». Il me connaissait depuis des années via ce biais, et IRL depuis moins longtemps par les hasards de la vie, et il a fait le lien cet été avec des yeux ébahis. C'était très drôle à voir. Donc, coucou à Gérald (qui n'a pas voulu me révéler son compte linuxfr) ;)

Je crée mon jeu vidéo E13 : un an, premier bilan

Posté par . Édité par 5 contributeurs. Modéré par Nils Ratusznik. Licence CC by-sa
34
16
sept.
2014
Jeu

« Je crée mon jeu vidéo » est une série d'articles sur la création d'un jeu vidéo, depuis la feuille blanche jusqu'au résultat final. On y parle de tout : de la technique, du contenu, de la joie de voir bouger des sprites, de la lassitude du développement solitaire, etc. Vous pourrez suivre cette série grâce au tag gamedev.

Dans l'épisode 12, on a parlé des interfaces graphiques et physiques. Dans cet épisode anniversaire, on va faire un premier bilan global de l'état du jeu. Et on discutera aussi d'autres événements liés aux jeux vidéos et qui me concernent et en quoi ça peut aider Akagoria.

Sommaire

Akagoria au bout d'un an

Il y a un an le 16 septembre 2013 était publié le premier épisode de cette série. Et même si Akagoria est né sans doute un peu avant, on peut considérer que sa date de naissance est bien ce premier épisode. Bon, ok, j'ai releasé early, mais pas très often il faut bien l'avouer. Au départ, je me suis donné deux à trois ans pour mener à bien cette aventure. Un an s'est déjà écoulé et je pensais que j'aurais avancé plus que ça. Donc, on va dire plutôt trois ans.

Les réussites

Dans ce qui a bien fonctionné, il y a tout d'abord ces épisodes. Douze épisodes ont été publiés de manière irrégulière mais cela me force à avancer, c'est un motivateur assez puissant. Ils me permettent de partager mes trouvailles, mais aussi de mettre de l'ordre dans mes pensées et de les concrétiser quand il le faut. Et c'est important parce qu'entre une idée dans un TODO et sa réalisation, il y a souvent un delta non négligeable et ça peut prendre plus de temps que prévu. En plus, ces épisodes me permettent de me poser la question : quelle est la prochaine étape ? J'ai à peu près toujours un ou deux épisodes en tête pour le futur quand je publie un nouvel épisode. Là par exemple, je sais ce que sera le prochain, et je sais ce qu'il me reste à faire pour qu'il puisse sortir. Yapluka. Voici donc la liste des douze épisodes pour ceux qui veulent faire du replay :

Je me suis permis de faire quelques statistiques sur ces épisodes. Sur le graphique suivant, on peut voir le nombre de pertinentages et le nombre de commentaires pour chaque épisode. On voit qu'il n'y a pas vraiment de corrélation entre les deux. Les moyennes sont respectivement de 50 et 51 mais cachent en fait de grandes disparités avec quelques épisodes qui font beaucoup monter la moyenne. La leçon de ces chiffres, c'est que ces épisodes vous plaisent et que vous y êtes fidèles.

Statistiques des épisodes

L'autre réussite de cette première année, c'est d'avoir publié pas mal de code. Je n'avais jamais vraiment publié de code publiquement auparavant et montrer son code au monde entier, ça force vraiment à faire les choses à peu près bien, à mettre de la doc, etc. Je suis plutôt satisfait d'avoir réussi à produire ces codes-sources là. Bien sûr, ils sont perfectibles mais ils permettent déjà d'avoir quelques briques de base. Les bibliothèques notamment sont largement réutilisables par d'autres jeux. Voici les bouts de code produits :

  • libes, une bibliothèque pour gérer les systèmes à entités. Cette bibliothèque se stabilise doucement mais sûrement ;
  • libtmx, une bibliothèque pour lire le format TMX de Tiled, plutôt stable pour l'instant (en attendant les évolutions du format lors de la prochaine sortie de Tiled) ;
  • libsuit, une bibliothèque d'entrées physiques et de widgets, encore en développement et qui suivra les besoins du jeu au fur et à mesure ;
  • MapMaker est un logiciel de création de carte d'altitude, dont la partie commune est stable mais dont la partie spécifique à Akagoria est encore en développement, car je ne suis pas encore satisfait de la manière de construire la carte primitive ;
  • SlowTree est un logiciel de génération procédurale de sprite en vue de haut, encore en développement car je veux continuer à explorer certaines pistes pour certains types de sprites.

J'en profite pour remercier tous les contributeurs occasionnels à ces projets et particulièrement Sébastion Rombauts qui a contribué à libes et à MapMaker grâce à de nombreux patchs. Grâce à lui, vous aurez une version sans bug du bruit à base de simplexe dont on avait parlé dans l'épisode 10 et qui avait manifestement un bug.

Les difficultés

Dans le moins bon, je n'ai sans doute pas avancé autant que je l'espérais au départ. Dans ma tête, j'étais parti sur un planning de deux ans avec un développement important du code la première année, et un développement du reste ensuite (scénario, dialogues, sprites, musiques, etc). Or, le code est très très loin d'être opérationnel pour commencer à développer activement le reste.

Comment expliquer ce retard ? Tout d'abord, le temps est une denrée très aléatoire quand on développe un jeu amateur. À l'automne dernier, j'ai eu pas mal de temps libre et donc j'en ai beaucoup profité pour avancer sur plein de fronts. Mais à partir de début 2014, j'ai baissé le rythme drastiquement, en partie à cause d'une activité professionnelle plus prenante (y compris sur mes week-ends), en partie parce que j'ai fait un peu de hors pistes en allant explorer des choses complètement inutiles pour mon jeu. Je voyais le temps défiler et j'étais dans l'incapacité d'avancer convenablement. C'était très frustrant.

Retour sur les choix techniques

Au bout d'un an, il est temps de revenir sur les choix techniques faits au départ pour se demander s'ils sont pertinents.

Commençons par le plus simple. Il y a des choix que je ne regrette pas : SFML, Box2D, TMX. Ces trois technologies sont très efficaces et répondent bien au besoin d'un jeu amateur. SFML est une excellente abstraction et, même si on aimerait qu'elle soit un peu plus complète, j'apprécie sa simplicité et sa documentation riche et utile. Box2D est également une excellente bibliothèque, assez simple à prendre en main. On arrive à faire ce qu'on veut, même si parfois on se demande si la manière dont on procède est bien la bonne manière de faire. TMX enfin, avec son éditeur Tiled, est un format qui me donne vraiment entière satisfaction. Sa généricité n'empêche pas qu'on puisse le spécialiser comme on le souhaite via son système de propriétés. En fait, le plus difficile dans ces trois choix est de les faire cohabiter intelligemment.

Finissons par le choix d'utiliser les systèmes à entités. Là, je dois dire qu'au bout d'un an, je reste perplexe. D'un côté, je vois bien que théoriquement, ce paradigme n'offre que des avantages (en particulier dans le style de jeu que je vise). D'un autre côté, dans la pratique, c'est assez complexe. Ce qui est dur, c'est de bien délimiter les systèmes en fonctionnalités et de choisir les bons composants. Qu'est-ce qui relève de l'état du jeu, qu'est-ce qui relève de données statiques ? La frontière est mince et mouvante. Ce qui serait simple, ce serait d'avoir déjà l'ensemble des systèmes et l'ensemble des composants. Construire les bons systèmes et les bons composants quand on part de zéro et qu'on n'a quasiment aucun modèle, c'est très compliqué. Je pense que j'aurais été beaucoup plus vite pour développer mon jeu avec un paradigme objet plus traditionnel. Mais je pense que je vais rester sur les systèmes à entités pour aller jusqu'au bout de l'expérimentation et contruire un jeu complet avec ce paradigme.

Perspectives

Quelles sont les perspectives ? Tout d'abord, il faut que j'arrive à finir le code (ou en tout cas une partie suffisamment importante du code) pour commencer le contenu du jeu. Je pense que je vais passer en mode sale pour le code qui reste à faire. Comprendre par là que je préfère avoir un code qui marche mais pas tout à fait nickel plutôt que d'essayer à tout prix d'avoir un code propre et bien pensé. En un mot : fonctionnalités plutôt que réutilisabilité. J'espère que dans un an, pour le deuxième anniversaire, on y sera.

L'autre point important, c'est d'avancer sur les sprites. Pour l'instant, Naha m'en a fait quelques uns qui sont plutôt réussis. Il va falloir continuer et sans doute accélérer un peu le rythme parce qu'il manque encore beaucoup de choses à ce niveau. J'aimerais pouvoir générer tout un tas de choses sur la carte primitive et qu'on puisse la modifier facilement sans devoir passer du temps à des tâches répétitives. Par exemple, pour définir une forêt, il est plus simple de tenter de la générer (éventuellement en guidant cette génération) plutôt que de placer les arbres un à un.

Ma première GameJam

J'ai participé à ma première game jam au début de l'été. C'était un moment fort enrichissant à plusieurs niveaux. Pour ceux qui ne savent pas ce qu'est une game jam, c'est une sorte de concours en temps limité où le but est de développer un jeu.

Cette game jam, baptisée Game Cancoillote, et organisée par deux de mes étudiants, s'est déroulé fin juin sur un week-end, dans les locaux de l'Université de Franche-Comté. Ils avaient bien fait les choses, on avait une grande salle pour toutes les équipes, et bien sûr à manger et à boire, ce qui a favorisé une excellente ambiance !

Il y avait cinq équipes qui ont utilisé des technologies très différentes. Une équipe a utilisé GameMaker, une autre Java, une autre HTML5/JS, une autre un moteur de jeu propriétaire, et la dernière, la mienne, a utilisé ce bon vieux couple C++/SFML. Même au niveau des concepts de jeu, il y a eu quelques surprises. Celui que j'ai préféré était l'équipe HTML5/JS qui a fait un jeu d'aventure en pointer-et-cliquer. Ils ont imaginé un scénario par rapport au thème de la game jam et ont utilisé l'université comme décor. Ils ont pris des photos panoramiques de divers endroits, ils ont aussi utilisé certaines vues de Google Maps (ouais, problème de copyright toussa). Et ensuite, ils ont incrusté un personnage en dessin. Et bien ça rend plutôt bien.

Pour ma part, j'étais dans une équipe composée de cinq personnes : trois étudiants (la major d'une spécialité de Master, le major et le second de licence 3) et deux profs (une collègue et moi-même). Sur le papier, une très bonne équipe. Deux des étudiants avaient déjà fait un jeu (dont un en projet semestriel avec moi), mais ma collègue était une novice complète. On avait déjà réfléchi à une idée globale de jeu avant la game jam tout en sachant que nous allions devoir nous adapter au thème (non-connu à l'avance) de la game jam. Notre idée était de partir sur le principe de pierre-feuille-ciseaux avec trois machins qui lutteraient les uns contre les autres.

Ce qui est bien, c'est que l'adaptation a été simple. Le thème de la game jam était une équation qui disait en gros qu'il fallait qu'il y ait de la physique dans le jeu et qu'il ne devait pas y avoir de gagnant dans le jeu. Nous avons alors imaginé qu'on pouvait avoir un jeu solo (donc pas de gagnant) où on contrôle une boule représentant une des trois entités et qu'on serait au milieu d'une sorte d'arène en train d'éviter ou de chasser les boules des autres entités qui arriveraient au hasard. Chaque rencontre donnerait lieu à une collision et une réaction : la disparition d'une boule en cas d'entités différentes. Et pour le fun, on a pris une variante japonaise : le guerrier, le tigre et la mère du guerrier. Voilà à quoi ça ressemble :

The game with no name

Au final, ce n'est pas si mal que ça pour le temps imparti. Nous n'avons pas eu trop de difficultés. Nous avons utilisé Box2D pour la physique (même si on aurait pu développer le moteur physique nous-même dans ce cas simple). Les avatars de chaque camp utilisent des photos prises dans la grande toile et j'aimerais bien les modifier pour pouvoir publier le jeu correctement sans avoir de problème de copyright. En revanche, l'illustration en bas à droite est l'œuvre d'une graphiste présente à la gam jam et qui a fait quelques dessins pour toutes les équipes (elle était très demandée).

Pour la beauté du geste, on a même fait un écran d'accueil avec cette même illustration.

The game with no name

Au niveau du nom, nous n'avons pas été très originaux. Une fois que tout sera d'aplomb niveau copyright, j'essaierai d'imaginer un nouveau nom (ou si vous avez des idées, n'hésitez pas à les partager en commentaire).

Un club de développement de jeu vidéo

La grosse nouveauté pour moi cette année, c'est que je lance dans mon université un club de développement de jeux vidéo. Je l'ai baptisé Dead Pixels Society. L'idée est de faire un jeu vidéo sur l'année universitaire avec une équipe d'une dizaine d'étudiants (pas forcément uniquement des informaticiens d'ailleurs) de tous niveaux.

Je ne sais pas ce que ça peut donner, mais j'ai des étudiants déjà très motivés par cette idée. Évidemment, j'espère aussi initier des étudiants complètement novices aux arcanes du développement d'un jeu vidéo. Pour cela, j'ai réalisé quelques fiches sur des aspects techniques basiques d'un jeu vidéo : la boucle de jeu, les systèmes de coordonnées 2D, les couleurs et la transparence, les sprites et animations, les moteurs physiques, etc. L'idée est de faire passer quelques concepts sans entrer dans les détails et sans s'attacher à une technologie en particulier. J'ai encore quelques idées de fiches mais je crois avoir brossé une bonne partie du sujet. Si vous avez des suggestions, n'hésitez pas à m'en faire part en commentaire.

J'aurai sans doute l'occasion de revenir sur les activités de ce club au cours des prochains épisodes ou dans des journaux. Si nous arrivons à enrichir le paysage des jeux libres d'un nouveau jeu par an, je me dis qu'on aura réussi quelque chose de bien. Cette activité parallèle à Akagoria va aussi sans doute me permettre d'avancer sur mon propre jeu. Outre le fait que j'ai maintenant un créneau fixe chaque semaine consacré au développement de jeux vidéo, il y a aussi les idées (et les bouts de code) qui peuvent émerger dans un autre jeu et dont je pourrai me resservir.

Je crée mon jeu vidéo E14 : formats de données

Posté par . Édité par Nils Ratusznik, Benoît Sibaud et palm123. Modéré par NeoX. Licence CC by-sa
Tags :
32
2
jan.
2015
Jeu

«Je crée mon jeu vidéo» est une série d'articles sur la création d'un jeu vidéo, depuis la feuille blanche jusqu'au résultat final. On y parlera de tout : de la technique, du contenu, de la joie de voir bouger des sprites, de la lassitude du développement solitaire, etc. Vous pourrez suivre cette série grâce au tag gamedev.

Dans l'épisode 13, on a fait le bilan d'une année de développement. Un des constats était que le temps manquait, et au vu de la durée entre cet épisode-là et celui-ci, on peut dire que c'est toujours le cas. Dans ce nouvel épisode, on va discuter non seulement de formats de données, mais aussi de compilation croisée.

Sommaire

Formats de données

État des lieux

Actuellement, dans Akagoria, trois formats de données sont gérés directement ou indirectement : XML, Protobuf et YAML. Le terme format de sérialisation de données serait probablement plus approprié, puisque pour chacun de ces formats, il peut y avoir plusieurs dialectes.

Concrètement :

  • XML est utilisé pour la carte à travers le dialecte TMX, défini par Tiled. J'utilise ma propre bibliothèque, libtmx, pour avoir une vue de la carte indépendante du XML. La bibliothèque utilise elle-même TinyXML-2.
  • Protocol Buffers est utilisé par le format Nanim de devnewton. Dans les dernières versions, Nanimstudio peut exporter les données en JSON plutôt qu'en Protocol Buffers. Actuellement, j'utilise protobuf couplée avec l'analyseur lexical généré depuis nanim.proto.
  • YAML est utilisé pour les autres données du jeu, dans des formats que j'ai défini moi-même. J'utilise yaml-cpp qui est relativement simple à utiliser. Je l'avais déjà utilisé dans MapMaker avec satisfaction et je préfère ce format à XML car moins verbeux.

Et pour le futur, il y aura sans doute d'autres données à gérer. Par exemple, les dialogues. Dans Andor's Trail, ils sont gérés en JSON. Comme on peut le voir sur un exemple, le format gère aussi les récompenses et l'enchaînement des quêtes. On peut aussi penser au format des sauvegardes, au format de la configuration du joueur, etc.

Solution

La solution serait de n'avoir qu'un seul format de sérialisation, ce qui réduirait le nombre de bilbiothèques utilisées à une seule. Et on voit bien le problème :

  • soit il faut choisir XML puisque c'est celui utilisé par Tiled, ce qui veut dire qu'il faut transformer les autres données en XML. J'imagine déjà la tête de devnewton si je lui dis qu'il me faut un export XML dans NanimStudio.
  • soit il faut coder un convertisseur depuis les différents formats vers celui choisi, mettons YAML. Dans cette deuxième hypothèse, pour Nanim, on peut utiliser l'export JSON qui est à peu près un sous-ensemble de YAML.

La deuxième hypothèse a d'énormes avantages : pas de dépendance forte à un format externe, facilité de lecture puisque tout se ressemble. Mais elle a aussi quelques inconvénients notables : obligation de redéfinir des dialectes dans le format unique, obligation de réécrire des analyseurs sémantiques pour ces dialectes. Ces inconvénients sont une variante de la réinvention de roue.

Quelle réponse au problème global ?

En fait, il y a une question à laquelle nous n'avons pas répondu : pourquoi s'emmerder avec des formats de fichiers ? Pourquoi ne pas tout coder en dur ? Bonne question. La réponse usuelle est que des données à part permettent de faire des changements sans avoir à recompiler le jeu. En particulier, les données dans des fichiers permettent à des non-informaticiens de pouvoir les manipuler assez facilement. Bon, ça c'est quand on développe un jeu avec des non-informaticiens. Quand on développe un jeu à peu près tout seul, la non-recompilation est un avantage en soi.

Mais surtout quand on fait du libre, on aime les formats ouverts, mais aussi les formats standardisés, parce qu'ils permettent par la suite de créer des outils génériques pour les manipuler. Et ce qui manque le plus dans les jeux libres, ce sont ces formats standardisés. Au final, chacun refait la même chose dans son coin et on n'avance pas. On ne peut pas capitaliser sur un ensemble de formats communs. Et surtout, on n'a aucun outil pour les manipuler.

L'exemple de Tiled est parlant. Le format TMX est à peu près le seul format sur lequel tout le monde s'appuie dans pas mal de jeux libres. Certes il n'est pas parfait, mais son système de propriétés fait qu'on peut lui ajouter des fonctionnalités à peu de frais, tout en restant compatible avec le seul éditeur du format connu jusqu'à présent. Mais est-ce bien suffisant ? La communauté du libre a toujours trouvé les ressources pour pallier ce genre de problème mais dans le cas des jeux vidéos, elle reste assez inerte.

Les grands studios ne développent pas que des jeux, ils développent aussi beaucoup d'outils. Certains moteurs libres de jeux proposent également des éditeurs, mais le problème des formats est toujours posé. Choisir un moteur de jeu, c'est choisir les formats qui vont avec et donc se lier à une technologie en particulier. Ce que je dis sonne un peu comme un yakafokon, parce que standardiser des formats de ce genre relève du parcours du combattant et nécessite une expérience que je suis sans doute très loin d'avoir.

En attendant une solution globale, je peux au moins éliminer Protobuf et utiliser l'export JSON de NanimStudio.

Compilation croisée pour Windows

Après des premiers essais infructueux dûs à des bugs dans les outils de compilation, j'avais mis de côté cet aspect des choses, à savoir fournir un binaire pour Windows. Mais il y a eu des mises à jour, notamment de Ming, et j'ai retenté. J'utilise l'excellent crossroad qui a été présenté ici-même il y a quelques temps (et qui a changé un peu donc lisez la doc si vous l'utilisez).

Première chose à dire, il faut vraiment s'armer de patience quand on tente ce genre de compilation. Parce qu'on tombe sur des erreurs de compilation qu'on ne trouve pas ailleurs. Alors bon, des fois, c'est tellement cryptique que la seule solution, c'est petit patch en mode « la Rache ». Et dans d'autres cas, c'est tout à fait légitime. Par exemple, le compilateur a l'air plus strict sur les standards. Saviez-vous qu'il n'y avait pas les constantes genre M_PI dans <cmath> ? En fait, elles sont dans les spécifications Unix et du coup, quand on les utilise et qu'on veut compiler pour un système non-Unix, ça provoque une erreur de compilation parce que la constante n'existe pas.

Autre chose à prendre en compte, on compile beaucoup de choses. Parce que même s'il y a déjà des paquets disponibles (grâce à OpenSuse), il manque beaucoup de choses. Pour cet essai, j'ai dû recompiler SFML, Box2D, protobuf, tinyxml2 et yaml-cpp. Sans compter mes propres bibliothèques. Il faut faire attention à plein de choses, il faut compiler le strict minimum pour que ça se passe au mieux. Et surtout, on a plein d'avertissements sur des pages et des pages ! Bref, on serre les fesses à chaque commande.

Au final, on se retrouve avec un joli zip de 40 Mio qui contient tous les binaires et les fichiers de développement. Et… Kaboum ! Ça ne marche toujours pas. Bon ben, faute de temps, je retenterai une prochaine fois. Mais je suis assez content de crossroad. Il fait exactement ce qu'on attend de lui et il aide juste ce qu'il faut pour la compilation croisée.

D'ici à la prochaine fois

N'oubliez pas que le week-end du 23 au 25 janvier 2015 a lieu la Global Game Jam. C'est un exercice amusant, et il y a de plus en plus de sites partout en France, n'hésitez pas à y faire un tour. Personnellement, je serai à Besançon, avec mes étudiants qui organisent le site.