Je crée mon jeu vidéo E16 : Nouveautés

Posté par . Édité par Benoît Sibaud et palm123. Modéré par ZeroHeure. Licence CC by-sa
42
6
mai
2016
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 15, j'arrêtais… les systèmes à entités. Et j'avais un peu fait miroiter des nouveautés. Donc dans cet épisode, on va en parler de ces nouveautés. Depuis ce dernier épisode qui date quand même d'août dernier, je n'ai pas avancé d'un iota sur Akagoria même (ce qui veut dire que vous auriez pu avoir cet épisode depuis des mois !).

Sommaire

Il y a beaucoup de nouvelles fonctionnalités, je vais tenter à chaque fois de décrire la manière dont j'ai procédé pour les coder (qui n'est sans doute pas parfaite). J'ai mis cette nouvelle version d'Akagoria sur un dépôt github. Comme annoncé au dernier épisode, c'est une refonte complète du code, je suis donc reparti d'un dépôt vierge.

Fonctionnalités

Messages et autre données

Tout d'abord, une grosse avancée est d'avoir mis beaucoup de données dans des fichiers textes au format YAML. Pour cela, j'ai fait un DataManager qui est responsable du chargement de toutes les données au démarrage, ce qui fait que tout le code lié à l'analyse lexicale du YAML est restreint à cette classe. Ensuite, on peut accéder aux données via des méthodes simples qui prennent en paramètre des clefs.

On verra par la suite que les dialogues, par exemple, sont dans ces fichiers textes et ce que ça peut apporter. Ici, je vais commencer par parler des différents messages qu'on peut avoir. Il y a d'abord tous les messages qui apparaissent dans l'interface du jeu, ils sont désormais tous dans un fichier et on peut récupérer les messages via une clef. Ensuite, il y a les messages de notification, pratiques pour signaler diverses choses au joueur : quand il vient d'arriver dans une nouvelle zone, quand il a gagné un niveau, quand il approche d'un danger, etc.

Tous ces messages de notification sont gérés dans MessageManager (ouais, je kiffe les managers) qui va se charger d'afficher les messages le temps voulu.

Voici par exemple le message de bienvenue :

Message de bienvenue

Dialogues et traduction

Pour les dialogues, c'est quasiment pareil, ils sont tous dans un fichier. L'intérêt, c'est qu'il n'y a qu'une seule source pour les chaînes à traduire : ces fichiers. Le but est de n'avoir aucune chaîne à traduire dans le code. Pour l'instant, j'y arrive bien. Après, j'utilise une astuce. En effet, les chaînes de caractères dans un fichier YAML peuvent être entourées soit de guillemets simples ', soit de guillemets doubles ". Bon, en fait, il y a des différences entre les deux mais on les oublie pour l'instant. Par convention, les chaînes à traduire sont placées entre guillemets doubles. Par conséquent, on peut appeler xgettext en lui faisant croire que c'est du C et qu'il doit analyser toutes les chaînes de caractère, ça ne pose aucune problème. Et on se retrouve avec un fichier de traduction classique.

Vous aurez remarqué que les messages des captures d'écran sont en français. J'ai réalisé moi-même la traduction des quelques chaînes qui existent jusqu'à présent. Ça m'a permis de voir que ça marchait correctement. Techniquement, dans le code, j'ai utilisé Boost.Locale qui est capable de lire les fichiers générés par gettext. C'est très facile à utiliser et comme toutes mes chaînes sont dans des fichiers, j'ai juste besoin de faire la traduction au chargement des fichiers.

Une des difficultés a été de passer la chaîne traduite à SFML. En effet, SFML est un peu bête. Pour lui, une std::string contient uniquement de l'ASCII plutôt que de l'UTF-8. Et donc, pour que SFML comprenne que la chaîne utilisée contient des caractères hors de l'ASCII, il est obligatoire de la transformer en UTF-32 ! Heureusement, SFML fournit des fonctions de conversion. Ça reste assez pénible. J'ai fait un bout de code qui permet la conversion facilement. Ce choix de SFML est surprenant à l'heure où l'UTF-8 est partout.

En jeu, voilà à quoi ressemble un dialogue.

Un dialogue

Le gros rectangle orange est un personnage non-joueur (faites travailler votre imagination). Le rond rouge indique qu'on peut interagir avec lui via un dialogue simple (c'est-à-dire qu'il ne donne pas de quête, il veut juste parler). De la même manière que pour les messages, il existe un DialogManager qui permet de faire avancer les dialogues et de les afficher. À la fin du dialogue, un événement de jeu est envoyé pour permettre éventuellement de modifier l'état global du jeu. Actuellement, la fin du dialogue avec ce premier personnage non-joueur charge un deuxième dialogue pour ce personnage non-joueur (qui est un résumé du premier dialogue). On verra où est codée cette logique un peu plus loin.

Une limite actuelle est que les sauts de lignes doivent être placés manuellement dans le texte. Pour aider à bien placer les sauts de ligne et à vérifier que le texte tient bien dans le rectangle de dialogue, j'ai fait un petit utilitaire appelé akagoria_dialog_validator qui va passer en revue tous les dialogues, en prenant en compte les traductions. Bon, ce n'est pas parfait, il y a de la duplication de code par rapport au jeu (danger !) mais ça fonctionne pour l'instant.

Sauvegarde

Un point important dans un jeu, ce sont les sauvegardes. En particulier dans un RPG à monde ouvert où il faut sauvegarder tout l'état du jeu actuel pour pouvoir le reprendre plus tard. Je me suis donc attaqué à cet aspect assez tôt de manière à voir les problèmes assez vite. J'ai utilisé Boost.Serialization, qui est un peu vieux par rapport à d'autres bibliothèque de sérialisation comme cereal qui gère le C++11, mais elle fait le travail très correctement.

Ensuite, il a fallu décider quoi sauvegarder. Pour l'instant, ça reste assez simple. J'ai sauvegardé les informations concernant le héros (sa position et ses points de vie/magie), les informations concernant les personnages non-joueurs (position, dialogue en cours), et les prérequis (dont on va parler un peu plus loin). J'utilise pour l'instant un format texte pour bien vérifier que j'ai toutes les informations dont j'ai besoin. Plus tard, je pourrai passer à un format binaire très facilement grâce à Boost.Serialization.

Pour finir une petite interface permet de charger et sauvegarder. J'ai volontairement limité les possibilités de sauvegarde à trois emplacements pour deux raisons : d'une part, je trouve que ça fait un peu vieille école ce genre de limitation, ça a un certain charme, j'aime bien ; d'autre part, ça évite de devoir taper à la main un nom puisque je garde à l'esprit que l'utilisateur peut vouloir utiliser une manette pour jouer. De toute façon, d'après mon expérience, trois emplacements sont très largement suffisants.

Visuellement, ça ressemble à l'image suivante qui est issue de l'écran de démarrage.

Un chargement de sauvegarde

Dans le futur, j'aimerais indiquer le nom de la région dans laquelle se trouve le joueur pour l'aider à différencier les trois sauvegardes (en plus de la date).

UI et pilotage

Vous avez vu tout un tas de petits bouts d'interface utilisateur. Ces interfaces ont une couleur bien connue des fans de RPG. Techniquement, j'ai essayé de ne pas éparpiller le code qui affiche ces interfaces. J'ai donc fait tout un ensemble de classes qui gèrent les interfaces. Le code contient beaucoup de constantes pour faciliter les modifications. Ces classes sont ensuite utilisées dans les différents gestionnaires (managers) correspondants et sont affichées au besoin. Par exemple, s'il y a un dialogue en cours, on affiche l'interface de dialogue.

Pour piloter tout ça, ça n'a pas été simple. J'utilise une sorte de machine à état pour déterminer quoi faire quand. Toute la logique est encapsulée dans une classe de pilotage du jeu qui intervient directement dans la boucle de jeu. L'idée est que le personnage peut être dans différent mode : marche, dialogue, sauvegarde. Pour chaque mode, les touches n'ont pas la même utilité : haut et bas servent à avancer et reculer en mode marche, tandis qu'ils servent à choisir l'emplacement de sauvegarde en mode sauvegarde. Le pilote de jeu gère tout ça de manière transparente et fait passer d'un mode à l'autre en fonction des actions du joueur. Pour l'instant, la coordination reste assez simple. À noter qu'il y a un autre pilote pour l'écran de démarrage, ce qui montre que cette technique est assez flexible.

En jeu, ce sont les différents gestionnaires qui détectent qu'il faut passer dans un autre mode. Par exemple, les sauvegardes sont réalisées près d'un autel en forme d'étoile et avec des particules bleu ciel. Si on presse la touche action (X pour l'instant) suffisamment près d'un autel de ce type, le dialogue de sauvegarde apparaît.

Une sauvegarde

Pour information, l'autre autel (celui forme de spirale avec des particules rouges) permet de remettre des points de vie. J'ai réalisé ces dessins moi-même avec Inkscape. Petit aparté, la vue de dessus est un vrai challenge (qui me plaît beaucoup), il faut imaginer comment donner une représentation horizontale à des objets qu'on a l'habitude d'imaginer verticalement. J'ai fait au mieux pour ces autels, mais j'aimerais bien savoir si ça rend suffisamment bien. N'hésitez pas à me le dire dans les commentaires.

Prérequis et événements

Une fonctionnalité importante pour pouvoir gérer le cours du jeu est le système de prérequis. Les prérequis permettent de savoir si une action est possible ou pas. Techniquement, c'est juste un ensemble d'identifiants. Pour l'instant, les prérequis peuvent être utilisés directement sur la carte avec Tiled sur des événements. Il était possible jusqu'à présent de déclencher un événement de jeu quand le personnage était dans une certaine zone. Cette fonctionnalité est utilisée pour changer d'étage par exemple et descendre dans des souterrains. Dorénavant, il est possible d'ajouter des prérequis sur la zone. Si le joueur a ces prérequis, alors l'événement de jeu est envoyé, sinon rien ne se passe.

Ce mécanisme va servir à implémenter une partie de la logique du jeu. Par exemple, on va pouvoir empêcher le joueur d'accéder à des endroits clos en mettant un prérequis qui sera accordé si le joueur réalise une certaine action. Les possibilités sont déjà assez importantes. La seule limitation que je vois pour l'instant est qu'il n'est pas possible de coder des conditions complexes puisque ces prérequis s'apparentent à des booléens. Je verrai bien à l'usage.

Un exemple d'utilisation est le message de bienvenue. En fait, le personnage commence sur une zone d'événement avec un prérequis qu'on donne automatiquement au chargement initial du jeu. Cet événement va afficher le message de bienvenue et le prérequis va être supprimé ce qui empêchera le message de bienvenue de réapparaître quand on repassera sur la zone.

Histoire

Dernière fonctionnalité du jour, le déroulement de l'histoire. C'est une partie haut niveau qui est généralement implémentée soit en utilisant des fichiers de configuration, soit en utilisant un langage de script type Lua pour pouvoir gérer des situations complexes avec des bouts de code. Pour ma part, actuellement, je reste avec du C++. J'ai mis la logique de l'histoire dans sa propre classe. Cette classe est simplement à l'affût des événements de jeu, comme la fin d'un dialogue, et met à jour l'état global du jeu (ajout de prérequis, apparition de personnages, ajout de dialogue à des personnages, etc).

Je ne compte pas introduire de fichier de configuration ni de langage de script pour cette partie. Dans le premier cas, il me semble que les possibilités seraient un peu trop limitées. Dans le second cas, il faudrait faire la traduction depuis les objets du jeu vers le langage de script et vice-versa, ce qui demanderait beaucoup de travail. Bref, je suis obligé de recompiler quand je modifie mon histoire mais ça me convient pour l'instant.

Et la suite ?

La suite, ça s'annonce funky. Déjà parce que je suis papa depuis quelques semaines et que ça change la vie (et le temps qu'on peut consacrer à un jeu qu'on développe sur son temps libre). Et ensuite, parce qu'il y a beaucoup de choses en attente. Je n'en parle pas maintenant (ouais, j'adore le suspense), mais il y a déjà trois épisodes en cours d'écriture (dont certains ont été commencés depuis un bout de temps) et je pense qu'il y en aura bientôt un quatrième ! Bref, la série ne s'arrête pas, le jeu non plus mais ça avance à son rythme.

  • # Félicitations !

    Posté par . Évalué à 10.

    Déjà parce que je suis papa depuis quelques semaines

    Félicitations !

    • [^] # Re: Félicitations !

      Posté par (page perso) . Évalué à 10. Dernière modification le 06/05/16 à 17:58.

      Du coup faut instancier un BabyManager aussi.

      • [^] # Re: Félicitations !

        Posté par (page perso) . Évalué à 7.

        Oui, et cela lance une tâche très gourmande en ressource. C'est pour ça qu'il n'y a plus beaucoup de temps pour le reste ;)

        Toutes mes félicitations à l'heureux papa !

      • [^] # Re: Félicitations !

        Posté par . Évalué à 10.

        class BabyManager {
        public:
          bool isHungry() const;
          void eat();
        
          bool isDirty() const;
          void changeDiaper();
        
          bool isTired() const;
          void sleep();
        };
        • [^] # Re: Félicitations !

          Posté par . Évalué à 4.

          Mouais, le problème c'est qu'au début les trois fonctions isHungry(), isDirty(), et isTired() ont exactement le même code :

          if isCrying() return Probably;
          else return Maybe;

          Tu peux faire évoluer en mettant un timer sur le change dans isDirty()

          if now()-lastChanged()<2sec return ProbablyNot;

          Yth.

  • # Fichier de traduction

    Posté par . Évalué à 2.

    Pourquoi ne pas utiliser le format gettext ?
    Il y a déjà de nombreux outils disponibles afin de fusionner des traduction existante (po) dans le nouveau template a traduire (pot). Si tu commence a géré beaucoup de langue, cette tache peut devenir chronophage sans les bon outils.

    De plus, des outils collaboratifs de traduction existe autant être compatible avec.

    Bon courage, et merci pour ces articles.

    • [^] # Re: Fichier de traduction

      Posté par (page perso) . Évalué à 7.

      Pourquoi ne pas utiliser le format gettext ?

      Par conséquent, on peut appeler xgettext en lui faisant croire que c'est du C et qu'il doit analyser toutes les chaînes de caractère, ça ne pose aucune problème. Et on se retrouve avec un fichier de traduction classique.

      « Rappelez-vous toujours que si la Gestapo avait les moyens de vous faire parler, les politiciens ont, eux, les moyens de vous faire taire. » Coluche

  • # La suite

    Posté par (page perso) . Évalué à 10.

    Je ne sais pas pourquoi, ça me semble naturel d'attendre longtemps pour un E17.

    • [^] # Re: La suite

      Posté par . Évalué à 2.

      Le pire, c'est que si je me bougeais, je pourrai le sortir la semaine prochaine le E17 !

      • [^] # Re: La suite

        Posté par . Évalué à 4. Dernière modification le 07/05/16 à 23:24.

        La gestation est arrivée à son terme ? J'espère pour ta femme que sa phase de travail n'a pas duré aussi longtemps. :-) Félicitations pour le petit (ou la petite).

        Sinon pour faire une suggestion sur ton jeu. Dans ta dépêche tu demandes des retours sur les dessins vus de haut (personnages, autels…) et je trouve que cela rend assez bien, les arbres aussi. Par contre pour les terrains, la couleur uniforme (vert et marron) ça fait un peu « plat » et ça contraste avec les autres éléments. Ne pourrais tu pas voir avec David Tschumperlé pour faire des textures de terrains avec G'MIC ? Ça pourrait être sympa de voir deux projets, dont l'avancement est présenté régulièrement ici, coopérer.

        Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

        • [^] # Re: La suite

          Posté par . Évalué à 4.

          La gestation est arrivée à son terme ? J'espère pour ta femme que sa phase de travail n'a pas duré aussi longtemps. :-)

          Le bébé est arrivé avec 3 jours de retard ! :D

          Par contre pour les terrains, la couleur uniforme (vert et marron) ça fait un peu « plat » et ça contraste avec les autres éléments. Ne pourrais tu pas voir avec David Tschumperlé pour faire des textures de terrains avec G'MIC ?

          Je suis entièrement d'accord avec toi sur le côté «plat». Ces tuiles sont issues de la génération aléatoire du terrain de l'épisode 11. C'était fait de manière très basique pour l'instant.

          En fait, j'aimerais pouvoir générer ou construire un tileset qui soit propre à Akagoria. Et pour ça, j'aimerais avoir beaucoup de tuiles différentes, même pour un même biome, parce que le fond de carte est ce qu'on va voir le plus souvent. Actuellement, quand je me ballade sur la carte, je trouve qu'il n'y a pas assez de variété dans le fond de carte (ça vient aussi du fait que la carte est nue mais quand même).

          Utiliser G'MIC est une option très intéressante (et j'y pense depuis un moment) parce qu'il a des effets très intéressants, et parce qu'il a un langage qui permet de manipuler les images et donc de scripter éventuellement la génération des tuiles.

          • [^] # Re: La suite

            Posté par . Évalué à 3.

            Je suis entièrement d'accord avec toi sur le côté «plat». Ces tuiles sont issues de la génération aléatoire du terrain de l'épisode 11. C'était fait de manière très basique pour l'instant.

            AMHA tu n'a pas vraiment à y toucher, il faut juste enrichir la carte ensuite (la génération de carte défini la surface, puis tu fais une étape d'habillage de la carte en fonction du type de terrain et de l'altitude).

            Utiliser G'MIC est une option très intéressante (et j'y pense depuis un moment) parce qu'il a des effets très intéressants, et parce qu'il a un langage qui permet de manipuler les images et donc de scripter éventuellement la génération des tuiles.

            Tu ne compte tout de même pas construire les effets dynamiquement ? Je me souviens sur Total Annihilation, il devait pas y avoir plus de 4 tuiles différentes pour chaque type de terrain et ça passait pas mal (il y avait aussi des objets posés ci et là pour habiller la carte (des pierres, des arbres et des ressources).

            Une chose intéressante à faire AMHA serait d'avoir des tuiles ayant une importances différente (par exemple une tuile terre avec une grosse flaque d'eau dessus) et de lui donner une probabilité bien plus faible d'être choisie ça te permet de couvrir ta map de tuiles identiques et d'avoir éparpillé de manière éparses des éléments qui donnent un peu plus de profondeur à ta carte.

            Après tu peux affiner le truc encore plus si tu as pleins de tuiles différentes et faire en sorte que les flaques apparaissent plus fréquemment quand on s'approche de la mer par exemple.

            Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

            • [^] # Re: La suite

              Posté par . Évalué à 4.

              L'un n'empêche pas l'autre.

              Actuellement, j'ai 5 couches de cartes par étage : le fond de carte avec des tuiles (celui dont on parle), une couche basse avec des tuiles (pour mettre par exemple des routes, des petits éléments genre flaque ou cailloux), une couche basse avec des sprites (pour mettre des éléments plus gros qu'une tuile genre un autel), une couche haute avec des tuiles (pour mettre des trucs genre des toits, des plafonds), et une couche haute avec des sprites (pour mettre des arbres par exemple). Le personnage est affiché entre les couches basses et hautes.

              Donc, pas vraiment besoin d'avoir des tuiles spécifiques avec des flaques, je pourrai les ajouter où je veux sans aucun problème. Après, il faut juste avoir assez de décors pour que les cartes ne soient pas monotones. Mais même avec ça, je pense que c'est une bonne idée d'avoir des fonds de cartes qui soient variables, même pour un même biome.

  • # Commentaire sur le programme

    Posté par . Évalué à 1.

    Bravo pour ton travail.

    Voila quelques remarques:

    code :
    static constexpr unsigned INITIAL_WIDTH = 1280;
    static constexpr unsigned INITIAL_HEIGHT = 720;

    Tu dois bien avoir un moyen de connaitre la résolution d’écran et de pouvoir initialiser le jeux en full screen. Ensuite tu peux calculer le ratio pour pouvoir afficher ton jeux toujours de la meme façon, surtout pour les dialogues par exemple.

    Sinon pas plus , bon courage !

    • [^] # Re: Commentaire sur le programme

      Posté par . Évalué à 2.

      Tu dois bien avoir un moyen de connaitre la résolution d’écran et de pouvoir initialiser le jeux en full screen.

      Dans l'idée, je préfère laisser le choix à l'utilisateur d'être en fullscreen ou pas. Maintenant, c'est vrai que je pourrais mettre le fullscreen par défaut. Et oui, il y a moyen de connaître la résolution de l'écran avec SFML.

      Ensuite tu peux calculer le ratio pour pouvoir afficher ton jeux toujours de la meme façon, surtout pour les dialogues par exemple.

      Ça, j'ai déjà une classe qui s'en occupent. Les éléments d'interface ont des tailles fixes quelle que soit la résolution et positionnés de manière relative (par exemple, au milieu).

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.