3ème opus de la série, où l'on parlera de la partie "persistance" dans "univers persistants". Que ce soit pour définir le monde ou pour récupérer l'état du serveur après une panne ou une séance de maintenance, pouvoir compter sur sa couche de persistance est indispensable.
Base de données relationnelle? Listes de clés-valeurs? XML? Gros fichiers binaires? Une chose est sûre, il faudra bien d'une manière ou d'une autre sauver ses précieuses données. AdH a fait très tôt le choix de la base de données relationnelle (Postgresql, pour être précis, mais cela aurait pu être Mysql, ou encore Oracle si on avait les sous).
Voici quelques avantages des bases de données relationnelles:
- Les données sont accessibles directement à partir des outils fournis avec le système. Changer une position ou réparer un inventaire, c'est juste quelques lignes de SQL.
- Les données sont en sécurité! Ces systèmes sont stables, utilisés dans d'innombrables projets à travers le monde. Les données sont centralisées, et les outils sont fournis pour sauvegarder et restaurer la base.
- Il existe probablement déjà une bibliothèque pour accéder à la base depuis votre langage de programmation préféré.
Les données sont accessibles pour d'autres applications. Il est très aisé d'extraire des statistiques pour les afficher sur une page Web, ou encore de générer des rapports sur l'activité des joueurs.
Et quelques inconvénients:
Ecrire le schéma prend du temps, et écrire la couche relationnelle dans le code en prend également.
- Les changements de schéma peuvent être lourds: une colonne qui change dans une table, et il faut potentiellement changer ses index, ses procédures stockées, son SQL dans le code, et enfin le code lui-même.
- Il faut installer une base pour faire tourner le serveur, en s'assurant que les binaires sont bien à la même version que le SQL.
Ces derniers temps, l'on a beaucoup parlé des bases de données de type clé-valeur, souvent utilisées dans le Web. Très dynamiques, elles permettent d'étendre le modèle de données très simplement, et sont d'une extensibilité sans pareille. Rajouter des paramètres à ses objets devient un jeu d'enfant. Par contre, être aussi dynamique a un coût: la base peut vite devenir bordélique, et les clés et les valeurs de plus en plus complexes et difficiles à interpréter. Sauver du XML en place des valeurs peut être une bonne manière de permettre de sauver simplement des données complexes, mais on se retrouve alors à définir un schéma et donc perdre tous les avantages du dynamisme.
Enfin, reste la solution du bon vieux fichier. Facile à lire, un peu plus difficile à modifier si l'on veut juste changer un petit quelque chose au milieu, il a l'avantage de ne nécessiter l'installation d'aucun logiciel supplémentaire. Par contre, la protection des données, la définition du schéma, leur accès en dehors de l'application, tout cela est entre vos mains. Ça fait un peu beaucoup, non?
Une fois que l'on est content de sa couche de persistance, encore faut-il décider de quand y accéder. D'un côté, il est possible de tout charger en mémoire au démarrage, et de tout sauver de temps en temps (mettons toutes les 10 minutes, dans un thread séparé pour un maximum de performances). C'est rapide. Mais cela veut dire qu'il faut mapper strictement toutes les données de la couche de persistance vers les objets du code. C'est du boulot! Enfin, il faut trouver le bon équilibre pour les sauvegardes: trop souvent, et les performances souffrent. Trop rarement, et un crash inopiné zappe une bonne partie de l'avancement des joueurs, à leur grande colère. Et enfin, arrive un moment où tout ne tient plus en mémoire... De l'autre côté, l'on peut accéder la couche de persistance uniquement quand on a besoin d'une donnée, et l'écrire à chaque fois qu'elle change. On ne perd rien en cas de crash, par contre, le système peut devenir très lent.
Il s'agit de trouver le bon équilibre entre les deux: quelles données peuvent être chargées à la volée? Peut-on les cacher en mémoire? Quelles donnés ont besoin d'être écrites immédiatement, quelles autres peuvent attendre? Comment s'assurer que tout est synchronisé? Typiquement, on fera la distinction entre les données importantes (une transaction entre deux joueurs, un avancement de quête) et celles qui le sont moins (monstre tué, ressources prises). Personne ne se plaindra si après un crash, une créature n'est pas exactement à la même position, ou si la mine contient encore les minerais que l'on a extrait (mais attention quand même, si un joueur découvre comment faire crasher le serveur, il pourrait se fabriquer une mine infinie?).
Enfin, quand il s'agit de coder tout ça, rappelez vous que moins on en écrit, mieux on se porte. Profitez au maximum des bibliothèques, écrivez du code générique pour binder vos données le plus efficacement possible, ou, encore mieux, utilisez des couches de persistance toutes faites (
Hibernate?).
Et sauvegardez. Tout. Tout le temps.