Last active
January 6, 2025 11:00
-
-
Save ShutovKS/fe691f1a270897e033ed73348e59f345 to your computer and use it in GitHub Desktop.
Project management in the form of a "state machine".
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #region | |
| using System; | |
| using System.Collections.Generic; | |
| using System.Threading; | |
| using Cysharp.Threading.Tasks; | |
| using Zenject; | |
| #endregion | |
| // ReSharper disable SuspiciousTypeConversion.Global | |
| namespace Infrastructure.CoreController | |
| { | |
| /// <summary> | |
| /// Интерфейс контроллера "машины состояний" проекта. | |
| /// </summary> | |
| public interface IProjectManagement | |
| { | |
| /// <summary> | |
| /// Событие изменения состояния. | |
| /// </summary> | |
| event Action<IState> StateChanged; | |
| /// <summary> | |
| /// Текущее состояние. | |
| /// </summary> | |
| Type CurrentStateType { get; } | |
| /// <summary> | |
| /// Переход в новое состояние без аргументов. | |
| /// </summary> | |
| void ChangeState<TState>() where TState : IState; | |
| /// <summary> | |
| /// Переход в новое состояние с аргументами. | |
| /// </summary> | |
| void ChangeState<TState, TArg>(TArg arg) where TState : IState; | |
| } | |
| /// <summary> | |
| /// Реализация "машины состояний" проекта. | |
| /// </summary> | |
| public class ProjectStateMachine : IProjectManagement | |
| { | |
| public event Action<IState> StateChanged; | |
| public Type CurrentStateType => _activeState?.GetType(); | |
| private readonly IProjectStatesFactory _projectStatesFactory; | |
| private readonly Dictionary<Type, IState> _stateCache = new(); | |
| private IState _previousState; | |
| private IState _activeState; | |
| private CancellationTokenSource _updateLoopCts; | |
| public ProjectStateMachine( | |
| IProjectStatesFactory projectStatesFactory | |
| ) | |
| { | |
| _projectStatesFactory = projectStatesFactory; | |
| } | |
| #region Public API | |
| public async void ChangeState<TState>() where TState : IState | |
| { | |
| if (_activeState is TState) | |
| return; | |
| await ExitCurrentStateAsync(); | |
| SetActiveState<TState>(); | |
| await EnterActiveStateAsync(); | |
| } | |
| public async void ChangeState<TState, T0>(T0 arg) where TState : IState | |
| { | |
| if (_activeState is TState) | |
| return; | |
| await ExitCurrentStateAsync(); | |
| SetActiveState<TState>(); | |
| await EnterActiveStateAsync(arg); | |
| } | |
| #endregion | |
| #region Internal State Logic | |
| private async UniTask ExitCurrentStateAsync() | |
| { | |
| _updateLoopCts?.Cancel(); | |
| _updateLoopCts?.Dispose(); | |
| _updateLoopCts = null; | |
| if (_activeState is IExitable exitable) | |
| await exitable.OnExitAsync(); | |
| } | |
| private void SetActiveState<TState>() where TState : IState | |
| { | |
| _previousState = _activeState; | |
| _activeState = ResolveState<TState>(); | |
| var stateType = _activeState.GetType(); | |
| if (!_stateCache.ContainsKey(stateType)) | |
| { | |
| _stateCache.Add(stateType, _activeState); | |
| if (_activeState is IInitializable initializable) | |
| { | |
| initializable.Initialize(); | |
| } | |
| } | |
| StateChanged?.Invoke(_activeState); | |
| } | |
| private IState ResolveState<TState>() where TState : IState | |
| { | |
| var stateType = typeof(TState); | |
| if (_stateCache.TryGetValue(stateType, out var cachedState)) | |
| { | |
| if (cachedState.IsReusable) | |
| { | |
| return cachedState; | |
| } | |
| _stateCache.Remove(stateType); | |
| cachedState.Dispose(); | |
| } | |
| var newState = _projectStatesFactory.CreateProjectState<TState>(); | |
| return newState; | |
| } | |
| private async UniTask EnterActiveStateAsync(object arg = null) | |
| { | |
| switch (_activeState) | |
| { | |
| case IEnterable enterable when arg is null: | |
| // Без аргумента | |
| await enterable.OnEnterAsync(Unit.Default); | |
| break; | |
| case IEnterable<object> enterableWithArg when arg is not null: | |
| // С аргументом | |
| await enterableWithArg.OnEnterAsync(arg); | |
| break; | |
| case IEnterable enterableDefault: | |
| // Обработка кейса, если вдруг пришёл null, но состояние ждёт аргумента | |
| await enterableDefault.OnEnterAsync(Unit.Default); | |
| break; | |
| } | |
| if (_activeState is IUpdateable updateable) | |
| { | |
| _updateLoopCts = new CancellationTokenSource(); | |
| RunUpdateLoopAsync(updateable, _updateLoopCts.Token).Forget(); | |
| } | |
| } | |
| #endregion | |
| #region Update Loop | |
| private static async UniTaskVoid RunUpdateLoopAsync(IUpdateable updateable, CancellationToken cancellationToken) | |
| { | |
| try | |
| { | |
| while (true) | |
| { | |
| cancellationToken.ThrowIfCancellationRequested(); | |
| await updateable.OnUpdateAsync(); | |
| if (updateable.TickIntervalMilliseconds > 0) | |
| { | |
| await UniTask.Delay(updateable.TickIntervalMilliseconds, cancellationToken: cancellationToken); | |
| } | |
| else | |
| { | |
| await UniTask.Yield(cancellationToken); | |
| } | |
| } | |
| } | |
| catch (OperationCanceledException) | |
| { | |
| // Ignore | |
| } | |
| } | |
| #endregion | |
| } | |
| #region State Interfaces | |
| /// <summary> | |
| /// Общий интерфейс для состояний. | |
| /// </summary> | |
| public interface IState : IInitializable, IDisposable | |
| { | |
| /// <summary> | |
| /// Флаг, говорит нужно ли переиспользовать это состояние (true), | |
| /// либо при следующем вызове создаём новое (false). | |
| /// </summary> | |
| bool IsReusable { get; } | |
| } | |
| /// <summary> | |
| /// Интерфейс с методом инициализации состояния. | |
| /// </summary> | |
| public interface IInitializable | |
| { | |
| void Initialize(); | |
| } | |
| /// <summary> | |
| /// Интерфейс для состояний, способных выполнять "OnEnter" без аргумента. | |
| /// При этом он сам наследует generic-вариант с типом Unit. | |
| /// </summary> | |
| public interface IEnterable : IEnterable<Unit> | |
| { | |
| } | |
| /// <summary> | |
| /// Интерфейс для состояний, поддерживающих асинхронный вход с аргументом. | |
| /// </summary> | |
| public interface IEnterable<TArg> | |
| { | |
| UniTask OnEnterAsync(TArg arg); | |
| } | |
| /// <summary> | |
| /// Структура-заглушка | |
| /// </summary> | |
| public struct Unit | |
| { | |
| public static Unit Default => default; | |
| } | |
| /// <summary> | |
| /// Интерфейс для состояний, поддерживающих асинхронный выход. | |
| /// </summary> | |
| public interface IExitable | |
| { | |
| UniTask OnExitAsync(); | |
| } | |
| /// <summary> | |
| /// Интерфейс для состояний, поддерживающих периодический Update. | |
| /// </summary> | |
| public interface IUpdateable | |
| { | |
| /// <summary> | |
| /// Интервал (мс) между вызовами OnUpdateAsync. | |
| /// </summary> | |
| int TickIntervalMilliseconds { get; } | |
| /// <summary> | |
| /// Асинхронный метод, вызываемый раз в TickIntervalMilliseconds. | |
| /// </summary> | |
| UniTask OnUpdateAsync(); | |
| } | |
| #endregion | |
| #region Factory | |
| /// <summary> | |
| /// Фабрика для создания состояний. Подключается к Zenject (DI). | |
| /// </summary> | |
| public interface IProjectStatesFactory | |
| { | |
| TState CreateProjectState<TState>() where TState : IState; | |
| } | |
| /// <summary> | |
| /// Реализация фабрики состояний. | |
| /// </summary> | |
| public class ProjectStatesFactory : IProjectStatesFactory | |
| { | |
| private readonly DiContainer _container; | |
| public ProjectStatesFactory(DiContainer container) | |
| { | |
| _container = container; | |
| } | |
| public TState CreateProjectState<TState>() where TState : IState | |
| => _container.Instantiate<TState>(); | |
| } | |
| #endregion | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment