Une fois de plus, ça fait un bon moment que je n’ai rien partagé sur ce blog… Mais j’ai travaillé sur beaucoup de choses dernièrement, notamment:
- l’integration de la nouvelle interface utilisateur (UI) de Unity pour le HUD (= Head Up Display = les informations visibles à l’écran pendant le jeu), le menu du Link et la carte du monde. Graphiquement, la différence n’est pour l’instant pas énorme, mais ça devrait me permettre d’intégrer plus facilement divers effets et animations pour la suite.
- la possibiliter de contrôler l’UI avec le joypad et avec le combo clavier/souris (y compris la molette). Ce n’est pas encore parfait, mais c’est fonctionnel.
- pas mal de « polish » (= rajout de détails) sur l’île Zephyr
- et un nouveau système de sauvegarde… Et c’est de ça dont je vais parler aujourd’hui.
Accrochez-vous, l’article est un peu long.
I – Un nouveau système de sauvegarde? Pourquoi? Dans quel but?
Le but principal de ce nouveau système est de remplacer le système actuel basé sur les « points de sauvegarde » qui impose au joueur de sauver sa progression à certains endroits/moments précis du jeu. Cela coupe totalement la fluidité du déroulement du jeu et le joueur peut oublier de sauvegarder (ou même ne pas voir le point de sauvegarde) et peut ainsi perdre sa progression, ce qui est évidemment très embêtant.
Jusqu’à présent, c’était ce système qui était présent dans le jeu, car je n’avais pas eu assez de temps et d’énergie à consacrer à ce point.
Mais de nos jours, dans les jeux récents, les sauvegardes sont automatiques et le joueur peut même ne pas les remarquer (souvent, une icône apparait dans un coin de l’écran pour indiquer une sauvegarde en cours). C’est ce que je souhaiterais avoir pour mon jeu, mais ça pose pas mal de problèmes en terme de design, d’architecture logicielle et de performances temps réel.
L’icone « engrenage » dans le coin inférieur gauche indique que le jeu est en cours de sauvegarde.
II – Comment est-ce que ça devrait marcher?
D’habitude, avant de me lancer dans la programmation à proprement parler, j’essaie de réfléchir aux différents cas qui peuvent se présenter (c’est plus ou moins une approche par « use case » ou « cas d’utilisation »). Voici les cas principaux:
- le joueur commence une nouvelle partie. Que devrait-il se passer? Idéalement, le jeu devrait lui demander un pseudo/identifiant qui sera utilisé pour identifier la sauvegarde. Pour l’instant, une nouvelle sauvegarde sera créée automatiquement car je n’ai pas le temps de faire quelque chose de mieux, mais ça devrait être corrigé à l’avenir.
- le joueur est dans le jeu. La partie devrait être sauvegardée régulièrement sur l’emplacement (le « slot ») courant. Le problème, c’est qu’il n’y a pas forcément de slot courant. « Comment ça? » vous demandez-vous sans doute. Et bien, un nouveau slot est créé pour chaque nouvelle partie lancée. Mais quand *JE* teste le jeu, je ne le lance pas du début à chaque fois. Généralement, je lance un niveau avec des variables modifiées afin de choisir spécifiquement « où et quand » je veux être situé dans le monde et dans l’histoire, et je teste à partir de là. Cela signifie que je n’ai pas de « slot » spécifique associé à mon test. Du coup, où dois-je sauver la partie? Dois-je la sauver? Puis-je la sauver? Comment faire si je veux tester le système de sauvegarde lui-même?
- le joueur veut charger une partie. Je dois lister toutes les parties précédemment enregistrées avec diverses infos pour chaque slot: quel niveau est en cours? Combien d’items le joueur a débloqués? Combien de quêtes sont complétées? Peut-être une image pour rappeler au joueur à quoi ressemble l’endroit? Et surement aussi son identifiant/pseudo…
Evidemment, à chaque fois que je sauvegarde le jeu, je ne veux pas qu’on puisse remarquer le moindre ralentissement (lag). Mais malheureusement, lorsqu’on écrit sur le disque dur, les opérations sont assez lentes (en comparaison avec le nombre d’images par secondes affichées par une carte graphique par exemple). Je dois donc utiliser des threads pour que les fichiers de sauvegarde puissent être écrits en arrière-plan pendant que le jeu continue de tourner de manière fluide (en tout cas c’est le but).
III – Comment le faire marcher?
1 – La classe de base
Jusqu’à présent, j’avais une simple classe/structure principalement basée sur les dictionnaires .NET pour sauver chaque aspect du jeu:
- les infos du joueur: vie, items, munitions, position, état, …
- les collectables et les quêtes: ce qui a été demandé, ce qui a été accompli/collecté, combien de fois, où, …
- les infos des mécaniques de gameplay: quelle porte est ouverte, quel interrupteur est connecté à quoi, l’emplacement des plateformes mobiles, etc…
- les infos des ennemis: position, état, …
- les infos des PNJ (personnages non joueurs): qu’est-ce qui a été dit à qui, …
- les infos du monde: le niveau courant, le moment de la journée, le temps de jeu, …
L’idée est de ne PAS sauver l’intégralité du jeu, mais juste ce qui est REELLEMENT nécessaire pour recharger une partie. Ca devrait permettre d’avoir des sauvegardes assez légères et des temps de chargement assez courts.
Bien sûr, toutes ces infos doivent être accessibles de n’importe où dans le code et à n’importe quel moment, c’est pourquoi j’ai utilisé une sorte de classe Singleton pour stocker l’info (ce n’est pas aussi simple que ça, mais c’est l’idée principale).
2 – Les structures de plus haut niveau
Maintenant que je sais QUOI sauvegarder, voyons COMMENT le faire. Pour répondre aux problèmes et objectifs listés dans la partie II, j’ai choisi de mettre en place:
- des sauvegardes de NIVEAU 0, c’est à dire les fichiers sur le disque dur.
- des sauvegardes de NIVEAU 1, qui sont une version clone des fichiers du disque, mais stockées en RAM (sous forme de variables dans le code)
- une UNIQUE sauvegarde de NIVEAU 2, qui est celle qui sera directement lue et modifiée par tous les objets du jeu, en RAM évidemment.
L’idée est d’avoir plusieurs couches de sauvegarde, chacune communiquant UNIQUEMENT avec les couches immédiatement supérieures ou inférieures, en fonction de leur but principal et de leur vitesse de traitement.
La moteur temps réel du jeu travaille uniquement avec la sauvegarde de NIVEAU 2. Elle est unique. De temps en temps, quand les conditions sont OK (le joueur n’est pas en train de combattre, il est dans une zone sans danger, il n’est pas dans une zone où la sauvegarde est interdite, …), je fais une copie complète dans un des slots de NIVEAU 1. Et enfin, quand le jeu n’est pas déjà en train d’écrire sur le disque et que les conditions sont OK (la dernière sauvegarde date de X minutes, …), je copie le slot de NIVEAU 1 dans un fichier sur le disque (NIVEAU 0). Les données sont écrites sur le disque via les serialiser binaires de la librairie .NET.
Bien sûr, afin de copier les données dans les slots de NIVEAU 1 et 0, je dois savoir quel slot choisir. Par défaut, il n’y a pas de slot courant. Dans ce cas, aucune copie n’est faite. Si le joueur a démarré une nouvelle partie, un nouveau slot est créé et ce sera le slot courant pour la suite. Si le joueur charge une partie, ce slot sera considéré comme le slot courant. Et quand je suis en mode « Test », je peux créer un nouveau slot « à la volée » à tout moment en appuyant sur une touche magique. Dès lors, ce nouveau slot est considéré comme le slot courant. Cette méthode semble répondre à tous les cas que j’ai listés en II.
Quelques infos supplémentaires:
- la sauvegarde de NIVEAU 2 est celle qui est constamment lue et écrite par tous les objets du jeu; Ainsi, chaque objet peut savoir si un dialogue a été lu, si une quête a été accomplie ou si un object a été récupéré. C’est extrêmement rapide à lire et à écrire car c’est une (pas si) simple variable en RAM. C’est l’information la plus récente sur le jeu.
- Les sauvegardes de NIVEAU 1 sont une sorte de tampon entre la sauvegarde « vivante » de NIVEAU 2 et les sauvegardes sur disque de NIVEAU 0. Ce sont des copies des données de NIVEAU 2 que je fais de temps à autre. Elles sont principalement utiles car l’écriture sur le disque est LENTE. Ca signifie qu’il est possible que le jeu écrive un fichier PENDANT qu’il est en train de modifier la sauvegarde, ce qui créé un accès simultané aux données. Ceci est à proscrire à tout prix si je veux garantir l’intégrité des données. Si je n’avais pas ces sauvegardes intermédiaires de NIVEAU 1, le jeu pourrait définitivement altérer les sauvegardes sur le disque et la sauvegarde en mémoire… D’où l’intérêt de ces sauvegardes tampons de NIVEAU 1.
- les sauvegardes de NIVEAU 0 sont les sauvegardes sur le disque. Elle sont écrites depuis ou copiées dans les sauvegardes de NIVEAU 1, et cette opération peut durer plusieurs frames.
Bien sûr, avec ce système, il est aussi possible de sauver à des moments ou endroits bien spécifiques (quand le joueur quitte, finit une quête, ouvre un menu). C’est assez permissif.
3 – Et le chargement?
Pour l’instant, j’ai surtout parlé du processus de sauvegarde, car c’est le plus compliqué. Le chargement est assez immédiat: on charge toutes les données de NIVEAU 0 depuis le disque dur et on les copie dans les slots de NIVEAU 1 quand le jeu est lancé. Quand le joueur charge une partie, on copie les données de NIVEAU 1 dans la sauvegarde de NIVEAU 2 et on charge le niveau correspondant dans Unity.
On peut se demander si tout ceci n’est pas trop lourd en mémoire, mais chaque slot devrait contenir quelques 10 000 entrées de type « string ». Avec un rapide calcul approximatif, on trouve une taille de quelques 100 Ko en mémoire pour chaque sauvegarde. Mes plus gros tests sur le disque dur montent jusqu’à 2 Mo chacun, ce qui reste raisonnable.
Le nouvel écran de chargement. Simple mais fonctionnel.
4 – Performances
Vous l’avez surement compris, l’écriture sur le disque dur est lente. Donc les copies de NIVEAU 1 vers le NIVEAU 0 sont lentes. Mais ce n’est pas un problème car ça a été pensé comme une opération threadée et ça ne ralentit pas le jeu. Pendant cette opération, j’affiche la fameuse icone de chargement dans un coin de l’écran. Au début du jeu, c’est à peine visible car c’est très rapide (et qu’il n’y a quasiment rien à sauver). Toutefois Unity n’est pas « thread safe », ce qui m’a obligé à choisir avec précaution mes structures de données (car je n’ai pas accès par exemple aux données de type Transform et/ou Monobehavior dans le thread).
Mais un plus gros problème est la copie de NIVEAU 2 vers le NIVEAU 1: celle-ci DOIT être une copie PROFONDE, car sinon on peut se retrouver à nouveau face au problème d’accès simultané au données (à cause du type « class » de beaucoup de variables C#; ce n’est pas super intuitif, n’hésitez pas à creuser les recherches sur google si vous voulez en savoir plus). Malheureusement, les copies profondes sont assez lentes et ne peuvent pas être threadées (enfin pas facilement) car je copie depuis le NIVEAU 2 qui est « vivant »/constamment modifié. On commence à voir un léger lag à partir de 20 000 entrées dans les dictionnaires de données, ce qui devrait être OK pour la totalité du jeu, mais je n’en suis pas encore sûr. Il faudra faire d’autres tests quand le jeu sera plus abouti.
Toutefois, si l’avenir revèle que les copies de NIVEAU 2 à NIVEAU 1 sont trop lente, j’ai quelques pistes en tête pour améliorer tout ça, mais je ne les détaillerai pas ici car cet article est déjà trop long…
IV – Et maintenant?
Voilà, j’espère que ces explications permettent d’avoir un bon aperçu du nouveau système de sauvegarde. Je sais qu’il n’est pas parfait et qu’il y aura quelques bugs à venir et à corriger, mais c’est très appréciable de ne pas avoir à penser à sauvegarder pendant le jeu. L’expérience est bien plus immersive.
Il reste cependant quelques problèmes potentiels:
- les slots de sauvegarde ne sont pas clairement identifiés et il est difficile d’en trouver une particulière s’il y en a beaucoup, mais j’espère corriger ça prochainement (même s’il faudra certainement coder un clavier virtuel pour les gens qui jouent au joypad)
- Il n’y a pas de capture d’écran des sauvegardes (alors que c’était le cas dans les versions précédentes) car j’ai changé le système de sauvegarde. Jusqu’à présent, je prenais une capture d’écran juste avant l’ouverture du menu ce qui causait un mini lag, mais qui n’était pas visible car le menu apparaissait dans la foulée. Mais maintenant, le jeu est continuellement sauvegardé et même un mini lag est TRES visible.
- J’aimerais ajouter des sauvegardes de NIVEAU -1, qui seraient des copies de la dernière sauvegarde de NIVEAU 0. Pourquoi? Car le nouveau système de sauvegarde n’a pas été encore été suffisamment testé et je suis à peu près sûr que certains joueurs rencontreront des cas qui crasheront le système. Pour éviter qu’ils ne perdent toute leur progression, faire des copies sur le disque parait être une méthode suffisamment simple et efficace.
Pour le moment, le système est opérationnel pour le Toulouse Game Show ce week end, ce qui était l’objectif principal. Au passage, n’hésitez pas à venir me voir sur le stand ce week end!
J’espère que cet article n’était pas trop long, ou technique ou ennuyeux… Mais j’en doute!
A+
Laisser un commentaire