Finite-State-Machine Partie1

L’article est traduit sur une base anglaise. Certain termes techniques seront gardés en anglais, particulièrement les mots-clés. Il se peut que certains mots ne soient pas appropriés ou le résultat d’une mauvaise traduction, dans ce cas, placez un commentaire ou un message et je corrigerai.

Cet article va présenter les bases d’une state-machine (machine à état ou automate fini selon wikipedia) en expliquant les bases de programmation requises au développement de l’outil. Des versions plus avancées suivront dans les parties 2 et 3. Le but étant d’obtenir un système fonctionnel sans pour autant noyer le lecteur dans trop de détails.

Nous parlerons d’abord des principes de base d’une FSM, en abordant les delegates et la reflexivité.

Les bases de la FSM

Comment pourrions-nous définir notre FSM? Tout programme, aussi simple soit-il peut contenir ce genre de code:

void ControlMethod(int currentState)
{
    switch(currentState){
        case 0:
            Action0();
            break;
        case 1:
            Action1();
            break;
        case 2:
            Action2();
            break;
        default:
            ActionError();
            break;
    }
}

Ceci est déjà une FSM. Le code contient des actions spécifiques à un état du programme. À partir de ce principe, nous allons développer une class qui va prendre en charge la gestion des différents états de manière robuste and flexible.

Dès que l’on écrit une condition avec if, on développe un code qui peut se retrouver dans un des deux états possibles. Plus on ajoute de conditions, plus la machine devient complexe et plus on obtient d’états dans lesquels le programme peut se retrouver. Il est possible d’avoir plusieurs états en même temps. Considérons OpenGl qui s’avère être une FSM géante, l’API peut être en mode 2D, c’est un premier état et en mode rendu noir et blanc, c’est un autre état. Le problème, c’est plus il y en a plus ca devient difficile à maintenir, les larges API ne sont plus le travail d’un seul homme.

Donc l’idée se trouve dans le terme finite qui signifierait définie ou limitée. En construisant une FSM, nous définissons les limites de chaque état et transition d’un objet de sorte qu’il ne soit que dans un seul état à la fois. Ainsi, nous pouvons facilement placer le code correspondant.

Essayons de visualiser une FSM que nous utilisons tous les jours avec une TV.

Mode Fonctions télécommande
Off
  • Seul le bouton power est fonctionnel.
  • Pression du bouton power fait transiter vers le mode On.
On
  • Pression du bouton power fait transiter vers le mode Off.
  • Pression des boutons up/down change le programme en cours.
  • Pression du bouton menu fait transiter vers le mode Menu.
Menu
  • Pression du bouton power fait transiter vers le mode Off.
  • Pression des boutons up/downfait naviguer dans le menu.
  • Pression du bouton menu fait transiter vers le mode On.

La logique contenue dans chacun des états va contenir des additions, néanmoins nous voila avec l’interface utilisateur. Nous remarquons aussi clairement les limites du programme et voyons que certaines fonctions ne sont valables que dans certains états.

Il est aussi recommandé de preparer une FSM sur le papier. Il existe de nombreux logiciels, certains sont très avancés et payant, plus ou moins prévus pour ce sujet, il y en a même des gratuits en ligne (draw.io).

States

L’image au dessus indique chaque état de l’objet ainsi que les conditions qui provoquent une transition. Ces conditions peuvent être vérifiées en Update ou être déclenchées via un système d’évènement/souscription. Cela peut par exemple venir d’une collision comme c’est le cas avec “Hit?” (attaque?) qui soustrait de l’énergie et si elle se trouve en dessous de 0 provoque alors la transition vers Dying. Par contre, la bulle “Player/Enemy Approaches” (Joueur/Ennemie approche) serait vérifier en Update puisqu’il nous faut constamment savoir si cette condition est validée.

Création de la FSM

La première étape est de créer une classe, sans surprise, nous la nommons StateMachine et elle hérite de MonoBehaviour (MB). L’héritage de MB n’est pas nécessaire cependant il simplifie la démarche sur quelques points. Mais juste au cas où, nous pourrions nous en passer. Évite (on peut se tutoyer?) de l’appeler StateMachineBehaviour car le nom est déjà utilisé par une class contenue dans le namespace UnityEngine.

public class StateMachine:MonoBehaviour
{
}

Nous parlons de state (état) et de machine (machine) donc on peut considérer que la classe sera la machine et il nous manque l’état. Nous allons donc créer une classe interne pour cela:

using UnityEngine;
using System;
public class StateMachine : MonoBehaviour
{
    class State
    {
        public Enum name = null;
      
        public State(Enum name)
        {
            this.name = name;
        }
    }
}

Notre class n’est pas publique ainsi elle ne peut être modifiée que par la StateMachine. Pour le moment, elle ne contient qu’une énumération et un constructeur. L’énumération sera utilisée pour définir les états. Nous pourrions aussi utiliser des strings, la même tout partout. Ne pas oubliez le namespace System.

Nous devrions maintenant définir ce qu’est un état. Pour le moment, c’est juste un nom. Ce que nous voulons, c’est que la machine pointe vers un état, l’état actuel ou courant, et l’Update correspondante est lancée. Aussi, pour chaque transition d’état, nous voulons un appel sur des méthodes d’entrée et de sortie.

Nous avons donc besoin de trois references vers des méthodes,des delegates (prononce daylaygayde).

using UnityEngine;
using System;
public class StateMachine:MonoBehaviour
{
    class State{
        public Enum name = null;
        public Action updateMethod = () =>; { };
        public Action<Enum> enterMethod = (state) =>; { };
        public Action<Enum> exitMethod = (state) =>; { };
        
        public State(Enum name)
        {
            this.name = name;
        }
    }
}

Tous les ajouts sont publiques, nous pouvons les utiliser dans la machine. Le premier est un delegate Action tout simple, les deux suivants sont de type Action et prennent un paramètre de type Enum.

Pourquoi un delegate?

Quel est le but d’un delegate? Pourquoi ne pas appeler la méthode directement?
En C/C++, les delegates sont des pointeurs vers des méthodes. Une méthode étant stockée en mémoire, on peut en passer l’adresse telle une variable.

Considérons une méthode qui prend une autre méthode comme paramètre, ainsi la seconde méthode peut être utilisée dans le première (les méthodes d’ordre comme Sort utilisent ce principe). Le programmeur définit un pointeur vers une méthode et l’adresse de la méthode est passée. Le pointeur contient alors l’adresse de la méthode et déréfencier le pointeur mène à la méthode. C’est confus. On va passer trop de temps sur C/C++, ce lien peut aider function pointer.

Maintenant en C#, les développeurs de .NET ont poussé le concept un poil plus loin. En C/C++, le pointeur est une variable de 4 octets (32bit) qui accepte une méthode correspondant à la signature. En C#, delegate est en fait une classe et contient toute une flopée de méthodes pour nous aider.

Voyons comment déclarer un delegate. La méthode classique:

public delegate void MyDelegateDeclaration();
public MyDelegateDeclaration myDelegateInstance = null;
void Start(){
    myDelegateInstance = MyMethod;
    myDelegateInstance();
}
void MyMethod(){ Debug.Log("Hell...oh World...");}

La première ligne déclare le delegate de la même manière une class serait déclarée. La syntax est légèrement différente mais l’idée reste la même. POur une class, nous placerions d’abord le modificateur d’accès, puis le mot-clé class, le nom de notre choix qui sera changé en un type puis l’héritage et du générique si nécessaire. Pour le delegate, le modificateur d’accès vient en premier, tout pareil, puis le type de retour de la méthode, puis le mot-clé delegate, puis le nom que nous avons choisi pour notre type de delegate et enfin les paramètres. Ceci définit deux étapes, le nom du type de delegate et la signature des méthodes qui peuvent y être passées. Dans notre cas, seule une méthode qui renvoie void et prend aucun paramètre pourra être assignée au delegate. Dans l’exemple, MyMethod correspond à ces critères.

La seconde ligne crée l’objet. Tout comme une classe, vient le type en premier (MyDelegateDeclaration est désormais un type), ensuite le nom de la variable que nous souhaitons. Le principe de nomination est le même que pour une variable dite classique. myDelegateInstance est une référence qui peut pointer vers un object de type MyDelegateDeclaration qui en fait est une méthode.
Il nous est possible de créer autant de delegate de ce type que nous le souhaitons tant que le nom change à chaque fois.

Dans notre exemple, dans la méthode Start, nous passons l’adresse de MyMethod au delegate. Les parenthèses ne sont pas utilisées pour indiquer que nous ne souhaitons pas appeler la méthode et utiliser la valeur qu’elle renvoie (void dans notre cas) mais nous voulons l’adresse (ceci est équivalent à &MyMethod en C/C++). La ligne suivante est une invocation du delegate, dans ce cas nous utilisons les parenthèses pour indiquer que nous faisons un appel. Il est aussi possible d’utiliser la méthode Invoke.

Puisqu’un delegate est un objet, il peut être null. Il est donc important de vérifier que le delegate contient une valeur ou le compilateur lancera une exception (Null Reference Exception).

Voyons maintenant la méthode moderne pour les delegates:

Action myDelegateInstance = null;
void Start(){
    myDelegateInstance = MyMethod;
    if(myDelegateInstance != null){
        myDelegateInstance();
    }   
}

Ceci utilise les génériques et fut introduit plus tard dans le framework .NET.
Cela fonctionne exactement de la même manière et est en fait une enveloppe autour de l’ancienne version. La seule différence est l’économie de ligne et une lecture plus simple (à mon goût). Comme tous génériques, la liste de paramètres est passée dans les signes < > et si une valeur de retour est nécessitée, il nous faut alors utiliser les Func. Je vous recommanderais de voir la page de msdn pour que nous puissions continuer sans trop reste sur ce sujet.

Pourquoi est-ce que nous voulons des delegates pour notre FSM? Un état est une encapsulation d’un nom et trois méthodes. Cependant, nous ne savons pas quels noms d’état l’utilisateur va créer, alors il nous faut développer un système qui ne se préoccupe pas des noms, simplement des adresses. En utilisant des delegates, nous pouvons définir une classe qui prend le nom et les méthodes qui y correspondent.

Débutons avec la machine

La machine gère une collection d’états et optempère la transition entre l’état en cours et l’état suivant. Dans cette première version de la FSM, nous ne considerons aucune restriction, il sera donc possible de passer d’un état à n’importe quel autre état. Plus tard, nous ajouterons des contraintes de sorte que notre système soit plus robuste.

La première addition est l’état en cours. Tout simplement, c’est une référence à un objet de type State. Nous voulons aussi être en mesure de connaitre ce State donc il nous faut une méthode.

using UnityEngine;
using System;
public class StateMachine:MonoBehaviour
{
    class State{ // Code }

    private State currentState = null;
    public Enum CurrentState { get { return this.currentState.name; } }
}

La variable currentState est privée et un getter est declaré. Il vous est possible de revoir cette méthode de sorte qu’elle renvoie une string par exemple.

Passons à l’ initialisation.

private bool initialized = false;
private bool debugMode = false;
private Dictionary < Enum, State > states;
private Action OnUpdate = ( ) => { };

protected void InitializeStateMachine<T > (bool debug) where T : struct
{
   if (!typeof(T).IsEnum) 
   {
      throw new ArgumentException("T must be an enumerated type");
   }
   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));      
        if (i == 0)
        {
            this.currentState = states[(Enum)values.GetValue(i)];;
        }
    }
    this.debugMode = debug;
    this.currentState.enterMethod(currentState.name);
    this.OnUpdate = this.currentState.updateMethod;
}

Ok, nous voilà en face de quelque chose de plus sérieux. La variable initialized fonctionne comme sentinel pour éviter de multiples initialisations. La variable debugMode permet d’imprimer les infos sur le console lors des transitions. Le dictionnaire crée un mappage entre le contenu d’une énumération et son état. Ainsi, il nous est possible de passer l’énumération de notre état et recevoir une reference vers un object State en retour. Enfin, le delegate OnUpdate pointe vers la méthode Update de l’état en cours.

Passons à la méthode, elle est générique et doit recevoir une énumération. Il n’existe pas de manière adequate pour déclarer une contrainte de ce type. Le seul moyen serait de passer struct comme nous le faisons et ensuite de faire une vérification. Cela n’empêche pas l’utilisateur de passer un int ou autre value type mais par contre cela crashera le programme sur les premières lignes. Une fois toutes les vérifications faites, nous prenons l’énumération qui nous a été passée et récupérons tous ses composants dans une boucle de sorte à créer un nouvel object State pour chacun. Si tout se passe bien, on renvoie true sinon false. Si quelque chose a failli, le resultat est stocké dans la variable initialized. Ainsi, il n’est pas possible d’utiliser la FSM si elle n’a pas pu enregistrer toutes les énumérations. Lors du premier passage, (i == 0), nous utilisons un état par defaut, le premier de l’enum. Nous placerons donc en premier l’état de depart. Il est cependant possible de changer desuite après l’initialisation si un autre état est requis.

Le reste de la méthode ne fait qu’assigner les valeurs dans les variables correspondantes. Le gros de cette méthode est de faire une iteration de toutes les valeurs de l’énumeration et créer des objets State pour chacune.

En fin de méthode, nous appelons enterMethod en passant comme paramètre l’état précédent, dans notre cas, Initial.

private Action OnUpdate = ( ) => { };

Cette ligne au-dessus peut paraitre bizarre si on n’a jamais utilisé de méthode lambdas. En fait, nous créons une méthode vide pour notre delegate de sorte qu’il ne soit pas null. En début d’article, nous avons vu que les delegates sont des objets et peuvent être null. en cas de nullité, si nous invoquons le delegate on crée un crash. En passant par defaut une méthode vide, on évite le crash puisque notre delegate aura toujours quelque chose à lancer, même si ce n’est rien. De toute manière, nous allons écrire par dessus dans peu. Mais dans le cas où nous n’initialiserions pas la FSM en Awake, cela crasherait comme nous le verrons par la suite. Certains préfereront une verification de nullité. Pareil.

Voici un exemple d’utilisation de notre méthode d’initialisation:

public class MyClassExample : StateMachine
{
    private void Awake(){
        InitializeStateMachine <MyClassState> (true);
    }
}
public enum MyClassState{ Start, Move, Run, Die }

C’est tout. Nous avons créé un nouveau script qui hérite de StateMachine. La FSM a enregistré 4 états. Il nous faut maintenant créer les objets.

private bool CreateNewState(Enum newstate)
{
    if (this.Initialized() == false) { return false; }

    if (this.states.ContainsKey(newstate) == true)
    {
        Debug.Log(newstate + " is already registered in " + this.GetType().ToString());
        return false;
    }

    State s = new State(newstate);
    Type type = this.GetType();
    s.enterMethod = StateTest.GetMethodInfo<Action<Enum>> (this, type, "Enter" + newstate, DoNothingEnterExit);
    s.updateMethod = StateTest.GetMethodInfo <Action> (this, type, "Update" + newstate, DoNothingUpdate);
    s.exitMethod = StateTest.GetMethodInfo <Action<Enum>>  (this, type, "Exit" + newstate, DoNothingEnterExit);

    this.states.Add(newstate, s);

    return true;
}

Rien de spécial au début. Ensuite, nous créons un nouvel objet State. Nous ajoutons une référence de type Type dans laquelle nous passons le type de notre script. Ce qui est important de retenir sur cette ligne est que ce n’est pas notre classe StateMachine qui est dans la variable type mais la classe qui fait l’appel. Dans notre exemple, c’est MyClassExample. Donc type est un objet de type Type qui contient des informations sur MyClassExample qui elle même hérite de StateMachine.

Viennent ensuite trois appels de GetMethodInheritance que nous expliquerons plus tard. Cette méthode renvoie une référence vers une méthod qui est passée au delegate de l’objet State que nous venons de créer. Le but de cette méthode est de joindre le delegate et une méthode en particulier si elle existe. Nous allons devoir utiliser de la reflexivité pour cela. Vite fait, pensons aux méthodes Awake/Start/Update/… d’un MB, Unity doit vérifier si la méthode est implémentée dans le script pour faire l’appel. Nous utiliserons un procédé similaire.

Une fois cela fait, nous poussons le nouvel object State dans le dictionnaire. Il nous est maintenant possible de vérifier si un état existe via une enum et récupérer ses infos.

Reflexivité

Ok donc le mot est laché, ça semble compliqué et en fait c’est beaucoup plus que ça. Non, je déconne, c’est comme le reste, il suffit de savoir ce dont on a besoin.
Le but de la reflexivité est de fournir une interface pour obtenir des informations sur une assembly et des meta-datas. Meta-data signifie des informations concernant les types et le programme au lieu des variables que nous avons l’habitude d’utiliser.
Néanmoins, l’utilisation des metas se fait avec du code traditionel.
Dans notre cas, nous voulons interroger sur le contenu de nos classes. Nous souhaitons savoir si la classe avec laquelle nous travaillons contient certaines méthodes.

Considérons que nous voulons savoir si la classe contient la méthode Start:

Type ourType = this.GetType();   
MethodInfo methodInfo = ourType.GetMethod("Start", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); 
if(methodInfo != null)
{
    Debug.Log("Le type donné contient la méthode Start");
}

Il nous faudra un namespace:

using System.Reflection;

Et tadaaa. C’est tout. Encore une fois pas de magie, juste du code simple, mais pas forcément efficace…oui reflexivité et assez lent donc il vaut mieux limiter son usage sur du jeu video.
Dans notre cas, nous recherchons une méthode donc nous utilisons GetMethod qui est une méthode membre de la classe Type. Ensuite, il suffit de passer les paramètres, nous voulons une méthode qui est nommée “Start” qui est public ou private et non-static (nous demandons instance).

BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance

Cela crée une opération OR qui retourne une valeur contenant les trois informations.

Si le compilateur trouve une correspondance dans le assembly manifest (the fichier contenant toutes les infos sur le programme), il renvoie un résumé d’information, sinon null. Nous aurions aussi pu utiliser GetMethods (pluriel) pour obtenir une collection de méthodes pour ensuite vérifier une par une si c’est la bonne. GetMethod le fait en interne.

Un point important se trouve dans le fait que notre object Type est en aucun cas lié à notre classe. On pourrait penser que l’objet a une connection avec l’objet initial mais que nenni. Dans notre exemple, nous utilisons type.GetType(), l’appel renvoie un nouvel objet de type Type contenant les infos sur le type mais il est impossible de retrouver l’objet initial. Un peu comme la notice d’une TV sait tout de la TV mais n’est pas la TV. Ici, se trouve la documentation officielle.

Avec la méthode de reflexivité, nous pouvons récuperer les méthodes qui nous interessent.

private static T GetMethodInfo<T>(object obj, Type type, string method, T Default) where T : class
{
    Type ourType = type;
    MethodInfo methodInfo = ourType.GetMethod(method, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
    if (methodInfo != null)
    {
        return Delegate.CreateDelegate(typeof(T), obj, methodInfo) as T;
    }
    return Default;
}

La méthode est générique avec une contrainte (le générique doit être une classe). Le premier paramètre est l’objet sur lequel nous travaillons, le second paramètre est le type sur lequel nous travaillons (…), ok ça se répète. Le paramètre de string contient le nom de la méthode que nous recherchons. Le dernier paramètre contient une méthode de defaut si la recherche ne rend rien.

Si GetMethod trouve une méthode, nous créons un delegate avec Delegate.CreateDelegate qui est casté en T. Sinon, ca renvoie le defaut.
Ceci peut nécessité quelques expliquations. Quand on pense méthode, on doit comprendre le mécanisme interne. Si nous avons une classe Chien avec une méthode Manger et nous créons 10 instances, le compilateur ne crée qu’une seule version en mémoire de la méthode Manger. Lorsqu’une instance appelle la méthode, le compilateur convertit:
monChien.Manger();
Chien.Manger(this Chien obj);

La méthode est en fait statique et l’objet est passé comme paramètre et peut donc être utilisé dans la methode. Le même mécanisme est employé explicitement pour les méthodes d’extension.

CreateDelegate ajoute un delegate, donc c’est un object qui contient des informations vers une méthode. Le premier paramètre est le type de delegate dont nous avons besoin. Le second paramètre prend une référence vers un objet, lors de l’invocation du delegate, le compilateur passera cet objet en place du paramètre this.
Le troisième est un objet de type MethodInfo qui contient les infos concernant notre méthode.

  • 1 – Construire l’objet delegate
  • 2 – Obtenir la référence de l’objet avec lequel le delegate doit travailler
  • 3 – Obtenir la méthode vers laquelle il doit pointer

D’où sort Default? Remontons à la ligne où nous faisons l’appel, nous passons
DoNothingUpdate et DoNothingEnterExit qui sont des méthodes vides.

public class StateMachine : MonoBehaviour
{
    private static void DoNothingUpdate() { }
    private static void DoNothingEnterExit(Enum state) { }
}

Revenons à GetMethodInfo pour revoir les paramètres:

s.enterMethod = StateTest.GetMethodInfo<Action<Enum>>(this, type, "Enter" + newstate, DoNothingEnterExit);
s.updateMethod = StateTest.GetMethodInfo<Action>(this, type, "Update" + newstate, DoNothingUpdate);
s.exitMethod = StateTest.GetMethodInfo<Action<Enum>>(this, type, "Exit" + newstate, DoNothingEnterExit);

Arrêtons-nous sur les troisièmes:

"Enter"+newState
"Update"+newState
"Exit" + newState"

Notre processus de reflexivité va rechercher les méthodes dont le nom correspond à Enter/Update/Exit et le nom de l’état. Avec ceci:

public enum MyClassState{ Start, Move, Die }

Nous faisons cela:

public class MyClassExample : StateMachine
{
    private void EnterStart(Enum previous){}
    private void UpdateStart(){}
    private void ExitStart(Enum nextState){}

    private void EnterMove(Enum previous){}

    private void UpdateDie(){} 
}

IL faut remarquer que toutes les méthodes ne sont pas ajoutées. Par exemple, l’état Move n’a pas d’Update. Si rien ne doit s’y faire, il n’y a aucune raison de l’ajouter. Enter et Exit prennent un paramètre qui correspond à l’état précédent et suivant pour nous permettre de lancer une action si cela est nécessaire. L’Update ne nécessite pas ce procédé.

La nomination n’est pas strict, il vous est possible de la changer pour des choses plus fantaisistes comme Enter_StateName ou StateName_Entry ou autre. Il suffit de changer le paramètre en accordance.

Enfin, la reflexivité ets un processus assez lent. Il est donc recommandé de l’utiliser en dehors des Updates et de considerer un groupe d’objets qui peuvent être recyclés en lieu d’une simple création/destruction, voyez ceci par exempleObjectPool article.

Changer un état

La dernière étape consiste à faire transiter un état.

private bool inTransition = false;

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

    if (this.inTransition)
    {
        if (this.debugMode == true)
        {
            Debug.LogWarning(this.GetType().ToString() + " requests transition to state " + newstate + " when still transitioning to state ");
        }
        return false;
    }
    if (this.debugMode == true)     
    {
        Debug.Log(this.GetType().ToString() + " transition: " + this.currentState.name + " => " + newstate);
    }
    this.inTransition = true;
    State transitionTarget = this.states[newstate];
    if (this.currentState.name == transitionTarget.name) 
    {
       Debug.LogError(this.GetType().ToString() + " transition: " + this.currentState.name + " => " + newstate +" is same state");
       return false;
    }
    if (transitionTarget == null || this.currentState == null)
    {
        Debug.LogError(this.GetType().ToString() + " cannot finalize transition; source or target state is null!");
        return false;
    }
    this.currentState.exitMethod(transitionTarget.name);
    transitionTarget.enterMethod(this.currentState.name);
    this.currentState = transitionTarget;
    this.inTransition = false;
    this.OnUpdate = this.currentState.updateMethod;
    return true;
}

Le point de ChangeCurrentState est principalement de s’assurer que nous ne faisons rien d’inapproprié. Est-ce que l’état existe? Essayons-nous de passer d’un état au même état? La variable inTransition évite de faire un appel de transition alors que nous sommes au milieu d’une transition. Cela évite un appel de la méthode lors de l’appel de Enter/ExitState. Dans les prochains articles, nous ferons en sorte que cela soit possible, mais pas pour le moment.

Une fois que toutes les conditions sont ok, nous faisons l’appel de sortie de l’état en cours avec l’état suivant comme paramètre et l’appel d’entrée de l’état suivant avec l’état en cours comme paramètre.

Once all conditions are ok, we call the exit of the current state with the next state as parameter and then the enter of the next state with the current state as parameter

this.currentState.exitMethod(transitionTarget.name);
transitionTarget.enterMethod(this.currentState.name);

Ensuite, inTransition passe à false et la méthode renvoie true pour dire que tout à bien marcher.

La dernière ligne de code nous indique que nous avons une Update qui devrait être utilisée.

protected virtual void Update()
{
    this.OnUpdate();
}

Le delegate est appelé dans l’Update de MB. Cette dernière est virtual pour que nous puissions l’utiliser dans les classes qui héritent. Grâce à ChangeCurrentState, ce delegate pointe toujours vers l’Update de l’état en cours.

La FSM n’est pas limitée à l’Update, si Fixed/LateUpdate sont requises, il suffit alors de répéter le processus appliquer à Update. Awake/Start n’ont pas vraiment de raison d’être ajoutées. En tout cas, je n’en vois pas.

Le script et une utilisation de base

Le script se trouve au bout de ce lien:

github-mark@1200x630

Et voici une utilisation simplifiée.

using UnityEngine;
using System.Collections;
using System;

public class MyClassExample: StateTest 
{
     protected virtual void Awake() 
    {
        InitializeStateMachine<StateClass>(true);
    }

    void EnterStart(Enum previous) { Debug.Log("Enter Start"); }
    void ExitStart(Enum next) { Debug.Log("Exit Start"); }

    void EnterRun(Enum previous) { Debug.Log("Enter Run"); }
    void EnterDie(Enum next) { Debug.Log("Enter Die"); }
    void UpdateRun() { Debug.Log("update run");}

    protected override void Update() 
    {
        base.Update();
        if (Input.GetKeyDown(KeyCode.S)) { ChangeCurrentState(StateClass.Start); }
        if (Input.GetKeyDown(KeyCode.R)) { ChangeCurrentState(StateClass.Run); }
        if (Input.GetKeyDown(KeyCode.D)) { ChangeCurrentState(StateClass.Die); }
    }
}
public enum StateClass 
{
    Start, Run, Die
}

L’Update fait un appel vers base.Update. Ne pas oublier le override ou placer un new pour cacher la méthode de base. L’exemple est très simple et utilise les Inputs pour transiter. La console imprime les changements d’état. Essayez de passer de Run à Run et voyez comment la console vous informe du problème. Cela indique que dans votre code, votre logique n’est pas si logique puisque cela considère que le code n’est pas en mesure de connaitre son état en cours.
Si vous changez le paramètre de InitializeStateMachine en false, aucune impression n’apparait.

Conclusion

C’est une introduction très sommaire pour commencer cependant cette version est déjà utilisable telle qu’elle est. Dans les prochains articles, nous ajouterons des contraintes pour nous aider à mieux développer.
Si vous avez des commentaires, lancez.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s