Help creating a type-safe state machine in C#

Godot Version

v4.2.1.stable.mono.official [b09f793f5]

Question

I’ll get straight to the point, and this will be a little bit long but I want to show exactly what I’m trying to achieve. I have a state machine I’ve been using which is a heavy modification of another state machine from another project, but it has several drawbacks that I want to address. First, here’s the state machine code:

using Godot;
using System;
using System.Linq;
using System.Diagnostics;
using System.Collections.Generic;

public partial class SimpleStateMachine : Node
{
	public List<SimpleState> States;

	public string CurrentState;

	public string LastState;
	
	private Stopwatch _timer;

	public delegate void StateChangedEventHandler(object sender,
		StateChangedEventArgs e);

	public event StateChangedEventHandler StateChanged;

	protected SimpleState iterationstate = null;

    public override void _Ready()
    {
        base._Ready();

        States = GetNode<Node>("States").GetChildren().OfType<SimpleState>().ToList();
        _timer = new Stopwatch();
    }

	private void SetState(SimpleState _state, Godot.Collections.Dictionary<string, Variant> message = null)
	{
		if (_state == null) return;

		if (iterationstate != null)
		{
			if (_state.Name == CurrentState) return;
			
			iterationstate.OnExit(_state.GetType().ToString());
		}

		_timer.Reset();
		LastState = CurrentState;
		CurrentState = _state.GetType().ToString();

		iterationstate = _state;
		iterationstate.OnStart(message);
		iterationstate.OnUpdate();

		_timer.Start();
		StateChanged?.Invoke(this, new StateChangedEventArgs(
			CurrentState, LastState));
	}

	public void ChangeState(string stateName, Godot.Collections.Dictionary<string, Variant> message = null)
	{
		foreach (SimpleState _state in States)
		{
			if (stateName == _state.GetType().ToString())
			{
				SetState(_state, message);
				return;
			}
		}
	}

	public long GetTimeInCurrentState(TimeElapsedType type = TimeElapsedType.Milliseconds)
	{
		return type switch
		{
			TimeElapsedType.Milliseconds => _timer.ElapsedMilliseconds,
			TimeElapsedType.Seconds => _timer.Elapsed.Seconds,
			TimeElapsedType.Ticks => _timer.ElapsedTicks,
			_ => throw new ArgumentOutOfRangeException(nameof(type),
				type, "Invalid type")
		};
	}

    public override void _PhysicsProcess(double delta)
    {
	    iterationstate?.UpdateState(delta);
    }
}

And here’s my State code:

using Godot;

public partial class SimpleState : Node
{
	private bool _hasBeenInitialized = false;
	private bool _onUpdateHasFired = false;

	public virtual void OnStart(Godot.Collections.Dictionary<string, Variant> message = null)
	{
		_hasBeenInitialized = true;
	}

	public virtual void OnUpdate()
	{
		if (!_hasBeenInitialized) return;

		_onUpdateHasFired = true;
	}

	public virtual void UpdateState(double dt)
	{
		if (!_onUpdateHasFired) return;
	}

	public virtual void OnExit(string nextState)
	{
		if (!_hasBeenInitialized) return;

		_hasBeenInitialized = false;
		_onUpdateHasFired = false;
	}
}

First, one obvious drawback this creates is that changing states is done by passing strings around. I’d ideally want to avoid this, because even in my current implementation I use a class with static string variables to avoid magic strings, I’d want a state machine that is completely type safe, and uses no strings for anything.
My next concern with this implementation that right now, is that the transitions from one state to another happened inside the state machine itself, in a single function. This function was called every tick to check for all the possible conditions, and execute a transition if needed from one state to another. This is bad, because the number of these checks grow exponentially the more states I add, even just 5 states result in this function being massive. And last, I tried to create a different state machine, but in almost every implementation I come up with, where the states handle their own transitions, it always ended up with a situation where the State machine had a reference to the states, and the states also had a reference to the state machine itself, which feels completely wrong. Furthermore, since this IS godot, I want to come up with a solution that fits within Godot’s design philosophy. Any help, tips or directions are greatly appreciated!

Hi
I guess that every state inherit from SimpleState. To type safe change state you should use method with generic type.

public void ChangeState<T>Godot.Collections.Dictionary<string, Variant> message = null) 
where T : SimpleState
	{
         var _state = States.FirstOrDefault(s => s.GetType() == typeof(T));
         if (_state != null)
	     {
			SetState(_state, message);
		 }
	}

Instead of passing name the class name as argument, call the method with type.
In this example SimpleState1 inherit from SimpleState

stateMachine.ChangeState<SimpleState1>()

To better perfomance you can store States in Dictionary<Type, SimpleSatate> instead of List<SimpleState>

public Dictionary<Type, SimpleState> States;

public void ChangeState<T>Godot.Collections.Dictionary<string, Variant> message = null) 
where T : SimpleState
	{
         if(States.TryGetValue(typeof(T), out var _state))
	     {
			SetState(_state, message);
		 }
	}

I don’t have experience with state machines, so I’m not sure if I can help you with your second question. But if you share code with call ChangeState method and how you check conditions I will try help.

I’m can use Node names/paths for my states you already inherit from nodes,
all state nodes are in state machine, this allow me get states just with GetNode<>()
this allow me have two same states with different settings.


    private State currentState;
    public State CurrentState { get => currentState; set => ChangeState(value); }


 public void ChangeState(string statePath)
    {
        ChangeState(GetNodeOrNull<State>(statePath));
    }


    public void ChangeState(State newState)
    {
        if (newState is null)
            GD.PrintErr("Error in ChangeState: The provided newState is null");
        if (CurrentState is not null && newState.StateName.Equals(CurrentState.StateName))
            return;
        CurrentState?.OnExitState();
        currentState = newState;
        CurrentState.OnEnterState();
    }

My states can be used inside and outside system

I like use [GlobalClass] in states and state machines.
Your simple classes should be abstract.

my states checking if they finish and they decide what next state, some events can start some states but they they triggered only when they appears not every frame.

Personally I would use an Enum for your state, that way its just comparing numbers., no need to be looking up stuff in a list.

Modelling Workflows With Finite State Machines in .NET - Lloyd Atkinson

If you need multiple states at once then use a bitwise enum instead.

Enum, Flags and bitwise operators - Alan Zucconi

Is should not working with lists for comparison. Instead, providing a state class and updating instances of that class would result in cleaner and less redundant code. It also makes adding new states much easier. Enums can be suitable for small systems that you don’t plan to develop further