L’expérience est une chose merveilleuse, je ne saurais dire combien ma vision des choses a évolué au cours de ma pourtante si petite carrière actuelle. Je pense que c’est d’ailleurs valable pour la majorité des gens, l’expérience change votre façon de voir les choses jusqu’au moment où vous décidez que vous n’apprendrez plus jamais rien (et là vous êtes un « vieux »).
Pour illustrer ce principe j’aimerais aborder aujourd’hui l’évolution de ma façon de traiter le volume considérable de données auxquelles on fait face lorsqu’on réalise un jeu.
Peut-être que cela n’a pas l’air comme ça, mais c’est dingue le volume de petites choses externes à l’application que l’on manipule dans un jeu. Que cela soit la description des niveaux, les fichiers d’animations des personnages, les sons, tout cela doit être exprimé quelque part.
Personellement je n’ai pas connu l’époque où le code de l’application comportait des parties en « dure » pour gérer tel ou tel niveau. A l’époque j’étais encore en train de faire des démos sur Amiga ou même sur Amstrad CPC où l’inclusion même des données au code était monnaie courante. A tel point que quand j’écrivais du code pour lire des fichiers externes, mes congénères me regardaient bizarrement, une autre époque je vous dis.
Mais cet époque pris fin lorsque tous le monde se mit à écrire du code « data driven ». Sous cette appellation se cache simplement le fait d’écrire un moteur générique qui va interpréter un ensemble de données externes à l’application pour créer votre expérience interractive. On peut alors considérer l’exécutable de votre jeu comme un « player » permettant de lancer tel ou tel niveau de jeu. Cela a permis de mettre fin à l’épisode ubuesque du programmeur qui intègre directement les sprites du jeu dans le code source assembleur de l’application.
C’est alors que rentre en ligne de compte la « chaine de production », c’est à dire que de nombreux intervenants vont mettre les mains dans le projet en créant chacun des éléments du jeu. Le programmeur n’est alors qu’un acteur dont la position n’est plus aussi centrale qu’avant. Il fournit l’exécutable du jeu ainsi que des outils qui vont être utiliser par d’autres gens qui vont mettre en oeuvre l’ensemble pour créer le jeu. Cela nous ramène à notre notion de données et surtout, sous quelles formes sont présentes ces données.
Au départ nous avions une approche assez fermée de la façon de créer les données. Ainsi pour Desperados nous avions écrit un éditeur complet qui générait un fichier binaire unique contenant toute les données nécessaire à l’exécution d’un niveau par l’applicatif principal. Cela fonctionnait mais pour certaines petites choses, la mise à jour de l’éditeur était un processus lourd et contraignant. L’ajout d’une quelconque fonctionnalité nécessité la mise à jour du format de fichier ainsi que la synchronisation entre l’exécutable principale du jeu et l’éditeur.
Cette situation était génante, mais par manque de temps et surtout en vertu de l’application de la formule « if it isn’t broke, don’t fix it » nous avons conservé ce mécanisme tout au long du développement. Mais l’idée trotait déjà dans ma tête d’utiliser « autre chose ».
Nous n’avons pas eu l’occasion de suite de mettre en cause cette façon de faire. Notre projet suivant étant Robin des bois, un jeu basé sur le même moteur. Nous avons donc principalement effectué une refonte de l’éditeur de Desperados (quoi que cela s’avéra un peu plus complexe que cela, mais j’y reviendrais un autre jour). L’occasion d’expérimenter de nouvelles choses se présenta lorsque les cartons de certaines nouvelles fonctionnalités arrivèrent sur notre bureau.
Ainsi dans Robin des bois, il y avait un système de code de couleurs pour les ennemis. Nous devions donc stocker quelques part les caractéristiques des sbires du Prince Jean. Au départ la personne en charge de l’éditeur proposa d’utiliser la bonne vieille méthode du « rajoutons un module dans l’éditeur et écrivons dans le fichier binaire », mais il ne savait pas que déjà dans ma petite tête se déroulait « La Grande Croisade contre les fichiers binaires ». Je proposais à ce moment là d’utiliser un fichier CSV. Il s’agissait en fait de laisser Die… hmm je veux dire le Game Designer modifier les données du jeu en éditant un fichier excel puis en exportant sous forme d’un fichier CSV.
Il se trouva que ce fut une « bonne pioche ». En effet non seuleument nous économisions le temps de développement du module dans l’éditeur de niveau (et croyez moi c’est fou ce que cela peut prendre comme temps ce genre de petites choses) mais de surcroit notre Game Designer était content d’avoir un contrôle aisé des variables des personnages non joueurs dans un outil qu’il connaissait bien. Pour résumer nous avions réduit non seuleument les temps de développement au niveau de la programmation mais nous avions aussi réduit la durée de mise au point des caractéristiques des personnages non joueurs. Ce dernier point peut sembler sortir de nulle part, mais il faut bien voir que notre fichier binaire nécessité de lancer l’éditeur, charger le niveau (souvent très lourd), d’exporter le fichier binaire (très long) puis de lancer le jeu à comparer à l’édition sous excel (rapide), l’exportation du fichier (rapide) et lancer le jeu.
Nous étions gagnant sur les deux tableaux mais j’avais le sentiment que nous pouvions aller plus loin encore. Je n’ai pas eu à attendre longtemps une opportunité de vérifier ce sentiment, l’éditeur de niveau décidant de se jeter sur moi.
L’éditeur de niveau prenait du retard par rapport au planing. Or dans notre cas de figure cela devient d’autant plus génant que vous avez une pièce remplie de Level Designer qui vous harcèlent chaque jour pour pouvoir commencer à travailler. Je décidais donc de filer un coup de main aux gens en charge de l’éditeur avec comme but de rattraper le planing. J’héritais alors du module s’occupant de la sauvegarde et du rechargement d’un fichier de niveau. Il ne s’agissait pas du module qui écrivait le fichier binaire pour le moteur du jeu mais du fichier de travail de l’éditeur.
La problématique à laquelle j’avais à faire face était que chaque jour apportait son lot de nouvelles fonctionnalités, ce qui signifait sauvegarder plus de données dans le fichier de travail. Ce fichier de travail était aussi un fichier binaire, on m’exposa donc la stratégie pour ajouter de nouvelles données dans ce fichier, on s’accroche au pinceau…
Nous avions donc dans le code de l’éditeur un objet en charge de la relecture de ce format de travail. Au début du fichier binaire il y avait un identifiant indiquant la version du fichier. Une nouvelle version du fichier nécessitait de dupliquer tous le code de l’objet chargé de la relecture et de modifier le code afin de prendre en compte les modifications. Au chargement l’éditeur regardait juste s’il avait un objet pouvant recharger la version du fichier, dans ce cas là il instancie l’objet et ce dernier recharge le niveau.
Cela signifie que à chaque changement dans le format de fichier aussi miniscule qu’il soit, comme par exemple rajouter un attribut « peut pas être ouverte par un nain » sur une porte, il faut dupliquer l’objet de chargement, éditer ce code et toutes les dépendances associés. Non seuleument ce processus est long et pénible mais de surcroit il est très facile d’introduire des bugs dans ce processus. Il fallait donc trouver autre chose.
C’est dans ces cas là que la veille technologique s’avère souvent utile. Personnellement je n’ai jamais vu de bon programmeur qui ne soit pas perpétuellement à l’écoute de ce qui se passe de nouveau autour de lui, lise beaucoup de magazines, de livres et de sites web. Car c’est en restant toujours informé qu’on trouve les bonnes idées pour progresser dans son travail. J’ai un peu l’impression d’enfoncer des portes ouvertes en disant cela, de toute manière chaque fois que j’en ai parlé avec un « bon » il avait à peu de chose près la même vue sur le sujet.
Donc je lisais un article sur le XML lorsque je me suis dit que c’était ce qu’il me fallait pour régler mon problème d’éditeur. A la base le XML est plus un concept qu’autre chose : une généralisation du HTML où les tags peuvent avoir le nom que vous voulez. Bien entendu par dessus cela se greffe d’autres notions plus orienté pour le développement web comme le XLST. Mais la façon d’exprimer les données comme le permettait le XML me suffisait.
Je pris alors le temps de démonter l’ancien code de l’éditeur, et de programmer rapidement un « parseur » pour mon fichier XML en me basant sur la librairie Xerces de l’Apache Fundation. Le Document Object Model alias DOM, transforme votre document XML en arbre dans lequel il est facile de se promener. L’autre alternative existante étant SAX, (the simple API for XML) qui est une API plus optimisé pour lire le document XML en générant des évènements à chaque élément lexicaux du fichier.
La stratégie pour lire les différents éléments du fichier fut alors très simple :
- chaque feuille de l’arbre XML correspond à un type « simple » (nombre, chaine de charactère) qui est directement mis dans la variable adéquat d’un objet.
- à chaque noeud de l’arbre XML correspond un objet en charge de s’occuper des feuilles liées à ce noeud et de sous traiter les types plus complexes fils de ce noeud aux objets correspondant.
Cela me donnait une structure très flexible pour traiter les fichiers, l’ajout d’un nouvel attribut pour un objet s’effectuant en quelques lignes de code et la refonte d’un objet nécessitant juste la dissociation des deux cas, cas qui étaient identifiés à l’aide du nom du noeud. Cela donnait un code de ce genre là :
void GEGeometryElement::Load( DOM_Node& nodeRoot )
{
ENUMERATE_CHILD( nodeRoot )
{
bool bProcessed = false;
IMPLEMENT_SUBOBJECT( "IGraphicalElement", IGraphicalElement );
// Yes shape is not exporting anything
IMPLEMENT( "GPPolypoint" )
{
m_shape.GPPolypoint::Load( nodeChild );
bProcessed = true;
}
IMPLEMENT( "GPShape" )
{
m_shape.Load( nodeChild );
bProcessed = true;
}
if( bProcessed == false )
{
throw ELoadFailed( XMLTool::ExtractFullNameString( nodeChild ) );
}
}
}
Dans cet exemple l’éditeur essaye de relire une node du type
En cas d’ajout d’un attribut on a juste à rajouter le « IMPLEMENT(« nom_unique_du_nouveau_noeud ») » qui va bien. La fonction de sauvegarde ressemble beaucoup à la fonction de rechargement, je ne vais donc pas la recopier ici.
Pour résumer l’histoire de l’éditeur, une fois l’implémentation de la gestion du XML réalisé, les modifications sur l’éditeur sont allés beaucoup plus rapidement. C’est surtout le délai entre la décision d’implémentation d’une feature et la livraison à l’utilisateur qui fut drastiquement réduite. Malgrès l’aspect « nouveau » du XML il fut rapidement adopté par mes collègues car on venait juste de remplacer leur tromblon par une mitrailleuse et il n’y avait pas besoin d’avancer beaucoup d’autres arguments.
Ce que j’en ai tiré de cet expérience c’est que la veille technologique est indispensable. Cette remarque fait très « enfoçage de portes ouvertes » mais j’ai déjà si souvent croisé des gens qui n’ont jamais réfléchi à la méthodologie qu’ils mettent en oeuvre parce qu’elle fonctionne et qu’ils y sont habitués depuis si longtemps que je me dis qu’une piqure de rappel ne fait jamais de mal.
Mon deuxième point est qu’il faut essayer de réutiliser au maximum certains standard de l’industrie. Pourquoi ? J’aurais sans doute pu faire mon petit format de fichier bien à moi, un peu dans le même genre, non ? Oui mais là l’avantage c’est que « quelqu’un d’autre » écrit le parser, le débogue et l’optimise. Moi je me contente juste d’utiliser le produit final, en choisissant même les variantes en fonction de ce que je veux faire. Ainsi dans un programme plus récent que j’ai écrit je n’avais besoin que d’effectuer une lecture sur le fichier j’ai donc utilisé SAX qui est plus performant et dont les fonctionnalités suffisaient pour la tache. Le format de fichier n’a pas bougé d’un iota, juste quelques petits bouts de code ont du être écrit.
Ce fut néanmoins la dernière évolution de l’éditeur pour la gamme de jeu reposant sur le moteur de Desperados. Néanmoins nous avions déjà pas mal d’idées pour l’avenir concernant les éventuels « éditeurs » que nous aurions à produire pour un nouveau jeu. Une des principales limitation du modèle présenté était l’aspect monolithique du programme. Cela avait pour inconvénient d’avoir une très grosse application plus difficilement gérable et surtout une seule personne à la fois pouvait travailler sur un niveau alors que la gamme de métier travaillant sur un niveau était plutôt large.
C’est pourquoi l’idée consistait à utiliser un fichier XML unique pour un niveau, mais à utiliser plusieurs éditeurs, chacun spécialisé dans un domaine particulier. Avec le format XML il était simple de ne parser que la partie intéressant l’éditeur dans le fichier et de simplement réécrire sur le fichier de sortie le reste sans le retoucher. D’autant plus que en utilisant un programme de « source control » (tels que subversion), la nature textuelle du XML permet à plusieurs gens de travailler sur le même fichier et de fusionner les modications au final.
Mais je garde d’autres anecdotes concernant les fichiers XML pour un prochain article, car depuis j’ai un peu généralisé l’emploi de ce format de fichier pour faire plein de choses.