Si vous avez lu le tutoriel sur le pattern singleton précédent, voici le moment de voir comment utiliser le pattern Singleton avec Unity.
Prérequis : les bases du C# - POO - Bases Unity
Niveau : intermédiaire
Depuis que l'on utilise un moteur de jeu avec son architecture, on va devoir considérer des aspects supplémentaires pour pouvoir utiliser le pattern singleton avec Unity.
Spécificités du moteur de jeu Unity
La première chose à prendre en considération c'est qu'ici on utilise un framework en quelque sorte, et comme tout framework il utilise sa propre architecture. L'utilisation casi permanente de la classe MonoBehaviour en est un exemple marquant.
Et comme vous le savez cette classe possède des méthodes spécifiques comme Start() ou encore Update(). Ainsi peut-on placer notre "lazzy loading" dans la méthode Start() ?
De plus avec Unity on n'instancie pas directement des classes héritant de MonoBehaviour par le mot clé "new". On utilise l'ajout de composant à la pace.
gameObject.AddComponent<ClasseSingleton>()
Mais peut on se passer de la classe MonoBehaviour ?
Il est tout à fait possible de se passer de cette classe mais alors on perd toutes ses méthodes qui, selon les cas, peuvent être indispensables. On peut penser par exemple à la méthode OnDestroy() qui permet de placer du code utile à cette classe lorsque l'application est fermée.
Enfin lorsque l'on change de scène, toutes les instances crées sont détruites et avec elles toutes les informations que ces instances pouvaient "transporter". Si on doit garder intactes nos informations contenues dans nos classes de type singleton, on doit mettre en place un système pour conserver nos instances.
Pattern Singleton avec Unity
Encore une fois partons d'un premier essai, en considérant la classe suivante.
using UnityEngine;
public class FileDatasSingleton : MonoBehaviour
{
private static FileDatasSingleton instance;
private FileDatasSingleton(){} //au cas où certains fous tenteraient qd même d'utiliser le mot clé "new"
// Méthode d'accès statique (point d'accès global)
public static FileDatasSingleton Instance { get; private set; }
void Awake()
{
if (instance != null && instance != this)
Destroy(gameObject); // Suppression d'une instance précédente (sécurité...sécurité...)
instance = this;
}
//méthode appelée par les "clients" de cette classe
public void loadFile(){
//votre code
}
}
Ainsi pour utiliser cette classe de type Singleton, ajouter le script à un GameObject, puis depuis un autre script lancer l'appel à la méthode loadFile.
FileDatasSingleton.Instance.loadFile();
- La création de l'instance unique se fait ici lors de la méthode Awake(), qui est appelée juste avant la méthode Start() lorsque le composant est lancé.
- Finalement une sécurité est ajoutée au cas où une autre instance existerait déjà.
Ajouter ce script sur un GameObject, et au lancement l'instance sera crée, disponible alors pour des appels sur ses méthodes.
Ajouter le lazzy loading
Traditionnellement le pattern singleton utilise la création "décalée" de l'instance unique, repoussant ainsi sa création lors du premier appel par la méthode d'accès global loadfile();
Voyons donc comment améliorer notre pattern Singleton avec Unity.
using UnityEngine;
public class FileDatasSingleton : MonoBehaviour
{
// Static singleton instance
private static FileDatasSingleton instance;
// Static singleton property
public static FileDatasSingleton Instance
{
// ajout ET création du composant à un GameObject nommé "SingletonHolder"
get { return instance ?? (instance = new GameObject("SingletonHolder").AddComponent());}
private set{ instance = value;}
}
//méthode appelée par les "clients" de cette classe
public void loadFile(){ //votre code }
}
Ici la création de l'instance unique ne se fait plus dans la méthode Awake() qui est automatiquement lancée. On ne contrôlerait donc pas le moment où l'instance sera crée.
A l'opposé ici l'instanciation se fait uniquement lorsque l'on tente de récupérer le champ instance par le biais de Instance.
FileDatasSingleton.Instance
Si l'instance est déjà crée on la retourne, sinon on crée un nouveau GameObject et on lui ajoute notre composant (ie script).
// ajout ET création du composant à un GameObject nommé "SingletonHolder"
get { return instance ?? (instance = new GameObject("SingletonHolder").AddComponent());}
On notera pour se faire que l'on utilise une écriture optimisée du code, mais on aurait très bien pu écrire ce bout de code d'une autre manière.
// ajout ET création du composant à un GameObject nommé "SingletonHolder"
get { if( instance != null){
return instance;
}
else {
return instance = new GameObject("SingletonHolder").AddComponent<FileDatasSingleton>();
}
}
Mais c'est presque pareil, non ?
L'effet est sans conteste le même cependant le code reste juste moins compact.
En utilisant donc ce lazzy loading, on verra apparaître un GameObject nommé "SingletonHolder" dans la hiérarchie de l'éditeur de Unity, lorsque le premier appel client sera effectué.
Conserver le Singleton au travers des scènes
Vous aurez probablement, à un moment ou un autre, besoin de conserver votre singleton au travers des scènes de votre jeu vidéo. Ainsi nous allons voir comment implémenter cette fonctionnalité.
Une chance pour nous c'est que Unity propose aussi une méthode hyper simple pour conserver les GameObject, et donc leurs composants.
DontDestroyOnLoad(gameObject);
Finalement avec un simple ajout dans le code de la classe on obtient
using UnityEngine;
public class FileDatasSingleton : MonoBehaviour
{
// Static singleton instance
private static FileDatasSingleton instance;
// Static singleton property
public static FileDatasSingleton Instance
{
// ajout ET création du composant à un GameObject nommé "SingletonHolder"
get { return instance ?? (instance = new GameObject("SingletonHolder").AddComponent());}
private set{ instance = value;}
}
void Awake(){
DontDestroyOnLoad(gameObject);//le GameObject qui porte ce script ne sera pas détruit
}
//méthode appelée par les "clients" de cette classe
public void loadFile(){ //votre code }
}
Ainsi vous pouvez changer de scène sans aucun souci et tous vos objets de type Singleton seront conservés.
Encore plus fort ! Si vous revenez sur la scène où la première instance a été crée, grâce à notre code précédent, on ne créera pas un nouvel objet. Nous atteignons finalement notre objectif.
Nous voici arrivés au terme de notre première implémentation du pattern Singleton avec Unity. Mais si vous voulez vous pouvez allez encore plus loin avec une classe générique.