mirror of
https://gitlab.com/MaddoScientisto/cirnogodot.git
synced 2026-06-09 04:25:54 +00:00
Enemy AI
This commit is contained in:
parent
383fc740df
commit
ede8f2028a
34 changed files with 1418 additions and 417 deletions
96
Scripts/Components/FSM/Enemy/3D/Alert.cs
Normal file
96
Scripts/Components/FSM/Enemy/3D/Alert.cs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
using Cirno.Scripts.Enums;
|
||||
using Cirno.Scripts.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
|
||||
|
||||
public partial class Alert : EnemyStateBase3D
|
||||
{
|
||||
public override EnemyState StateId => EnemyState.Alert;
|
||||
|
||||
[Export] public EnemyStorage3D Storage { get; private set; }
|
||||
[Export] public PlayerDetection3D PlayerDetection { get; private set; }
|
||||
[Export] public NavigationProvider3D NavigationModule { get; private set; }
|
||||
[Export] public bool DebugEnabled { get; set; } = false;
|
||||
|
||||
private bool _isPlayerInRange = false;
|
||||
|
||||
public override void EnterState()
|
||||
{
|
||||
base.EnterState();
|
||||
// player detection
|
||||
// damage receiver will be a module
|
||||
|
||||
NavigationModule.Init(MainObject);
|
||||
|
||||
PlayerDetection.SetRange(Storage.Root.EnemyResource.PlayerDetectionRange);
|
||||
|
||||
PlayerDetection.PlayerInRange += PlayerDetectionOnPlayerInRange;
|
||||
|
||||
PlayerDetection.PlayerOutOfRange += PlayerDetectionOnPlayerOutOfRange;
|
||||
|
||||
GD.Print("Entered alert");
|
||||
}
|
||||
|
||||
private void PlayerDetectionOnPlayerOutOfRange()
|
||||
{
|
||||
_isPlayerInRange = false;
|
||||
}
|
||||
|
||||
public override void ExitState()
|
||||
{
|
||||
base.ExitState();
|
||||
PlayerDetection.PlayerInRange -= PlayerDetectionOnPlayerInRange;
|
||||
|
||||
PlayerDetection.PlayerOutOfRange -= PlayerDetectionOnPlayerOutOfRange;
|
||||
NavigationModule.SetTarget(null);
|
||||
// DamageReceiver.HealthProvider.ResourceDepleted -= HealthProviderOnResourceDepleted;
|
||||
// DamageReceiver.ChangeState(false);
|
||||
// NavigationModule.SetTarget(null);
|
||||
}
|
||||
|
||||
private void PlayerDetectionOnPlayerInRange()
|
||||
{
|
||||
//_isPlayerInRange = true;
|
||||
}
|
||||
|
||||
public override void PhysicsProcessState(double delta)
|
||||
{
|
||||
base.PhysicsProcessState(delta);
|
||||
if (PlayerDetection.IsPlayerInRange(Storage.Root.EnemyResource.ViewRange) && PlayerDetection.IsPlayerInSight())
|
||||
{
|
||||
StateMachine.SetState(EnemyState.Shooting);
|
||||
return;
|
||||
}
|
||||
|
||||
// if player is outside disengage range, change to idle (later on, search)
|
||||
if (MainObject.GlobalPosition.DistanceTo(GameController.Instance.PlayerPosition.Value) >=
|
||||
Storage.Root.EnemyResource.PlayerDisengageRange)
|
||||
{
|
||||
StateMachine.SetState(EnemyState.Idle);
|
||||
return;
|
||||
}
|
||||
|
||||
// Move towards last known position
|
||||
if (PlayerDetection.LastKnownPlayerPosition.HasValue)
|
||||
{
|
||||
MoveTowardsPosition(PlayerDetection.LastKnownPlayerPosition.Value);
|
||||
}
|
||||
|
||||
NavigationModule.Move(Storage.EnemyData.MovementSpeed);
|
||||
|
||||
//Storage.FacingDirection = MainObject.Velocity.SnapToCardinal().Normalized();
|
||||
Storage.FacingDirection = MainObject.Velocity.ToVector2().Normalized();
|
||||
Storage.AimingDirection = Storage.FacingDirection;
|
||||
}
|
||||
|
||||
private void MoveTowardsPosition(Vector3 position)
|
||||
{
|
||||
NavigationModule.SetTarget(position);
|
||||
}
|
||||
|
||||
public override void ProcessState(double delta)
|
||||
{
|
||||
base.ProcessState(delta);
|
||||
}
|
||||
}
|
||||
1
Scripts/Components/FSM/Enemy/3D/Alert.cs.uid
Normal file
1
Scripts/Components/FSM/Enemy/3D/Alert.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dvtdw2hcp4rm2
|
||||
19
Scripts/Components/FSM/Enemy/3D/Controlled.cs
Normal file
19
Scripts/Components/FSM/Enemy/3D/Controlled.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using Cirno.Scripts.Enums;
|
||||
using Godot;
|
||||
|
||||
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
|
||||
|
||||
public partial class Controlled : EnemyStateBase3D
|
||||
{
|
||||
public override EnemyState StateId => EnemyState.Controlled;
|
||||
|
||||
[Export] public EnemyStorage3D Storage { get; private set; }
|
||||
|
||||
public override void EnterState()
|
||||
{
|
||||
base.EnterState();
|
||||
// player detection
|
||||
// damage receiver will be a module
|
||||
|
||||
}
|
||||
}
|
||||
1
Scripts/Components/FSM/Enemy/3D/Controlled.cs.uid
Normal file
1
Scripts/Components/FSM/Enemy/3D/Controlled.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://mpws3eyrmx0q
|
||||
19
Scripts/Components/FSM/Enemy/3D/Dead.cs
Normal file
19
Scripts/Components/FSM/Enemy/3D/Dead.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using Cirno.Scripts.Enums;
|
||||
using Godot;
|
||||
|
||||
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
|
||||
|
||||
public partial class Dead : EnemyStateBase3D
|
||||
{
|
||||
public override EnemyState StateId => EnemyState.Dead;
|
||||
|
||||
[Export] public EnemyStorage3D Storage { get; private set; }
|
||||
|
||||
public override void EnterState()
|
||||
{
|
||||
base.EnterState();
|
||||
// player detection
|
||||
// damage receiver will be a module
|
||||
|
||||
}
|
||||
}
|
||||
1
Scripts/Components/FSM/Enemy/3D/Dead.cs.uid
Normal file
1
Scripts/Components/FSM/Enemy/3D/Dead.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://3irm5sccr2fc
|
||||
16
Scripts/Components/FSM/Enemy/3D/EnemyFSMAnimatedSprite3D.cs
Normal file
16
Scripts/Components/FSM/Enemy/3D/EnemyFSMAnimatedSprite3D.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
using Godot;
|
||||
|
||||
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
|
||||
|
||||
public partial class EnemyFSMAnimatedSprite3D : AnimatedSprite3D
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
var enemyFsmProxy = this.GetParentOrNull<EnemyProxy3D>();
|
||||
|
||||
if (enemyFsmProxy?.EnemyResource?.AnimationFrames != null)
|
||||
{
|
||||
this.SpriteFrames = enemyFsmProxy.EnemyResource.AnimationFrames;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://de31afbiua8xu
|
||||
78
Scripts/Components/FSM/Enemy/3D/EnemyProxy3D.cs
Normal file
78
Scripts/Components/FSM/Enemy/3D/EnemyProxy3D.cs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
using Cirno.Scripts.Enums;
|
||||
using Cirno.Scripts.Resources;
|
||||
using Cirno.Scripts.Resources.Loot;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
|
||||
|
||||
public partial class EnemyProxy3D : CharacterBody3D, IActivable
|
||||
{
|
||||
[Export] public EnemyStateMachine3D EnemyFSM { get; private set; }
|
||||
|
||||
[Export] public EnemyResource EnemyResource { get; private set; }
|
||||
|
||||
[Export] public Array<LootDrop> ExtraLoot { get; private set; } = [];
|
||||
|
||||
[Export] public bool OverrideLoot { get; set; } = false;
|
||||
|
||||
[Export]
|
||||
public AiState StartingAiState { get; private set; }
|
||||
|
||||
[ExportCategory("Defeat Script")]
|
||||
[Export] public Node DefeatScript { get; set; }
|
||||
|
||||
[Export] public ActivationType ActivationType { get; private set; } = ActivationType.Toggle;
|
||||
|
||||
[Signal] public delegate void DeathEventHandler(EnemyProxy3D enemy);
|
||||
|
||||
public void Init(EnemyResource enemyResource)
|
||||
{
|
||||
this.EnemyResource = enemyResource;
|
||||
|
||||
}
|
||||
|
||||
public void TriggerDeath()
|
||||
{
|
||||
EmitSignalDeath(this);
|
||||
}
|
||||
|
||||
public void RequestAlert(Vector3 destination)
|
||||
{
|
||||
//EnemyFSM.SetState(EnemyState.Alert);
|
||||
}
|
||||
|
||||
public bool Activate(ActivationType activationType = ActivationType.Toggle)
|
||||
{
|
||||
switch (activationType)
|
||||
{
|
||||
case ActivationType.Toggle:
|
||||
EnemyFSM.SetState(EnemyState.Controlled);
|
||||
break;
|
||||
case ActivationType.Enable:
|
||||
// Enable or disable AI
|
||||
EnemyFSM.SetState(EnemyState.Shooting);
|
||||
break;
|
||||
case ActivationType.Disable:
|
||||
EnemyFSM.SetState(EnemyState.Idle);
|
||||
// Enable or disable AI
|
||||
break;
|
||||
case ActivationType.Use:
|
||||
break;
|
||||
case ActivationType.Destroy:
|
||||
EnemyFSM.SetState(EnemyState.Dead);
|
||||
break;
|
||||
case ActivationType.Open:
|
||||
break;
|
||||
case ActivationType.Close:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Toggle()
|
||||
{
|
||||
this.Activate();
|
||||
}
|
||||
}
|
||||
1
Scripts/Components/FSM/Enemy/3D/EnemyProxy3D.cs.uid
Normal file
1
Scripts/Components/FSM/Enemy/3D/EnemyProxy3D.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dwregubt4iila
|
||||
9
Scripts/Components/FSM/Enemy/3D/EnemyStateBase3D.cs
Normal file
9
Scripts/Components/FSM/Enemy/3D/EnemyStateBase3D.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using Cirno.Scripts.Enums;
|
||||
using Godot;
|
||||
|
||||
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
|
||||
|
||||
public abstract partial class EnemyStateBase3D : BaseState<EnemyState, CharacterBody3D>
|
||||
{
|
||||
|
||||
}
|
||||
1
Scripts/Components/FSM/Enemy/3D/EnemyStateBase3D.cs.uid
Normal file
1
Scripts/Components/FSM/Enemy/3D/EnemyStateBase3D.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://62p771loh356
|
||||
10
Scripts/Components/FSM/Enemy/3D/EnemyStateMachine3D.cs
Normal file
10
Scripts/Components/FSM/Enemy/3D/EnemyStateMachine3D.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
using Cirno.Scripts.Enums;
|
||||
using Godot;
|
||||
|
||||
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
|
||||
|
||||
public partial class EnemyStateMachine3D : StateMachineBase<EnemyState, CharacterBody3D>
|
||||
{
|
||||
[Export] public override EnemyState InitialState { get; protected set; } = EnemyState.Init;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://c651imhj6rjsh
|
||||
31
Scripts/Components/FSM/Enemy/3D/EnemyStorage3D.cs
Normal file
31
Scripts/Components/FSM/Enemy/3D/EnemyStorage3D.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cirno.Scripts.Resources;
|
||||
using Cirno.Scripts.Resources.Loot;
|
||||
using Godot;
|
||||
|
||||
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
|
||||
|
||||
public partial class EnemyStorage3D : Node
|
||||
{
|
||||
[Export]
|
||||
public EnemyProxy3D Root { get; private set; }
|
||||
|
||||
public Node3D RootAsNode => Root;
|
||||
|
||||
public EnemyResource EnemyData => Root.EnemyResource;
|
||||
|
||||
public Vector3 HomePosition { get; set; }
|
||||
|
||||
public Vector2 MovementDirection { get; set; }
|
||||
public Vector2 FacingDirection { get; set; }
|
||||
|
||||
public Vector2 AimingDirection { get; set; }
|
||||
public Vector3 KnockbackVelocity { get; set; } = Vector3.Zero;
|
||||
|
||||
public float MovementSpeed => Root.EnemyResource.MovementSpeed;
|
||||
|
||||
public IEnumerable<LootDrop> LootDrops => Root.OverrideLoot ? Root.ExtraLoot : Root.EnemyResource.LootDrops.Concat(Root.ExtraLoot);
|
||||
|
||||
public AiState AiState { get; set; }
|
||||
}
|
||||
1
Scripts/Components/FSM/Enemy/3D/EnemyStorage3D.cs.uid
Normal file
1
Scripts/Components/FSM/Enemy/3D/EnemyStorage3D.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://chq5a73kw0c0m
|
||||
93
Scripts/Components/FSM/Enemy/3D/Idle.cs
Normal file
93
Scripts/Components/FSM/Enemy/3D/Idle.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
using Cirno.Scripts.Enums;
|
||||
using Godot;
|
||||
|
||||
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
|
||||
|
||||
public partial class Idle : EnemyStateBase3D
|
||||
{
|
||||
public override EnemyState StateId => EnemyState.Idle;
|
||||
|
||||
[Export] public EnemyStorage3D Storage { get; private set; }
|
||||
[Export] public PlayerDetection3D PlayerDetection { get; private set; }
|
||||
|
||||
[Export] public bool DebugEnabled { get; set; } = false;
|
||||
|
||||
private bool _isPlayerInRange = false;
|
||||
|
||||
public override void EnterState()
|
||||
{
|
||||
base.EnterState();
|
||||
PlayerDetection.SetRange(Storage.Root.EnemyResource.PlayerDetectionRange);
|
||||
|
||||
_isPlayerInRange = PlayerDetection.IsPlayerInRange(Storage.Root.EnemyResource.ViewRange);
|
||||
|
||||
PlayerDetection.PlayerInRange += PlayerDetectionOnPlayerInRange;
|
||||
|
||||
PlayerDetection.PlayerOutOfRange += PlayerDetectionOnPlayerOutOfRange;
|
||||
|
||||
// player detection
|
||||
// damage receiver will be a module
|
||||
GD.Print("Entered Idle");
|
||||
}
|
||||
|
||||
public override void ExitState()
|
||||
{
|
||||
base.ExitState();
|
||||
|
||||
PlayerDetection.PlayerInRange -= PlayerDetectionOnPlayerInRange;
|
||||
|
||||
PlayerDetection.PlayerOutOfRange -= PlayerDetectionOnPlayerOutOfRange;
|
||||
|
||||
// DamageReceiver.HealthProvider.ResourceDepleted -= HealthProviderOnResourceDepleted;
|
||||
//
|
||||
// DamageReceiver.HealthProvider.ResourceDecreased -= HealthProviderOnResourceDecreased;
|
||||
// DamageReceiver.ChangeState(false);
|
||||
|
||||
}
|
||||
|
||||
private void HealthProviderOnResourceDepleted()
|
||||
{
|
||||
ChangeState(EnemyState.Dead);
|
||||
}
|
||||
|
||||
private void HealthProviderOnResourceDecreased(float oldvalue, float newvalue, float maxvalue)
|
||||
{
|
||||
Storage.AiState = AiState.Enabled;
|
||||
ChangeState(EnemyState.Alert);
|
||||
}
|
||||
|
||||
private void PlayerDetectionOnPlayerInRange()
|
||||
{
|
||||
_isPlayerInRange = true;
|
||||
GD.Print("Player In Range");
|
||||
}
|
||||
|
||||
private void PlayerDetectionOnPlayerOutOfRange()
|
||||
{
|
||||
_isPlayerInRange = false;
|
||||
}
|
||||
|
||||
public override void PhysicsProcessState(double delta)
|
||||
{
|
||||
base.PhysicsProcessState(delta);
|
||||
if (Storage.AiState is AiState.Enabled && _isPlayerInRange)
|
||||
{
|
||||
if (PlayerDetection.IsPlayerInSight())
|
||||
{
|
||||
GD.Print("Moving to alert");
|
||||
StateMachine.SetState(EnemyState.Alert);
|
||||
}
|
||||
}
|
||||
|
||||
if (DebugEnabled)
|
||||
{
|
||||
DebugDraw3D.DrawText(MainObject.GlobalPosition - new Vector3(0,16,0), "Idle");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public override void ProcessState(double delta)
|
||||
{
|
||||
base.ProcessState(delta);
|
||||
}
|
||||
}
|
||||
1
Scripts/Components/FSM/Enemy/3D/Idle.cs.uid
Normal file
1
Scripts/Components/FSM/Enemy/3D/Idle.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://jpdgfn701crh
|
||||
25
Scripts/Components/FSM/Enemy/3D/Init.cs
Normal file
25
Scripts/Components/FSM/Enemy/3D/Init.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using Cirno.Scripts.Enums;
|
||||
using Godot;
|
||||
|
||||
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
|
||||
|
||||
public partial class Init : EnemyStateBase3D
|
||||
{
|
||||
public override EnemyState StateId => EnemyState.Init;
|
||||
[Export] public EnemyStorage3D Storage {get; private set;}
|
||||
|
||||
[Export] public PlayerDetection3D DetectionProvider { get; private set; }
|
||||
|
||||
public override void EnterState()
|
||||
{
|
||||
//DamageReceiver.HealthProvider.MaxResource = StorageModule.Root.EnemyResource.MaxHealth;
|
||||
DetectionProvider.Initialize(MainObject);
|
||||
Storage.AiState = Storage.Root.StartingAiState;
|
||||
|
||||
Storage.HomePosition = MainObject.GlobalPosition;
|
||||
// TODO: Hide wings
|
||||
// TODO: Hide aiming reticule
|
||||
|
||||
StateMachine.SetState(EnemyState.Idle);
|
||||
}
|
||||
}
|
||||
1
Scripts/Components/FSM/Enemy/3D/Init.cs.uid
Normal file
1
Scripts/Components/FSM/Enemy/3D/Init.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cy34e3htvbvnl
|
||||
84
Scripts/Components/FSM/Enemy/3D/NavigationProvider3D.cs
Normal file
84
Scripts/Components/FSM/Enemy/3D/NavigationProvider3D.cs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
using Cirno.Scripts.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
|
||||
|
||||
public partial class NavigationProvider3D : Node
|
||||
{
|
||||
private Vector3? _lastTargetPosition;
|
||||
private CharacterBody3D _characterBody;
|
||||
//private NavigationAgent3D _navigationAgent;
|
||||
|
||||
[Export] public NavigationAgent3D NavigationAgent { get; private set; }
|
||||
|
||||
[Export]
|
||||
public EnemyStorage3D StorageModule { get; private set; }
|
||||
|
||||
public void Init(CharacterBody3D characterBody)
|
||||
{
|
||||
_characterBody = characterBody;
|
||||
|
||||
if (NavigationAgent is not null) return;
|
||||
//_navigationAgent = this.GetNode<NavigationAgent3D>("NavigationAgent");
|
||||
}
|
||||
|
||||
public void SetTarget(Vector3? target)
|
||||
{
|
||||
_lastTargetPosition = target;
|
||||
}
|
||||
|
||||
public void Move(float movementSpeed)
|
||||
{
|
||||
if (!_lastTargetPosition.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NavigationAgent.SetTargetPosition(_lastTargetPosition.Value);
|
||||
|
||||
var currentAgentPosition = _characterBody.GlobalPosition;
|
||||
|
||||
if (NavigationAgent.IsNavigationFinished())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var nextPathPosition = NavigationAgent.GetNextPathPosition();
|
||||
|
||||
var newVelocity = currentAgentPosition.DirectionTo(nextPathPosition) * movementSpeed;
|
||||
|
||||
newVelocity += StorageModule.KnockbackVelocity;
|
||||
|
||||
if (NavigationAgent.AvoidanceEnabled)
|
||||
{
|
||||
NavigationAgent.SetVelocity(newVelocity);
|
||||
}
|
||||
else
|
||||
{
|
||||
_on_navigation_agent_3d_velocity_computed(newVelocity);
|
||||
}
|
||||
|
||||
_characterBody.MoveAndSlide();
|
||||
}
|
||||
|
||||
public void _on_navigation_agent_3d_velocity_computed(Vector3 safeVelocity)
|
||||
{
|
||||
if (_characterBody is null) return;
|
||||
_characterBody.Velocity = safeVelocity;
|
||||
if (_characterBody.Velocity.Length() > 0)
|
||||
{
|
||||
StorageModule.FacingDirection = _characterBody.Velocity.Normalized().ToVector2();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsNavigable(Vector3 newPos)
|
||||
{
|
||||
NavigationAgent.SetTargetPosition(newPos);
|
||||
return NavigationAgent.IsTargetReachable();
|
||||
}
|
||||
|
||||
public bool IsNavigationFinished()
|
||||
{
|
||||
return NavigationAgent.IsNavigationFinished();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://k5k8wf821ytg
|
||||
120
Scripts/Components/FSM/Enemy/3D/PlayerDetection3D.cs
Normal file
120
Scripts/Components/FSM/Enemy/3D/PlayerDetection3D.cs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
using Cirno.Scripts.Components.FSM._3DPlayer;
|
||||
using Godot;
|
||||
|
||||
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
|
||||
|
||||
public partial class PlayerDetection3D : Area3D
|
||||
{
|
||||
[Signal]
|
||||
public delegate void PlayerInRangeEventHandler();
|
||||
|
||||
[Signal]
|
||||
public delegate void PlayerOutOfRangeEventHandler();
|
||||
|
||||
[Export(PropertyHint.Layers3DPhysics)]
|
||||
public uint ObstaclesCollisionMask { get; private set; }
|
||||
|
||||
public Vector3? LastKnownPlayerPosition { get; set; }
|
||||
public Vector3? LastKnowPlayerVelocity { get; set; }
|
||||
|
||||
//public bool PlayerInActiveArea { get; private set; }
|
||||
private CollisionShape3D _collisionShape;
|
||||
|
||||
private bool _initialized = false;
|
||||
// public override void _Ready()
|
||||
// {
|
||||
// CallDeferred(MethodName.Initialize);
|
||||
// }
|
||||
|
||||
private CharacterBody3D _mainBody;
|
||||
|
||||
public void Initialize(CharacterBody3D mainBody)
|
||||
{
|
||||
_mainBody = mainBody;
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
public void SetRange(float range)
|
||||
{
|
||||
_collisionShape ??= this.GetNode<CollisionShape3D>("CollisionShape3D");
|
||||
if (_collisionShape.Shape is CylinderShape3D shape3D)
|
||||
{
|
||||
shape3D.Radius = range;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsPlayerInRange(float range)
|
||||
{
|
||||
if (!_initialized) return false;
|
||||
if (GameController.Instance is null) return false;
|
||||
|
||||
if (!GameController.Instance.PlayerPosition.HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.GlobalPosition.DistanceTo(GameController.Instance.PlayerPosition.Value) <= range;
|
||||
}
|
||||
|
||||
public bool IsPlayerInSight()
|
||||
{
|
||||
if (!_initialized) return false;
|
||||
if (GameController.Instance is null) return false;
|
||||
//if (_cachedPlayer == null) return false;
|
||||
if (!GameController.Instance.PlayerPosition.HasValue) return false;
|
||||
|
||||
var found = HasLineOfSight(this.GlobalPosition, GameController.Instance.PlayerPosition.Value);
|
||||
|
||||
// var spaceState = GetWorld2D().DirectSpaceState;
|
||||
//
|
||||
// // It needs to use its own collision mask because it's detecting obstacles rather than the player
|
||||
// var query = PhysicsRayQueryParameters2D.Create(this.GlobalPosition, GameController.Instance.PlayerPosition.Value, ObstaclesCollisionMask,
|
||||
// [GetRid()]);
|
||||
// //query.CollideWithBodies = true;
|
||||
// //query.CollideWithAreas = true;
|
||||
// // var query = PhysicsRayQueryParameters2D.Create(this.GlobalPosition, _cachedPlayer.GlobalPosition, CollisionMask, new Array<Rid> { GetRid() });
|
||||
// var result = spaceState.IntersectRay(query);
|
||||
//
|
||||
// // If count is 0 then the player is in sight, otherwise there is level geometry in the way
|
||||
// var found = result.Count == 0;
|
||||
if (found)
|
||||
{
|
||||
LastKnownPlayerPosition = GameController.Instance.PlayerPosition;
|
||||
LastKnowPlayerVelocity = GameController.Instance.PlayerVelocity;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
private void _on_body_entered(Node3D area)
|
||||
{
|
||||
if (area is not IsoPlayerFSMProxy player) return;
|
||||
EmitSignal(SignalName.PlayerInRange);
|
||||
//PlayerInActiveArea = true;
|
||||
}
|
||||
|
||||
private void _on_body_exited(Node3D area)
|
||||
{
|
||||
if (area is not IsoPlayerFSMProxy player) return;
|
||||
EmitSignal(SignalName.PlayerOutOfRange);
|
||||
//PlayerInActiveArea = false;
|
||||
}
|
||||
|
||||
public bool HasLineOfSight(Vector3 startPos, Vector3 endPos)
|
||||
{
|
||||
var spaceState = GetWorld3D().DirectSpaceState;
|
||||
|
||||
// It needs to use its own collision mask because it's detecting obstacles rather than the player
|
||||
var query = PhysicsRayQueryParameters3D.Create(startPos, endPos, ObstaclesCollisionMask,
|
||||
[GetRid(), _mainBody.GetRid()]);
|
||||
query.CollideWithBodies = true;
|
||||
|
||||
//query.CollideWithAreas = true;
|
||||
// var query = PhysicsRayQueryParameters2D.Create(this.GlobalPosition, _cachedPlayer.GlobalPosition, CollisionMask, new Array<Rid> { GetRid() });
|
||||
var result = spaceState.IntersectRay(query);
|
||||
|
||||
// If count is 0 then the player is in sight, otherwise there is level geometry in the way
|
||||
var found = result.Count == 0;
|
||||
|
||||
return found;
|
||||
}
|
||||
}
|
||||
1
Scripts/Components/FSM/Enemy/3D/PlayerDetection3D.cs.uid
Normal file
1
Scripts/Components/FSM/Enemy/3D/PlayerDetection3D.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://extjdng8nk6r
|
||||
171
Scripts/Components/FSM/Enemy/3D/Shooting.cs
Normal file
171
Scripts/Components/FSM/Enemy/3D/Shooting.cs
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
using Cirno.Scripts.Enums;
|
||||
using Cirno.Scripts.Utils;
|
||||
using Cirno.Scripts.Weapons;
|
||||
using Godot;
|
||||
|
||||
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
|
||||
|
||||
public partial class Shooting : EnemyStateBase3D
|
||||
{
|
||||
public override EnemyState StateId => EnemyState.Shooting;
|
||||
|
||||
[Export] public EnemyStorage3D Storage { get; private set; }
|
||||
|
||||
[Export] public PlayerDetection3D PlayerDetection { get; private set; }
|
||||
|
||||
[Export] public Weapon3D EquippedWeapon;
|
||||
[Export] public NavigationProvider3D NavigationModule { get; private set; }
|
||||
|
||||
private bool _isPlayerInRange = false;
|
||||
private Vector3? _currentStrafeTarget = null;
|
||||
private float _strafeSpeed => Storage.EnemyData.StrafeSpeed;
|
||||
|
||||
private double _responseTimer = 0;
|
||||
public override void EnterState()
|
||||
{
|
||||
base.EnterState();
|
||||
|
||||
GD.Print("Entering Shooting");
|
||||
|
||||
PlayerDetection.PlayerOutOfRange += PlayerDetectionOnPlayerOutOfRange;
|
||||
|
||||
//DamageReceiver.ChangeState(true);
|
||||
|
||||
//DamageReceiver.HealthProvider.ResourceDepleted += HealthProviderOnResourceDepleted;
|
||||
|
||||
EquippedWeapon.WeaponData = Storage.Root.EnemyResource.Weapon;
|
||||
|
||||
_currentStrafeTarget = null;
|
||||
|
||||
_responseTimer = 0;
|
||||
|
||||
}
|
||||
|
||||
private void PlayerDetectionOnPlayerOutOfRange()
|
||||
{
|
||||
GD.Print("Player out of range, returning to alert");
|
||||
StateMachine.SetState(EnemyState.Alert);
|
||||
}
|
||||
|
||||
public override void ExitState()
|
||||
{
|
||||
base.ExitState();
|
||||
|
||||
PlayerDetection.PlayerOutOfRange -= PlayerDetectionOnPlayerOutOfRange;
|
||||
|
||||
//DamageReceiver.HealthProvider.ResourceDepleted -= HealthProviderOnResourceDepleted;
|
||||
|
||||
_currentStrafeTarget = null;
|
||||
|
||||
//DamageReceiver.ChangeState(false);
|
||||
|
||||
}
|
||||
|
||||
public override void PhysicsProcessState(double delta)
|
||||
{
|
||||
base.PhysicsProcessState(delta);
|
||||
|
||||
if (PlayerDetection.IsPlayerInRange(Storage.Root.EnemyResource.ViewRange) && PlayerDetection.IsPlayerInSight())
|
||||
{
|
||||
// SHOOT
|
||||
Shoot();
|
||||
|
||||
if (_strafeSpeed > 0)
|
||||
{
|
||||
// Check if a strafe position is needed
|
||||
if (!_currentStrafeTarget.HasValue || NavigationModule.IsNavigationFinished())
|
||||
{
|
||||
if (_responseTimer < Storage.EnemyData.ResponseTime)
|
||||
{
|
||||
_responseTimer += delta;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentStrafeTarget = CalculateStrafePosition();
|
||||
_responseTimer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GD.Print("Back to alert because the player could not be detected anymore");
|
||||
StateMachine.SetState(EnemyState.Alert);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_currentStrafeTarget.HasValue)
|
||||
{
|
||||
NavigationModule.SetTarget(_currentStrafeTarget.Value);
|
||||
NavigationModule.Move(_strafeSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3? CalculateStrafePosition()
|
||||
{
|
||||
var playerPos = PlayerDetection.LastKnownPlayerPosition.Value;
|
||||
var enemyPos = MainObject.GlobalPosition;
|
||||
|
||||
// Calculate direction to player
|
||||
var directionToPlayer = (playerPos - enemyPos).Normalized();
|
||||
|
||||
// Get perpendicular vectors (left and right strafing directions)
|
||||
var leftStrafe = directionToPlayer.Rotated(Vector3.Up, -Mathf.Pi / 2);
|
||||
var rightStrafe = directionToPlayer.Rotated(Vector3.Up,Mathf.Pi / 2);
|
||||
|
||||
// Randomly decide left or right first
|
||||
bool tryLeftFirst = GD.Randf() < 0.5f;
|
||||
|
||||
for (float factor = 1f; factor > 0.25f; factor *= 0.75f)
|
||||
{
|
||||
float strafeDistance = Mathf.Lerp(Storage.EnemyData.MinStrafeDistance, Storage.EnemyData.MaxStrafeDistance, factor);
|
||||
var newPos = enemyPos + (tryLeftFirst ? leftStrafe : rightStrafe) * strafeDistance;
|
||||
if (NavigationModule.IsNavigable(newPos) && PlayerDetection.HasLineOfSight(newPos, playerPos))
|
||||
{
|
||||
return newPos;
|
||||
}
|
||||
}
|
||||
|
||||
for (float factor = 1f; factor > 0.25f; factor *= 0.75f)
|
||||
{
|
||||
float strafeDistance = Mathf.Lerp(Storage.EnemyData.MinStrafeDistance, Storage.EnemyData.MaxStrafeDistance, factor);
|
||||
var newPos = enemyPos + (tryLeftFirst ? rightStrafe : leftStrafe) * strafeDistance;
|
||||
if (NavigationModule.IsNavigable(newPos) && PlayerDetection.HasLineOfSight(newPos, playerPos))
|
||||
{
|
||||
return newPos;
|
||||
}
|
||||
}
|
||||
|
||||
return null; // No valid strafe position found
|
||||
}
|
||||
|
||||
private Vector2 GetShootDirection()
|
||||
{
|
||||
// if (Storage.EnemyData.PredictPlayer && PlayerDetection.LastKnowPlayerVelocity.HasValue)
|
||||
// {
|
||||
// var predictedDirection = MathFunctions.PredictInterceptPosition(MainObject.GlobalPosition,
|
||||
// PlayerDetection.LastKnownPlayerPosition.Value, PlayerDetection.LastKnowPlayerVelocity.Value,
|
||||
// EquippedWeapon.WeaponData.BulletData.BulletSpeed);
|
||||
// if (predictedDirection.HasValue) return (predictedDirection.Value - MainObject.GlobalPosition).Normalized();
|
||||
//
|
||||
// }
|
||||
|
||||
return ( PlayerDetection.LastKnownPlayerPosition.Value - MainObject.GlobalPosition).ToVector2().Normalized();
|
||||
}
|
||||
|
||||
private void Shoot()
|
||||
{
|
||||
if (EquippedWeapon == null) return;
|
||||
if (!PlayerDetection.LastKnownPlayerPosition.HasValue) return;
|
||||
|
||||
var direction = GetShootDirection();
|
||||
|
||||
// Shoot at the player's last known position
|
||||
EquippedWeapon.ShootDirection = direction;
|
||||
//StorageModule.FacingDirection = direction;
|
||||
Storage.FacingDirection = direction.SnapToCardinal().Normalized();
|
||||
Storage.AimingDirection = Storage.FacingDirection;
|
||||
|
||||
EquippedWeapon.Shoot();
|
||||
}
|
||||
}
|
||||
1
Scripts/Components/FSM/Enemy/3D/Shooting.cs.uid
Normal file
1
Scripts/Components/FSM/Enemy/3D/Shooting.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://crahxykgis2bp
|
||||
|
|
@ -8,39 +8,35 @@ public partial class Alert : EnemyStateBase
|
|||
{
|
||||
public override EnemyState StateId => EnemyState.Alert;
|
||||
|
||||
[Export]
|
||||
public EnemyStorageModule StorageModule { get; private set; }
|
||||
|
||||
[Export]
|
||||
public PlayerDetectionModule PlayerDetection { get; private set; }
|
||||
|
||||
[Export]
|
||||
public GenericDamageReceiver DamageReceiver { get; private set; }
|
||||
|
||||
[Export]
|
||||
public NavigationMovementModule NavigationModule { get; private set; }
|
||||
|
||||
[Export] public EnemyStorageModule StorageModule { get; private set; }
|
||||
|
||||
[Export] public PlayerDetectionModule PlayerDetection { get; private set; }
|
||||
|
||||
[Export] public GenericDamageReceiver DamageReceiver { get; private set; }
|
||||
|
||||
[Export] public NavigationMovementModule NavigationModule { get; private set; }
|
||||
|
||||
private bool _isPlayerInRange = false;
|
||||
|
||||
|
||||
public override void EnterState()
|
||||
{
|
||||
base.EnterState();
|
||||
NavigationModule.Init(MainObject);
|
||||
|
||||
|
||||
PlayerDetection.SetRange(StorageModule.Root.EnemyResource.PlayerDetectionRange);
|
||||
|
||||
|
||||
//_isPlayerInRange = PlayerDetection.IsPlayerInRange(StorageModule.Root.EnemyResource.ViewRange);
|
||||
//GD.Print($"Player In Range: {_isPlayerInRange}");
|
||||
|
||||
|
||||
PlayerDetection.PlayerInRange += PlayerDetectionOnPlayerInRange;
|
||||
|
||||
|
||||
PlayerDetection.PlayerOutOfRange += PlayerDetectionOnPlayerOutOfRange;
|
||||
|
||||
DamageReceiver.ChangeState(true);
|
||||
|
||||
|
||||
DamageReceiver.HealthProvider.ResourceDepleted += HealthProviderOnResourceDepleted;
|
||||
}
|
||||
|
||||
|
||||
private void HealthProviderOnResourceDepleted()
|
||||
{
|
||||
ChangeState(EnemyState.Dead);
|
||||
|
|
@ -55,12 +51,12 @@ public partial class Alert : EnemyStateBase
|
|||
{
|
||||
base.ExitState();
|
||||
PlayerDetection.PlayerInRange -= PlayerDetectionOnPlayerInRange;
|
||||
|
||||
|
||||
PlayerDetection.PlayerOutOfRange -= PlayerDetectionOnPlayerOutOfRange;
|
||||
|
||||
|
||||
DamageReceiver.HealthProvider.ResourceDepleted -= HealthProviderOnResourceDepleted;
|
||||
DamageReceiver.ChangeState(false);
|
||||
NavigationModule.SetTarget(null);
|
||||
NavigationModule.SetTarget(null);
|
||||
}
|
||||
|
||||
private void PlayerDetectionOnPlayerInRange()
|
||||
|
|
@ -71,12 +67,13 @@ public partial class Alert : EnemyStateBase
|
|||
public override void PhysicsProcessState(double delta)
|
||||
{
|
||||
base.PhysicsProcessState(delta);
|
||||
if (PlayerDetection.IsPlayerInRange(StorageModule.Root.EnemyResource.ViewRange) && PlayerDetection.IsPlayerInSight())
|
||||
if (PlayerDetection.IsPlayerInRange(StorageModule.Root.EnemyResource.ViewRange) &&
|
||||
PlayerDetection.IsPlayerInSight())
|
||||
{
|
||||
StateMachine.SetState(EnemyState.Shooting);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// if player is outside disengage range, change to idle (later on, search)
|
||||
if (MainObject.GlobalPosition.DistanceTo(GameManager.Instance.PlayerPosition.Value) >=
|
||||
StorageModule.Root.EnemyResource.PlayerDisengageRange)
|
||||
|
|
@ -84,13 +81,13 @@ public partial class Alert : EnemyStateBase
|
|||
StateMachine.SetState(EnemyState.Idle);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Move towards last known position
|
||||
if (PlayerDetection.LastKnownPlayerPosition.HasValue)
|
||||
{
|
||||
MoveTowardsPosition(PlayerDetection.LastKnownPlayerPosition.Value);
|
||||
}
|
||||
|
||||
|
||||
NavigationModule.Move(StorageModule.EnemyData.MovementSpeed);
|
||||
|
||||
StorageModule.FacingDirection = MainObject.Velocity.SnapToCardinal().Normalized();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue