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