Sauvegarder des données sous Windows avec XNA

La sauvegarde des données est aujourd’hui quelque chose d’assez banale et ce, même sur les petites productions. C’est effectivement pratique pour le joueur de pouvoir régler une fois les paramètres et option du jeu et d’avoir sa progression de sauvegardée. Comme toujours le Framework XNA met à notre disposition une fonctionnalité intéressante pour sauvegarder et charger des données assez facilement. Le stockage est un sujet assez vaste car il est très dépendant du système sur lequel vous développez, ici nous ne traiterons que du cas sur PC sous Windows avec XNA, nous aurons l’occasion de revenir sur les autres cas dans des articles dédiés.

Mise en situation

L’objectif de ce tutoriel va être de sauvegarder des paramètres système dans l’espace de stockage de l’utilisateur et de les charger au lancement du jeu. Lorsque le jeu démarre on va vérifier qu’ils existent et si ce n’est pas le cas on utilisera des valeurs par défaut. La configuration du jeu sera stockée dans une classe très simple que voici :

[Serializable]
public class GameConfiguration
{
	public int Width;
	public int Height;
	public bool IsFullScreen;
	public float SoundVolume;
	public float MusicVolume;

	public GameConfiguration()
	{
		Width = 1280;
		Height = 720;
		IsFullScreen = false;
		SoundVolume = 0.7f;
		MusicVolume = 0.9f;
	}

	public GameConfiguration(int width, int height, 
		bool fullscreen, float soundVolume, float musicVolume)
	{
		Width = width;
		Height = height;
		IsFullScreen = fullscreen;
		SoundVolume = soundVolume;
		MusicVolume = musicVolume;
	}
}

Vous noterez que la classe est sérialisable ce qui est logique car son contenu va être sérialisé au format XML. Voyons maintenant comme travailler avec cette classe et surtout comment sauvegarder son contenu. Avant de commencer à rentrer dans le vif du sujet sachez que les sauvegardes seront stockées dans le dossier c:\Users\{Nom d’utilisateur}\Documents\SavedGames\{Nom de votre projet}\, vous noterez que c’est l’endroit où sont stocké les fichiers de sauvegarde (paramètres, statistiques, parcours du joueur, etc…) de  tous les jeux qui utilisent XNA et son système de stockage.

Je vous propose de créer une classe qui sera chargé de charger et sauvegarder des données dans l’espace de stockage de l’utilisateur. Je vous donne le début de la classe et nous la compléterons petit à petit avec les 2 méthodes manquantes.

using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Storage;

namespace StorageGameProject
{
    public class StorageManager
    {
        private const string ConfigurationFileName = "game.config";
        private StorageDevice _storageDevice;

        public StorageManager()
        {
            IAsyncResult result =  
		StorageDevice.BeginShowSelector(PlayerIndex.One, null, null);
            _storageDevice = StorageDevice.EndShowSelector(result);
        }

        public GameConfiguration LoadGameConfiguration()
        {

        }

        public void SaveGameConfiguration(GameConfiguration gameConfig)
        {

        }

        private StorageContainer GetContainer(string name)
        {
            IAsyncResult result = _storageDevice.BeginOpenContainer(name, null, null);
            result.AsyncWaitHandle.WaitOne();

            StorageContainer container = _storageDevice.EndOpenContainer(result);
            result.AsyncWaitHandle.Close();

            return container;
        }
    }
}

Dans un premier temps il faut ajouter les espaces de nom permettant de travailler avec les fichiers, les fichiers XML et le gestionnaire de stockage de XNA. Intéressons nous au constructeur de cette classe qui appel deux méthodes statiques issues de l’objet StorageDevice de XNA. Il faut savoir que sur PC, les actions de ces méthodes sont invisibles pour l’utilisateur car BeginShowSelector() sert à afficher à l’écran la liste des périphériques de stockage disponible et à inviter l’utilisateur a en sélectionner un, la méthode EndShowSelector() étant quant à elle appelée quand l’utilisateur a fait son choix. On retrouve ce fonctionnement sur Xbox 360 mais sur PC c’est totalement transparent pour l’utilisateur car il n’a qu’un seul stockage de disponible, son disque dur 😉

Choix du périphérique de stockage sur Xbox 360
Choix du périphérique de stockage sur Xbox 360

La méthode BeginShowSelector() va dans notre cas sélectionner le disque dur de l’utilisateur comme espace de stockage et EndShowSelector() va renvoyez une instance d’un objet de type StorageDevice pointant sur ce périphérique. Maintenant que le périphérique de stockage est défini, on peut l’utiliser pour sauvegarder ou charger des données. Vous noterez qu’on passe un paramètre PlayerIndex pour indiquer qu’on veux récupérer l’espace de stockage pour le joueur 1, on peut donc travailler avec les espaces de stockages des 4 joueurs au besoin.

Les conteneurs

Un conteneur est un endroit dans l’espace de stockage qui contient un ou plusieurs fichiers, par exemple nous pourrons avoir un conteneur « Sauvegardes » où on stockera tous les fichiers de sauvegarde, un conteneur « Paramètres » où on stockera tous les paramètres relatif à l’application ou encore un conteneur « Score » qui contiendrait tous les scores et succès remportés pour le joueur. Au final un conteneur n’est rien de plus qu’un dossier.

L’ouverture d’un conteneur est une opération asynchrone c’est à dire que l’exécution du jeu n’est pas bloquée durant la phase d’ouverture. Comme pour la récupération du stockage, on va utiliser les méthodes BeginOpenContainer() et EndOpenContainer() pour ouvrir un conteneur nommé.

Le chargement

Maintenant que nous savons comment récupérer l’espace de stockage utilisateur et ouvrir un conteneur, nous allons pouvoir stocker des données. En début de tutoriel j’ai donné une classe qui gère la configuration du jeu (GameConfiguration) et nous allons la créer au lancement du jeu pour récupérer les paramètres d’affichage.

public class Game1 : Microsoft.Xna.Framework.Game
{
	GraphicsDeviceManager graphics;
	SpriteBatch spriteBatch;
	StorageManager storageManager;
	GameConfiguration gameConfiguration;

	public Game1()
	{
		graphics = new GraphicsDeviceManager(this);
		Content.RootDirectory = "Content";

		storageManager = new StorageManager();
		gameConfiguration = storageManager.LoadGameConfiguration();

		graphics.PreferredBackBufferWidth = gameConfiguration.Width;
		graphics.PreferredBackBufferHeight = gameConfiguration.Height;

		if (gameConfiguration.IsFullScreen)
			graphics.ToggleFullScreen();
	}
}

C’est donc le fichier Game1.cs qui se contente d’initialiser notre gestionnaire de stockage ainsi qu’une instance de GameConfiguration. La méthode LoadGameConfiguration() du gestionnaire de stockage sera chargée de récupérer les informations enregistrées si elles existent, sinon, elle renverra une instance par défaut qui contient des paramètres… par défaut. Son résultat permettra de configurer l’affichage du jeu.

public GameConfiguration LoadGameConfiguration()
{
	StorageContainer container = GetContainer("config");

	// Pas de sauvegarde
	if (!container.FileExists(ConfigurationFileName))
		return new GameConfiguration();

	// La sauvegarde existe
	Stream stream = container.OpenFile(ConfigurationFileName, FileMode.Open);

	XmlSerializer serializer = new XmlSerializer(typeof(GameConfiguration));

	GameConfiguration config = (GameConfiguration)serializer.Deserialize(stream);

	stream.Close();

	// Ne pas oublier de fermer le flux ET le container
	container.Dispose();

	return config;
}

La première étape consiste à récupérer le conteneur dans lequel les paramètres doivent être enregistrés, j’ai choisis de le nommer « config », il y aura donc un dossier « config » dans l’espace de stockage. Si le conteneur n’existe pas alors on retourne une instance de GameConfiguration qui possède des paramètres par défaut, sinon on ouvre un flux et on ouvre le fichier ou mode ouverture. L’objet XmlSerializer va nous permettre de désérialiser le contenu du fichier et en récupérer une instance, vous comprenez maintenant pourquoi il fallait que notre classe de configuration ai l’attribut « Serializable« . Il ne faut surtout pas oublier de fermer le flux et le conteneur. Nous sommes assurés que le jeu récupère bien des données.

La sauvegarde

Intéressons nous maintenant à la sauvegarde de données. Voyez plutôt le minuscule bout de code suivant :

// Sauvegarde des paramètres
if (state.IsKeyDown(Keys.S))
{
	gameConfiguration = new GameConfiguration(640, 480, true, 0.5f, 0.4f);
	storageManager.SaveGameConfiguration(gameConfiguration);
}

Si l’utilisateur presse la touche S alors les paramètres de configuration sont modifiés et ils sont sauvegardés dans le stockage utilisateur grâce à notre fantastique StorageManager (Lisez le avec un accent à l’américaine bien prononcé, ça le fera encore plus :P). On va maintenant terminer la classe de gestion du stockage avec l’implémentation de sa dernière méthode.

public void SaveGameConfiguration(GameConfiguration gameConfig)
{
	StorageContainer container = GetContainer("Config");

	if (container.FileExists(ConfigurationFileName))
		container.DeleteFile(ConfigurationFileName);

	Stream stream = container.CreateFile(ConfigurationFileName);

	XmlSerializer serializer = new XmlSerializer(typeof(GameConfiguration));
	serializer.Serialize(stream, gameConfig);
	stream.Close();

	container.Dispose();
}

Après récupération du conteneur on supprime le fichier de configuration et on le réécrit à la fin avec les nouvelles données qui sont passées en paramètre à la méthode.

Conclusion

Il n’y a pas à dire, la sauvegarde de données sur PC est vraiment très simple et grâce à cette nouvelle fonctionnalité vous allez pouvoir stocker facilement la configuration du jeu et les sauvegardes. Comme je l’ai annoncé en introduction le stockage est un gros morceau car il est très dépendant du système utilisé. Par exemple sur Xbox il faut bien faire attention que tous les conteneurs soient fermés avant d’essayer d’en ouvrir un autre. Sachez qu’il existe des composants pour XNA qui proposent une abstraction de haut niveau sur le stockage en proposant une API qui passe partout (mobile, console, pc), cependant il faut à mon sens, connaitre un peu le fonctionnement du stockage en natif avant de passer sur quelque chose de plus haut niveau.

Vous pouvez retrouver les sources de ce tutoriel sur mon espace Github à cette adresse.