feat: add state machine

This commit is contained in:
Bragin Stepan
2026-03-02 19:00:06 +05:00
parent 99c88c071f
commit 7737ee3158
50 changed files with 828 additions and 128 deletions

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d79e4112f0c64a90b05cae78e957a829
timeCreated: 1772454294

View File

@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using _Project.Develop.Runtime.Entities;
namespace _Project.Develop.Runtime.Logic.Gameplay.Features.AI
{
public class AIBrainsContext : IDisposable
{
private readonly List<EntityToBrain> _entityToBrains = new ();
public void SetFor(Entity entity, IBrain brain)
{
foreach (EntityToBrain item in _entityToBrains)
{
if (item.Entity == entity)
{
item.Brain.Disable();
item.Brain.Dispose();
item.Brain = brain;
item.Brain.Enable();
return;
}
}
_entityToBrains.Add(new EntityToBrain(brain, entity));
brain.Enable();
}
public void Update(float deltaTime)
{
for (int i = 0; i < _entityToBrains.Count; i++)
{
if (_entityToBrains[i].Entity.IsInit == false)
{
int lastIndex = _entityToBrains.Count - 1;
_entityToBrains[i].Brain.Dispose();
_entityToBrains[i] = _entityToBrains[lastIndex];
_entityToBrains.RemoveAt(lastIndex);
i--;
continue;
}
_entityToBrains[i].Brain.Update(deltaTime);
}
}
public void Dispose()
{
foreach (EntityToBrain entityToBrain in _entityToBrains)
entityToBrain.Brain.Dispose();
_entityToBrains.Clear();
}
private class EntityToBrain
{
public Entity Entity;
public IBrain Brain;
public EntityToBrain(IBrain brain, Entity entity)
{
Brain = brain;
Entity = entity;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 647b53823c00413081b7c9221d5bb5c1
timeCreated: 1772458604

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using Assets._Project.Develop.Runtime.Utilities.StateMachineCore;
namespace _Project.Develop.Runtime.Logic.Gameplay.Features.AI
{
public class AIStateMachine : StateMachine<IUpdatableState>
{
public AIStateMachine(List<IDisposable> disposables) : base(disposables)
{
}
public AIStateMachine() : base (new List<IDisposable>())
{
}
protected override void UpdateLogic(float deltaTime)
{
base.UpdateLogic(deltaTime);
CurrentState?.Update(deltaTime);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1e225964fa5a4120a1e4bf1cc8d47641
timeCreated: 1772455227

View File

@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using _Project.Develop.Runtime.Entities;
using _Project.Develop.Runtime.Logic.Gameplay.Features.AI.States;
using _Project.Develop.Runtime.Utilities.Conditions;
using Assets._Project.Develop.Runtime.Infrastructure.DI;
using Assets._Project.Develop.Runtime.Utilities.Timer;
namespace _Project.Develop.Runtime.Logic.Gameplay.Features.AI
{
public class BrainsFactory
{
private readonly DIContainer _container;
private readonly AIBrainsContext _aiBrainsContext;
private readonly TimerServiceFactory _timerServiceFactory;
public BrainsFactory(DIContainer container)
{
_container = container;
_aiBrainsContext = _container.Resolve<AIBrainsContext>();
_timerServiceFactory = _container.Resolve<TimerServiceFactory>();
}
public StateMachineBrain CreateGhostBrain(Entity entity)
{
AIStateMachine stateMachine = CreateRandomMovementStateMachine(entity);
StateMachineBrain brain = new (stateMachine);
_aiBrainsContext.SetFor(entity, brain);
return brain;
}
private AIStateMachine CreateRandomMovementStateMachine(Entity entity)
{
List<IDisposable> disposables = new ();
RandomMovementState randomMovementState = new (entity, 0.5f);
EmptyState emptyState = new ();
TimerService movementTimer = _timerServiceFactory.Create(2f);
disposables.Add(randomMovementState.Entered.Subscribe(movementTimer.Restart));
disposables.Add(movementTimer);
TimerService idleTimer = _timerServiceFactory.Create(3f);
disposables.Add(emptyState.Entered.Subscribe(idleTimer.Restart));
disposables.Add(idleTimer);
FuncCondition movementTimerEndedCondition = new (() => movementTimer.IsOver);
FuncCondition idleTimerEndedCondition = new (() => idleTimer.IsOver);
AIStateMachine stateMachine = new (disposables);
stateMachine.AddState(randomMovementState);
stateMachine.AddState(emptyState);
stateMachine.AddTransition(randomMovementState, emptyState, movementTimerEndedCondition);
stateMachine.AddTransition(emptyState, randomMovementState, idleTimerEndedCondition);
return stateMachine;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0b566da239ac47d5b8c828cc1ac54cbe
timeCreated: 1772455908

View File

@@ -0,0 +1,14 @@
using System;
namespace _Project.Develop.Runtime.Logic.Gameplay.Features.AI
{
public interface IBrain : IDisposable
{
void Enable();
void Disable();
void Update(float deltaTime);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 77beb88ee43645a5b0f85bf7d1518e4a
timeCreated: 1772458164

View File

@@ -0,0 +1,41 @@
namespace _Project.Develop.Runtime.Logic.Gameplay.Features.AI
{
public class StateMachineBrain : IBrain
{
private readonly AIStateMachine _stateMachine;
private bool _isEnabled;
public StateMachineBrain(AIStateMachine stateMachine)
{
_stateMachine = stateMachine;
}
public void Enable()
{
_stateMachine.Enter();
_isEnabled = true;
}
public void Update(float deltaTime)
{
if (_isEnabled == false)
return;
_stateMachine.Update(deltaTime);
}
public void Disable()
{
_stateMachine.Exit();
_stateMachine.Dispose();
_isEnabled = false;
}
public void Dispose()
{
_stateMachine.Dispose();
_isEnabled = false;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 45f2a68dedcb4a61a4ae6dc8a50227e4
timeCreated: 1772458266

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a0534b2062654595bef4d1e59081a533
timeCreated: 1772454304

View File

@@ -0,0 +1,12 @@
using Assets._Project.Develop.Runtime.Utilities.StateMachineCore;
namespace _Project.Develop.Runtime.Logic.Gameplay.Features.AI.States
{
public class EmptyState : State, IUpdatableState
{
public void Update(float deltaTime)
{
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3f40181e821046968e6c69ecef7e2384
timeCreated: 1772454318

View File

@@ -0,0 +1,67 @@
using _Project.Develop.Runtime.Entities;
using _Project.Develop.Runtime.Utils.ReactiveManagement;
using Assets._Project.Develop.Runtime.Utilities.StateMachineCore;
using UnityEngine;
namespace _Project.Develop.Runtime.Logic.Gameplay.Features.AI.States
{
public class RandomMovementState : State, IUpdatableState
{
private ReactiveVariable<Vector3> _moveDirection;
private ReactiveVariable<Vector3> _rotateDirection;
private float _cooldownBetweenDirection;
private float _time;
public RandomMovementState(Entity entity, float cooldownBetweenDirection)
{
_moveDirection = entity.MoveDirection;
_rotateDirection = entity.RotateDirection;
_cooldownBetweenDirection = cooldownBetweenDirection;
}
public override void Enter()
{
base.Enter();
Vector3 randomDirection = new Vector3(Random.Range(-1f, 1f), 0, Random.Range(-1f, 1f)).normalized;
_moveDirection.Value = randomDirection;
_rotateDirection.Value = randomDirection;
_time = 0;
}
public void Update(float deltaTime)
{
_time += deltaTime;
if (_time >= _cooldownBetweenDirection)
{
Vector3 newDirection = GenerateNewDirection();
_moveDirection.Value = newDirection;
_rotateDirection.Value = newDirection;
_time = 0;
}
}
public override void Exit()
{
base.Exit();
_moveDirection.Value = Vector3.zero;
}
private Vector3 GenerateNewDirection()
{
Vector3 inverseDirection = -_moveDirection.Value.normalized;
Quaternion randomTurn = Quaternion.Euler(0, Random.Range(-30, 30), 0);
return randomTurn * inverseDirection;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: dd6446a9b2f646bc870251ae870904b3
timeCreated: 1772454385

View File

@@ -3,7 +3,7 @@ using _Project.Develop.Runtime.Entities;
using _Project.Develop.Runtime.Utilities.Conditions;
using _Project.Develop.Runtime.Utils.ReactiveManagement;
using _Project.Develop.Runtime.Utils.ReactiveManagement.Event;
using Unity.Mathematics;
using UnityEngine;
namespace _Project.Develop.Runtime.Logic.Gameplay.Features.Energy.Systems
{
@@ -11,52 +11,52 @@ namespace _Project.Develop.Runtime.Logic.Gameplay.Features.Energy.Systems
{
private ReactiveEvent<int> _regenEnergyRequest;
private ReactiveEvent<int> _regenEnergyEvent;
private ReactiveVariable<int> _currentEnergy;
private ReactiveVariable<int> _maxEnergy;
private ICompositeCondition _canRegen;
private IDisposable _regenRequestDispose;
public void OnInit(Entity entity)
{
_currentEnergy = entity.CurrentEnergy;
_maxEnergy = entity.MaxEnergy;
_regenEnergyRequest = entity.RegenEnergyRequest;
_regenEnergyEvent = entity.RegenEnergyEvent;
_canRegen = entity.CanRegenEnergy;
_regenRequestDispose = _regenEnergyRequest.Subscribe(OnRegenRequest);
}
private void OnRegenRequest(int percentage)
{
if (percentage <= 0)
throw new ArgumentException("Energy regen percentage must be positive", nameof(percentage));
if (_canRegen.Evaluate() == false)
return;
int energyDifference = _maxEnergy.Value - _currentEnergy.Value;
if (energyDifference <= 0)
return;
int regenAmount= (int)math.floor(_maxEnergy.Value * (percentage / 100f));
int regenAmount = (int)Mathf.Floor(_maxEnergy.Value * (percentage / 100f));
if (regenAmount < 1 && _maxEnergy.Value > 0)
regenAmount = 1;
int valueAdded = math.min(regenAmount, energyDifference);
int valueAdded = Mathf.Min(regenAmount, energyDifference);
_currentEnergy.Value += valueAdded;
_regenEnergyEvent.Invoke(valueAdded);
}
public void OnDispose()
{
_regenRequestDispose.Dispose();

View File

@@ -3,7 +3,6 @@ using _Project.Develop.Runtime.Entities;
using _Project.Develop.Runtime.Utilities.Conditions;
using _Project.Develop.Runtime.Utils.ReactiveManagement;
using _Project.Develop.Runtime.Utils.ReactiveManagement.Event;
using Unity.Mathematics;
using UnityEngine;
namespace _Project.Develop.Runtime.Logic.Gameplay.Features.Energy.Systems
@@ -12,47 +11,47 @@ namespace _Project.Develop.Runtime.Logic.Gameplay.Features.Energy.Systems
{
private ReactiveEvent<int> _regenEnergyRequest;
private ReactiveEvent<int> _regenEnergyEvent;
private ReactiveVariable<int> _currentEnergy;
private ReactiveVariable<int> _maxEnergy;
private ICompositeCondition _canRegen;
private IDisposable _regenRequestDispose;
public void OnInit(Entity entity)
{
_currentEnergy = entity.CurrentEnergy;
_maxEnergy = entity.MaxEnergy;
_regenEnergyRequest = entity.RegenEnergyRequest;
_regenEnergyEvent = entity.RegenEnergyEvent;
_canRegen = entity.CanRegenEnergy;
_regenRequestDispose = _regenEnergyRequest.Subscribe(OnRegenRequest);
}
private void OnRegenRequest(int value)
{
if (value <= 0)
throw new ArgumentException("Energy regen value must be positive", nameof(value));
if (_canRegen.Evaluate() == false)
return;
int energyDifference = _maxEnergy.Value - _currentEnergy.Value;
if (energyDifference <= 0)
return;
int valueAdded = math.min(value, energyDifference);
int valueAdded = Mathf.Min(value, energyDifference);
_currentEnergy.Value += valueAdded;
_regenEnergyEvent.Invoke(valueAdded);
}
public void OnDispose()
{
_regenRequestDispose.Dispose();