Sauvegarder la progression du joueur

10606587153_231241c2b1_b

Dans les jeux vidéo de première génération, il n’y avait pas de sauvegarde. Le joueur devait terminer le jeu d’une traite, sans éteindre la console sous peine de tout perdre. Cela avait un réel impact sur la difficulté (ainsi que sur la satisfaction à terminer un titre).

Ce temps est bien loin, l’absence de sauvegarde également. Si certains jeux limitent la sauvegarde (dans les phases de combat pour Mass Effect), ou ne laissent carrément pas le choix au joueur (dans Rise of the Triads), le joueur peut quand même se raccrocher à une bouée en cas de pépin.

Aujourd’hui, un jeu vidéo sans sauvegarde est presque impensable. Je dis « presque » parce qu’il y a toujours moyen de se servir de ça comme élément de gameplay. Mais ça reste une exception. Voyons ensemble comment il est possible de stocker des informations de progression avec Unity.

Disclaimer

Cet article a été originalement écrit le 14/11/2014 sur mon précédent blog. Certaines informations présentées ici peuvent donc ne plus fonctionner telles quelles.

Introduction

Nous allons voir deux façon de stocker des informations. Les deux se valent, les deux ont des avantages, et les deux ont des inconvénients. C’est comme le débat Windows / Linux : les deux ont des avantages, les deux ont des inconvénients (si, si, des deux côtés, arrête de faire ton fanboy cher lecteur 😉 ). Pour ce tutoriel, nous n’aurons pas vraiment besoin d’une scène, le focus sera sur le code.

Les PlayerPrefs.

Unity permet de stocker des valeurs (entier, nombre flottant et chaînes de caractères) via les PlayerPrefs. Cette API est prévue pour stocker les préférences du joueur. Par exemple le mapping de touches ou le volume sonore. Ce n’est pas prévu pour stocker de grandes quantités d’informations. Sa taille est d’ailleurs limitée à 1Mo pour le WebPlayer.

Il est cependant possible de « détourner » son utilisation pour stocker des informations de progression. C’est un conteneur de données après tout !

Dans un fichier

Nous avons accès à ‘API C# dans les scripts, il est donc possible de manipuler nous même un fichier pour y mettre des infos dedans et sauvegarder nos données. L’avantage est que l’on a complètement la main sur ce qu’on fait, où on le fait et comment. L’inconvénient est qu’il faut coder soi même la logique de sauvegarde. Et coder encore plus pour blinder les potentiels problèmes de lecture, fichiers corrompus, fichiers introuvables… car il est tout à fait possible que le joueur essaie de bidouiller ces fichiers. Ne me regardez pas comme ça, je suis persuadé que vous avez déjà utilisé un Trainer pour vous donner de l’or et de l’équipement dans Diablo 2 !

Via les PlayerPrefs

L’utilisation des PlayerPrefs est simple. Très simple. Vous avez un conteneur d’information dédié à votre jeu. Tout ce que vous mettrez dedans n’entrera pas en conflit avec d’autres jeux Unity installés sur le terminal. Pas besoin non plus de vous soucier de l’endroit où c’est stocké.

Au niveau du code, c’est comme ça que ça se passe :

PlayerPrefs.SetFloat("mon_flottant", 0.5F);
PlayerPrefs.SetInt("mon_entier", 15);
PlayerPrefs.SetString("ma_chaine", "hello");

Rien de plus. Avec ces 3 méthodes, vous pouvez stocker les infos que vous voulez dans les PlayerPrefs avec la limitation citée plus haut. La lecture est quand à elle… à peu près aussi compliquée :

PlayerPrefs.GetFloat("mon_flottant");
PlayerPrefs.GetInt("mon_entier");
PlayerPrefs.GetString("ma_chaine");

Vous n’avez rien à initialiser, ces méthodes sont statiques et donc accessible de partout. La simplicité même. Pour plus d’infos sur l’utilisation des PlayerPrefs, je vous envoie directement vers la documentation officielle.

Via un fichier géré manuellement

La seconde façon de faire et de gérer les choses soi même. Comme on dit : on n’est jamais mieux servi que par soi-même ! Aller, on va se créer notre propre système de sauvegarde de données.

La première chose à faire est de créer notre gestionnaire statique. On va créer quelque chose de similaires aux PlayerPrefs. Cette classe sera statique et accessible de partout. Elle aura deux méthodes :

public class MyStorage
{
    public static void Save(object entity, string fileName)
    {

    }

    public static object Load(string fileName)
    {
        return null;
    }
}

Pour la sauvegarde et la lecture, nous allons nous baser sur la sérialisation générique. Pour cela, il faudra que l’objet que vous passez à la méthode Save ait l’attribut [Serializable]. C’est important. Quelque chose comme ça :

[Serializable]
public class MaClasseDeDonnees
{
    [...]
}

Nous allons donc créer un fichier dans la mémoire du terminal. Mais… où ? Les différents terminaux (pc, mobile, console…) n’ont pas du tout la même structure. Alors comment faire pour que le code fonctionne partout ? On va utiliser une variable « magique » : Application.persistentDataPath. Cette propriété en lecture seule nous donne un chemin sur le terminal dédié à l’application. Attention cependant, les version WebPlayer n’ont pas accès à l’écriture des fichiers, vous devrez donc passer par les PlayerPrefs en secours. Je ne couvrirai pas cette partie là, avec quelques instructions préprocesseur, vous pouvez le gérer assez simplement.

Pour sérialiser notre objet conteneur, nous allons passer par un BinaryFormatter :

public static void Save(object entity, string fileName)
{
    BinaryFormatter formatter = new BinaryFormatter();
    FileStream stream = File.Create(Application.persistentDataPath + "/" + fileName);
    formatter.Serialize(stream, entity);
    stream.Close();
}

Grosso modo, on sérialise l’objet via le BinaryFormatter directement dans le fichier. Et c’est bon ! Votre objet est binarisé dans le fichier, il est prêt à être rechargé plus tard. Ce fichier ne sera pas supprimé lorsque le joueur quittera l’application. Le risque comme abordé plus haut est que le joueur s’amuse à modifier ce fichier et le corrompe. Notez que le code ci dessous écrase le fichier s’il est déjà présent sans plus de procès.

Voilà comment lire le fichier, c’est le processus inverse :

public static object Load(string fileName)
{
    BinaryFormatter formatter = new BinaryFormatter();
    FileStream stream = File.Open(Application.persistentDataPath + "/" + fileName, FileMode.Open);
    var entity = formatter.Deserialize(stream);
    stream.Close();
    return entity;
}

On ouvre le fichier et on dé-sérialise. Et surtout on n’oublie pas de fermer le flux ! C’est important si vous ne voulez pas créer des locks sur les fichiers.

La méthode la plus sensible est bien entendu Load. En effet, en chargeant des données depuis un fichier, il y a un fort risque qui n’est pas du tout géré dans le code ci dessus. Plusieurs dangers sont présents :

  • Le fichier a été supprimé : vous aurez une belle FileNotFoundException en ouvrant le fichier
  • Le fichier a été modifié (corrompu) : vous aurez la plupart du temps une EndOfStreamException.

En gérant ces deux cas de figure, vous aurez un système solide de stockage des données. Je ne détaille pas la gestion de ces exception car c’est quelque chose de très spécifique. C’est à vous de voir comment vous comptez gérer ces problèmes dans vos applications et jeux.

Et voilà, sur ce, je vous laisse à votre code !

Crédits photo : Bob Mical

 

Advertisements

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s