Unity : Gérer le cycle de vie avec Action
Unity est un outil extraordinaire dès lors qu'il s'agit de composer des espaces en 3d, de créer des plateformes de jeux 2D, d'importer des personnages et leurs animations.
En revanche, certaines tâches plus "standard" nécessitent, plus ou moins, de réinventer la roue à chaque fois.
Témoin l'état du jeu, le Gamestate.
Les jeux vidéo sont la plupart du temps des machines à état.
Elles démarrent sur la page d'introduction - appelons l'état Intro.
Puis on lance une partie et on choisit son personnage - appelons l'état ChooseCharacter.
On démarre le jeu : état Playing.
On perd une vie , une animation nous rappelle que la fin se rapproche : état LosingLife.
On rejoue, on perd, on finit : état GameWon , ou état GameLost.
Puis retour à l'intro.
Très classique.
***
En général, dans Unity, on va rapidement créer un script GameManager.cs qui va gérer les passages d'un état à l'autre. On aura une enum du genre :
public enum GameState {
Intro,
ChooseCharacter,
Playing,
LosingLife,
GameWon,
GameLost
}
Si vous avez déjà été confronté au problème, vous avez certainement du créer un certain nombre d'UI pour représenter les différents états de votre jeu :
- un écran d'introduction, avec le titre
- un écran de choix de personnage
- l'écran de jeu
- l'écran présentant la perte d'une vie
- ...
Mais comment afficher ces écrans ?Comment gérer le passage fluide d'un écran à l'autre ? Chez moi, c'est le script GameManager qui s'en occupe. C'est un moyen centralisé de gérer le workflow. Le problème, c'est que ça implique d'ajouter les références de toutes les pages à GameManager. GameManager va se mettre à afficher / ou à masquer les autres écrans en fonction du GameState. GameManager devient du coup vite complexe, et se met à gérer le fonctionnement d'autres objets :
Mauvaise séparation des responsabilités !!
***
Récemment, j'ai changé de façon de faire, en utilisant des Action pour gérer la transition entre les différents états, au lieu d'avoir GameManager qui modifie les affichages. Le principe est simple :
- un objet MonoBehavior susceptible d'influer sur d'autres objets ajoute un objet Action :
public class GameManager : MonoBehaviour
{
// events
public static Action<GameState> StateHasChanged;
public GameState? State { get; set; }
...
///
/// Switches to next state
///
public void switchToNextState(GameState newState)
{
State = newState;
// Send message to observers
StateHasChanged?.Invoke(newState);
...
}
...
Ici, la fonction switchToNextState gère le changement d'état, lorsqu'elle est appelée, elle publie le changement d'état à tous les objets qui observent.
- des objets qui observent l'Action quand elle est invoqué
public class GameOver : MonoBehaviour
{
private void Awake()
{
GameManager.StateHasChanged += stateHasChanged;
}
// Use this for initialization
void Start()
{
// Masque tous les fils
CommonTools.HideChildren(gameObject);
}
// Update is called once per frame
void Update()
{
}
private void stateHasChanged(GameState newState)
{
if (newState == GameState.GameOver)
{
// Affiche tous les fils
CommonTools.ShowChildren(gameObject);
}
else
{
// Masque tous les fils
CommonTools.HideChildren(gameObject);
}
}
...
Ici, l'objet GameOver qui gère la page, attend l'event StateHasChanged et réagit quand il passe dans l'état GameOver.
Remarque : Vous noterez les méthodes qui affichent / masquent les contrôles enfants.
En effet, il n'est pas possible de désactiver ( .setActive(false)) l'objet GameOver, sinon il ne pourra plus recevoir de changement d'état et ne fonctionnera plus !! Il faut donc afficher / effacer les enfants uniquement :
///
/// Tools used in gameObjects
///
public class CommonTools : MonoBehaviour
{
public static void HideChildren(GameObject go)
{
// Hide children
for (int i = 0; i < go.transform.childCount; i++)
{
var child = go.transform.GetChild(i);
child.gameObject.SetActive(false);
}
}
public static void ShowChildren(GameObject go)
{
// Show
for (int i = 0; i < go.transform.childCount; i++)
{
var child = go.transform.GetChild(i);
child.gameObject.SetActive(true);
}
}
...
C'est la seule petite contrainte par rapport au fait de désactiver tout l'objet GameOver.
En revanche, les gains sont très intéressants :
- Plus aucune référence à GameOver dans GameManager : les responsabilités sont bien séparées, pas de risque de confusion
- Il est facile d'ajouter un état intermédiaire : on ajoute un GameState, une page, sans besoin de retoucher GameManager