Finite-State-Machine Partie2

Ceci est le deuxième article sur la FSM. Il est recommandé de lire le précédent article car celui-ci continue avec le code.

En fin d’article se trouve un lien vers le script sur GitHub. Il est préférable de le télécharger pour suivre le tutoriel sans rater une ligne.

Nous sommes sur le point d’ajouter des fonctions d’usage de manière à rendre notre système plus robuste. Notre FSM ne tient pas compte d’un problème majeur, nous sommes en mesure de passer d’un état à un autre sans restriction. Il se peut que ce soit suffisant pour vore programme, il se peut aussi qu’il soit préférable d’évoter certaines transitions.

Développer une FSM plus approfondie

Une FSM ne devrait pas se limiter à une collection d’états et des transitions, elle devrait aussi comporter des limitations de mouvements.
À la fin du premier article, il est possible de sauter entre états et la seule limitations est que les deux états sont enregistrés et nous ne transitons pas vers l’état en cours.

fsm (1)

L’image au-dessus définit un organigramme assez simple:

  • Start mène à Walk et sera notre état initial
  • Walk mène à Run, Attack, Pause et Die
  • Run mène à Walk, Attack, Pause et Die
  • Attack mène à Walk, Run, Pause et Die
  • Pause mène à Walk, Run et Attack
  • Die mène à Start

Certains états sont limités à certaines transitions et cela ressemble dèjà plus à une vraie FSM. Par exemple, Die (Meurt) ne peut aller que vers Start. On peut argumenter sur le fait qu’il suffit d’être attentif à ce que l’on code et ceci n’est pas nécessaire. Vrai. Allez-y, montrez-moi. Sur un projet de grande ampleur, même les meilleurs codeurs se plantent. Le fait que la FSM vous fasse savoir que votre code fait une transition illégale vous permet de fixer le programme. Si après 1 mois de test approfondi, aucune mauvaise transition n’apparait, alors il est possible de retourner vers la FSM du premier article. Nous pouvons considérer la FSM que nous allons développer comme une FSM de debugging.

Etendre la classe State

La première chose à faire est d’ajouter des fonctionalités.

class State
{
    // Code déjà existant
       
    public bool forceTransition = false;
    public List <Enum> transitions = null;
        
    public State(Enum name)
    {
        this.name = name;
        this.transitions = new List <Enum>();
    }
}

Nous ajoutons une variable forceTransition qui permet un pontage des restrictions que nous allons implementer. Cela peut paraitre contradictoire, mais cela sera utile dans certaines circonstances.

La seconde addition est une liste de transitions que l’utilisateur va définir comme légales. De cette manière, nous créons une relation de un vers plusieurs (ou un vers un, un vers aucun est peu probable).

Initialisation de la FSM

Dans le premier article, nous faisons l’initialisation en une ligne en passant un paramètre générique. Il nous faudra désormais indiquer quelles sont les transitions. Il nous faut modifier la méthode d’initialisation.

protected void InitializeStateMachine<T>(Enum initialState, bool debug)
{
   if (this.initialized == true)
   {
       Debug.LogError("The StateMachine component on " + this.GetType().ToString() + " is already initialized.");
       return;
   }
   this.initialized = true;

   var values = Enum.GetValues(typeof(T));
   this.states = new Dictionary<Enum, State>();
   for (int i = 0; i < values.Length; i++)
   {
      this.initialized = this.CreateNewState((Enum)values.GetValue(i));
   }
   this.currentState = this.states[initialState];
   this.inTransition = false;
   this.debugMode = debug;

   this.currentState.enterMethod(currentState.name);
   this.OnUpdate = this.currentState.updateMethod;
}

Le seul ajout est dans le paramètre qui d

Ajouter des états

Ensuite, nous voulons avoir la possibilité d’ajouter des états définis par l’utilisateur.

protected bool AddTransitionsToState(Enum newState, Enum[] transitions, bool forceTransition = false)
{
    if (this.Initialized() == false) { return false; }
    if (this.states.ContainsKey(newState) == false) { return false; }

    State s = states[newState];
    s.forceTransition = forceTransition;
    foreach (Enum t in transitions)
    {
        if (s.transitions.Contains(t) == true)
        {
            Debug.LogError("State: " + newState + " already contains a transition for " + t + " in " + this.GetType().ToString());
            continue;
        }
        s.transitions.Add(t);
    }
    return true;
}

Encore une fois, aucune magie occulte pour faire tourner. Le premier paramètre est l’état que nous déclarons, une instance sera alors créer, le second paramètre est la liste de transitions légales. Le dernier paramètre définit si nous pouvons transiter vers n’importe quel état. Cela annule le principe de la liste de transitions légales et est défini comme false par defaut. La méthode réutilise CreateNewState et Initialized de l’article précédent donc voyez la-bas pour les explications.
Tous les états contenus dans l’array sont ajoutés à la liste de transitions de cet état récemment déclaré.

En ce qui concerne l’état Walk:

public enum PlayerState{ Start, Walk, Run, Attack, Die, Pause }
public class PlayerController:StateMachine
{
    void Start()
    {
         InitializeStateMachine<PlayerState>(PlayerState.Walk, true);
         AddTransitionsToState(PlayerState.Walk, new Enum[]{ PlayerState.Run,PlayerState.Attack,
               PlayerState.Die, PlayerState,Pause });
    }
}

Le dernier paramètre est ignoré de sorte que forceTransition soit laissé comme false.
Nous créons l’état Walk et ajoutons Run, Attack, Die et Pause comme transitions légales.

Changer d’état

Avant de changer de transiter, nous devons nous assurer que l’état cible existe et est une transition légale de l’état en cours.

protected bool IsLegalTransition(Enum fromstate, Enum tostate)
{
    if (this.Initialized() == false) { return false; }

    if (this.states.ContainsKey(fromstate) && this.states.ContainsKey(tostate))
    {
        if (this.states[fromstate].forceTransition == true || this.states[fromstate].transitions.Contains(tostate) == true)
        {
            return true;
        }
    }
    return false;
}

La méthode est protégée pour que nous puissions l’utiliser dans la classe qui hérite. D’abord, nous faisons les vérifications d’usage, puis est-ce que l’état est enregistré (cela s’est fait dans CreateNewState de l’article 1) et enfin pouvons-nous y transiter.

Il nous alors possible d’implementer le changement d’état.

protected bool ChangeCurrentState(Enum newstate, bool forceTransition = false)
{
    if (this.Initialized() == false) { return false; }

    if (this.inTransition)
    {
        if (this.debugMode == true)
        {
            Debug.LogWarning(this.GetType().ToString() + " demande une transition " + newstate + " alors qu'il est encore en transition");
        }
        return false;
    }

    if (forceTransition || this.IsLegalTransition(this.currentState.name, newstate))
    {
        if (this.debugMode == true)
        {
            Debug.Log(this.GetType().ToString() + " transition: " + this.currentState.name + " => " + newstate);
        }

        State transitionSource = this.currentState;
        State transitionTarget = this.states[newstate];
        this.inTransition = true;
        this.currentState.exitMethod(transitionTarget.name);
        transitionTarget.enterMethod(transitionSource.name);
        this.currentState = transitionTarget;

        if (transitionTarget == null || transitionSource == null)
        {
            Debug.LogError(this.GetType().ToString() + " cannot finalize transition; source or target state is null!");
        }
        else
        {
            this.inTransition = false;
        }
    }
    else
    {
        Debug.LogError(this.GetType().ToString() + " demande la transition: " + this.currentState.name + " => " + newstate + " qui n'est pas définie!");
        return false;
    }

    this.OnUpdate = this.currentState.updateMethod;
    return true;
}

Le premier paramètre indique l’état cible. Le second paramètre va forcer la transition si nous nécessitons la transition à tout prix. Cela pourrait être un état crash et nous souhaitons que tous les états existants puissent y transiter (c’est un état de crash après tout). Si nous avons 20 états, ca peut nous éviter une vingtainer de copier/coller laborieux.
Le reste ne change pas trop de l’implémentation originale mis à part la section concernant la transition forcée.
Si nous tentons de transiter vers un état non légal alors le programme plantera et une impression indiquera dans la console que nous faisons une transition illégale.

Utiliser la FSM

public class PlayerController : StateTest 
{
     private float timer = 0f;
     private Enum previousState;
     protected virtual void Awake() 
     {
        InitializeStateMachine<PlayerState>(PlayerState.Start, true);
        AddTransitionsToState(PlayerState.Start, 
              new Enum[]{PlayerState.Walk});
        AddTransitionsToState(PlayerState.Walk, 
              new Enum[] { PlayerState.Run, PlayerState.Attack, PlayerState.Pause, PlayerState.Die });
        AddTransitionsToState(PlayerState.Run, 
              new Enum[] { PlayerState.Walk, PlayerState.Attack, PlayerState.Pause, PlayerState.Die });
        AddTransitionsToState(PlayerState.Attack, 
              new Enum[] { PlayerState.Walk, PlayerState.Run, PlayerState.Pause, PlayerState.Die });
        AddTransitionsToState(PlayerState.Pause, 
              new Enum[] { PlayerState.Walk, PlayerState.Attack, PlayerState.Run});
        AddTransitionsToState(PlayerState.Die, 
              new Enum[] { PlayerState.Start });
    }

    void EnterStart(Enum previous) { 
         if((PlayerState)previous == PlayerState.Die)
         {
              Debug.Log("I am back from the dead");
         }
         Debug.Log("Starting game"); 
    }

    void EnterWalk(Enum previous) { Debug.Log("Setting walk"); }
    void UpdateWalk() { Debug.Log("I am walking");}

    void EnterRun(Enum previous) { Debug.Log("EnterRun"); }
    void UpdateRun() { Debug.Log("I am running");}
    void EnterRun(Enum previous) { Debug.Log("EnterRun"); }
    
    void EnterAttack(Enum previous)
    {
         timer = 0f;
         this.previousState = previous;
    }
    void UpdateAttack() { Debug.Log("I am attacking");
         timer += Time.deltaTime;
         if(timer >= 1.0f){ ChangeCurrentState(previous); }
    }
    void EnterPause(Enum previous)
    {
         previousState = previous;
         // Stopping the game!!
    }
    void UpdatePause()
    {
        if (Input.GetKeyDown(KeyCode.P) && (PlayerState)Current != PlayerState.Pause) { ChangeCurrentState(previous); }
    }
    void ExitPause(Enum nextState)
    {
         // Restarting the game!!
    }

    void EnterDie(Enum next) { Debug.Log("I am so dead right now"); }
    void UpdateDie(){
        // Should we restart
        if (Input.GetKeyDown(KeyCode.S)) { ChangeCurrentState(StateParent.Start); }
    }
    protected override void Update() 
    {
        base.Update();
        if (Input.GetKeyDown(KeyCode.P) && (PlayerState)Current != PLayerState.Pause) { ChangeCurrentState(StateParent.Pause); }
        if (Input.GetKeyDown(KeyCode.W)) { ChangeCurrentState(StateParent.Walk); }
        if (Input.GetKeyDown(KeyCode.A)) { ChangeCurrentState(StateParent.Attack); }
        if (Input.GetKeyDown(KeyCode.R)) { ChangeCurrentState(StateParent.Run); }
        if (Input.GetKeyDown(KeyCode.D)) { ChangeCurrentState(StateParent.Die); }
    }
}
public enum PlayerState 
{
    Start, Walk, Run, Attack, Die , Pause
}

L’exemple est simple encore une fois. Je vous laisse le soin de développer votre propre version. En simple, ça imprime l’état en cours et les transitions. Vous remarquerez que certains états n’ont pas les trois méthodes (Enter/Exit/Update) indiquant que seules les méthodes utiles doivent être implémentées.
À notr aussi que grâce à notre FSM, la touche S n’est vérifier que si nous sommes morts. Une implémentation basique nécessiterait de vérifier dans l’Update et ensuite de voir si nous sommes morts.

Le script peut être téléchargé de GitHub avec le lien ci-dessous:

github-mark@1200x630

Conclusion

C’est la fin de la deuxième partie. Nous pouvons maintenant déterminer des transitions légales et crasher le programme si nous tentons une transition que nous ne souhaitons pas.

Leave a comment