Skip to content

Instantly share code, notes, and snippets.

@ShutovKS
Last active January 6, 2025 11:00
Show Gist options
  • Select an option

  • Save ShutovKS/fe691f1a270897e033ed73348e59f345 to your computer and use it in GitHub Desktop.

Select an option

Save ShutovKS/fe691f1a270897e033ed73348e59f345 to your computer and use it in GitHub Desktop.
Project management in the form of a "state machine".
#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