[Unity3d] Yet Another State Machine for Unity3d

By | May 20, 2015

Advertisement from Google

If logic in your controller to complex for ‘if’ or ‘switch\case’ constructions, here is small State Machine implementation for you.

Let’s start with example how to use this state machine, and then we go through implementation details:

public class Test : MonoBehaviour
{
    private StateMachine stateMachine;

    private State firstState = new State();
    private State secondState = new CustomState();
    private State finishState = new State();

    // Use this for initialization
    void Start ()
    {
        var initChild = new State()
            .AddStartAction(() => Debug.Log(Time.frameCount + " childInit Start"))
            .AddUpdateAction(() => Debug.Log(Time.frameCount + " childInit Update"))
            .AddStopAction(() => Debug.Log(Time.frameCount + " childInit Stop"));
        var childStateMachine = new StateMachine(initChild);
        stateMachine = new StateMachine(firstState);

        firstState
            .AddStartAction(() => Debug.Log(Time.frameCount + " 1 Start"))
            .AddUpdateAction(() => Debug.Log(Time.frameCount + " 1 Update"))
            .AddStopAction(() => Debug.Log(Time.frameCount + " 1 Stop"))
            .AddTransition(
                state => Time.frameCount == 5, 
                secondState)
            .AddTransition(
                state => Time.frameCount == 15,
                secondState);

        secondState
            .AddTransition(
                state => Time.frameCount == 10,
                firstState)
            .AddTransition(
                state => Input.GetKey(KeyCode.Q),
                childStateMachine);

        childStateMachine
            .AddTransition(s => Input.GetKey(KeyCode.Y), finishState);

        finishState
            .AddStartAction(() => Debug.Log(Time.frameCount + " finish Start"));

        stateMachine.Start();
    }

    void Update()
    {
        stateMachine.Update();
    }
}

public class CustomState : State
{
    public CustomState()
    {
        StartAction = CustomStart;
        UpdateAction = CustomUpdate;
        StopAction = CustomStop;
    }

    public void CustomStart()
    {
        Debug.Log(Time.frameCount + " custom start");
    }

    public void CustomUpdate()
    {
        Debug.Log(Time.frameCount + " custom update");
    }

    public void CustomStop()
    {
        Debug.Log(Time.frameCount + " custom stop");
    }
}

Each state might have couple Start, Update and Stop actions. You can add them through AddStartAction\AddUpdateAction\AddStopAction methods. Those actions are not required, so state could have only Start and Update, but not Stop action, or could be even without any actions.

Also, each state might have transitions. To add them use AddTransition method. It takes predicate and next state as arguments.

If state machine is in a some state it will check all registered transitions of the current state. If any of conditions return true, it will go to next state and will call all actions Stop for current state and Start for next state.


Advertisement from Google

If current state has transitions, but all conditions return false, state machine just call Update action of the current state and will check transitions again only in the next frame.

Here is State and StateMachine implementation:

public class State
{
    private readonly List<StateTransition> transitions = new List<StateTransition>();

    protected Action StartAction { get; set; }
    protected Action StopAction { get; set; }
    protected Action UpdateAction { get; set; }

    protected internal bool IsStarted { get; set; }

    protected internal void Start()
    {
        if (!IsStarted && StartAction != null)
        {
            StartAction();
            IsStarted = true;
        }
    }

    protected internal void Update()
    {
        if (IsStarted && UpdateAction != null)
        {
            UpdateAction();
        }
    }

    protected internal void Stop()
    {
        if (IsStarted && StopAction != null)
        {
            StopAction();
            IsStarted = false;
        }
    }

    public State AddStartAction(Action startAction)
    {
        StartAction += startAction;
        return this;
    }

    public State AddUpdateAction(Action updateAction)
    {
        UpdateAction += updateAction;
        return this;
    }

    public State AddStopAction(Action stopAction)
    {
        StopAction += stopAction;
        return this;
    }

    public State AddTransition(Predicate<State> condition, State nextState)
    {
        transitions.Add(new StateTransition(condition, nextState));
        return this;
    }

    public bool CanGoToNextState(out State nextState)
    {
        for (int i = 0; i < transitions.Count; i++)
        {
            if (transitions[i].Condition(this))
            {
                nextState = transitions[i].NextState;
                return true;
            }
        }

        nextState = null;
        return false;
    }

    public bool HasNextState()
    {
        return transitions.Count != 0;
    }
}

StateMachine:

public class StateMachine : State
{
    private readonly State initialState;
    public State CurrenState { get; private set; }

    public StateMachine(State initialState)
    {
        this.initialState = initialState;
        StartAction = Start;
        UpdateAction = Update;
        StopAction = Stop;
    }

    public new void Start()
    {
        if (IsStarted)
        {
            return;
        }

        CurrenState = initialState;
        CurrenState.Start();
        IsStarted = true;
    }

    public new void Update()
    {
        if (CurrenState == null)
        {
            return;
        }

        if (!CurrenState.IsStarted)
        {
            CurrenState.Start();
        }

        CurrenState.Update();

        State nextState;
        while (CurrenState.CanGoToNextState(out nextState))
        {
            if (CurrenState.IsStarted)
            {
                CurrenState.Stop();
            }

            CurrenState = nextState;

            if (!CurrenState.IsStarted)
            {
                CurrenState.Start();
            }

            CurrenState.Update();
        }

        if (!CurrenState.HasNextState())
        {
            Stop();
        }
    }

    public new void Stop()
    {
        if (CurrenState == null)
        {
            return;
        }

        if (CurrenState.IsStarted)
        {
            CurrenState.Stop();
        }

        CurrenState = null;
    }
}

As you can see, you can easily extend State by inherit from State class and specifying Start\Stop\Update actions.
You can also use StateMachine as a State, so you can create hierarchical structure.

Leave a Reply

Your email address will not be published. Required fields are marked *