Gérer des objets persistants entre les scènes

9586793153_1ef7e608ab_z

Dans vos projets Unity, vous utilisez plusieurs scènes pour diviser votre univers de jeu. Certaines des ces scènes auront des éléments qui devront être conservés d’une scène à une autre. Par exemple : en passant d’un niveau à un autre, la musique ne doit pas s’arrêter. En chargeant une nouvelle scène avec Application.LoadLevel, Unity va supprimer la totalité des objets de la scène courante et notre musique avec ! Il y a un moyen de contourner ce fonctionnement.

L'Atelier de Drakulo déménage vers Esprit Unity

Il est possible de débrayer la destruction de certains objets mais cette méthode à elle seule peut être problématique. En effet, si un objet est défini comme « immortel » (non détruit lors d’un changement de scène), il ne sera plus jamais détruit jusqu’à la sortie du jeu. Pour reprendre le cas de la musique, on voudrait qu’elle soit jouée dans les niveaux de jeu, mais pas dans les menus. Cela veut dire qu’il faut gérer manuellement la suppression de ces objets et leur création s’ils n’existent pas encore.

Si vous avez beaucoup d’objets dans ce cas de figure, pas tous présents sur les mêmes scènes, ça peut rapidement devenir l’enfer à gérer… C’est là que le gestionnaire d’objets persistants entre en jeu.

Disclaimer

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

Solution

Ma solution est de créer un manager générique qui va s’occuper de tout ça. C’est lui qui tâchera de :

  1. Créer les GameObjects nécessaires qui n’existent pas encore
  2. Supprimer les GameObjects qui ne sont plus voulus

Cette  sélection se fera via un fichier de configuration qui définira quel objet doit être présent, dans quelle scène. Un seul fichier pour faciliter les modifications futures et ajustements. Un seul fichier pour les gouverner tous. 😉

Mise en place

Situation d’exemple

Pour faciliter la compréhension de la suite, on va se placer dans la situation type suivante. Notre jeu sera composé de 5 scènes différentes :

tuto_scenes

 

On veut mettre en place les éléments suivants :

  • Une musique MenuMusic devra être jouée uniquement sur la scène Menu
  • Une musique LevelMusic devra être jouée uniquement sur les scènes Level_1 et Level_2
  • Une musique BossMusic devra être jouée uniquement sur la scène Boss
  • Un gestionnaire de score devra être présent sur les scènes Level_1, Level_2 et Boss
  • La scène Credits ne doit rien avoir : pas de son ni de gestionnaire de score

Le fichier de configuration

Pour le fichier de configuration, on va utiliser du XML mais vous pouvez utiliser n’importe quel autre format pour stocker ces informations. L’objectif est de rendre ce fichier facile à modifier.

La structure va être très simple :

scences > scene > object

Le noeud scenes sera la racine du fichier.
Le noeud scene représentera une scène du jeu.
Le noeud object représentera un GameObject qui devra être présent sur la scène.


 
  
 
 
  
  
 
 
  
  
 
 
  
  
 
 

Les objets

Pour permettre la gestion automatique des objets persistants, il va falloir faire un petit travail de préparation. Tout d’abord, chacun des GameObjects que nous allons utiliser dans le manager doit être « immortel ». Dans la mesure où tous les GameObjets n’auront pas forcément un script attaché (par exemple pour lire une musique), le plus clair est d’en créer un : DontDestroyMeOnLoad qui se chargera de rendre nos GameObjects « immortels » (oui j’aime bien ce mot). Ils seront alors facilement identifiables via ce script.

public class DontDestroyMeOnLoad : MonoBehaviour {
  void Awake()
  {
    DontDestroyOnLoad(gameObject);
  }
}

Pour empêcher la destruction d’un GameObject au changement de scène, il suffit d’appeler la méthode statiqueDontDestroyOnLoad en lui passant en paramètre le GameObject à « immortaliser ». Pourquoi mettre cet appel dans la méthode Awake et pas Start ? Eh bien parce que la méthode Awake est appelée avant Start. Ainsi, notre code d’identification des élus est exécuté en priorité sur le reste des autres scripts de la scène.

Créons maintenant les GameObjects que nous voulons utiliser :

  • MenuMusic, auquel on va attacher notre musique de menu
  • LevelMusic, auquel on va attacher notre musique de jeu
  • BossMusic, auquel on va attacher notre musique de boss
  • ScoreManager, auquel on pourrait attacher notre script de gestion du score

La scène dans laquelle vous allez créer ces GameObjects n’a pas d’importance. Considérez ça comme un brouillon, un espace de travail temporaire. N’oubliez pas d’attacher le script DontDestroyMeOnLoad à chacun d’eux !

tuto_objects

Une fois les GameObjects prêts, il faut créer un Prefab pour chacun d’eux. Celà va permettre au manager d’instancier ces GameObjects à la volée, lorsque cela sera nécessaire. Pour les créer, faites un drag’n’drop de chacun desGameObjects créé ci dessus depuis la fenêtre hiérarchie vers la fenêtre projet. Placez les dans un dossier nommé Resources.
tuto_resources
Une fois les Prefabs créés, vous pouvez supprimer les GameObjects de la scène, ils ne servent plus à rien. Nous verrons plus tard pourquoi les placer dans le dossier Resources. C’est important !

Le manager

On a notre fichier de configuration, nos Prefabs sont prêts, il est maintenant temps de lier le tout. On arrive maintenant au cœur de la chose : le manager. On va créer le script ensemble, petit à petit, pas de panique.

Ce script devra être attaché à un GameObject présent sur toutes les scènes où vous voulez l’utiliser. Il ne sera pas immortel contrairement aux objets qu’il va gérer. Allons-y et créons le script PersistentManager.

public class PersistentManager : MonoBehaviour {
    public TextAsset ConfigFile;
 
    void Start () {
        LoadConfigFile();
        DestroyObjects();
        CreateObjects();
        Destroy(gameObject);
    }
  
    private void LoadConfigFile()
    {
    }
 
    private void DestroyObjects()
    {
    }
 
    private void CreateObjects()
    {
    }
}
Avant de se lancer dans le code, créez un Prefab de notre PersistentManager : nouveau GameObject (renommez le au nom du script : PersistentManager) et attachez lui le script avant d’en faire un Prefab. Affectez le fichier de configuration au script… Hop. Notre Prefab est prêt à être utilisé, on peut repasser au scripting.
Le code est découpé en 3 méthodes explicites que l’on va détaille une à une:
  • LoadConfigFile
  • CreateObjects
  • DestroyObjects
A la fin de la métode Start, vous pouvez voir que le manager s’autodétruit. En effet, une fois son travail effectué, il ne sert plus à rien alors autant libérer un peu de place non ?

Pour charger la configuration, je ne vais pas entrer dans les détails, c’est du parsing de XML que l’on va stocker dans un Dictionary. Voilà le source :

private Dictionary _scenesConfig;
 
private void LoadConfigFile()
{
    _scenesConfig = new Dictionary&lft;string string="">();
  
    // Load and parse the XML file
    var xmlData = new XmlDocument();
    xmlData.LoadXml(ConfigFile.text);
   
    // Get the list of scene nodes
    var sceneNodeList = xmlData.GetElementsByTagName("scene");
   
    // Loop through scenes
    foreach(XmlNode node in sceneNodeList)
    {
        var currentSceneName = node.Attributes["name"].Value;
    
        // Fetch all objects defined in the config file
        var childList = node.ChildNodes;
        var childCount = node.ChildNodes.Count;
        var nameArray = new string[childCount];
        var i = 0;
        foreach(XmlNode child in childList)
        {
            nameArray[i] = child.Attributes["name"].Value;
            i++;
        }
    
        _scenesConfig[currentSceneName] = nameArray;
    }
}
 Notre configuration est maintenant chargée en mémoire, on peut s’en servir. Prochaine étape : la création des objets manquants, ça se passe dans la méthode CreateObjects.
private void CreateObjects()
{
 var currentScene = Application.loadedLevelName;
   
 if(_scenesConfig.ContainsKey(currentScene))
 {
  foreach(string objectName in _scenesConfig[currentScene])
  {
   if(!GameObject.Find(objectName))
   {
    var resource = Resources.Load (objectName);
    var o = GameObject.Instantiate(resource);
    o.name = objectName;
   }
  }
 }
}

Le fonctionnement est assez simple : on parcourt le tableau des objets que l’on veut voir sur cette scène. S’ils ne sont pas présents, on les crée. Comme on n’a pas de lien direct vers nos Prefabs, il faut passer par Ressources.Load pour les récupérer. Ne pas avoir besoin d’un lien direct vers les Prefabs facilite grandement la tâche : il suffit de créer unPrefab et il sera directement utilisable en modifiant le fichier de configuration. C’est pour cette raison qu’ils doivent être placés dans un dossier nommé Resources (plus d’infos sur la doc).

Il y a une autre petite astuce à noter au niveau du nommage. Lorsque l’on utilise la méthode Instantiate, le nom de l’objet dupliqué est suffixé par « (Clone) ». C’est quelque chose qui nous embête un peu puisqu’on recherche nos objets par leur nom. On force donc son nom au nom du Prefab.

Enfin, dernière étape, la destruction des objets dont on n’a plus besoin avec DestroyObjects.

private void DestroyObjects()
{
var currentScene = Application.loadedLevelName;

if(_scenesConfig.ContainsKey(currentScene))
{
DontDestroyMeOnLoad[] objects = (DontDestroyMeOnLoad[]) GameObject.FindObjectsOfType(typeof(DontDestroyMeOnLoad));

var objectsToKeep = _scenesConfig[currentScene];
var keepTabLength = objectsToKeep.Length;
foreach(DontDestroyMeOnLoad current in objects)
{
var keepCurrent = false;
var i = 0;
var loop = true;
while(i < keepTabLength && loop)
{
if(current.name == objectsToKeep[i])
{
loop = false;
keepCurrent = true;
}
i++;
}

if(!keepCurrent)
{
Destroy(current.gameObject);
}
}
}
}
[/code]

Cette partie est la plus complexe des trois. Là encore, on récupère le nom de la scène courante pour pouvoir piocher dans la configuration. Cette fois, il faut raisonner en sens inverse. On ne va pas chercher les éléments manquants mais… ceux en trop ! C’est là que la création du script  DontDestroyMeOnLoad prend toute son importance. Plutôt que de rechercher dans toute la hiérarchie de la scène, nous n’allons parcourir que les quelques éléments que nous avons spécialement affectés. La récupération de cette liste se fait en utilisant la méthode FindObjectsOfType. Faites attention lorsque vous utilisez cette méthode dans vos scripts. Comme expliqué sur la documentation :
Cette méthode est très lente. Il n’est pas recommandé de l’utiliser à chaque boucle.

Dans notre cas, elle sera utilisée une seule fois par changement de scène donc ça ne posera pas de problème.

Pour finir, on vérifie si le nom des objets remontés est présent dans la configuration de la scène courante. Si ce n’est pas le cas, on les supprime tout simplement.

Conclusion

Si l’on reprends notre exemple, voilà ce qui va se passer :

Lancement de la scène Menu. La manager détecte que la musique de menu n’est pas là donc il instancie son Prefab. Le manager disparaît rapidement de la hiérarchie :

tuto_menu
On appuie sur espace et on passe à la scène Level_1. La musique de menu est bien supprimée et le Prefab de la musique de niveau est créé.
tuto_level_1
On appuie sur espace et on passe à la scène Level_2. La musique continue sans interruption.
tuto_level_2
On appuie sur espace et on passe à la scène Boss. La musique de niveau est bien supprimée et le Prefab de la musique de boss est ajouté.
tuto_boss
On appuie sur espace et on passe à la scène Credits. La musique de boss est supprimée et plus aucun son n’est joué.
tuto_credits

Voilà, c’en est fini. Vous avez maintenant un système autonome de gestion des objets persistants entre les scènes. Et à l’utilisation, c’est simple comme bonjour !

Le code source des deux scripts DontDestroyMeOnLoad et PersistentManager sont disponibles sur le dépôt github Unity Toolbox.

Vous pouvez également télécharger le projet complet d’exemple.

Merci à Yannick Comte pour la relecture.
Crédits image : David Baxendale (image redimensionnée)

L'Atelier de Drakulo déménage vers Esprit Unity

Publicité

Votre 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 )

Photo Facebook

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

Connexion à %s