From 5c3e0f6581e493692d2d70b2335c3e76d9fc836a Mon Sep 17 00:00:00 2001 From: Syntriax Date: Sat, 29 Mar 2025 21:40:30 +0300 Subject: [PATCH] feat: added basic state machine system & Engine.Systems class library --- Engine.Systems/Engine.Systems.csproj | 13 +++++ Engine.Systems/StateMachine/IState.cs | 22 ++++++++ Engine.Systems/StateMachine/State.cs | 52 +++++++++++++++++++ .../StateMachine/StateBehaviourBase.cs | 37 +++++++++++++ Engine.Systems/StateMachine/StateMachine.cs | 49 +++++++++++++++++ .../StateMachine/StateTransition.cs | 11 ++++ 6 files changed, 184 insertions(+) create mode 100644 Engine.Systems/Engine.Systems.csproj create mode 100644 Engine.Systems/StateMachine/IState.cs create mode 100644 Engine.Systems/StateMachine/State.cs create mode 100644 Engine.Systems/StateMachine/StateBehaviourBase.cs create mode 100644 Engine.Systems/StateMachine/StateMachine.cs create mode 100644 Engine.Systems/StateMachine/StateTransition.cs diff --git a/Engine.Systems/Engine.Systems.csproj b/Engine.Systems/Engine.Systems.csproj new file mode 100644 index 0000000..2ba7eb9 --- /dev/null +++ b/Engine.Systems/Engine.Systems.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Engine.Systems/StateMachine/IState.cs b/Engine.Systems/StateMachine/IState.cs new file mode 100644 index 0000000..ff20be4 --- /dev/null +++ b/Engine.Systems/StateMachine/IState.cs @@ -0,0 +1,22 @@ +namespace Syntriax.Engine.StateMachine; + +public interface IState +{ + event StateUpdateEventHandler? OnStateUpdate; + event StateTransitionedFromEventHandler? OnStateTransitionedFrom; + event StateTransitionedToEventHandler? OnStateTransitionedTo; + event StateTransitionReadyEventHandler? OnStateTransitionReady; + + string Name { get; } + + IState? GetNextState(); + + void Update(); + void TransitionTo(IState from); + void TransitionFrom(IState to); + + delegate void StateUpdateEventHandler(IState sender); + delegate void StateTransitionedFromEventHandler(IState sender, IState toState); + delegate void StateTransitionedToEventHandler(IState sender, IState fromState); + delegate void StateTransitionReadyEventHandler(IState sender, IState toState); +} diff --git a/Engine.Systems/StateMachine/State.cs b/Engine.Systems/StateMachine/State.cs new file mode 100644 index 0000000..5ebe352 --- /dev/null +++ b/Engine.Systems/StateMachine/State.cs @@ -0,0 +1,52 @@ +namespace Syntriax.Engine.StateMachine; + +public class State : IState +{ + public event IState.StateUpdateEventHandler? OnStateUpdate = null; + public event IState.StateTransitionedFromEventHandler? OnStateTransitionedFrom = null; + public event IState.StateTransitionedToEventHandler? OnStateTransitionedTo = null; + public event IState.StateTransitionReadyEventHandler? OnStateTransitionReady = null; + + private readonly List transitions = []; + private readonly Dictionary possibleTransitions = []; + + public string Name { get; set; } = "Default State Name"; + public IReadOnlyList Transitions => transitions; + public IReadOnlyDictionary PossibleTransitions => possibleTransitions; + + public void RemoveTransition(string name) + { + if (!possibleTransitions.TryGetValue(name, out StateTransition stateTransition)) + return; + + transitions.Remove(stateTransition); + possibleTransitions.Remove(name); + } + + public void AddTransition(string name, StateTransition stateTransition) + { + if (transitions.Contains(stateTransition)) + return; + + transitions.Add(stateTransition); + possibleTransitions.Add(name, stateTransition); + } + + public void Update() + { + if (GetNextState() is IState transitionState) + OnStateTransitionReady?.Invoke(this, transitionState); + OnStateUpdate?.Invoke(this); + } + + public void TransitionTo(IState from) => OnStateTransitionedTo?.Invoke(this, from); + public void TransitionFrom(IState to) => OnStateTransitionedFrom?.Invoke(this, to); + + public IState? GetNextState() + { + foreach (StateTransition stateTransition in transitions) + if (stateTransition.CanTransition) + return stateTransition.State; + return null; + } +} diff --git a/Engine.Systems/StateMachine/StateBehaviourBase.cs b/Engine.Systems/StateMachine/StateBehaviourBase.cs new file mode 100644 index 0000000..d51f888 --- /dev/null +++ b/Engine.Systems/StateMachine/StateBehaviourBase.cs @@ -0,0 +1,37 @@ +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.StateMachine; + +public abstract class StateBehaviourBase : Behaviour, IState +{ + public event IState.StateUpdateEventHandler? OnStateUpdate = null; + public event IState.StateTransitionedFromEventHandler? OnStateTransitionedFrom = null; + public event IState.StateTransitionedToEventHandler? OnStateTransitionedTo = null; + + public abstract event IState.StateTransitionReadyEventHandler? OnStateTransitionReady; + + public abstract string Name { get; } + + protected virtual void OnUpdateState() { } + public void Update() + { + OnUpdateState(); + OnStateUpdate?.Invoke(this); + } + + protected virtual void OnTransitionedToState(IState from) { } + public void TransitionTo(IState from) + { + OnTransitionedToState(from); + OnStateTransitionedTo?.Invoke(this, from); + } + + protected virtual void OnTransitionedFromState(IState to) { } + public void TransitionFrom(IState to) + { + OnTransitionedFromState(to); + OnStateTransitionedFrom?.Invoke(this, to); + } + + public abstract IState? GetNextState(); +} diff --git a/Engine.Systems/StateMachine/StateMachine.cs b/Engine.Systems/StateMachine/StateMachine.cs new file mode 100644 index 0000000..791427f --- /dev/null +++ b/Engine.Systems/StateMachine/StateMachine.cs @@ -0,0 +1,49 @@ +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.StateMachine; + +public class StateMachine : Behaviour +{ + public event OnStateChangedEventHandler? OnStateChanged = null; + + private IState _state = new State(); + public IState State + { + get => _state; + set + { + if (_state == value) + return; + + IState previousState = _state; + previousState.OnStateTransitionReady -= OnStateTransitionReady; + + _state = value; + previousState.TransitionFrom(value); + value.TransitionTo(_state); + OnStateChanged?.Invoke(this, previousState, value); + + value.OnStateTransitionReady += OnStateTransitionReady; + } + } + + private void OnStateTransitionReady(IState sender, IState toState) + { + State = toState; + while (State.GetNextState() is IState nextState) + State = nextState; + } + + protected override void OnUpdate() + { + if (State is null) + return; + + while (State.GetNextState() is IState nextState) + State = nextState; + + State.Update(); + } + + public delegate void OnStateChangedEventHandler(StateMachine sender, IState previousState, IState newState); +} diff --git a/Engine.Systems/StateMachine/StateTransition.cs b/Engine.Systems/StateMachine/StateTransition.cs new file mode 100644 index 0000000..d0cff8e --- /dev/null +++ b/Engine.Systems/StateMachine/StateTransition.cs @@ -0,0 +1,11 @@ +namespace Syntriax.Engine.StateMachine; + +public readonly record struct StateTransition(IState State, IReadOnlyList> Conditions) +{ + public static implicit operator (IState state, IReadOnlyList> conditions)(StateTransition value) + => (value.State, value.Conditions); + public static implicit operator StateTransition((IState state, IReadOnlyList> conditions) value) + => new(value.state, value.conditions); + + public bool CanTransition => !Conditions.Any(c => !c.Invoke()); +}