What do you think about my statemachine implementation, any suggestions/fixes

Hello, I would be very grateful if you could give me feedback about the state machine implementation. Thanks!

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Sisus.Init;

namespace iCare.Core {
    public sealed class StateMachine : ITick {
        sealed class Transition {
            public readonly string ToState;
            public readonly Func<bool> Condition;

            public Transition(string toState, Func<bool> condition) {
                ToState = toState;
                Condition = condition;
            }
        }

        readonly Dictionary<string, IState> _statesDict = new();
        readonly Dictionary<string, List<Func<bool>>> _stateEnterConditions = new();
        readonly Dictionary<string, List<Func<bool>>> _stateExitConditions = new();
        readonly Dictionary<string, List<Action>> _stateEnterActions = new();
        readonly Dictionary<string, List<Action>> _stateExitActions = new();
        readonly Dictionary<string, List<Action>> _stateTickActions = new();
        readonly Dictionary<string, List<Transition>> _stateTransitions = new();

        IState _currentState;
        string _currentStateKey;
        List<Action> _currentStateTickActions;
        List<Transition> _currentTransitions;

        #region Add

        public StateMachine Add([DisallowNull] string key, [DisallowNull] IState state) {
            _statesDict.Add(key, state);
            return this;
        }

        public StateMachine AddEnterCondition([DisallowNull] string key, [DisallowNull] Func<bool> condition) {
            ValidateState(key);
            _stateEnterConditions.AddCondition(key, condition);
            return this;
        }

        public StateMachine AddEnterCondition([DisallowNull] Func<bool> condition) {
            foreach (var key in _statesDict.Keys) {
                AddEnterCondition(key, condition);
            }

            return this;
        }

        public StateMachine AddExitCondition([DisallowNull] string key, [DisallowNull] Func<bool> condition) {
            ValidateState(key);
            _stateExitConditions.AddCondition(key, condition);
            return this;
        }

        public StateMachine AddExitCondition([DisallowNull] Func<bool> condition) {
            foreach (var key in _statesDict.Keys) {
                AddExitCondition(key, condition);
            }

            return this;
        }

        public StateMachine AddEnterAction([DisallowNull] string key, [DisallowNull] Action action) {
            ValidateState(key);
            _stateEnterActions.AddAction(key, action);
            return this;
        }

        public StateMachine AddEnterAction([DisallowNull] Action action) {
            foreach (var key in _statesDict.Keys) {
                AddEnterAction(key, action);
            }

            return this;
        }

        public StateMachine AddExitAction([DisallowNull] string key, [DisallowNull] Action action) {
            ValidateState(key);
            _stateExitActions.AddAction(key, action);
            return this;
        }

        public StateMachine AddExitAction([DisallowNull] Action action) {
            foreach (var key in _statesDict.Keys) {
                AddExitAction(key, action);
            }

            return this;
        }

        public StateMachine AddTickAction([DisallowNull] string key, [DisallowNull] Action action) {
            ValidateState(key);
            _stateTickActions.AddAction(key, action);
            return this;
        }

        public StateMachine AddTickAction([DisallowNull] Action action) {
            foreach (var key in _statesDict.Keys) {
                AddTickAction(key, action);
            }

            return this;
        }

        public StateMachine AddTransition([DisallowNull] string fromState, [DisallowNull] string toState,
            [DisallowNull] Func<bool> condition) {
            ValidateState(fromState);
            ValidateState(toState);

            if (!_stateTransitions.TryGetValue(fromState, out var transitions)) {
                transitions = new List<Transition>();
                _stateTransitions.Add(fromState, transitions);
            }

            transitions.Add(new Transition(toState, condition));
            return this;
        }

        public StateMachine AddTransition([DisallowNull] string toState, [DisallowNull] Func<bool> condition) {
            foreach (var key in _statesDict.Keys) {
                AddTransition(key, toState, condition);
            }

            return this;
        }

        #endregion

        #region Core

        public void Tick(float deltaTime) {
            _currentState?.OnStateTick(deltaTime);
            if (_currentStateTickActions != null) {
                foreach (var action in _currentStateTickActions) {
                    action?.Invoke();
                }
            }


            if (_currentTransitions == null) return;
            foreach (var transition in _currentTransitions.Where(transition => transition.Condition())) {
                TrySet(transition.ToState);
                break;
            }
        }

        public void ForceSet([DisallowNull] string newStateKey) {
            if (_currentStateKey == newStateKey) throw new InvalidOperationException("Cannot set the same state as the current state");
            ValidateState(newStateKey);

            var oldState = _currentState;
            var newState = _statesDict[newStateKey];

            if (oldState != null) {
                oldState.OnStateExit();
                if (_stateExitActions.TryGetValue(_currentStateKey, out var exitActions)) {
                    foreach (var action in exitActions) action();
                }
            }

            _currentState = newState;
            _currentStateKey = newStateKey;
            newState.OnStateEnter();
            if (_stateEnterActions.TryGetValue(newStateKey, out var enterActions)) {
                foreach (var action in enterActions) action();
            }

            _currentStateTickActions = _stateTickActions.GetValueOrDefault(newStateKey);
            _currentTransitions = _stateTransitions.GetValueOrDefault(newStateKey);
        }

        public bool TrySet([DisallowNull] string newStateKey) {
            if (_currentStateKey != null && !_stateExitConditions.IsAllMet(_currentStateKey)) return false;
            if (!_stateEnterConditions.IsAllMet(newStateKey)) return false;

            ForceSet(newStateKey);
            return true;
        }

        public bool IsActiveState([DisallowNull] string stateKey) {
            return _currentStateKey == stateKey;
        }

        #endregion

        #region Validate

        [Conditional("DEBUG")]
        void ValidateState(string stateKey) {
            if (!_statesDict.ContainsKey(stateKey)) throw new InvalidOperationException($"State with key {stateKey} does not exist");
        }

        #endregion
    }
}

The reason im keeping states in dictionary i want to be able to set using ket like

            _stateMachine.ForceSet(TestKeys.State1); // i have extension method to use enums directly

Without even looking at your implementation above (no offense, just read on)…

This is my position on generic finite state machines (FSMs) frameworks and coding with them:

All generic FSM solutions I have seen do not actually improve the problem space.

I’m kind of more of a “get it working first” guy.

Ask yourself, “WHY would I use FSM solution XYZ when I just need a variable and a switch statement?”

Your mileage may vary.

“I strongly suggest to make it as simple as possible. No classes, no interfaces, no needless OOP.” - Zajoman on the Unity3D forums.

1 Like

Kurt… if there is any way you can recognize your posting habit it would help a lot. There was a message about state machines less than a week ago. You posted mostly the same (non-answer). I replied that I had that day rewritten my video player as an FSM and that it now, finally worked flawlessly.

I seem to recall you even gave it a thumbs-up of sorts.

The OP is posting his solution for a FSM. You know like if he was to post his implementation of a sorting algorithm or his customized JSON parser. It is an exercise in “learning by doing” not “listening to other’s opinions”.

He can implement an FSM and find out that it didn’t meet his expectations or be pleased as punch. His experiences are more likely to impact him that your standard message on FSMs, object pooling and such.

Please just think about it…

2 Likes

Hey tley,

I’m just pointing out that most times I see people reach for object pooling or generic FSM solutions, they materially increase the difficulty of their problem space and needlessly increase the complexity of the engineering, resulting in higher workload for everything thereafter.

The same applies to just about any kind of attempt to abstract / genericize any non-trivial system, to “make a library” for stuff, such as folks who demand that Unity add a “generic character controller” without even saying what kind of controller or recognizing that such a controller would embed a massive range of implicit and explicit assumptions, tangling up all further engineering.

But sure, example code is great. Just don’t drop in example code without understanding the kind of binders and blinders it will apply to your project when you do, that’s all. I’m a K.I.S.S. kinda guy (keep it simple).

yes for simple things if/switch will work great and statemachine will complicate thing unnecesary. But even though isnt this more simple then small switch state management

public sealed class TestFSM : MonoBehaviour {
    enum States {
        Freeze,
        Searching,
        OnTargetFound
    }

    StateMachine _fsm = new();

    void Awake() {
        _fsm
            .Add(States.Searching, new EmptyState())
            .Add(States.OnTargetFound, new EmptyState())
            .Add(States.Freeze, new EmptyState())
            .AddEnterAction(States.Searching, () => Debug.Log("Entering Searching"))
            .AddTickAction(States.Searching, () => Debug.Log("Searching Tick"))
            .AddExitAction(States.Searching, () => Debug.Log("Exiting Searching"))
            .AddEnterAction(States.OnTargetFound, () => Debug.Log("OnTargetFound"))
            .AddTickAction(States.OnTargetFound, () => Debug.Log("OnTargetFound Tick"))
            .AddExitAction(States.OnTargetFound, () => Debug.Log("Exiting OnTargetFound"));

        _fsm.TrySet(States.Searching);

        //To change
        _fsm.TrySet(States.OnTargetFound);
        //or 
        _fsm.AddTransition(States.Searching, States.OnTargetFound, () => true); //replace true with your condition
    }

    void Update() => _fsm.Tick(Time.deltaTime);
}

possible enum version (chatgpt wrote it)

using UnityEngine;

public sealed class TestFSM : MonoBehaviour {
    enum States {
        Freeze,
        Searching,
        OnTargetFound
    }

    States _currentState;

    void Awake() {
        // Initial state
        ChangeState(States.Searching);
    }

    void Update() {
        // Call the current state's tick logic
        TickState(Time.deltaTime);
    }

    void ChangeState(States newState) {
        // Call exit logic for the current state
        switch (_currentState) {
            case States.Searching:
                Debug.Log("Exiting Searching");
                break;
            case States.OnTargetFound:
                Debug.Log("Exiting OnTargetFound");
                break;
            case States.Freeze:
                Debug.Log("Exiting Freeze");
                break;
        }

        // Update state
        _currentState = newState;

        // Call enter logic for the new state
        switch (_currentState) {
            case States.Searching:
                Debug.Log("Entering Searching");
                break;
            case States.OnTargetFound:
                Debug.Log("Entering OnTargetFound");
                break;
            case States.Freeze:
                Debug.Log("Entering Freeze");
                break;
        }
    }

    void TickState(float deltaTime) {
        // Call tick logic for the current state
        switch (_currentState) {
            case States.Searching:
                Debug.Log("Searching Tick");
                // Example condition to transition to OnTargetFound
                if (ConditionToFindTarget()) ChangeState(States.OnTargetFound);
                break;

            case States.OnTargetFound:
                Debug.Log("OnTargetFound Tick");
                // Example condition to freeze
                if (ConditionToFreeze()) ChangeState(States.Freeze);
                break;

            case States.Freeze:
                Debug.Log("Freeze Tick");
                // Example logic to transition out of Freeze
                if (ConditionToSearchAgain()) ChangeState(States.Searching);
                break;
        }
    }

    bool ConditionToFindTarget() {
        return Random.value > 0.95f;
    }

    bool ConditionToFreeze() {
        return Random.value > 0.95f;
    }

    bool ConditionToSearchAgain() {
        return Random.value > 0.95f;
    }
}
1 Like