Les fichiers XML avec XNA

Lorsque l’on développe un jeu, il y a un moment où l’utilisation d’une base de données, même minimaliste devient indispensable pour y stocker par exemple, certaines informations comme les caractéristiques d’une entité, les dialogues, etc.. Effectivement on ne peut pas tout écrire en dur dans le code car c’est sale et c’est surtout difficilement éditable, surtout pour des non programmeurs. Le Framework XNA met à notre disposition une solution très simple à mettre en oeuvre pour utiliser les fichiers XML avec le gestionnaire de contenu. On pourra ainsi créer le fichier, le modifier à la main et le charger dans le jeu depuis le gestionnaire de contenu (ce fichier sera transformé lui aussi en xnb). Dans un premier temps je vais vous présenter rapidement ce que l’on peut faire avec ce système puis nous verrons en deuxième partie comment mettre oeuvre cette technique.

Mise en situation

J’avais une classe EnnemyManager qui était chargée de créer des vagues d’ennemies dans mon jeu. La méthode qui ajoute une vague d’ennemie génère un identifiant qui correspond à un type d’ennemie. Ainsi dans ma classe ennemie j’avais quelque chose comme ça :

public Ennemy(EnnemyType type)
{
  _type = type;

  switch (type)
  {
    case EnnemyType.AnciensA:
	  _textureName = "Ennemies/AnciensA_48";
	  _framerate = 200;
	  _animationSize = new Vector2(48, 48);
	  _animationIndex = new int[] { 1, 2, 3, 4 };
	  _speed = 0.3f;
	  _live = 100;
	  // Etc...
	  break;
	case EnnemyType.AnciensB:
	 // Et encore d'autres cas...
  }
}

Je vous epargne le reste du switch (qui à force contenait 6 types). J’ai finalement réécris mon constructeur pour qu’il ressemble à ça  :

public Ennemy(EnnemyDescription description)
{
	_type = (AlienType)description.Id;
	_textureName = description.AssetName;
	_framerate = description.Framerate;
	_animationSize = new Vector2(description.AnimationSize[0], description.AnimationSize[1]);
	_animationIndex = description.AnimationIndex;
	_speed = description.Speed;
	_live = description.Health;

	// D'autres trucs
}

La première chose qui saute au yeux est la taille du constructeur qui a vraiment diminué ! C’est normal car on passe d’un switch avec des valeurs de variable en dur à une simple initialisation avec un objet de description. Voyons de plus prés le contenu de la classe EnnemyDescription.cs :

namespace SpaceGame.Data.Description
{
    public enum EnnemyType
    {
        OrganicsA = 0, OrganicsB, RobotA, RobotB, AncienA, AncienB
    }

    public class EnnemyDescription
    {
        public int     Id              { get; set; }
        public string  AssetName       { get; set; }
        public int     Health          { get; set; }
        public int     Framerate       { get; set; }
        public float   Speed           { get; set; }
        public int[]   AnimationSize   { get; set; }
        public int[]   AnimationIndex  { get; set; }
    }
}

Cette classe est très simple, on y retrouve les informations de base caractérisant un ennemie dans le jeu. La propriété Id est un entier qui représente la valeur de l’énumération EnnemyType, ainsi (int)EnnemyType.OrganicsA vaudra 0.

Maintenant voyons comment charger ces données pour initialiser un ennemie. Dans un premier je charge les données depuis le gestionnaire de contenu :

// Chargement depuis le gestionnaire de contenu
// Dans mon cas c'est une collection de description
SpaceCollection<EnnemyDescription> ennemiesDescription  = Content.Load<SpaceCollection<AlienDescription>>("Datas/ennemies");

// Récupération de la 1ère description qui correspond à
// EnnemyType.AnciensA (qui vaut 0 rappellez vous)
EnnemyDescription desc = ennemiesDescription[(int)EnnemyType.AnciensA];

// Création de l'ennemie
Ennemy ennemy = new Ennemy(desc);

C’est redoutablement efficace ! Dans mon cas je fais plus que charger une simple description d’ennemie car je charge en réalité toutes les descriptions dans une collection, que je stock dans la partie statique du jeu, ainsi je peux y accéder à n’importe quel moment. Voyons un extrait du fichier XML présent dans le dossier Content :

<?xml version="1.0" encoding="utf-8"?>
<XnaContent xmlns:Collection="SpaceGame.Data.Collection" xmlns:Description="SpaceGame.Data.Description">
  <Asset Type="Collection:SpaceCollection[Description:EnnemyDescription]">
    <collection>
      <Item>
        <Key>0</Key>
        <Value>
          <Id>0</Id>
          <AssetName>Ennemys/OrganicsA_48</AssetName>
          <Health>100</Health>
          <Framerate>150</Framerate>
          <Speed>1</Speed>
          <AnimationSize>48 48</AnimationSize>
          <AnimationIndex>1 2 3 4</AnimationIndex>
        </Value>
      </Item>
      <Item>
        <Key>1</Key>
        <Value>
          <Id>1</Id>
          <AssetName>Ennemys/OrganicsB_48</AssetName>
          <Health>100</Health>
          <Framerate>150</Framerate>
          <Speed>1</Speed>
          <AnimationSize>48 48</AnimationSize>
          <AnimationIndex>1 2 3 4</AnimationIndex>
        </Value>
      </Item>
    </collection>
  </Asset>
</XnaContent>

C’est très pratique car on peut facilement l’éditer à la main ou même construire un logiciel qui va le générer, le travail d’équipe sera largement facilité durant la phase d’affinage de Gameplay 🙂

Bon passons maintenant à la 2éme partie où nous allons mettre en place ce système avec un exemple très simple.

Paramétrage du projet

Il va nous falloir 3 projets différents pour mettre en place cette fonctionnalité, en réalité un projet est suffisant si vous ne faites qu’exploiter le fichier XML, mais comme nous voulons aussi le créer il nous faut 2 autres projets supplémentaires, que je détail ci-dessous.

  1. Le projet de jeu XNA pour Windows (ou Xbox ou WP7)
  2. Un projet de bibliothéque XNA pour Windows (ou Xbox ou WP7)
  3. Un projet C# sans interface graphique (mode console)

Projet de jeu XNA

La première étape va être de créer un nouveau projet de jeu (Windows Game 4.0). Pour ce tutoriel je l’ai appelé DemonixisGame (original vous ne trouvez pas ?). Ce projet contiendra le jeu, sa logique, etc…

Projet de bibliothéque XNA

Vous allez maintenant ajouter un nouveau projet en faisant un clic droit sur le nom de la solution, puis Ajouter > Nouveau projet > XNA Game Studio 4.0 > Windows Game Library 4.0, dans le cadre de ce tutoriel je l’ai nommé DemonixisGame.Data car c’est un projet qui ne contiendra que des données et des schémas de description, rien d’autre. Je vous invite à référencer tout de suite ce projet sur le projet de jeu et sur le projet de contenu (Clic droit sur le dossier référence des 2 projets > Ajouter une référence > Projet > Demonxis.Data).

Projet d’application Windows (console)

Enfin, il faut maintenant créer un dernier projet qui lui ne sera pas un projet XNA, mais une simple application Windows en console. Clic droit sur la solution > Ajouter > Nouveau projet > Visual C# > Application console. Vous pouvez la nommer DemonixisGame.ConsoleSerializer pour rester original. Ce dernier projet va nous permettre de générer le fichier XML à partir de données qu’on entrera en dur la première fois afin de créer la bonne structure du fichier (oui il faut une première fois pour tout ^^). Encore une fois il faut ajouter une référence vers le projet de donnée (Demonixis.Data). Vous pouvez constatez que le projet de bibliothèque est un pont entre tous tous les projets et c’est d’ailleurs son but (pour information, quand vous compilerez votre projet complet vous aurez un fichier DLL DemonixisGame.Data.dll à côté de votre exécutable de jeu).

Le projet console va utiliser l’IntermediateSerializer pour créer le fichier XML, il faut donc référencer les fichiers DLL suivants qui sont présents dans le dossier c:\Program Files (x86)\Microsoft XNA\Reference\Windows\ .

  1. Microsoft.Xna.Framework.dll
  2. Microsoft.Xna.Framework.Content.Pipeline.dll

Mise en place

Notre objectif va être d’exploiter la classe HeroDescription ci-dessous, elle est à ajouter dans le projet de bibliothèque (les données)

namespace DemonixisGame.Data
{
    public class HeroDescription
    {
        public string Name { get; set; }
        public int Health { get; set; }
        public int Mana { get; set; }
        public int WeaponId { get; set; }
    }
}

Maintenant dans le projet de jeu, pour exploiter ces données, on va créer un objet de type HeroDescription grâce au gestionnaire de contenu. Pour que cette opération fonctionne il faut bien évidemment que le dossier Content contienne un fichier xml avec le bon nom.

using DemonixisGame.Data;

private HeroDescription heroDescription;

protected override void LoadContent()
{
    spriteBatch = new SpriteBatch(GraphicsDevice);
    heroDescription = Content.Load<HeroDescription>("MageDescription");
}

C’est le fichier MageDescription.xml qui sera chargé. On va maintenant passer au projet Windows Console pour créer le fichier XML qui sera utilisé par le gestionnaire de contenu.

La sérialisation

La première chose à faire est de changer la version cible du Framework .Net de votre projet afin d’utiliser la version 4.0 et pas la version 4.0 Client Profile (Projet > propriétés > Application > Framework Cible).

Il faut ensuite ajouter quelques espaces de noms afin de pouvoir utiliser les fonctionnalités XML du Framework .Net, l’IntermediateSerializer du Content Pipeline de XNA et enfin les données issues de votre jeu (via le projet de bibliothèque).

using System.Xml;
using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Intermediate;
using DemonixisGame.Data;

Ensuite dans la fonction Main, nous allons créer un objet de type HeroDescription et renseigner ses propriétés.

static void Main(string[] args)
{
  HeroDescription heroDescription = new HeroDescription()
  {
    Health = 100, Mana = 200, 
	Name = "Vivi", WeaponId = 12
  };

  XmlWriterSettings settings = new XmlWriterSettings();
  settings.Indent = true;

  using (XmlWriter writer = XmlWriter.Create("MageDescription.xml", settings))
  {
    IntermediateSerializer.Serialize<HeroDescription>(writer, heroDescription, null);
  }
}

Ce simple code permet de créer le fichier XML représentant notre classe HeroDescription.cs. Une fois le projet compilé vous pouvez vous rendre dans le dossier « Visual Studio 2010\Projects\DemonixisGame\DemonixisGame.ConsoleSerializer\bin\Debug ». LanceZ le programme et vous verrez apparaitre à côté de l’exécutable un fichier XML MageDescription.xml avec ce contenu :

<?xml version="1.0" encoding="utf-8"?>
<XnaContent xmlns:Data="DemonixisGame.Data">
  <Asset Type="Data:HeroDescription">
    <Name>Vivi</Name>
    <Health>100</Health>
    <Mana>200</Mana>
    <WeaponId>12</WeaponId>
  </Asset>
</XnaContent>

Il ne reste plus qu’à déposer ce fichier dans le dossier Content du projet de contenu et compiler le jeu pour vérifier que tout fonctionne bien. Si vous avez une erreur de compilation il est très probable que ce soit à cause d’une référence manquante sur le projet de contenu (Il faut qu’il ai une référence vers le projet de données pour connaitre le type sérialisé). Vous pouvez maintenant utiliser le contenu de ce fichier directement dans votre jeu et faire des affinages des données au besoin.

Ne vous limitez pas à de simple objet de description, vous pouvez sérialiser des collections (génériques ou standards), des tableaux, etc…

Conclusion

Nous avons vue comment utiliser et exploiter le système de sérialisation proposé par XNA, celui-ci s’avère relativement pratique pour stocker des données et les réutiliser plus tard dans son jeu. La prochaine étape pour exploiter au mieux cette fonctionnalité, pourrait être de créer une interface graphique permettant de créer et éditer les données.

J’attire votre attention sur le fait que le fichier xnb généré durant la sérialisation est parfaitement compatible avec le Framework MonoGame 😉 Vous pouvez donc utiliser cette technique sur vos projet Windows, Linux, Mac, Android et j’en passe.