feat: add auto attack state

This commit is contained in:
Bragin Stepan
2026-03-03 17:34:02 +05:00
parent 7737ee3158
commit c04b0a259a
22 changed files with 377 additions and 21 deletions

View File

@@ -52,13 +52,13 @@ namespace _Project.Develop.Runtime.Entities
.AddInDeathProcess() .AddInDeathProcess()
.AddDeathProcessInitialTime(new ReactiveVariable<float>(2)) .AddDeathProcessInitialTime(new ReactiveVariable<float>(2))
.AddDeathProcessCurrentTime() .AddDeathProcessCurrentTime()
.AddAttackProcessInitialTime(new ReactiveVariable<float>(3)) .AddAttackProcessInitialTime(new ReactiveVariable<float>(1))
.AddAttackProcessCurrentTime() .AddAttackProcessCurrentTime()
.AddInAttackProcess() .AddInAttackProcess()
.AddStartAttackRequest() .AddStartAttackRequest()
.AddStartAttackEvent() .AddStartAttackEvent()
.AddEndAttackEvent() .AddEndAttackEvent()
.AddAttackDelayTime(new ReactiveVariable<float>(2)) .AddAttackDelayTime(new ReactiveVariable<float>(1))
.AddAttackDelayEndEvent() .AddAttackDelayEndEvent()
.AddInstantAttackDamage(new ReactiveVariable<float>(50)) .AddInstantAttackDamage(new ReactiveVariable<float>(50))
.AddAttackCanceledEvent() .AddAttackCanceledEvent()
@@ -102,9 +102,6 @@ namespace _Project.Develop.Runtime.Entities
.AddMustCancelAttack(mustCancelAttack); .AddMustCancelAttack(mustCancelAttack);
entity entity
.AddSystem(new AttackByInputSystem(_playerInput))
.AddSystem(new MoveDirectionByInputSystem(_playerInput))
.AddSystem(new RotateDirectionByMoveInputSystem(_playerInput))
.AddSystem(new RigidbodyMovementSystem()) .AddSystem(new RigidbodyMovementSystem())
.AddSystem(new RigidbodyRotationSystem()) .AddSystem(new RigidbodyRotationSystem())

View File

@@ -1682,5 +1682,29 @@ namespace _Project.Develop.Runtime.Entities
return AddComponent(new _Project.Develop.Runtime.Logic.Gameplay.Features.Attack.InAttackCooldown() {Value = value}); return AddComponent(new _Project.Develop.Runtime.Logic.Gameplay.Features.Attack.InAttackCooldown() {Value = value});
} }
public _Project.Develop.Runtime.Logic.Gameplay.Features.AI.CurrentTarget CurrentTargetC => GetComponent<_Project.Develop.Runtime.Logic.Gameplay.Features.AI.CurrentTarget>();
public _Project.Develop.Runtime.Utils.ReactiveManagement.ReactiveVariable<_Project.Develop.Runtime.Entities.Entity> CurrentTarget => CurrentTargetC.Value;
public bool TryGetCurrentTarget(out _Project.Develop.Runtime.Utils.ReactiveManagement.ReactiveVariable<_Project.Develop.Runtime.Entities.Entity> value)
{
bool result = TryGetComponent(out _Project.Develop.Runtime.Logic.Gameplay.Features.AI.CurrentTarget component);
if(result)
value = component.Value;
else
value = default(_Project.Develop.Runtime.Utils.ReactiveManagement.ReactiveVariable<_Project.Develop.Runtime.Entities.Entity>);
return result;
}
public _Project.Develop.Runtime.Entities.Entity AddCurrentTarget()
{
return AddComponent(new _Project.Develop.Runtime.Logic.Gameplay.Features.AI.CurrentTarget() { Value = new _Project.Develop.Runtime.Utils.ReactiveManagement.ReactiveVariable<_Project.Develop.Runtime.Entities.Entity>() });
}
public _Project.Develop.Runtime.Entities.Entity AddCurrentTarget(_Project.Develop.Runtime.Utils.ReactiveManagement.ReactiveVariable<_Project.Develop.Runtime.Entities.Entity> value)
{
return AddComponent(new _Project.Develop.Runtime.Logic.Gameplay.Features.AI.CurrentTarget() {Value = value});
}
} }
} }

View File

@@ -0,0 +1,7 @@
using _Project.Develop.Runtime.Entities;
using _Project.Develop.Runtime.Utils.ReactiveManagement;
namespace _Project.Develop.Runtime.Logic.Gameplay.Features.AI
{
public class CurrentTarget : IEntityComponent { public ReactiveVariable<Entity> Value; }
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 78216e3102e549f39cb690e777d00e88
timeCreated: 1772470309

View File

@@ -0,0 +1,16 @@
using Assets._Project.Develop.Runtime.Utilities.StateMachineCore;
namespace _Project.Develop.Runtime.Logic.Gameplay.Features.AI
{
public class AIParallelState : ParallelState<IUpdatableState>, IUpdatableState
{
public AIParallelState(params IUpdatableState[] states) : base(states)
{ }
public void Update(float deltaTime)
{
foreach (IUpdatableState state in States)
state.Update(deltaTime);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d0ae2666ed0a43d590866fff4686222a
timeCreated: 1772538444

View File

@@ -3,8 +3,11 @@ using System.Collections.Generic;
using _Project.Develop.Runtime.Entities; using _Project.Develop.Runtime.Entities;
using _Project.Develop.Runtime.Logic.Gameplay.Features.AI.States; using _Project.Develop.Runtime.Logic.Gameplay.Features.AI.States;
using _Project.Develop.Runtime.Utilities.Conditions; using _Project.Develop.Runtime.Utilities.Conditions;
using _Project.Develop.Runtime.Utils.InputManagement;
using _Project.Develop.Runtime.Utils.ReactiveManagement;
using Assets._Project.Develop.Runtime.Infrastructure.DI; using Assets._Project.Develop.Runtime.Infrastructure.DI;
using Assets._Project.Develop.Runtime.Utilities.Timer; using Assets._Project.Develop.Runtime.Utilities.Timer;
using UnityEngine;
namespace _Project.Develop.Runtime.Logic.Gameplay.Features.AI namespace _Project.Develop.Runtime.Logic.Gameplay.Features.AI
{ {
@@ -12,14 +15,19 @@ namespace _Project.Develop.Runtime.Logic.Gameplay.Features.AI
{ {
private readonly DIContainer _container; private readonly DIContainer _container;
private readonly EntitiesLifeContext _entitiesLifeContext;
private readonly AIBrainsContext _aiBrainsContext; private readonly AIBrainsContext _aiBrainsContext;
private readonly TimerServiceFactory _timerServiceFactory; private readonly TimerServiceFactory _timerServiceFactory;
private readonly IPlayerInput _playerInput;
public BrainsFactory(DIContainer container) public BrainsFactory(DIContainer container)
{ {
_container = container; _container = container;
_playerInput = _container.Resolve<IPlayerInput>();
_aiBrainsContext = _container.Resolve<AIBrainsContext>(); _aiBrainsContext = _container.Resolve<AIBrainsContext>();
_timerServiceFactory = _container.Resolve<TimerServiceFactory>(); _timerServiceFactory = _container.Resolve<TimerServiceFactory>();
_entitiesLifeContext = _container.Resolve<EntitiesLifeContext>();
} }
public StateMachineBrain CreateGhostBrain(Entity entity) public StateMachineBrain CreateGhostBrain(Entity entity)
@@ -32,6 +40,43 @@ namespace _Project.Develop.Runtime.Logic.Gameplay.Features.AI
return brain; return brain;
} }
public StateMachineBrain CreateMainHeroBrain(Entity entity, ITargetSelector targetSelector)
{
AIStateMachine combatState = CreateAutoAttackStateMachine(entity);
PlayerInputMovementState movementState = new (entity, _playerInput);
ReactiveVariable<Entity> currentTarget = entity.CurrentTarget;
ICompositeCondition fromMovementToCombatStateCondition = new CompositeCondition()
.Add(new FuncCondition(() => currentTarget.Value != null))
.Add(new FuncCondition(() => _playerInput.Move.Value == Vector2.zero));
ICompositeCondition fromCombatToMovementStateCondition = new CompositeCondition(LogicOperationsUtils.Or)
.Add(new FuncCondition(() => currentTarget.Value == null))
.Add(new FuncCondition(() => _playerInput.Move.Value != Vector2.zero));
AIStateMachine behaviour = new ();
behaviour.AddState(combatState);
behaviour.AddState(movementState);
behaviour.AddTransition(combatState, movementState, fromCombatToMovementStateCondition);
behaviour.AddTransition(movementState, combatState, fromMovementToCombatStateCondition);
FindTargetState findTargetState = new (_entitiesLifeContext, entity, targetSelector);
AIParallelState parallelState = new (findTargetState, behaviour);
AIStateMachine rootStateMachine = new ();
rootStateMachine.AddState(parallelState);
StateMachineBrain brain = new (rootStateMachine);
_aiBrainsContext.SetFor(entity, brain);
return brain;
}
private AIStateMachine CreateRandomMovementStateMachine(Entity entity) private AIStateMachine CreateRandomMovementStateMachine(Entity entity)
{ {
List<IDisposable> disposables = new (); List<IDisposable> disposables = new ();
@@ -60,5 +105,46 @@ namespace _Project.Develop.Runtime.Logic.Gameplay.Features.AI
return stateMachine; return stateMachine;
} }
private AIStateMachine CreateAutoAttackStateMachine(Entity entity)
{
RotateToTargetState rotateToTargetState = new (entity);
AttackTriggerState attackTriggerState = new (entity);
ICondition canAttack = entity.CanStartAttack;
Transform transform = entity.Transform;
ReactiveVariable<Entity> currentTarget = entity.CurrentTarget;
ICompositeCondition fromRotateToAttackCondition = new CompositeCondition()
.Add(canAttack)
.Add(new FuncCondition(() =>
{
Entity target = currentTarget.Value;
if (target == null)
return false;
float angleToTarget = Quaternion.Angle(
transform.rotation,
Quaternion.LookRotation(target.Transform.position - transform.position));
return angleToTarget < 1f;
}
));
ReactiveVariable<bool> inAttackProcess = entity.InAttackProcess;
ICondition fromAttackToRotateStateCondition = new FuncCondition(() => inAttackProcess.Value == false);
AIStateMachine stateMachine = new ();
stateMachine.AddState(rotateToTargetState);
stateMachine.AddState(attackTriggerState);
stateMachine.AddTransition(rotateToTargetState, attackTriggerState, fromRotateToAttackCondition);
stateMachine.AddTransition(attackTriggerState, rotateToTargetState, fromAttackToRotateStateCondition);
return stateMachine;
}
} }
} }

View File

@@ -0,0 +1,25 @@
using _Project.Develop.Runtime.Entities;
using _Project.Develop.Runtime.Utils.ReactiveManagement.Event;
using Assets._Project.Develop.Runtime.Utilities.StateMachineCore;
namespace _Project.Develop.Runtime.Logic.Gameplay.Features.AI.States
{
public class AttackTriggerState : State, IUpdatableState
{
private ReactiveEvent _attackRequest;
public AttackTriggerState(Entity entity)
{
_attackRequest = entity.StartAttackRequest;
}
public override void Enter()
{
base.Enter();
_attackRequest.Invoke();
}
public void Update(float deltaTime)
{ }
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9d58a80b452346bda2d968af0f76fdfa
timeCreated: 1772469124

View File

@@ -0,0 +1,28 @@
using _Project.Develop.Runtime.Entities;
using _Project.Develop.Runtime.Utils.ReactiveManagement;
using Assets._Project.Develop.Runtime.Utilities.StateMachineCore;
namespace _Project.Develop.Runtime.Logic.Gameplay.Features.AI.States
{
public class FindTargetState : State, IUpdatableState
{
private ITargetSelector _targetSelector;
private EntitiesLifeContext _entitiesLifeContext;
private ReactiveVariable<Entity> _currentTarget;
public FindTargetState(
EntitiesLifeContext entitiesLifeContext,
Entity entity,
ITargetSelector targetSelector)
{
_currentTarget = entity.CurrentTarget;
_targetSelector = targetSelector;
_entitiesLifeContext = entitiesLifeContext;
}
public void Update(float deltaTime)
{
_currentTarget.Value = _targetSelector.SelectTargetFrom(_entitiesLifeContext.Entities);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1c92a70ea84c48f49ae461eae381a1ce
timeCreated: 1772539593

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
using _Project.Develop.Runtime.Entities;
namespace _Project.Develop.Runtime.Logic.Gameplay.Features.AI.States
{
public interface ITargetSelector
{
public Entity SelectTargetFrom(IEnumerable<Entity> targets);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 15c5398fa2f84d109f74fbe2cd14bcda
timeCreated: 1772538651

View File

@@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Linq;
using _Project.Develop.Runtime.Entities;
using _Project.Develop.Runtime.Logic.Gameplay.Features.Damage;
using _Project.Develop.Runtime.Utilities.Conditions;
using UnityEngine;
namespace _Project.Develop.Runtime.Logic.Gameplay.Features.AI.States
{
public class NearestDamageableTargetSelector : ITargetSelector
{
private readonly Entity _source;
private readonly Transform _sourceTransform;
public NearestDamageableTargetSelector(Entity entity)
{
_source = entity;
_sourceTransform = entity.Transform;
}
public Entity SelectTargetFrom(IEnumerable<Entity> targets)
{
IEnumerable<Entity> selectedTargets = FindSelectedTargets(targets);
IEnumerable<Entity> enumerable = selectedTargets.ToList();
if (enumerable.Any() == false)
return null;
Entity closetsTarget = enumerable.First();
float minDistance = GetDistanceTo(closetsTarget);
foreach (Entity target in enumerable)
{
float distance = GetDistanceTo(target);
if (distance < minDistance)
{
minDistance = distance;
closetsTarget = target;
}
}
return closetsTarget;
}
private IEnumerable<Entity> FindSelectedTargets(IEnumerable<Entity> targets)
{
return targets.Where(target =>
{
bool result = target.HasComponent<TakeDamageRequest>();
if (target.TryGetCanApplyDamage(out ICompositeCondition value))
result = result && value.Evaluate();
result = result && (target != _source);
return result;
});
}
private float GetDistanceTo(Entity target) => (_sourceTransform.position - target.Transform.position).magnitude;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5efdb899e3ea4373b66f0610ea05b5c5
timeCreated: 1772538727

View File

@@ -0,0 +1,37 @@
using _Project.Develop.Runtime.Entities;
using _Project.Develop.Runtime.Utils.InputManagement;
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 PlayerInputMovementState : State, IUpdatableState
{
private readonly IPlayerInput _playerInput;
private ReactiveVariable<Vector3> _rotateDirection;
private ReactiveVariable<Vector3> _moveDirection;
public PlayerInputMovementState(Entity entity, IPlayerInput playerInput)
{
_playerInput = playerInput;
_rotateDirection = entity.RotateDirection;
_moveDirection = entity.MoveDirection;
}
public void Update(float deltaTime)
{
_moveDirection.Value = new Vector3(_playerInput.Move.Value.x, 0, _playerInput.Move.Value.y);
_rotateDirection.Value = new Vector3(_playerInput.Move.Value.x, 0, _playerInput.Move.Value.y);
}
public override void Exit()
{
base.Exit();
_moveDirection.Value = Vector3.zero;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 449f5d14d62348c5a770bd34869f384f
timeCreated: 1772468559

View File

@@ -0,0 +1,28 @@
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 RotateToTargetState : State, IUpdatableState
{
private ReactiveVariable<Vector3> _rotateDirection;
private ReactiveVariable<Entity> _currentTarget;
private Transform _transform;
public RotateToTargetState(Entity entity)
{
_rotateDirection = entity.RotateDirection;
_currentTarget = entity.CurrentTarget;
_transform = entity.Transform;
}
public void Update(float deltaTime)
{
if (_currentTarget.Value != null)
_rotateDirection.Value = (_currentTarget.Value.Transform.position - _transform.position).normalized;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5a16d8f8d923476b9a5544d64f3b469e
timeCreated: 1772473180

View File

@@ -1,6 +1,7 @@
using System; using System;
using _Project.Develop.Runtime.Entities; using _Project.Develop.Runtime.Entities;
using _Project.Develop.Runtime.Logic.Gameplay.Features.AI; using _Project.Develop.Runtime.Logic.Gameplay.Features.AI;
using _Project.Develop.Runtime.Logic.Gameplay.Features.AI.States;
using _Project.Develop.Runtime.Utils.InputManagement; using _Project.Develop.Runtime.Utils.InputManagement;
using Assets._Project.Develop.Runtime.Infrastructure.DI; using Assets._Project.Develop.Runtime.Infrastructure.DI;
using UnityEngine; using UnityEngine;
@@ -29,9 +30,11 @@ namespace _Project.Develop.Runtime.Logic.Gameplay.Features
public void Run() public void Run()
{ {
_hero = _entitiesFactory.CreateTeleportWizard(Vector3.zero); _hero = _entitiesFactory.CreateHero(Vector3.zero);
_ghost = _entitiesFactory.CreateGhost(Vector3.zero + Vector3.forward * 5); _hero.AddCurrentTarget();
_brainsFactory.CreateMainHeroBrain(_hero, new NearestDamageableTargetSelector(_hero));
_ghost = _entitiesFactory.CreateGhost(Vector3.zero + Vector3.forward * 5);
_brainsFactory.CreateGhostBrain(_ghost); _brainsFactory.CreateGhostBrain(_ghost);
_isRunning = true; _isRunning = true;
@@ -49,7 +52,7 @@ namespace _Project.Develop.Runtime.Logic.Gameplay.Features
return; return;
GUI.Label(new Rect(10, 20, 200, 50), $"Health: {_hero.CurrentHealth.Value}/{_hero.MaxHealth.Value}"); GUI.Label(new Rect(10, 20, 200, 50), $"Health: {_hero.CurrentHealth.Value}/{_hero.MaxHealth.Value}");
GUI.Label(new Rect(10, 40, 200, 50), $"Energy: {_hero.CurrentEnergy.Value}/{_hero.MaxEnergy.Value}"); // GUI.Label(new Rect(10, 40, 200, 50), $"Energy: {_hero.CurrentEnergy.Value}/{_hero.MaxEnergy.Value}");
} }
} }
} }

View File

@@ -3,17 +3,24 @@ using System.Collections.Generic;
namespace _Project.Develop.Runtime.Utils.ReactiveManagement namespace _Project.Develop.Runtime.Utils.ReactiveManagement
{ {
public class ReactiveVariable<T> : IReadOnlyVariable<T> where T : IEquatable<T> public class ReactiveVariable<T> : IReadOnlyVariable<T>
{ {
private readonly List<Subscriber<T, T>> _subscribers = new List<Subscriber<T, T>>(); private readonly List<Subscriber<T, T>> _subscribers = new ();
private readonly List<Subscriber<T, T>> _toAddList = new List<Subscriber<T, T>>(); private readonly List<Subscriber<T, T>> _toAddList = new ();
private readonly List<Subscriber<T, T>> _toRemoveList = new List<Subscriber<T, T>>(); private readonly List<Subscriber<T, T>> _toRemoveList = new ();
public ReactiveVariable() => _value = default(T);
public ReactiveVariable(T value) => _value = value;
private T _value; private T _value;
private IEqualityComparer<T> _comparer;
public ReactiveVariable() : this(default) { }
public ReactiveVariable(T value) : this(value, EqualityComparer<T>.Default){ }
public ReactiveVariable(T value, IEqualityComparer<T> comparer)
{
_value = value;
_comparer = comparer;
}
public T Value public T Value
{ {
@@ -23,14 +30,14 @@ namespace _Project.Develop.Runtime.Utils.ReactiveManagement
T oldValue = _value; T oldValue = _value;
_value = value; _value = value;
if (_value.Equals(oldValue) == false) if (_comparer.Equals(oldValue, value) == false)
Invoke(oldValue, _value); Invoke(oldValue, _value);
} }
} }
public IDisposable Subscribe(Action<T, T> action) public IDisposable Subscribe(Action<T, T> action)
{ {
Subscriber<T, T> subscriber = new Subscriber<T, T>(action, RemoveSubscriber); Subscriber<T, T> subscriber = new (action, RemoveSubscriber);
_toAddList.Add(subscriber); _toAddList.Add(subscriber);
return subscriber; return subscriber;

View File

@@ -654,8 +654,8 @@ Camera:
near clip plane: 0.3 near clip plane: 0.3
far clip plane: 1000 far clip plane: 1000
field of view: 60 field of view: 60
orthographic: 0 orthographic: 1
orthographic size: 5 orthographic size: 14.12
m_Depth: -1 m_Depth: -1
m_CullingMask: m_CullingMask:
serializedVersion: 2 serializedVersion: 2
@@ -680,7 +680,7 @@ Transform:
m_GameObject: {fileID: 1496566168} m_GameObject: {fileID: 1496566168}
serializedVersion: 2 serializedVersion: 2
m_LocalRotation: {x: 0.34130225, y: -0, z: -0, w: 0.9399536} m_LocalRotation: {x: 0.34130225, y: -0, z: -0, w: 0.9399536}
m_LocalPosition: {x: 0, y: 11.01, z: -12.32} m_LocalPosition: {x: -0.8, y: 41.4, z: -39.5}
m_LocalScale: {x: 1, y: 1, z: 1} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: [] m_Children: []