State Machine – FSM – Part 2

The article is currently being reviewed so it might contain some leftovers of previous version that are outdated.

Continuing with our FSM, we are about to add some extra features in order to more robust. One key issue with the current state of our FSM is that it allows any movement of state. All are registered and there is no tree of movement and restrictions. This is not acceptable and will be fixed with this article. So before getting any further, you need to have gone through the previous article or at least copied the script at the end.

Create a new script and name it StateMachine, go to the link below and copy paste the content onto your new script.

github-mark@1200x630

Making a proper FSM

A Finite-State-Machine is not just a bunch of states, it has to define some limitations. In the previous article, the result script does not have any, you can jump from any state to any other state. Actually, the only limitations are that both states have to be registered and the origin and target states cannot be the same.

fsm (1)

Just to clarify, the picture shows that:

  • Start state leads to Walk and will be our initial state
  • Walk leads to Run, Attack, Pause and Die
  • Run leads to Walk, Attack, Pause and Die
  • Attack leads to Walk, Run, Pause and Die
  • Pause leads to Walk, Run and Attack
  • Die leads to Start

As you can see, some states are restricted to some targets and that sounds more like a plan. You could argue that you just have to be careful and avoid sending one state to the wrong. Trust me, this is not enough. Even with the following addition, your program will perform actions you expect to land somewhere and it happens to crash somewhere else. This will help you track down or stop the program before it runs in the wrong direction.

Extending the State

First thing we need is to add some features to the state class.

class State
{
    // existing code
       
    public bool forceTransition = false;
    public List <Enum> transitions = null;
        
    public State(Enum name)
    {
        this.name = name;
        this.transitions = new List <Enum>();
    }
}

So we add the forceTransition boolean that allows to bypass the restrictions we are about to implement. You might find this a contradiction, you may have find yourself in situations where it comes useful.

The second addition is the list of transitions the user will define as legal. This way we create a one to many relation (or one to one, or one to none).

Adding states

The next step is to enable the addition of user state.

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

    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;
}

And there it is, no big magic once again. The first parameter is the state we are creating, the second parameter is the array of states we allow to go to. The last parameter will define if the state should allow any transition, default is false. The method reuses Initialized method from previous article.

So for our Walk state:

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 });
    }
}

The last parameter is left blank as forceTransition should be false by default. We just added Run, Attack, Die and Pause to its transitions list.

Changing state

First, we need a method that would tell us if a transition is legit.

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;
}

The method is made protected so you can actually check in sub classes if the change of state is legit. So the first check is always the initialization. Then comes the check to see if both states are registered and finally either the state allows any transition or the source state allows the transition to the target state.

We can now implement the changing of state.

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() + " requests transition to state " + newstate +
                                    " when still transitioning");
        }
        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() + " requests transition: " + this.currentState.name + " => " + newstate + " is not a defined transition!");
        return false;
    }

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

The first parameter takes the state to which we want to move. The second parameter will force the transition in case we do need to get there whatever it takes and we did not set the current state to accept the target state. It could be for instance a crash state while we already have 20 states and we do not want to manually add it to each single state. The rest is pretty much the same as in the previous article apart from the call to the if statement checking if we HAVE to transit or if it is a legal transition.

Using the 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
}

The example is again simple, I leave to you to come up with your own version. Maybe you can share it in the comments. Basically, it is meant to print what happens. You will notice that some actions are only performed for the specific state. For instance, the S key is only check when dead. A better and more thorough implementation would have been to place the input in methods that are called within the appropriate state updates.

Conclusion

This is it for this part. We can now prevent wrong state transition so our FSM is more robust avoiding transition that should not be. Again, if you see some errors, problems or if there would be some features you would think of, let us know.

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