Very Simple StateMachine Pattern - C#

Generics have been around awhile now. It’s not likely that Enums will be added. I’m sure there’s a reason they’re not now. In fact, you can’t restrict to any value type in particular. You’re limited to saying where T : struct (which will only allow value types) and IConvertible which Enum implements. I used this “workaround” in my example above by then checking to see if the type was an Enum in the constructor. It’s not really a constraint, but allows you to do the double check. It’s certainly nothing you should be worried about using in your own code as long as you document the intent.

@rozgo Thanks for posting. This is a great foundation on which to build!

Wow, this is an old thread! In the meantime, I converted my sample Unityscript coroutine state machine to C#

1 Like

Here’s a multipurpose state machine class based on the original post. It’s intended to operate like a black box base class and run callback routines based on state/transition changes. Both states and transitions can be configured as needed. Oh, and it runs garbage free (tested with 2018.1/Win 7).

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using UnityEngine;

public class StateMachine : MonoBehaviour
{
    public enum State
    {
        A,
        B,
        C,
    }

    public enum Transition
    {
        ENTER,
        ARRIVE,
        EXIT
    }


    public State state;
    public bool debugStates;
    protected delegate IEnumerator Routine();
    public void SetState(int state) { this.state = (State)state; }// for easy connection to UI dropdown
    Dictionary<int, Dictionary<Transition, Routine>> states = new Dictionary<int, Dictionary<Transition, Routine>>();

    void Start()
    {
        InitStateMachine();

        //// Test
        AssignCoroutine(State.A, Transition.ENTER, TestRoutine);
        //AssignCoroutine(State.B, Transition.ENTER, TestRoutine);
        AssignCoroutine(State.C, Transition.ENTER, TestRoutine);

        StartStateMachine();
    }

    protected void InitStateMachine()
    {
        foreach (int key in Enum.GetValues(typeof(State)))
        {
            var transitions = new Dictionary<Transition, Routine>();
            foreach (Transition t in Enum.GetValues(typeof(Transition)))
            {
                Routine r = null;
                transitions.Add(t, r);
            }
            states.Add(key, transitions);
        }
    }

    protected void StartStateMachine()
    {
        StartCoroutine(Evaluate());
    }

    protected void AssignCoroutine(State state, Transition transition, Routine routine)
    {
        states[(int)state][transition] = routine;
    }

    protected void AddCoroutine(State state, Transition transition, Routine routine, bool clearExisting = false)
    {
        if (clearExisting) { states[(int)state][transition] = null; }
        states[(int)state][transition] += routine;
    }

    protected IEnumerator Evaluate()
    {
        while (true)
        {
            for (int i = 0; i < states.Count; i++)
            {
                if ((int)state == i)
                {
                    if (debugStates) { Debug.Log("ENTER: " + (State)i + "\n"); }
                    if (states[i][Transition.ENTER] != null) { yield return states[i][Transition.ENTER].Invoke(); }

                    if (debugStates) { Debug.Log("ARRIVE: " + (State)i + "\n"); }
                    if (states[i][Transition.ARRIVE] != null) { yield return states[i][Transition.ARRIVE].Invoke(); }

                    while ((int)state == i) { yield return null; } // yield until state change

                    if (debugStates) { Debug.Log("EXIT: " + (State)i + "\n"); }
                    if (states[i][Transition.EXIT] != null) { yield return states[i][Transition.EXIT].Invoke(); }
                }
            }
        }
    }

    IEnumerator TestRoutine()
    {
        Debug.Log("Test Routine\n");
        yield return new WaitForSeconds(1); // Simulate processing
        yield break;
    }
}
2 Likes

10 years later, and this still holds up, thanks a lot man!!
I mean, I’m doing simple ai though, nothing fancy, so this is all I need!

How to add a Animator.CrossFade inside this ?

    IEnumerator WalkState () {
        Debug.Log("Walk: Enter");
        Animator.Crossfade("animName",1.0f); // <=== Make error
        while (state == State.Walk) {
            yield return 0;
        }
        Debug.Log("Walk: Exit");
        NextState();
    }

Please don’t necro-respond to 12-year-old completely unrelated posts to fix your simple typographic mistakes.

CrossFade() is a public method requiring an instance of an Animator.

It is NOT a static method on the Animator class, which is what it appears you are attempting to do above.

1 Like