Le singleton dans Unity : un objet pour les gouverner tous…

Le singleton fait partie des design pattern (en français, patron de conception) en programmation. Pour faire court, un design pattern, c’est une bonne pratique, une manière de faire qui est répandue et habituelle parce qu’elle a prouvé de nombreux avantages dans certaines situations.

Le singleton en lui-même décrit le principe d’instancier une classe une seule et unique fois. En somme, on code la classe d’une telle manière que si on tente de créer une nouvelle instance alors qu’une instance existe déjà, ça sera abandonné : l’instance détruite. Ainsi, la classe correspond à un seul objet unique.

L’intérêt d’un Singleton dans le développement de jeu vidéo

Si vous créez un jeu vidéo, il vous faut un objet « permanent » pour stocker les données. Il faut aussi que les divers éléments de votre jeu aient un seul système central à interroger pour savoir l’état du jeu, l’état du joueur…

Est-ce que le jeu est en pause ? Est-ce que le joueur vient de perdre une vie et de passer en Game Over ? Il faut que ça soit une classe précise et unique qui ait l’info de l’état du jeu, pour éviter des situations où le jeu reste jouable sur un écran de Game Over.

En outre, dans le cas d’Unity, un Singleton bien implémenté permet de :

  1. Rendre votre application plus robuste, notamment en ayant un objet central chargé de gérer les évènements, les timer, l’état du jeu…
  2. Faciliter le débogage de votre application.
  3. Garder vos données d’une scène à l’autre (en rendant l’objet non destructible), donc le temps, la progression, les vies du joueur, et éviter par exemple que la musique se coupe d’une scène à l’autre…
  4. Permettre à vos autres objets de communiquer avec un GameObject précis, sans avoir à l’assigner dans l’inspecteur ou utiliser des fonctions pour chercher cet objet (d’où un gain de performances).
  5. Garder l’ensemble de vos scènes jouables individuellement. En mettant votre préfab « GameManager » sur toutes les scènes avec le pattern singleton, vous pouvez lancer le jeu de n’importe quelle scène, ou passer d’une scène à l’autre, ce qui rend plus pratique le développement. Toute scène en cours de création est « jouable » sans avoir à lancer l’application depuis le début.

Comment implémenter et utiliser un Singleton dans Unity 3D ?

La fabrication d’un singleton passe par l’utilisation de trois éléments :

  1. Le modificateur static, qui fait qu’un membre de la classe est commun à toutes les instances de la classe.
  2. La méthode Awake() de Unity (héritée de MonoBehavior), qui se lance au chargement de l’instance de script.
  3. La méthode DontDestroyOnLoad() de Unity, qui indique que l’objet ne doit pas être détruit lorsqu’une nouvelle scène est chargée.

Le code d’un GameManager fonctionnant en Singleton est comme suit :

public class GameManager : MonoBehaviour
{
    private static GameManager instance = null;
    public static GameManager Instance => instance;
    private void Awake()
    {
        if (instance != null && instance != this)
        {
            Destroy(this.gameObject);
            return;
        }
        else
        {
            instance = this;
        }
        DontDestroyOnLoad(this.gameObject);
		
	// Initialisation du Game Manager...
    }
}

Attention ! Rien d’autre ne doit être écrit au début de la fonction Awake(). Si la classe doit faire des choses à son initialisation, ajoutez le code code correspondant après que la vérification sur « instance » (donc après le DontDestroyOnLoad)

Nous définissions d’abord deux variables static, une privée et une publique. L’écriture de Instance sous forme de propriété Instance => instance permet de rendre Instance en lecture seule (et de ne modifier que le champ privé instance), elle est équivalente à celle-ci :

public static GameManager Instance
{
    get { return instance; }
}

Comme ces variables sont static, elles sont communes à toutes les instances de GameManager.

Ensuite, dans la méthode Awake, nous vérifions que le champ instance est défini, et si c’est le cas, qu’il est différent de l’instance actuel. Si c’est le cas, ça veut dire que nous sommes en train d’initialiser une instance doublon, et donc nous détruisons l’objet ! Avec l’instruction return;, nous stoppons la méthode Awake() à cet endroit.

Dans le cas contraire, nous ajoutons l’instance actuelle dans le champ instance. Et juste après, nous appliquons la méthode DontDestroyOnLoad() sur le GameObject actuel, celui qui contient notre script GameManager.

Ce qu’il fait qu’il reste dans la hiérarchie de la scène dans une section spécifique « DontDestroyOnLoad » et ne sera donc pas détruit en passant d’une scène à l’autre.

Position d’un GameObject dans la hiérarchie après la méthode DontDestroyOnLoad()

À partir de là, notre classe Singleton est accessible depuis les autres scripts, en appelant le champ public Instance.

En somme, si GameManager contient les champs et méthodes suivantes :

public int PlayerHP { get; private set; }
public void DecreasePlayerHP(int hitPoints = 1)
{
	PlayerHP -= hitPoints;
	if(PlayerHP <= 0)
	{
		GameOver();
	}
}
private void GameOver()
{
	// Séquence de game over
}

Une autre classe aura accès à ces champs et méthodes publiques via GameManager.Instance, de cette manière :

private void TauntAndDamage()
{
	if (GameManager.Instance.PlayerHP < 5)
	{
		Say("Haha, tu n'as plus beaucoup de vie !");
	}
	GameManager.Instance.DecreasePlayerHP(2);
}

N.B. : dans cette manière de faire, nous avons directement codé le fonctionnement en singleton sur une classe. Vous pourriez créer une classe « Singleton » avec ce comportement, et de faire hériter toutes vos classes XxxManager de cette classe Singleton (héritant elle-même de MonoBehaviour).

Cependant, vous n’aurez pas forcément besoin d’avoir plusieurs singletons dans votre programme. C’est une possibilité, mais en principe votre singleton principal peut lui-même servir de gestionnaire pour les autres classes à instancier un nombre limité de fois.

Quand ne faut-il pas utiliser de Singleton ?

Comme nous l’avons vu, le singleton est une façon très pratique de centraliser certains éléments de votre jeu. Correctement utilisé, il vous permet d’avoir un objet référent tout au long de l’application, et d’éviter des erreurs liées à des actions qui peuvent se passer en simultané.

Un singleton ne sera utile uniquement si votre classe a besoin de fonctions présentes sur MonoBehaviour et si elle hérite de cette classe (typiquement, si elle a besoin de composants attachés à un GameObject ou si elle effectue des actions lors des évènements de type Update(), ou utilise des Coroutine).

Par exemple, une classe de sauvegarde comme celle que je vous propose n’hérite pas de MonoBehaviour. Elle peut être appelée depuis les différentes classes car elle est statique, mais n’a pas besoin de fonctionner en Singleton. Tout au plus, on peut y intégrer un système pour s’assurer qu’un seul fichier est sauvegardé à la fois.

Évitez aussi de prendre la mauvaise habitude de donner trop de responsabilités à votre singleton. C’est une pratique déconseillée, un anti-pattern qui est appelé God Object : un objet qui sait trop de choses ou qui fait trop de choses. L’approche privilégiée en développement est de faire plusieurs objets spécialisés qui communiquent entre eux.

Cela rend le code plus modulable et plus évolutif, plutôt que d’aller modifier un gros monolithe de code. Vous préférerez savoir qu’un problème X vient de votre classe AudioManager, UserInterfaceManager ou InputManager, plutôt que d’un GameManager extrêmement long.

Bref, utilisez le singleton pour centraliser certains comportements et pourquoi pas pour référencer vos autres sous-systèmes, mais ne l’utilisez pas pour absolument tout, au risque de le rendre dur à comprendre et à contrôler. Mais bien utilisé, votre singleton vous rendra bien des services !

2 thoughts on “Le singleton dans Unity : un objet pour les gouverner tous…

Commentaires désactivés