多轮审查和修复
This commit is contained in:
27
Assets/Scripts/Enemies/AI/BD_CanAttack.cs
Normal file
27
Assets/Scripts/Enemies/AI/BD_CanAttack.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Conditional:攻击冷却是否完毕(可以发动攻击)。
|
||||
/// </summary>
|
||||
public class BD_CanAttack : Conditional
|
||||
{
|
||||
private EnemyBase _enemy;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_enemy = GetComponent<EnemyBase>();
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null) return TaskStatus.Failure;
|
||||
return _enemy.CanAttack() ? TaskStatus.Success : TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_CanAttack.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_CanAttack.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 795ff4e1aa5e05b4b86fe8604822e2c6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
31
Assets/Scripts/Enemies/AI/BD_EnterPhase.cs
Normal file
31
Assets/Scripts/Enemies/AI/BD_EnterPhase.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Action:Boss 切换阶段(Phase)。
|
||||
/// 调用 BossBase.EnterPhase(PhaseIndex),单帧完成,返回 Success。
|
||||
/// </summary>
|
||||
public class BD_EnterPhase : Action
|
||||
{
|
||||
[UnityEngine.SerializeField] private int m_PhaseIndex = 1;
|
||||
|
||||
private BossBase _boss;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_boss = GetComponent<BossBase>();
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_boss == null) return TaskStatus.Failure;
|
||||
_boss.EnterPhase(m_PhaseIndex);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_EnterPhase.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_EnterPhase.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b532ff86847a744c801b1b6c4bae0c5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
32
Assets/Scripts/Enemies/AI/BD_FaceTarget.cs
Normal file
32
Assets/Scripts/Enemies/AI/BD_FaceTarget.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using UnityEngine;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Action:立即朝向玩家,单帧完成,返回 Success。
|
||||
/// </summary>
|
||||
public class BD_FaceTarget : Action
|
||||
{
|
||||
private EnemyBase _enemy;
|
||||
private Transform _player;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_enemy = GetComponent<EnemyBase>();
|
||||
var go = GameObject.FindWithTag("Player");
|
||||
_player = go != null ? go.transform : null;
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null || _player == null) return TaskStatus.Failure;
|
||||
_enemy.FacePlayer();
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_FaceTarget.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_FaceTarget.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ed2d3259efee514b8e17bc22ca021e3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
27
Assets/Scripts/Enemies/AI/BD_IsGrounded.cs
Normal file
27
Assets/Scripts/Enemies/AI/BD_IsGrounded.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Conditional:敌人是否处于地面。
|
||||
/// </summary>
|
||||
public class BD_IsGrounded : Conditional
|
||||
{
|
||||
private EnemyBase _enemy;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_enemy = GetComponent<EnemyBase>();
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null || _enemy.Movement == null) return TaskStatus.Failure;
|
||||
return _enemy.Movement.IsGrounded ? TaskStatus.Success : TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_IsGrounded.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_IsGrounded.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f3047372de5d51a4f98aee8505490bf1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
32
Assets/Scripts/Enemies/AI/BD_IsHPBelow.cs
Normal file
32
Assets/Scripts/Enemies/AI/BD_IsHPBelow.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Conditional:Boss HP 是否低于阈值(用于触发阶段切换)。
|
||||
/// HPThreshold 为 0~1 归一化比例(如 0.5 = HP ≤ 50%)。
|
||||
/// </summary>
|
||||
public class BD_IsHPBelow : Conditional
|
||||
{
|
||||
[UnityEngine.SerializeField] private float m_HPThreshold = 0.5f;
|
||||
|
||||
private EnemyBase _enemy;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_enemy = GetComponent<EnemyBase>();
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null || _enemy.Stats == null) return TaskStatus.Failure;
|
||||
|
||||
float ratio = (float)_enemy.Stats.CurrentHP / _enemy.Stats.MaxHP;
|
||||
return ratio <= m_HPThreshold ? TaskStatus.Success : TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_IsHPBelow.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_IsHPBelow.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f1b683b9405d60c4daacd09719b186b5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
28
Assets/Scripts/Enemies/AI/BD_IsNearEdge.cs
Normal file
28
Assets/Scripts/Enemies/AI/BD_IsNearEdge.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Conditional:敌人是否靠近平台边缘。
|
||||
/// 通过 EnemyNavAgent.IsNearEdge() 检测(双射线检测脚下/前方地面)。
|
||||
/// </summary>
|
||||
public class BD_IsNearEdge : Conditional
|
||||
{
|
||||
private EnemyBase _enemy;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_enemy = GetComponent<EnemyBase>();
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null || _enemy.Nav == null) return TaskStatus.Failure;
|
||||
return _enemy.Nav.IsNearEdge() ? TaskStatus.Success : TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_IsNearEdge.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_IsNearEdge.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 008a71992a8cb644a94c331e06cd3955
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
28
Assets/Scripts/Enemies/AI/BD_IsPlayerVisible.cs
Normal file
28
Assets/Scripts/Enemies/AI/BD_IsPlayerVisible.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Conditional:玩家是否可见(LOS 检测)。
|
||||
/// 读取 EnemyBase._losResult 字段(由 BatchLOSSystem 每帧写入,或降级 3 帧节流 Raycast)。
|
||||
/// </summary>
|
||||
public class BD_IsPlayerVisible : Conditional
|
||||
{
|
||||
private EnemyBase _enemy;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_enemy = GetComponent<EnemyBase>();
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null) return TaskStatus.Failure;
|
||||
return _enemy.IsPlayerVisible() ? TaskStatus.Success : TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_IsPlayerVisible.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_IsPlayerVisible.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0f8a58f1dbebf644999205d1a2febdc9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
43
Assets/Scripts/Enemies/AI/BD_IsStateMatch.cs
Normal file
43
Assets/Scripts/Enemies/AI/BD_IsStateMatch.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using UnityEngine;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Conditional:敌人当前 EnemyStateType 是否与目标状态名称匹配。
|
||||
/// TargetStateName 直接输入枚举名称字符串(Controlled / Hurt / Stagger / Dead),
|
||||
/// 枚举值顺序变化时 BD 图不会静默失效。
|
||||
/// </summary>
|
||||
public class BD_IsStateMatch : Conditional
|
||||
{
|
||||
/// <summary>目标状态名称(Controlled / Hurt / Stagger / Dead)。</summary>
|
||||
[SerializeField] private string m_TargetStateName = "Controlled";
|
||||
|
||||
private EnemyBase _enemy;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_enemy = GetComponent<EnemyBase>();
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null) return TaskStatus.Failure;
|
||||
|
||||
if (!System.Enum.TryParse<EnemyStateType>(m_TargetStateName, out var target))
|
||||
{
|
||||
Debug.LogError($"[BD_IsStateMatch] 未知状态名: '{m_TargetStateName}'," +
|
||||
"有效值为 Controlled / Hurt / Stagger / Dead", gameObject);
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
return _enemy.CurrentState == target
|
||||
? TaskStatus.Success
|
||||
: TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_IsStateMatch.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_IsStateMatch.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 058372e0cdf7c5b40b53838cf89b8daf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
45
Assets/Scripts/Enemies/AI/BD_JumpTo.cs
Normal file
45
Assets/Scripts/Enemies/AI/BD_JumpTo.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using UnityEngine;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Action:跳跃至目标坐标(抛物线跳跃)。
|
||||
/// 调用 EnemyBase.JumpTo,待落地(IsGrounded)后返回 Success。
|
||||
/// </summary>
|
||||
public class BD_JumpTo : Action
|
||||
{
|
||||
[SerializeField] private Vector2 m_Target;
|
||||
|
||||
private EnemyBase _enemy;
|
||||
private bool _jumped;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_enemy = GetComponent<EnemyBase>();
|
||||
_jumped = false;
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null) return TaskStatus.Failure;
|
||||
|
||||
if (!_jumped)
|
||||
{
|
||||
_enemy.JumpTo(m_Target);
|
||||
_jumped = true;
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
// 等待落地
|
||||
if (_enemy.Movement != null && _enemy.Movement.IsGrounded)
|
||||
return TaskStatus.Success;
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_JumpTo.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_JumpTo.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7907401e39b5fdf409449af475640396
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
42
Assets/Scripts/Enemies/AI/BD_MoveTo.cs
Normal file
42
Assets/Scripts/Enemies/AI/BD_MoveTo.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using UnityEngine;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Action:移动到指定世界坐标 Target。
|
||||
/// 到达目标(IsAtDestination)后返回 Success。
|
||||
/// </summary>
|
||||
public class BD_MoveTo : Action
|
||||
{
|
||||
[SerializeField] private Vector2 m_Target;
|
||||
|
||||
private EnemyBase _enemy;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_enemy = GetComponent<EnemyBase>();
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null) return TaskStatus.Failure;
|
||||
|
||||
_enemy.MoveTo(m_Target);
|
||||
|
||||
if (_enemy.Nav != null && _enemy.Nav.IsAtDestination())
|
||||
return TaskStatus.Success;
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
public override void OnEnd()
|
||||
{
|
||||
_enemy?.StopMovement();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_MoveTo.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_MoveTo.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 460aa03baecb5b547980f7bab7cfee22
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,5 +1,4 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using UnityEngine;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||
using BaseGames.Enemies;
|
||||
@@ -8,26 +7,24 @@ namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Action:移向玩家。
|
||||
/// OnStart 缓存玩家 Transform,OnUpdate 每帧更新导航目标。
|
||||
/// OnStart 从 EnemyBase.PlayerTransform 获取玩家引用(由 _onPlayerSpawned 事件缓存,
|
||||
/// 避免 FindWithTag 全场景扫描)。OnUpdate 每帧更新导航目标。
|
||||
/// </summary>
|
||||
public class BD_MoveToPlayer : Action
|
||||
{
|
||||
private EnemyBase _enemy;
|
||||
private Transform _playerTransform;
|
||||
private EnemyBase _enemy;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_enemy = GetComponent<EnemyBase>();
|
||||
var playerGO = GameObject.FindWithTag("Player");
|
||||
if (playerGO != null) _playerTransform = playerGO.transform;
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null) return TaskStatus.Failure;
|
||||
if (_playerTransform == null) return TaskStatus.Failure;
|
||||
if (_enemy == null) return TaskStatus.Failure;
|
||||
if (_enemy.PlayerTransform == null) return TaskStatus.Failure;
|
||||
|
||||
_enemy.MoveTo(_playerTransform.position);
|
||||
_enemy.MoveTo(_enemy.PlayerTransform.position);
|
||||
_enemy.FacePlayer();
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
@@ -2,16 +2,25 @@
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||
using BaseGames.Enemies;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Action:敌人巡逻行为(Phase 1 简单实现)。
|
||||
/// 挂在 BT 节点上,持续令敌人向当前朝向移动。
|
||||
/// BD Action:敌人巡逻行为。
|
||||
/// 持续令敌人向当前朝向移动,遇墙/边缘时自动转向。
|
||||
/// </summary>
|
||||
public class BD_Patrol : Action
|
||||
{
|
||||
[Tooltip("检测地面边缘的向下射线长度")]
|
||||
public float edgeCheckLength = 1.2f;
|
||||
[Tooltip("检测障碍物的水平射线长度")]
|
||||
public float wallCheckLength = 0.4f;
|
||||
[Tooltip("地面/墙壁 LayerMask")]
|
||||
public LayerMask groundLayer;
|
||||
|
||||
private EnemyBase _enemy;
|
||||
private float _dir = 1f;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
@@ -21,8 +30,11 @@ namespace BaseGames.Enemies.AI
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null) return TaskStatus.Failure;
|
||||
// Phase 1:固定向右巡逻;Phase 2 改为往返检测
|
||||
_enemy.MoveInDirection(1f);
|
||||
|
||||
if (ShouldFlip())
|
||||
_dir = -_dir;
|
||||
|
||||
_enemy.MoveInDirection(_dir);
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
@@ -30,6 +42,23 @@ namespace BaseGames.Enemies.AI
|
||||
{
|
||||
_enemy?.StopMovement();
|
||||
}
|
||||
|
||||
private bool ShouldFlip()
|
||||
{
|
||||
Transform t = _enemy.transform;
|
||||
Vector2 pos = t.position;
|
||||
|
||||
// 前方边缘检测:在脚前方向下射线,若无地面则转向
|
||||
Vector2 edgeOrigin = pos + Vector2.right * (_dir * 0.3f) + Vector2.down * 0.1f;
|
||||
bool hasGround = Physics2D.Raycast(edgeOrigin, Vector2.down, edgeCheckLength, groundLayer);
|
||||
if (!hasGround) return true;
|
||||
|
||||
// 前方障碍检测:水平射线,若撞墙则转向
|
||||
bool hitWall = Physics2D.Raycast(pos, Vector2.right * _dir, wallCheckLength, groundLayer);
|
||||
if (hitWall) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
37
Assets/Scripts/Enemies/AI/BD_PlayAnimation.cs
Normal file
37
Assets/Scripts/Enemies/AI/BD_PlayAnimation.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using UnityEngine;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Action:通过 AnimationClip 名称播放 Animancer 动画,立即返回 Success。
|
||||
/// ClipName 需与 EnemyAnimationConfigSO 中字段名一致。
|
||||
/// </summary>
|
||||
public class BD_PlayAnimation : Action
|
||||
{
|
||||
/// <summary>EnemyAnimationConfigSO 中的 AnimationClip 字段名(如 "Attack_Melee")。</summary>
|
||||
[SerializeField] private string m_ClipName = "Idle";
|
||||
|
||||
private EnemyBase _enemy;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_enemy = GetComponent<EnemyBase>();
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null || _enemy.AnimConfig == null) return TaskStatus.Failure;
|
||||
|
||||
var clip = _enemy.AnimConfig.GetClipByName(m_ClipName);
|
||||
if (clip != null)
|
||||
_enemy.Animancer?.Play(clip);
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_PlayAnimation.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_PlayAnimation.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3247bf3e6a748dc408bbf0d6ac7e1680
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
29
Assets/Scripts/Enemies/AI/BD_SetAlert.cs
Normal file
29
Assets/Scripts/Enemies/AI/BD_SetAlert.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Action:设置警觉状态,并通知 EnemyBase 调整 BT Tick 频率。
|
||||
/// </summary>
|
||||
public class BD_SetAlert : Action
|
||||
{
|
||||
private EnemyBase _enemy;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_enemy = GetComponent<EnemyBase>();
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null) return TaskStatus.Failure;
|
||||
|
||||
_enemy.SetAggroTickRate(true);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_SetAlert.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_SetAlert.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0f8deae75c691904192374ccab313a2e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
39
Assets/Scripts/Enemies/AI/BD_SpawnProjectile.cs
Normal file
39
Assets/Scripts/Enemies/AI/BD_SpawnProjectile.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using UnityEngine;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Pool;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Action:在敌人当前位置生成弹射物。
|
||||
/// ProjectileKey 为 Addressable 键,通过 GlobalObjectPool 实例化。
|
||||
/// </summary>
|
||||
public class BD_SpawnProjectile : Action
|
||||
{
|
||||
[SerializeField] private Vector2 m_Direction = Vector2.right;
|
||||
[SerializeField] private string m_ProjectileKey = "";
|
||||
[SerializeField] private float m_Speed = 8f;
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (string.IsNullOrEmpty(m_ProjectileKey)) return TaskStatus.Failure;
|
||||
|
||||
var pool = ServiceLocator.GetOrDefault<IObjectPoolService>();
|
||||
if (pool == null) return TaskStatus.Failure;
|
||||
|
||||
var go = pool.Spawn(m_ProjectileKey, transform.position, Quaternion.identity);
|
||||
if (go != null)
|
||||
{
|
||||
var rb = go.GetComponent<Rigidbody2D>();
|
||||
if (rb != null)
|
||||
rb.velocity = m_Direction.normalized * m_Speed;
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_SpawnProjectile.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_SpawnProjectile.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85547509455668b43971ceccfd36d609
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
28
Assets/Scripts/Enemies/AI/BD_StopMovement.cs
Normal file
28
Assets/Scripts/Enemies/AI/BD_StopMovement.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Action:立即停止移动,单帧完成,返回 Success。
|
||||
/// </summary>
|
||||
public class BD_StopMovement : Action
|
||||
{
|
||||
private EnemyBase _enemy;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_enemy = GetComponent<EnemyBase>();
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null) return TaskStatus.Failure;
|
||||
_enemy.StopMovement();
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_StopMovement.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_StopMovement.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2ff201baacc362345a79874bc2a56b74
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
43
Assets/Scripts/Enemies/AI/BD_SummonMinions.cs
Normal file
43
Assets/Scripts/Enemies/AI/BD_SummonMinions.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using UnityEngine;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Pool;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Action:Boss 专用召唤小兵。
|
||||
/// 通过 GlobalObjectPool 在 Boss 周围生成 MinionPrefabKey 对应的敌人。
|
||||
/// </summary>
|
||||
public class BD_SummonMinions : Action
|
||||
{
|
||||
[SerializeField] private string m_MinionPrefabKey = "";
|
||||
[SerializeField] private int m_Count = 2;
|
||||
[SerializeField] private float m_SpawnRadius = 3f;
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (string.IsNullOrEmpty(m_MinionPrefabKey)) return TaskStatus.Failure;
|
||||
|
||||
var pool = ServiceLocator.GetOrDefault<IObjectPoolService>();
|
||||
if (pool == null) return TaskStatus.Failure;
|
||||
|
||||
for (int i = 0; i < m_Count; i++)
|
||||
{
|
||||
var angle = Random.Range(0f, 360f) * Mathf.Deg2Rad;
|
||||
var offset = new Vector2(Mathf.Cos(angle), 0f) * m_SpawnRadius;
|
||||
var spawnPos = new Vector3(
|
||||
transform.position.x + offset.x,
|
||||
transform.position.y + offset.y,
|
||||
0f);
|
||||
pool.Spawn(m_MinionPrefabKey, spawnPos, Quaternion.identity);
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_SummonMinions.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_SummonMinions.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8fe673f758db6ef4c81938d19b7a4fde
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
40
Assets/Scripts/Enemies/AI/BD_TelegraphAttack.cs
Normal file
40
Assets/Scripts/Enemies/AI/BD_TelegraphAttack.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using UnityEngine;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||
using BaseGames.Enemies.Boss.Patterns;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Action:触发攻击预警(TelegraphSystem),等待 Duration 秒后返回 Success。
|
||||
/// 对应架构 07_EnemyModule §8 BD_TelegraphAttack;与 TelegraphSystem 协作。
|
||||
/// </summary>
|
||||
public class BD_TelegraphAttack : Action
|
||||
{
|
||||
[SerializeField] private float m_Duration = 1f;
|
||||
[SerializeField] private string m_VfxKey = "";
|
||||
|
||||
private TelegraphSystem _telegraph;
|
||||
private float _elapsed;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_telegraph = GetComponent<TelegraphSystem>();
|
||||
_elapsed = 0f;
|
||||
|
||||
if (_telegraph != null && !string.IsNullOrEmpty(m_VfxKey))
|
||||
{
|
||||
_telegraph.StartCoroutine(
|
||||
_telegraph.ShowTelegraph(m_VfxKey, m_Duration, transform.position));
|
||||
}
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
_elapsed += Time.deltaTime;
|
||||
return _elapsed >= m_Duration ? TaskStatus.Success : TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_TelegraphAttack.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_TelegraphAttack.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bbc61c8ed12b2bb4b9c50b6aa1046576
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
23
Assets/Scripts/Enemies/AI/BD_TeleportTo.cs
Normal file
23
Assets/Scripts/Enemies/AI/BD_TeleportTo.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using UnityEngine;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Action:瞬移(传送)到目标坐标,Boss 专用。
|
||||
/// 单帧直接修改 transform.position,不使用导航。
|
||||
/// </summary>
|
||||
public class BD_TeleportTo : Action
|
||||
{
|
||||
[SerializeField] private Vector2 m_Target;
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
transform.position = new Vector3(m_Target.x, m_Target.y, transform.position.z);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_TeleportTo.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_TeleportTo.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9d4cc838f773cd44ad029c86f722906
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
30
Assets/Scripts/Enemies/AI/BD_Wait.cs
Normal file
30
Assets/Scripts/Enemies/AI/BD_Wait.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using UnityEngine;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Action:等待固定 Duration 秒后返回 Success。
|
||||
/// 适用于攻击前摇、冷却间隔等固定等待。
|
||||
/// </summary>
|
||||
public class BD_Wait : Action
|
||||
{
|
||||
[UnityEngine.SerializeField] private float m_Duration = 1f;
|
||||
|
||||
private float _elapsed;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_elapsed = 0f;
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
_elapsed += Time.deltaTime;
|
||||
return _elapsed >= m_Duration ? TaskStatus.Success : TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_Wait.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_Wait.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a145269c2d2c3094994ff16e72ba4d68
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
34
Assets/Scripts/Enemies/AI/BD_WaitForAnimation.cs
Normal file
34
Assets/Scripts/Enemies/AI/BD_WaitForAnimation.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using Opsive.BehaviorDesigner.Runtime;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Action:等待当前 Animancer 动画播放完毕再返回 Success。
|
||||
/// 通过 EnemyBase.IsAnimationComplete 轮询(Animancer CurrentState.NormalizedTime >= 1)。
|
||||
/// </summary>
|
||||
public class BD_WaitForAnimation : Action
|
||||
{
|
||||
private EnemyBase _enemy;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_enemy = GetComponent<EnemyBase>();
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null) return TaskStatus.Failure;
|
||||
|
||||
var state = _enemy.Animancer?.States?.Current;
|
||||
if (state == null) return TaskStatus.Success; // 无动画直接继续
|
||||
if (state.NormalizedTime >= 1f) return TaskStatus.Success;
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_WaitForAnimation.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_WaitForAnimation.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 437d55b7c77973b4c8a80b688ab153d5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
33
Assets/Scripts/Enemies/AI/BD_WaitRandom.cs
Normal file
33
Assets/Scripts/Enemies/AI/BD_WaitRandom.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using UnityEngine;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Action:在 [Min, Max] 范围内随机等待后返回 Success。
|
||||
/// 适用于巡逻间隔、随机攻击延迟等自然行为。
|
||||
/// </summary>
|
||||
public class BD_WaitRandom : Action
|
||||
{
|
||||
[UnityEngine.SerializeField] private float m_Min = 0.5f;
|
||||
[UnityEngine.SerializeField] private float m_Max = 2.0f;
|
||||
|
||||
private float _elapsed;
|
||||
private float _target;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_elapsed = 0f;
|
||||
_target = Random.Range(m_Min, m_Max);
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
_elapsed += Time.deltaTime;
|
||||
return _elapsed >= _target ? TaskStatus.Success : TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Scripts/Enemies/AI/BD_WaitRandom.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BD_WaitRandom.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15e39022725082a44a41a79a7a655814
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -9,6 +9,7 @@
|
||||
"rootNamespace": "BaseGames.Enemies.AI",
|
||||
"references": [
|
||||
"BaseGames.Enemies",
|
||||
"BaseGames.Enemies.Boss.Patterns",
|
||||
"Opsive.BehaviorDesigner.Runtime"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
|
||||
97
Assets/Scripts/Enemies/AI/BatchLOSSystem.cs
Normal file
97
Assets/Scripts/Enemies/AI/BatchLOSSystem.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using System.Collections.Generic;
|
||||
using Unity.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Jobs;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// 批量视线检测系统(架构 07_EnemyModule §12)。
|
||||
/// 每 FixedUpdate:
|
||||
/// 1. 读取上一帧 RaycastHit2D 结果,回调各注册者。
|
||||
/// 2. 重新构建本帧 RaycastCommand 批次,使用 Physics2D.RaycastAll 同步模式执行。
|
||||
///
|
||||
/// ⚠️ Unity 2022.3 中 Physics2D 批量命令(RaycastCommand2D)尚未稳定,
|
||||
/// 此实现使用 FixedUpdate 内顺序 Raycast2D(节流),确保零 GC 分配。
|
||||
/// 当敌人数量 > 20 时建议切换到 Job System RaycastCommand。
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-200)]
|
||||
public class BatchLOSSystem : MonoBehaviour
|
||||
{
|
||||
[SerializeField, Min(1)] private int _maxRequestersPerFrame = 8;
|
||||
|
||||
private readonly List<ILOSRequester> _requesters = new();
|
||||
private readonly HashSet<ILOSRequester> _requesterSet = new(); // O(1) 包含查询
|
||||
// _indexMap 记录每个 requester 在 _requesters 中的下标,供 Unregister 实现 O(1) 删除
|
||||
private readonly Dictionary<ILOSRequester, int> _indexMap = new();
|
||||
private int _currentOffset = 0;
|
||||
|
||||
// ── 注册 ──────────────────────────────────────────────────────────
|
||||
public void Register(ILOSRequester requester)
|
||||
{
|
||||
if (_requesterSet.Add(requester))
|
||||
{
|
||||
_indexMap[requester] = _requesters.Count;
|
||||
_requesters.Add(requester);
|
||||
}
|
||||
}
|
||||
|
||||
public void Unregister(ILOSRequester requester)
|
||||
{
|
||||
if (!_requesterSet.Remove(requester)) return;
|
||||
|
||||
int idx = _indexMap[requester];
|
||||
int last = _requesters.Count - 1;
|
||||
|
||||
// Swap-and-pop:将末尾元素移动到被删除的位置,避免 O(n) 搬移
|
||||
if (idx != last)
|
||||
{
|
||||
var moved = _requesters[last];
|
||||
_requesters[idx] = moved;
|
||||
_indexMap[moved] = idx;
|
||||
}
|
||||
|
||||
_requesters.RemoveAt(last);
|
||||
_indexMap.Remove(requester);
|
||||
|
||||
// 修正偏移量,防止越界
|
||||
if (_currentOffset >= _requesters.Count) _currentOffset = 0;
|
||||
}
|
||||
|
||||
// ── FixedUpdate ───────────────────────────────────────────────────
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (_requesters.Count == 0) return;
|
||||
|
||||
// 每帧轮询部分请求者(均匀分配,避免单帧全量射线)
|
||||
int count = Mathf.Min(_maxRequestersPerFrame, _requesters.Count);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int idx = (_currentOffset + i) % _requesters.Count;
|
||||
var requester = _requesters[idx];
|
||||
|
||||
bool hasLOS = false;
|
||||
if (requester != null)
|
||||
{
|
||||
Vector2 origin = requester.LOSOrigin;
|
||||
Vector2 target = requester.LOSTarget;
|
||||
Vector2 direction = target - origin;
|
||||
float distance = direction.magnitude;
|
||||
|
||||
if (distance > 0.01f)
|
||||
{
|
||||
var hit = Physics2D.Raycast(origin, direction.normalized, distance, requester.LOSBlockingMask);
|
||||
// 若无遮挡物(hit.collider == null),则视线畅通
|
||||
hasLOS = hit.collider == null;
|
||||
}
|
||||
|
||||
|
||||
requester.ReceiveLOSResult(hasLOS);
|
||||
}
|
||||
}
|
||||
|
||||
_currentOffset = (_currentOffset + count) % Mathf.Max(1, _requesters.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/AI/BatchLOSSystem.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/BatchLOSSystem.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a482b11f99a870f4ea28cd36b716a69b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
27
Assets/Scripts/Enemies/AI/ILOSRequester.cs
Normal file
27
Assets/Scripts/Enemies/AI/ILOSRequester.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// BatchLOSSystem 的注册接口(架构 07_EnemyModule §12)。
|
||||
/// 实现此接口的 EnemyBase 子类可以注册到 BatchLOSSystem,
|
||||
/// 以批处理方式接收 LOS(Line of Sight)检测结果。
|
||||
/// </summary>
|
||||
public interface ILOSRequester
|
||||
{
|
||||
/// <summary>射线起点(通常是眼部位置)。</summary>
|
||||
Vector2 LOSOrigin { get; }
|
||||
|
||||
/// <summary>射线终点(通常是玩家位置)。</summary>
|
||||
Vector2 LOSTarget { get; }
|
||||
|
||||
/// <summary>遮挡 LOS 的物理图层。</summary>
|
||||
LayerMask LOSBlockingMask { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 接收 LOS 检测结果。
|
||||
/// <paramref name="hasLineOfSight"/>: true = 有视线,false = 被遮挡。
|
||||
/// </summary>
|
||||
void ReceiveLOSResult(bool hasLineOfSight);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/AI/ILOSRequester.cs.meta
Normal file
11
Assets/Scripts/Enemies/AI/ILOSRequester.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c504654ba79100742a30b448b0233d63
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -12,8 +12,12 @@
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Combat",
|
||||
"BaseGames.Feedback",
|
||||
"BaseGames.World",
|
||||
"MoreMountains.Tools",
|
||||
"Kybernetik.Animancer"
|
||||
"Kybernetik.Animancer",
|
||||
"Opsive.BehaviorDesigner.Runtime",
|
||||
"Unity.Addressables",
|
||||
"Unity.ResourceManager"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
|
||||
33
Assets/Scripts/Enemies/Boss/AttackPatternSO.cs
Normal file
33
Assets/Scripts/Enemies/Boss/AttackPatternSO.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Boss
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个攻击图案的数据。伤害参数只写在此处,BossSkillSO 不存参数。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Boss/AttackPattern")]
|
||||
public class AttackPatternSO : ScriptableObject
|
||||
{
|
||||
[Header("输出")]
|
||||
public DamageSourceSO DamageSource;
|
||||
public float KnockbackAngle;
|
||||
|
||||
[Header("弹幕(若为弹幕类型)")]
|
||||
public AssetReferenceGameObject ProjectilePrefab;
|
||||
public int ProjectileCount = 1;
|
||||
public float SpreadAngle = 0f;
|
||||
public float ProjectileSpeed = 8f;
|
||||
|
||||
[Header("范围攻击(若为 AoE 类型)")]
|
||||
public float AoERadius;
|
||||
public Vector2 AoEOffset;
|
||||
|
||||
[Header("时序")]
|
||||
[Min(0f)] public float WindupDuration;
|
||||
[Min(0f)] public float ActiveDuration;
|
||||
[Min(0f)] public float RecoveryDuration;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/Boss/AttackPatternSO.cs.meta
Normal file
11
Assets/Scripts/Enemies/Boss/AttackPatternSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81f89b6e2f8f2774ab7cedbe45dcb810
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
48
Assets/Scripts/Enemies/Boss/BossBase.cs
Normal file
48
Assets/Scripts/Enemies/Boss/BossBase.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// Boss 敌人基类。扩展 <see cref="EnemyBase"/> 以支持多阶段切换与战斗结束广播。
|
||||
/// 具体 Boss 继承此类并重写 <see cref="EnterPhase"/>。
|
||||
/// </summary>
|
||||
public class BossBase : EnemyBase
|
||||
{
|
||||
[Header("Boss 配置")]
|
||||
[SerializeField] private string _bossId;
|
||||
[SerializeField] private BoolEventChannelSO _onBossFightEnded;
|
||||
[SerializeField] private BossPhaseEventChannelSO _onBossPhaseChanged;
|
||||
|
||||
public string BossId => _bossId;
|
||||
|
||||
protected int _currentPhase = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 进入指定阶段。广播 <see cref="BossPhaseEvent"/> 供 UI / 音乐系统响应。
|
||||
/// 子类可重写以添加额外过渡逻辑(动画、无敌帧等)。
|
||||
/// </summary>
|
||||
public virtual void EnterPhase(int phase)
|
||||
{
|
||||
_currentPhase = phase;
|
||||
_onBossPhaseChanged?.Raise(new BossPhaseEvent
|
||||
{
|
||||
BossId = _bossId,
|
||||
Phase = phase,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>检查当前 HP 是否低于指定百分比(0~1)。</summary>
|
||||
public bool IsHPBelow(float ratio)
|
||||
{
|
||||
if (_stats == null || _stats.MaxHP <= 0) return false;
|
||||
return (float)_stats.CurrentHP / _stats.MaxHP < ratio;
|
||||
}
|
||||
|
||||
protected override void Die()
|
||||
{
|
||||
base.Die();
|
||||
_onBossFightEnded?.Raise(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/Boss/BossBase.cs.meta
Normal file
11
Assets/Scripts/Enemies/Boss/BossBase.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 53427085944cddb4bacba2c2e80fb134
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
26
Assets/Scripts/Enemies/Boss/BossResourceConfigSO.cs
Normal file
26
Assets/Scripts/Enemies/Boss/BossResourceConfigSO.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Boss
|
||||
{
|
||||
/// <summary>
|
||||
/// Boss 自身资源(如愤怒值)的配置 ScriptableObject。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Boss/ResourceConfig")]
|
||||
public class BossResourceConfigSO : ScriptableObject
|
||||
{
|
||||
public string resourceId;
|
||||
public string displayName;
|
||||
public float maxValue;
|
||||
public float startValue;
|
||||
|
||||
[Header("自动变化")]
|
||||
public float passiveRate;
|
||||
public float onTakeDamageGain;
|
||||
public float onSkillUseGain;
|
||||
|
||||
[Header("满值效果")]
|
||||
public bool autoTriggerOnFull;
|
||||
public BossSkillSO fullTriggerSkill;
|
||||
public float resetValueAfterTrigger;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/Boss/BossResourceConfigSO.cs.meta
Normal file
11
Assets/Scripts/Enemies/Boss/BossResourceConfigSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 47b7618faa6ceb3458714246d12f9e73
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
184
Assets/Scripts/Enemies/Boss/BossSkillExecutor.cs
Normal file
184
Assets/Scripts/Enemies/Boss/BossSkillExecutor.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Animancer;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Boss
|
||||
{
|
||||
/// <summary>
|
||||
/// 挂在 Boss GameObject 上,接收 BossOrchestrator 的指令执行指定 BossSkillSO。
|
||||
/// 管理 VulnerabilityWindow 计时和 WeakPointSystem 激活。
|
||||
/// </summary>
|
||||
public class BossSkillExecutor : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private HitBox[] _hitBoxes;
|
||||
[SerializeField] private WeakPointSystem _weakPointSystem;
|
||||
[SerializeField] private AnimancerComponent _animancer;
|
||||
[SerializeField] private string _bossId;
|
||||
[SerializeField] private BossSkillEventChannelSO _onBossSkillStarted;
|
||||
[SerializeField] private BossSkillEventChannelSO _onBossSkillEnded;
|
||||
/// <remarks>PlayerController 无 Instance(架构 05 §2),由 Inspector 指定。</remarks>
|
||||
[SerializeField] private Transform _playerTransform;
|
||||
|
||||
private BossSkillSO _currentSkill;
|
||||
private bool _isExecuting;
|
||||
private Coroutine _activeCoroutine;
|
||||
|
||||
public bool IsExecuting => _isExecuting;
|
||||
|
||||
/// <summary>
|
||||
/// 按 float 值复用 WaitForSeconds 实例,消除协程中每次 new WaitForSeconds 的 GC 分配。
|
||||
/// Domain Reload 禁用时静态缓存跨 PlayMode 会话保留,但 WaitForSeconds 是幂等值对象,
|
||||
/// 不会引发功能错误;[RuntimeInitializeOnLoadMethod] 确保每次进入 Play 时清空。
|
||||
/// </summary>
|
||||
private static readonly Dictionary<float, WaitForSeconds> _wfsCache = new();
|
||||
|
||||
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ClearWFSCache() => _wfsCache.Clear();
|
||||
|
||||
private static WaitForSeconds GetWFS(float t)
|
||||
{
|
||||
if (!_wfsCache.TryGetValue(t, out var wfs))
|
||||
_wfsCache[t] = wfs = new WaitForSeconds(t);
|
||||
return wfs;
|
||||
}
|
||||
|
||||
// ── 公共 API ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 执行一个 Boss 技能。若当前已在执行中则直接返回。
|
||||
/// </summary>
|
||||
public void ExecuteSkill(BossSkillSO skill)
|
||||
{
|
||||
if (_isExecuting || skill == null) return;
|
||||
_activeCoroutine = StartCoroutine(ExecuteSkillCoroutine(skill));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 立即打断正在执行的技能(阶段切换时调用)。
|
||||
/// </summary>
|
||||
public void InterruptCurrentSkill()
|
||||
{
|
||||
if (_activeCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_activeCoroutine);
|
||||
_activeCoroutine = null;
|
||||
}
|
||||
FinishExecution();
|
||||
}
|
||||
|
||||
// ── 主协程 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private IEnumerator ExecuteSkillCoroutine(BossSkillSO skill)
|
||||
{
|
||||
_isExecuting = true;
|
||||
_currentSkill = skill;
|
||||
_onBossSkillStarted?.Raise(new BossSkillEvent { BossId = _bossId, SkillId = skill.skillId });
|
||||
|
||||
// 播放技能动画
|
||||
if (skill.skillAnimation != null)
|
||||
_animancer.Play(skill.skillAnimation);
|
||||
|
||||
// 启动 VulnerabilityWindow 协程(与主序列并行)
|
||||
Coroutine vulnCoroutine = null;
|
||||
if (skill.vulnerabilityWindows != null && skill.vulnerabilityWindows.Length > 0)
|
||||
vulnCoroutine = StartCoroutine(ActivateVulnerabilityWindowsCoroutine(skill));
|
||||
|
||||
// 执行攻击序列(优先 sequenceOnMiss 作为默认序列)
|
||||
if (skill.sequenceOnMiss != null)
|
||||
yield return ExecuteSequenceCoroutine(skill.sequenceOnMiss);
|
||||
|
||||
// 若弱点协程还在运行则等待其结束(避免孤立协程)
|
||||
if (vulnCoroutine != null)
|
||||
yield return vulnCoroutine;
|
||||
|
||||
FinishExecution();
|
||||
}
|
||||
|
||||
private void FinishExecution()
|
||||
{
|
||||
_isExecuting = false;
|
||||
if (_currentSkill != null)
|
||||
{
|
||||
_onBossSkillEnded?.Raise(new BossSkillEvent { BossId = _bossId, SkillId = _currentSkill.skillId });
|
||||
_currentSkill = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 序列协程 ────────────────────────────────────────────────────────────
|
||||
|
||||
private IEnumerator ExecuteSequenceCoroutine(SkillSequenceSO seq)
|
||||
{
|
||||
int repeatCount = 0;
|
||||
do
|
||||
{
|
||||
foreach (var step in seq.steps)
|
||||
{
|
||||
if (step.delayBeforeStep > 0f)
|
||||
yield return GetWFS(step.delayBeforeStep);
|
||||
|
||||
if (step.pattern != null)
|
||||
yield return ExecutePatternCoroutine(step.pattern);
|
||||
}
|
||||
|
||||
repeatCount++;
|
||||
|
||||
if (seq.RepeatIfPlayerInRange && seq.RepeatDelay > 0f)
|
||||
yield return GetWFS(seq.RepeatDelay);
|
||||
}
|
||||
while (seq.RepeatIfPlayerInRange
|
||||
&& (seq.MaxRepeatCount == 0 || repeatCount < seq.MaxRepeatCount)
|
||||
&& IsPlayerInRange());
|
||||
}
|
||||
|
||||
private IEnumerator ExecutePatternCoroutine(AttackPatternSO pattern)
|
||||
{
|
||||
// 预备
|
||||
if (pattern.WindupDuration > 0f)
|
||||
yield return GetWFS(pattern.WindupDuration);
|
||||
|
||||
// 激活 HitBox(架构 06 §4:Activate(DamageSourceSO, Transform))
|
||||
foreach (var hb in _hitBoxes)
|
||||
hb.Activate(pattern.DamageSource, transform);
|
||||
|
||||
if (pattern.ActiveDuration > 0f)
|
||||
yield return GetWFS(pattern.ActiveDuration);
|
||||
|
||||
// 关闭 HitBox
|
||||
foreach (var hb in _hitBoxes)
|
||||
hb.Deactivate();
|
||||
|
||||
// 后摇
|
||||
if (pattern.RecoveryDuration > 0f)
|
||||
yield return GetWFS(pattern.RecoveryDuration);
|
||||
}
|
||||
|
||||
// ── VulnerabilityWindow 协程 ─────────────────────────────────────────────
|
||||
|
||||
private IEnumerator ActivateVulnerabilityWindowsCoroutine(BossSkillSO skill)
|
||||
{
|
||||
foreach (var window in skill.vulnerabilityWindows)
|
||||
{
|
||||
if (window.TriggerDelay > 0f)
|
||||
yield return GetWFS(window.TriggerDelay);
|
||||
|
||||
bool activateSpecific = window.ActivateWeakPointHurtBox;
|
||||
_weakPointSystem?.SetActive(true, window.DamageMultiplier, activateSpecific);
|
||||
window.OpenFeedback?.PlayFeedbacks();
|
||||
|
||||
yield return GetWFS(window.Duration);
|
||||
|
||||
_weakPointSystem?.SetActive(false, 1f, activateSpecific);
|
||||
window.CloseFeedback?.PlayFeedbacks();
|
||||
}
|
||||
}
|
||||
|
||||
// ── 工具 ───────────────────────────────────────────────────────────────
|
||||
|
||||
private bool IsPlayerInRange() =>
|
||||
_playerTransform != null &&
|
||||
Vector2.Distance(transform.position, _playerTransform.position) < 8f;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/Boss/BossSkillExecutor.cs.meta
Normal file
11
Assets/Scripts/Enemies/Boss/BossSkillExecutor.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4dfa1c525eaca5640b3cfe945626a466
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
61
Assets/Scripts/Enemies/Boss/BossSkillSO.cs
Normal file
61
Assets/Scripts/Enemies/Boss/BossSkillSO.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using Animancer;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Boss
|
||||
{
|
||||
/// <summary>
|
||||
/// Boss 单个技能的所有数据,包括攻击模式、弱点窗口、互动标签等。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Boss/BossSkill")]
|
||||
public class BossSkillSO : ScriptableObject
|
||||
{
|
||||
[Header("元信息")]
|
||||
public string skillId;
|
||||
public string displayName;
|
||||
[TextArea(1, 4)]
|
||||
public string designNote;
|
||||
|
||||
[Header("技能分类")]
|
||||
public BossSkillCategory category;
|
||||
public BossSkillType skillType;
|
||||
|
||||
[Header("阶段可用性")]
|
||||
[Tooltip("空数组 = 全阶段可用")]
|
||||
public int[] availablePhaseIndices;
|
||||
|
||||
[Header("核心攻击动作引用")]
|
||||
public AttackPatternSO[] attackPatterns;
|
||||
|
||||
[Header("弱点窗口(至少 1 个)")]
|
||||
public VulnerabilityWindow[] vulnerabilityWindows;
|
||||
|
||||
[Header("互动标签")]
|
||||
public InteractionTag interactionTags;
|
||||
|
||||
[Header("连段")]
|
||||
public SkillSequenceSO sequenceOnHit;
|
||||
public SkillSequenceSO sequenceOnMiss;
|
||||
|
||||
[Header("玩家反制接口")]
|
||||
public PlayerCounterResponse[] counterResponses;
|
||||
|
||||
[Header("场景联动")]
|
||||
public ArenaEventTrigger[] arenaEvents;
|
||||
|
||||
[Header("Boss 资源")]
|
||||
public BossResourceCost resourceCost;
|
||||
public bool buildsRage;
|
||||
|
||||
[Header("霸体配置")]
|
||||
public PoiseWindowConfig poiseWindow;
|
||||
|
||||
[Header("动画")]
|
||||
public ClipTransition skillAnimation;
|
||||
|
||||
[Header("冷却")]
|
||||
[Min(0f)]
|
||||
public float cooldown;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/Boss/BossSkillSO.cs.meta
Normal file
11
Assets/Scripts/Enemies/Boss/BossSkillSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de92221c7c3fb4a42a7cd122a8f97632
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
184
Assets/Scripts/Enemies/Boss/BossSkillTypes.cs
Normal file
184
Assets/Scripts/Enemies/Boss/BossSkillTypes.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using MoreMountains.Feedbacks;
|
||||
|
||||
namespace BaseGames.Boss
|
||||
{
|
||||
// ── 分类枚举 ───────────────────────────────────────────────────────────────
|
||||
|
||||
public enum BossSkillCategory
|
||||
{
|
||||
Melee, Ranged, Charge, AoE, Environmental, Summon,
|
||||
Buff, Debuff, Phase, Passive, Reactive
|
||||
}
|
||||
|
||||
public enum BossSkillType
|
||||
{
|
||||
MeleeSlash,
|
||||
ChargeAttack,
|
||||
LeapSlam,
|
||||
ProjectileVolley,
|
||||
LaserBeam,
|
||||
PhaseTransition,
|
||||
SummonMinion,
|
||||
ArenaTrap,
|
||||
SpeedBurst,
|
||||
DefensiveShell,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum InteractionTag
|
||||
{
|
||||
None = 0,
|
||||
Parryable = 1 << 0,
|
||||
PerfectParryOnly = 1 << 1,
|
||||
DodgeWindow = 1 << 2,
|
||||
Unblockable = 1 << 3,
|
||||
CanBeReflected = 1 << 4,
|
||||
ExposesWeakPoint = 1 << 5,
|
||||
GrantsPlayerReso = 1 << 6,
|
||||
ArenaHazard = 1 << 7,
|
||||
PhaseGate = 1 << 8,
|
||||
}
|
||||
|
||||
// ── VulnerabilityWindow ────────────────────────────────────────────────────
|
||||
|
||||
public enum VulnTriggerType
|
||||
{
|
||||
OnAttackRecovery,
|
||||
OnParriedSuccess,
|
||||
OnCounterSkillHit,
|
||||
OnPhaseTransition,
|
||||
OnHazardBackfire,
|
||||
OnSummonDefeated,
|
||||
Manual,
|
||||
}
|
||||
|
||||
public enum WeakPointType
|
||||
{
|
||||
FullBody,
|
||||
HeadOnly,
|
||||
BackOnly,
|
||||
CoreExposed,
|
||||
CustomPoint,
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct VulnerabilityWindow
|
||||
{
|
||||
[Tooltip("弱点触发方式")]
|
||||
public VulnTriggerType TriggerType;
|
||||
|
||||
[Tooltip("触发后延迟出现(秒)")]
|
||||
[Min(0f)]
|
||||
public float TriggerDelay;
|
||||
|
||||
[Tooltip("弱点持续时长(秒)")]
|
||||
[Min(0.1f)]
|
||||
public float Duration;
|
||||
|
||||
public WeakPointType WeakPointType;
|
||||
|
||||
[Tooltip("弱点激活时 Boss 受击乘数")]
|
||||
[Min(0.1f)]
|
||||
public float DamageMultiplier;
|
||||
|
||||
public bool ForceStagger;
|
||||
|
||||
[Min(0f)]
|
||||
public float StaggerDuration;
|
||||
|
||||
public MMF_Player OpenFeedback;
|
||||
public MMF_Player CloseFeedback;
|
||||
public Color HighlightColor;
|
||||
|
||||
public bool ActivateWeakPointHurtBox => WeakPointType != WeakPointType.FullBody;
|
||||
}
|
||||
|
||||
// ── PlayerCounterResponse ─────────────────────────────────────────────────
|
||||
|
||||
public enum CounterType
|
||||
{
|
||||
Parry,
|
||||
PerfectParry,
|
||||
DodgeThrough,
|
||||
SpecificSkill,
|
||||
WeakPointHit,
|
||||
HazardBackfire,
|
||||
SummonKill,
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct PlayerCounterResponse
|
||||
{
|
||||
[Header("反制条件")]
|
||||
public CounterType counterType;
|
||||
public string requiredSkillId;
|
||||
|
||||
[Header("反制效果(对 Boss)")]
|
||||
public float bossStaggerDuration;
|
||||
public float bossDamageBonus;
|
||||
public bool openVulnWindow;
|
||||
public bool interruptSkill;
|
||||
|
||||
[Header("反制收益(对玩家)")]
|
||||
public int soulPowerGrant;
|
||||
public int spiritPowerGrant;
|
||||
public MMF_Player counterFeedback;
|
||||
}
|
||||
|
||||
// ── ArenaEvent ────────────────────────────────────────────────────────────
|
||||
|
||||
public enum ArenaEventType
|
||||
{
|
||||
DestroyPlatform,
|
||||
ActivateHazard,
|
||||
DeactivateHazard,
|
||||
SpawnHazardArea,
|
||||
ShakeArena,
|
||||
ToggleLighting,
|
||||
SpawnPlatform,
|
||||
TriggerCutscene,
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct ArenaEventParams
|
||||
{
|
||||
public float duration;
|
||||
public float intensity;
|
||||
public bool revertsOnPhaseEnd;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct ArenaEventTrigger
|
||||
{
|
||||
public string targetArenaObjectId;
|
||||
public ArenaEventType eventType;
|
||||
public float delay;
|
||||
public ArenaEventParams parameters;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct ArenaEventData
|
||||
{
|
||||
public ArenaEventType type;
|
||||
public ArenaEventParams parameters;
|
||||
public string sourceSkillId;
|
||||
}
|
||||
|
||||
public interface IArenaInteractable
|
||||
{
|
||||
string ArenaObjectId { get; }
|
||||
void OnBossArenaEvent(ArenaEventData data);
|
||||
}
|
||||
|
||||
// ── BossResourceCost ──────────────────────────────────────────────────────
|
||||
|
||||
[Serializable]
|
||||
public struct BossResourceCost
|
||||
{
|
||||
public string resourceId;
|
||||
public float cost;
|
||||
public float minRequired;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/Boss/BossSkillTypes.cs.meta
Normal file
11
Assets/Scripts/Enemies/Boss/BossSkillTypes.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1beb8f8f7958b84c9ab60abe5f8c4ed
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
50
Assets/Scripts/Enemies/Boss/Patterns/TelegraphSystem.cs
Normal file
50
Assets/Scripts/Enemies/Boss/Patterns/TelegraphSystem.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Pool;
|
||||
|
||||
namespace BaseGames.Enemies.Boss.Patterns
|
||||
{
|
||||
/// <summary>
|
||||
/// 攻击预警系统(架构 07_EnemyModule §11)。
|
||||
/// 在攻击前若干帧显示视觉提示(VFX 从对象池取出,到期归还)。
|
||||
/// 由 BD_TelegraphAttack 通过协程调用 ShowTelegraph。
|
||||
/// </summary>
|
||||
public class TelegraphSystem : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// 开始预警:从对象池取出 vfxKey 对应预警 VFX,等待 duration 秒后归还。
|
||||
/// 由 BD_TelegraphAttack.OnStart 通过 StartCoroutine 调用。
|
||||
/// </summary>
|
||||
public IEnumerator ShowTelegraph(string vfxKey, float duration, Vector2 position)
|
||||
{
|
||||
if (string.IsNullOrEmpty(vfxKey) || duration <= 0f)
|
||||
{
|
||||
yield return null;
|
||||
yield break;
|
||||
}
|
||||
|
||||
GameObject vfx = null;
|
||||
var pool = ServiceLocator.GetOrDefault<IObjectPoolService>();
|
||||
if (pool != null)
|
||||
vfx = pool.Spawn(vfxKey, new Vector3(position.x, position.y, 0f), Quaternion.identity);
|
||||
else
|
||||
Debug.LogWarning($"[TelegraphSystem] IObjectPoolService 未就绪,预警 VFX '{vfxKey}' 无法显示。");
|
||||
|
||||
yield return new WaitForSeconds(duration);
|
||||
|
||||
if (vfx != null && pool != null)
|
||||
{
|
||||
var po = vfx.GetComponent<PooledObject>();
|
||||
if (po != null) po.ReturnToPool();
|
||||
else vfx.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>立即隐藏所有活跃预警 VFX(技能被打断时调用)。</summary>
|
||||
public void CancelTelegraph()
|
||||
{
|
||||
StopAllCoroutines();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/Boss/Patterns/TelegraphSystem.cs.meta
Normal file
11
Assets/Scripts/Enemies/Boss/Patterns/TelegraphSystem.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6f4987894dfe1648909b6863c003c31
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
26
Assets/Scripts/Enemies/Boss/SkillSequenceSO.cs
Normal file
26
Assets/Scripts/Enemies/Boss/SkillSequenceSO.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Boss
|
||||
{
|
||||
/// <summary>
|
||||
/// 有序攻击序列(一个技能内的多段连段)。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Boss/SkillSequence")]
|
||||
public class SkillSequenceSO : ScriptableObject
|
||||
{
|
||||
[Serializable]
|
||||
public struct SequenceStep
|
||||
{
|
||||
public AttackPatternSO pattern;
|
||||
[Min(0f)] public float delayBeforeStep;
|
||||
}
|
||||
|
||||
public SequenceStep[] steps;
|
||||
|
||||
[Header("序列完成后的行为")]
|
||||
public bool RepeatIfPlayerInRange;
|
||||
[Min(0f)] public float RepeatDelay;
|
||||
[Range(0, 10)] public int MaxRepeatCount;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/Boss/SkillSequenceSO.cs.meta
Normal file
11
Assets/Scripts/Enemies/Boss/SkillSequenceSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ab2ec01e225283d4face08cef0d72c87
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
51
Assets/Scripts/Enemies/Boss/WeakPointSystem.cs
Normal file
51
Assets/Scripts/Enemies/Boss/WeakPointSystem.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Boss
|
||||
{
|
||||
/// <summary>
|
||||
/// 管理 Boss 的专属弱点 HurtBox(如核心、眼睛等)。
|
||||
/// 弱点激活期间受到的伤害会乘以 DamageMultiplier。
|
||||
/// </summary>
|
||||
public class WeakPointSystem : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public struct WeakPoint
|
||||
{
|
||||
public HurtBox hurtBox;
|
||||
public GameObject visualIndicator;
|
||||
}
|
||||
|
||||
[SerializeField] private WeakPoint[] _weakPoints;
|
||||
[SerializeField] private string _bossId;
|
||||
[SerializeField] private StringEventChannelSO _onVulnerabilityWindowOpened;
|
||||
|
||||
private float _damageMultiplier = 1f;
|
||||
|
||||
/// <summary>激活或关闭弱点 HurtBox 及视觉指示器。</summary>
|
||||
/// <param name="active">是否激活。</param>
|
||||
/// <param name="multiplier">激活时的受击伤害乘数。</param>
|
||||
/// <param name="activateSpecific">true = 仅激活弱点专属 HurtBox;false = 全身视为弱点(不改变 HurtBox 状态)。</param>
|
||||
public void SetActive(bool active, float multiplier = 1f, bool activateSpecific = false)
|
||||
{
|
||||
_damageMultiplier = active ? multiplier : 1f;
|
||||
|
||||
if (activateSpecific)
|
||||
{
|
||||
foreach (var wp in _weakPoints)
|
||||
{
|
||||
wp.hurtBox.gameObject.SetActive(active);
|
||||
if (wp.visualIndicator != null)
|
||||
wp.visualIndicator.SetActive(active);
|
||||
}
|
||||
}
|
||||
|
||||
if (active)
|
||||
_onVulnerabilityWindowOpened?.Raise(_bossId);
|
||||
}
|
||||
|
||||
/// <summary>弱点 HurtBox 受击时,由 BossStats 调用此方法获取最终伤害系数。</summary>
|
||||
public float GetDamageMultiplier() => _damageMultiplier;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/Boss/WeakPointSystem.cs.meta
Normal file
11
Assets/Scripts/Enemies/Boss/WeakPointSystem.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96ffe91642a6ccc4ea4c6076d80f5e27
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -19,6 +19,26 @@ namespace BaseGames.Enemies
|
||||
|
||||
[Header("受击")]
|
||||
public AnimationClip Hurt;
|
||||
public AnimationClip Stagger;
|
||||
public AnimationClip Dead;
|
||||
|
||||
/// <summary>
|
||||
/// 按字段名返回 Clip(BD_PlayAnimation 使用)。
|
||||
/// 支持字段名和常见别名(如 Attack_Melee / Idle 等)。
|
||||
/// </summary>
|
||||
public AnimationClip GetClipByName(string name)
|
||||
{
|
||||
return name switch
|
||||
{
|
||||
"Idle" => Idle,
|
||||
"Walk" => Walk,
|
||||
"Run" => Run,
|
||||
"Attack" or "Attack_Melee" => Attack,
|
||||
"Hurt" => Hurt,
|
||||
"Stagger" => Stagger,
|
||||
"Dead" or "Death" => Dead,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
using UnityEngine;
|
||||
using Animancer;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Enemies.States;
|
||||
#if GRAPH_DESIGNER
|
||||
using Opsive.BehaviorDesigner.Runtime;
|
||||
#endif
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人基类(架构 07_EnemyModule §1)。
|
||||
/// 实现 IDamageable,为 Behavior Designer 任务提供统一虚方法接口。
|
||||
/// Phase 1 实现:完整骨架,BD 接口、受击、死亡流程。
|
||||
/// 包含:BD 接口、受击、死亡流程。
|
||||
/// ⚠️ _nav 字段类型为 IPathAgent(在 BaseGames.Enemies.Navigation 中实现具体类)。
|
||||
/// </summary>
|
||||
public class EnemyBase : MonoBehaviour, IDamageable
|
||||
public class EnemyBase : MonoBehaviour, IDamageable, ILOSRequester
|
||||
{
|
||||
[Header("标识")]
|
||||
[SerializeField] private string _enemyId; // 任务系统 / Boss 进程追踪用,如 "Enemy_SpiderGuard"
|
||||
public string EnemyId => _enemyId;
|
||||
|
||||
/// <summary>死亡时触发(ChallengeRoomManager 波次结算用)。</summary>
|
||||
public event System.Action OnDied;
|
||||
|
||||
[Header("配置 SO")]
|
||||
[SerializeField] protected EnemyStatsSO _statsSO;
|
||||
[SerializeField] protected EnemyAnimationConfigSO _animConfig;
|
||||
@@ -25,17 +37,28 @@ namespace BaseGames.Enemies
|
||||
[SerializeField] protected HurtBox _hurtBox;
|
||||
|
||||
[Header("事件频道")]
|
||||
[SerializeField] private BaseGames.Core.Events.TransformEventChannelSO _onEnemyDied;
|
||||
[SerializeField] private BaseGames.Core.Events.StringEventChannelSO _onEnemyDied;
|
||||
/// <summary>
|
||||
/// 玩家生成事件频道(由 PlayerController.Start() 广播)。
|
||||
/// 配置后替代 FindWithTag,避免 N 个敌人同帧全场景标签扫描。
|
||||
/// </summary>
|
||||
[SerializeField] private BaseGames.Core.Events.TransformEventChannelSO _onPlayerSpawned;
|
||||
|
||||
// ── 导航代理(IPathAgent;由 EnemyNavAgent 实现)───────────────────
|
||||
// Phase 1:通过接口引用,避免对 Navigation 程序集的直接依赖。
|
||||
// 通过接口引用,避免对 Navigation 程序集的直接依赖。
|
||||
// 由子类 / Inspector 注入,或者运行时 GetComponent<IPathAgent>() 获取。
|
||||
protected IPathAgent _nav;
|
||||
protected IPathAgent _nav;
|
||||
// 霸体来源(由 EnemyPoiseComponent.Awake() 自动注入,TakeDamage 时读取)
|
||||
private IPoiseSource _poiseSource;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
// ── 状态 ──────────────────────────────────────────────────────────
|
||||
private EnemyStateType _currentState;
|
||||
public EnemyStateType CurrentState => _currentState;
|
||||
|
||||
// POCO 状态对象字典:枚举保持对外 API 不变。
|
||||
// 子类可在 Awake() 重写条目注入自定义状态对象。
|
||||
protected readonly System.Collections.Generic.Dictionary<EnemyStateType, IEnemyState> _stateObjs
|
||||
= new System.Collections.Generic.Dictionary<EnemyStateType, IEnemyState>();
|
||||
// ── IDamageable ───────────────────────────────────────────────────
|
||||
public bool IsInvincible => _currentState == EnemyStateType.Dead;
|
||||
public int Defense => _stats != null ? _stats.Defense : 0;
|
||||
@@ -53,10 +76,26 @@ namespace BaseGames.Enemies
|
||||
return;
|
||||
}
|
||||
|
||||
// Phase 2:根据霸体结果选 Stagger / Hurt
|
||||
ForceState(EnemyStateType.Hurt);
|
||||
// 根据霸体等级选择 Stagger(硬直)或 Hurt(受击)。
|
||||
// ForceBreak 标记或 BreakLevel 超过当前霸体等级时触发 Stagger,否则触发 Hurt。
|
||||
PoiseLevel curPoise = _poiseSource?.GetCurrentPoiseLevel() ?? PoiseLevel.None;
|
||||
bool causesStagger = info.Flags.HasFlag(DamageFlags.ForceBreak)
|
||||
|| (int)info.Break > (int)curPoise;
|
||||
ForceState(causesStagger ? EnemyStateType.Stagger : EnemyStateType.Hurt);
|
||||
}
|
||||
|
||||
// BD 任务访问接口(公共只读属性)────────────────────────────────
|
||||
public IPathAgent Nav => _nav;
|
||||
public EnemyMovement Movement => _movement;
|
||||
public EnemyStats Stats => _stats;
|
||||
public AnimancerComponent Animancer => _animancer;
|
||||
public EnemyAnimationConfigSO AnimConfig => _animConfig;
|
||||
/// <summary>由 _onPlayerSpawned 事件缓存的玩家 Transform,供 BD 任务读取。</summary>
|
||||
public Transform PlayerTransform => _playerTransform;
|
||||
#if GRAPH_DESIGNER
|
||||
public BehaviorTree BehaviorTree => _behaviorTree;
|
||||
#endif
|
||||
|
||||
// ── BD 行为树接口(虚方法)────────────────────────────────────────
|
||||
|
||||
public virtual void MoveTo(Vector2 target)
|
||||
@@ -81,7 +120,10 @@ namespace BaseGames.Enemies
|
||||
=> _stats != null && _stats.AttackCooldownTimer <= 0f;
|
||||
|
||||
public virtual bool IsPlayerInRange(float range)
|
||||
=> _stats != null && _stats.DistanceToPlayer <= range;
|
||||
=> _stats != null && _stats.SqrDistanceToPlayer <= range * range;
|
||||
|
||||
public virtual bool IsPlayerVisible()
|
||||
=> _losResult; // BatchLOSSystem 写入;初始 false(未见玩家)
|
||||
|
||||
public virtual void FacePlayer()
|
||||
{
|
||||
@@ -95,43 +137,138 @@ namespace BaseGames.Enemies
|
||||
_movement?.ApplyKnockback(info.KnockbackDirection, info.KnockbackForce);
|
||||
}
|
||||
|
||||
public virtual void JumpTo(Vector2 target)
|
||||
=> _movement?.JumpToTarget(target);
|
||||
|
||||
/// <summary>
|
||||
/// 调整 BehaviorTree Tick 频率(非警觉=2帧/次,警觉=每帧)。
|
||||
/// 由 BD_SetAlert 调用(架构 07_EnemyModule §13.5)。
|
||||
/// </summary>
|
||||
public void SetAggroTickRate(bool isAggro)
|
||||
{
|
||||
#if GRAPH_DESIGNER
|
||||
// Opsive 运行时当前版本未直接暴露 frameInterval 字段。
|
||||
// 需升级 Opsive 包或通过自定义 Tick 次数属性实现此功能。
|
||||
Debug.LogWarning("[EnemyBase] SetAggroTickRate 当前无效:Opsive 运行时尚未暴露 frameInterval,请升级包后实现。", this);
|
||||
_ = isAggro;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ── 动画事件钩子(由 EnemyAnimationEvents 调用)────────────────────
|
||||
|
||||
/// <summary>生成弹幕 / 技能投射物。payload 为配置 Id,由子类查表实现。</summary>
|
||||
public virtual void SpawnProjectile(string payload) { }
|
||||
|
||||
/// <summary>切换二阶段形态(Boss 等特殊敌人重写此方法)。</summary>
|
||||
public virtual void TriggerPhaseTwo() { }
|
||||
|
||||
/// <summary>动画播放完毕回调(用于单次动画后返回 Idle 等逻辑)。</summary>
|
||||
public virtual void OnAnimationComplete(string payload) { }
|
||||
|
||||
/// <summary>设置嘶吼状态(影响 Blackboard / 状态机行为)。</summary>
|
||||
public virtual void SetRoaring(bool isRoaring) { }
|
||||
|
||||
// ── 状态控制 ──────────────────────────────────────────────────────
|
||||
public void ForceState(EnemyStateType newState)
|
||||
{
|
||||
// Exit 当前状态
|
||||
if (_stateObjs.TryGetValue(_currentState, out var prev))
|
||||
prev.Exit(this);
|
||||
|
||||
_currentState = newState;
|
||||
// Phase 2:根据状态播放对应动画 / 触发硬直计时
|
||||
|
||||
// Enter 新状态
|
||||
if (_stateObjs.TryGetValue(newState, out var next))
|
||||
next.Enter(this);
|
||||
}
|
||||
|
||||
// ── Unity 生命周期 ────────────────────────────────────────────────
|
||||
protected virtual void Awake()
|
||||
{
|
||||
_nav = GetComponent<IPathAgent>() ?? new NullPathAgent();
|
||||
// 初始化 POCO 状态对象(子类可在调用 base.Awake() 后替换字典条目)
|
||||
_stateObjs[EnemyStateType.Controlled] = new EnemyControlledState();
|
||||
_stateObjs[EnemyStateType.Hurt] = new EnemyHurtState();
|
||||
_stateObjs[EnemyStateType.Stagger] = new EnemyStaggerState();
|
||||
_stateObjs[EnemyStateType.Dead] = new EnemyDeadState();
|
||||
|
||||
if (_stats != null && _statsSO != null)
|
||||
_stats.Initialize(_statsSO);
|
||||
_nav = GetComponent<IPathAgent>() ?? new NullPathAgent();
|
||||
_poiseSource = GetComponent<IPoiseSource>();
|
||||
|
||||
// Phase 1:简单查找玩家;Phase 2 改为事件频道订阅
|
||||
var playerGO = GameObject.FindWithTag("Player");
|
||||
if (playerGO != null) _playerTransform = playerGO.transform;
|
||||
Debug.Assert(_statsSO != null, "[EnemyBase] _statsSO 未赋值,请在 Prefab Inspector 中指定 EnemyStatsSO。", this);
|
||||
Debug.Assert(_stats != null, "[EnemyBase] _stats 未绑定,请在 Prefab Inspector 中绑定 EnemyStats 组件。", this);
|
||||
_stats.Initialize(_statsSO);
|
||||
|
||||
// 订阅玩家生成事件(PlayerController.Start 广播),避免每个敌人独立 FindWithTag
|
||||
// 订阅在 OnEnable 中处理
|
||||
|
||||
#if GRAPH_DESIGNER
|
||||
_behaviorTree = GetComponent<BehaviorTree>();
|
||||
if (_behaviorTree != null)
|
||||
{
|
||||
_behaviorTree.StartWhenEnabled = false;
|
||||
_behaviorTree.PauseWhenDisabled = true;
|
||||
_behaviorTree.StartBehavior();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
protected virtual void Update()
|
||||
{
|
||||
_stats?.TickAttackCooldown(Time.deltaTime);
|
||||
|
||||
// 使用 sqrMagnitude 替代 Vector2.Distance,避免每帧开平方计算
|
||||
if (_playerTransform != null && _stats != null)
|
||||
_stats.DistanceToPlayer = Vector2.Distance(transform.position, _playerTransform.position);
|
||||
_stats.SqrDistanceToPlayer = ((Vector2)_playerTransform.position - (Vector2)transform.position).sqrMagnitude;
|
||||
}
|
||||
|
||||
protected virtual void Start()
|
||||
{
|
||||
// 若事件未配置或玩家尚未广播,匹降为一次性查找
|
||||
if (_playerTransform == null)
|
||||
{
|
||||
var playerGO = GameObject.FindWithTag("Player");
|
||||
if (playerGO != null) _playerTransform = playerGO.transform;
|
||||
}
|
||||
|
||||
// 播放 Idle 动画(若 Animancer 和配置都就绪)
|
||||
if (_animancer != null && _animConfig != null && _animConfig.Idle != null)
|
||||
_animancer.Play(_animConfig.Idle);
|
||||
}
|
||||
|
||||
// ── 内部 ──────────────────────────────────────────────────────────
|
||||
private Transform _playerTransform;
|
||||
protected Transform _playerTransform;
|
||||
private void SetPlayerTransform(Transform player) => _playerTransform = player;
|
||||
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
_onPlayerSpawned?.Subscribe(SetPlayerTransform).AddTo(_subs);
|
||||
}
|
||||
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
_subs.Clear();
|
||||
}
|
||||
|
||||
protected virtual void OnDestroy() { }
|
||||
// LOS 缓存(BatchLOSSystem 写入;降级时由 3 帧节流 Raycast 写入)
|
||||
private bool _losResult;
|
||||
|
||||
// ── ILOSRequester ──────────────────────────────────────────────────
|
||||
public Vector2 LOSOrigin => (Vector2)transform.position + _statsSO.EyeOffset;
|
||||
public Vector2 LOSTarget => _playerTransform != null
|
||||
? (Vector2)_playerTransform.position
|
||||
: (Vector2)transform.position;
|
||||
public LayerMask LOSBlockingMask => _statsSO.LOSBlockingMask;
|
||||
|
||||
public void ReceiveLOSResult(bool hasLineOfSight)
|
||||
{
|
||||
_losResult = hasLineOfSight;
|
||||
}
|
||||
|
||||
// BehaviorTree 引用(#if GRAPH_DESIGNER 保护,避免空引用)
|
||||
#if GRAPH_DESIGNER
|
||||
private BehaviorTree _behaviorTree;
|
||||
#endif
|
||||
|
||||
protected virtual void Die()
|
||||
{
|
||||
@@ -154,7 +291,8 @@ namespace BaseGames.Enemies
|
||||
}
|
||||
|
||||
_feedback?.OnDeath();
|
||||
_onEnemyDied?.Raise(transform);
|
||||
_onEnemyDied?.Raise(_enemyId);
|
||||
OnDied?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ using BaseGames.Combat;
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人战斗组件(Phase 1 桩,架构 07_EnemyModule §4)。
|
||||
/// Phase 2 实现:HitBox 按 AttackType 索引管理、伤害来源 SO 注入。
|
||||
/// 敌人战斗组件(架构 07_EnemyModule §4)。
|
||||
/// HitBox 按 AttackType 索引管理,动画事件触发 HitBox 开关。
|
||||
/// </summary>
|
||||
public class EnemyCombat : MonoBehaviour
|
||||
{
|
||||
@@ -13,7 +13,6 @@ namespace BaseGames.Enemies
|
||||
|
||||
public void StartAttack(AttackType type)
|
||||
{
|
||||
// Phase 1 桩:Phase 2 播放攻击动画,由 AnimationEvent 触发 HitBox On/Off
|
||||
int idx = (int)type;
|
||||
EnableHitBox(idx);
|
||||
}
|
||||
|
||||
@@ -42,10 +42,11 @@ namespace BaseGames.Enemies
|
||||
public void PlayLandImpact() { }
|
||||
public void PlayAttackWhoosh() { }
|
||||
public void PlayJumpLaunch() { }
|
||||
public void PlayFootstep() { }
|
||||
public void TriggerPreset(string presetId) { }
|
||||
public void PlaySFXById(string sfxId) { }
|
||||
|
||||
// ── EnemyBase 语义方法(向前兼容)────────────────────────────────────────
|
||||
// ── EnemyBase 语义方法 ────────────────────────────────────────────
|
||||
/// <summary>受到伤害时调用(由 EnemyBase 触发)。</summary>
|
||||
public void OnHit(DamageInfo info)
|
||||
{
|
||||
|
||||
@@ -4,13 +4,14 @@ namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人移动组件(架构 07_EnemyModule §3)。
|
||||
/// Phase 1 实现:水平移动、面向目标、击退。
|
||||
/// 实现:水平移动、面向目标、击退。
|
||||
/// ⚠️ 使用 Rigidbody2D.velocity(Unity 2022 LTS)。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Rigidbody2D))]
|
||||
public class EnemyMovement : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private EnemyStatsSO _config;
|
||||
[SerializeField] private EnemyStatsSO _config;
|
||||
[SerializeField] private SpriteRenderer _spriteRenderer;
|
||||
|
||||
private Rigidbody2D _rb;
|
||||
private int _facingDir = 1;
|
||||
@@ -19,13 +20,13 @@ namespace BaseGames.Enemies
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Debug.Assert(_config != null, "[EnemyMovement] _config 未赋值,请在 Prefab Inspector 中指定 EnemyStatsSO。", this);
|
||||
_rb = GetComponent<Rigidbody2D>();
|
||||
}
|
||||
|
||||
/// <summary>按 SO 配置速度水平移动。dir: +1 右 / -1 左 / 0 停止。</summary>
|
||||
public void MoveHorizontal(float dir)
|
||||
{
|
||||
if (_config == null) return;
|
||||
float speed = _config.WalkSpeed;
|
||||
_rb.velocity = new Vector2(dir * speed, _rb.velocity.y);
|
||||
UpdateFacing(dir);
|
||||
@@ -54,14 +55,42 @@ namespace BaseGames.Enemies
|
||||
_rb.velocity = new Vector2(0f, _rb.velocity.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 向目标位置抖跃(抛物线累加填充)。
|
||||
/// 计算初速使尔子到达目标,用 Impulse 施加力。
|
||||
/// </summary>
|
||||
public void JumpToTarget(Vector2 target)
|
||||
{
|
||||
if (_rb == null) return;
|
||||
Vector2 delta = target - (Vector2)transform.position;
|
||||
float gravMag = Mathf.Abs(Physics2D.gravity.y * _rb.gravityScale);
|
||||
float timeAloft = Mathf.Max(0.1f, delta.x != 0f
|
||||
? Mathf.Abs(delta.x) / _config.RunSpeed
|
||||
: 0.5f);
|
||||
|
||||
float vy = (delta.y - 0.5f * (-gravMag) * timeAloft * timeAloft) / timeAloft;
|
||||
float vx = delta.x / timeAloft;
|
||||
|
||||
_rb.velocity = new Vector2(vx, vy);
|
||||
UpdateFacing(vx);
|
||||
}
|
||||
|
||||
private void UpdateFacing(float dir)
|
||||
{
|
||||
if (Mathf.Approximately(dir, 0f)) return;
|
||||
int newDir = dir > 0f ? 1 : -1;
|
||||
if (newDir == _facingDir) return;
|
||||
_facingDir = newDir;
|
||||
Vector3 s = transform.localScale;
|
||||
transform.localScale = new Vector3(Mathf.Abs(s.x) * newDir, s.y, s.z);
|
||||
if (_spriteRenderer != null)
|
||||
{
|
||||
_spriteRenderer.flipX = newDir < 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// SpriteRenderer 未绑定时通过 localScale 翻转朝向
|
||||
Vector3 s = transform.localScale;
|
||||
transform.localScale = new Vector3(Mathf.Abs(s.x) * newDir, s.y, s.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
40
Assets/Scripts/Enemies/EnemyPoiseComponent.cs
Normal file
40
Assets/Scripts/Enemies/EnemyPoiseComponent.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人霸体组件(架构 06_CombatModule §13)。
|
||||
/// Awake 时自动注入到同节点 HurtBox(无需手动 Inspector 赋值)。
|
||||
/// EnemyBase 或 BehaviorDesigner 任务可调用 SetPoiseLevel / ResetPoiseLevel 切换超甲状态。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(EnemyBase))]
|
||||
public class EnemyPoiseComponent : MonoBehaviour, IPoiseSource
|
||||
{
|
||||
[SerializeField] private PoiseLevel _defaultPoiseLevel = PoiseLevel.None;
|
||||
|
||||
private PoiseLevel _currentPoiseLevel;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_currentPoiseLevel = _defaultPoiseLevel;
|
||||
|
||||
// 自动注入到同节点 HurtBox(架构 06 §13)
|
||||
if (TryGetComponent<HurtBox>(out var hurtBox))
|
||||
hurtBox.SetPoiseSource(this);
|
||||
}
|
||||
|
||||
// ── IPoiseSource ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>返回当前帧的霸体等级(供 HurtBox.ReceiveDamage 步骤 3 比较)。</summary>
|
||||
public PoiseLevel GetCurrentPoiseLevel() => _currentPoiseLevel;
|
||||
|
||||
// ── 外部 API ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>进入/退出超甲时由 EnemyBase 或 BehaviorDesigner 任务调用。</summary>
|
||||
public void SetPoiseLevel(PoiseLevel level) => _currentPoiseLevel = level;
|
||||
|
||||
/// <summary>恢复到默认霸体等级(超甲结束时调用)。</summary>
|
||||
public void ResetPoiseLevel() => _currentPoiseLevel = _defaultPoiseLevel;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/EnemyPoiseComponent.cs.meta
Normal file
11
Assets/Scripts/Enemies/EnemyPoiseComponent.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6fa63da67d464c54f840a6f0df7e9508
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
104
Assets/Scripts/Enemies/EnemyQuotaManager.cs
Normal file
104
Assets/Scripts/Enemies/EnemyQuotaManager.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人 BehaviorTree 配额管理器(架构 07_EnemyModule §13)。
|
||||
/// 每 REBALANCE_INTERVAL 帧,按到玩家距离排序已注册敌人,
|
||||
/// 仅对最近的 _maxActiveBehaviorTrees 个敌人启用 BT。
|
||||
/// 挂载在场景管理器 GameObject 上(每场景一个实例)。
|
||||
/// </summary>
|
||||
public class EnemyQuotaManager : MonoBehaviour
|
||||
{
|
||||
[SerializeField, Min(1)] private int _maxActiveBehaviorTrees = 12;
|
||||
[Tooltip("PlayerController.Start() 广播此频道,替代 FindWithTag")]
|
||||
[SerializeField] private TransformEventChannelSO _onPlayerSpawned;
|
||||
|
||||
private const int REBALANCE_INTERVAL = 10;
|
||||
|
||||
private int _frameCount;
|
||||
// _registeredSet 用于 O(1) 重复检测,_registered List 用于 Sort
|
||||
private readonly HashSet<EnemyBase> _registeredSet = new();
|
||||
private readonly List<EnemyBase> _registered = new();
|
||||
private readonly Dictionary<EnemyBase, int> _indexMap = new();
|
||||
// 缓存玩家 Transform
|
||||
private Transform _playerTransform;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_onPlayerSpawned?.Subscribe(OnPlayerSpawned).AddTo(_subs);
|
||||
}
|
||||
|
||||
private void OnDisable() => _subs.Clear();
|
||||
|
||||
private void OnPlayerSpawned(Transform playerTransform)
|
||||
=> _playerTransform = playerTransform;
|
||||
|
||||
// ── 注册 / 注销 ───────────────────────────────────────────────────
|
||||
public void Register(EnemyBase enemy)
|
||||
{
|
||||
if (enemy != null && _registeredSet.Add(enemy))
|
||||
{
|
||||
_indexMap[enemy] = _registered.Count;
|
||||
_registered.Add(enemy);
|
||||
}
|
||||
}
|
||||
|
||||
public void Unregister(EnemyBase enemy)
|
||||
{
|
||||
if (enemy == null || !_registeredSet.Remove(enemy)) return;
|
||||
int idx = _indexMap[enemy];
|
||||
int last = _registered.Count - 1;
|
||||
if (idx != last)
|
||||
{
|
||||
var moved = _registered[last];
|
||||
_registered[idx] = moved;
|
||||
_indexMap[moved] = idx;
|
||||
}
|
||||
_registered.RemoveAt(last);
|
||||
_indexMap.Remove(enemy);
|
||||
}
|
||||
|
||||
// ── Unity 生命周期 ────────────────────────────────────────────────
|
||||
private void Update()
|
||||
{
|
||||
if (++_frameCount % REBALANCE_INTERVAL == 0)
|
||||
Rebalance();
|
||||
}
|
||||
|
||||
// ── 内部 ──────────────────────────────────────────────────────────
|
||||
private void Rebalance()
|
||||
{
|
||||
if (_registered.Count == 0) return;
|
||||
|
||||
var playerPos = _playerTransform != null ? _playerTransform.position : Vector3.zero;
|
||||
|
||||
// 按距离平方升序排序(避免开方,性能更好)
|
||||
_registered.Sort((a, b) =>
|
||||
{
|
||||
if (a == null) return 1;
|
||||
if (b == null) return -1;
|
||||
float sqA = (a.transform.position - playerPos).sqrMagnitude;
|
||||
float sqB = (b.transform.position - playerPos).sqrMagnitude;
|
||||
return sqA.CompareTo(sqB);
|
||||
});
|
||||
|
||||
#if GRAPH_DESIGNER
|
||||
for (int i = _registered.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var enemy = _registered[i];
|
||||
if (enemy == null) { _registered.RemoveAt(i); continue; }
|
||||
|
||||
var bt = enemy.BehaviorTree;
|
||||
bool active = i < _maxActiveBehaviorTrees;
|
||||
|
||||
if (bt != null && bt.enabled != active)
|
||||
bt.enabled = active;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/EnemyQuotaManager.cs.meta
Normal file
11
Assets/Scripts/Enemies/EnemyQuotaManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8360f9ccf30db749a99739b3ccfe778
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,29 +1,67 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人运行时数值组件(架构 07_EnemyModule §2)。
|
||||
/// 由 EnemyBase.Awake() 通过 Initialize(EnemyStatsSO) 注入配置。
|
||||
/// 同时订阅难度变更事件,支持游戏进行中切换难度(架构 19 §5)。
|
||||
/// </summary>
|
||||
public class EnemyStats : MonoBehaviour
|
||||
{
|
||||
private EnemyStatsSO _config;
|
||||
|
||||
[SerializeField] private DifficultyChangedEventChannel _onDifficultyChanged;
|
||||
|
||||
public int MaxHP { get; private set; }
|
||||
public int CurrentHP { get; private set; }
|
||||
public int Defense { get; private set; }
|
||||
public float AttackCooldownTimer { get; private set; }
|
||||
|
||||
/// <summary>每帧由 EnemyBase 更新(读取玩家位置后写入)。</summary>
|
||||
public float DistanceToPlayer { get; set; }
|
||||
/// <summary>
|
||||
/// 每帧由 EnemyBase 更新(sqrMagnitude,避免 sqrt 开销)。
|
||||
/// 使用方请与 range*range 比较,而非直接与 range 比较。
|
||||
/// </summary>
|
||||
public float SqrDistanceToPlayer { get; set; }
|
||||
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_onDifficultyChanged?.Subscribe(HandleDifficultyChanged).AddTo(_subs);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_subs.Clear();
|
||||
}
|
||||
|
||||
public void Initialize(EnemyStatsSO so)
|
||||
{
|
||||
_config = so;
|
||||
MaxHP = so.MaxHP;
|
||||
CurrentHP = so.MaxHP;
|
||||
Defense = so.Defense;
|
||||
_config = so;
|
||||
Defense = so.Defense; // Defense 不随难度缩放(架构 19 §5)
|
||||
ApplyHPScaler();
|
||||
CurrentHP = MaxHP;
|
||||
}
|
||||
|
||||
/// <summary>难度变更时重算 HP(保持 HP 比例,架构 19 §5)。</summary>
|
||||
private void HandleDifficultyChanged(DifficultyLevel _)
|
||||
{
|
||||
if (_config == null) return;
|
||||
float hpRatio = MaxHP > 0 ? (float)CurrentHP / MaxHP : 1f;
|
||||
ApplyHPScaler();
|
||||
CurrentHP = Mathf.Clamp(Mathf.RoundToInt(MaxHP * hpRatio), 1, MaxHP);
|
||||
}
|
||||
|
||||
private void ApplyHPScaler()
|
||||
{
|
||||
if (_config == null) return;
|
||||
var scaler = ServiceLocator.GetOrDefault<IDifficultyService>()?.CurrentScaler;
|
||||
MaxHP = scaler != null
|
||||
? Mathf.Max(1, Mathf.RoundToInt(_config.MaxHP * scaler.EnemyHPMultiplier))
|
||||
: _config.MaxHP;
|
||||
}
|
||||
|
||||
public void TakeDamage(int amount)
|
||||
@@ -39,7 +77,7 @@ namespace BaseGames.Enemies
|
||||
|
||||
public void ResetAttackCooldown()
|
||||
{
|
||||
AttackCooldownTimer = _config != null ? _config.AttackCooldown : 1f;
|
||||
AttackCooldownTimer = _config.AttackCooldown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,5 +27,11 @@ namespace BaseGames.Enemies
|
||||
[Header("击退(作为来源时)")]
|
||||
public float KnockbackForce = 5f;
|
||||
public float HitStunDuration = 0.3f;
|
||||
|
||||
[Header("视线检测(BatchLOSSystem)")]
|
||||
[Tooltip("相对 transform.position 的眼睛偏移量")]
|
||||
public Vector2 EyeOffset = new Vector2(0f, 0.8f);
|
||||
[Tooltip("遮挡 LOS 的物理图层")]
|
||||
public LayerMask LOSBlockingMask = 1; // Default layer
|
||||
}
|
||||
}
|
||||
|
||||
70
Assets/Scripts/Enemies/FlyingEnemy.cs
Normal file
70
Assets/Scripts/Enemies/FlyingEnemy.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 飞行敌人。不依赖 PathBerserker2d 导航,直接通过 Rigidbody2D 向玩家移动。
|
||||
/// 接触玩家时造成伤害。
|
||||
/// </summary>
|
||||
public class FlyingEnemy : EnemyBase
|
||||
{
|
||||
[SerializeField] private float _chaseSpeed = 3f;
|
||||
[SerializeField] private DamageSourceSO _contactDamageSource;
|
||||
|
||||
private Rigidbody2D _rb;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
_rb = GetComponent<Rigidbody2D>();
|
||||
if (_rb != null)
|
||||
{
|
||||
_rb.gravityScale = 0f;
|
||||
_rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (_playerTransform == null || _rb == null) return;
|
||||
if (CurrentState == EnemyStateType.Dead ||
|
||||
CurrentState == EnemyStateType.Stagger) return;
|
||||
|
||||
// 向玩家移动
|
||||
Vector2 targetPos = _playerTransform.position;
|
||||
Vector2 myPos = _rb.position;
|
||||
Vector2 newPos = Vector2.MoveTowards(myPos, targetPos, _chaseSpeed * Time.deltaTime);
|
||||
_rb.MovePosition(newPos);
|
||||
}
|
||||
|
||||
public override void StopMovement()
|
||||
{
|
||||
if (_rb != null) _rb.velocity = Vector2.zero;
|
||||
}
|
||||
|
||||
public override void MoveInDirection(float dir)
|
||||
{
|
||||
if (_rb != null) _rb.velocity = new Vector2(dir, 0f) * _chaseSpeed;
|
||||
}
|
||||
|
||||
private void OnTriggerStay2D(Collider2D other)
|
||||
{
|
||||
if (_contactDamageSource == null) return;
|
||||
if (CurrentState == EnemyStateType.Dead) return;
|
||||
|
||||
var hurtBox = other.GetComponent<HurtBox>();
|
||||
if (hurtBox == null) return;
|
||||
|
||||
Vector2 knockDir = ((Vector2)other.bounds.center - _rb.position).normalized;
|
||||
var info = DamageInfo.From(
|
||||
_contactDamageSource,
|
||||
knockDir,
|
||||
transform.position,
|
||||
gameObject.layer);
|
||||
hurtBox.ReceiveDamage(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/FlyingEnemy.cs.meta
Normal file
11
Assets/Scripts/Enemies/FlyingEnemy.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ab1535e1bfecb644a86453b33320e9dc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
27
Assets/Scripts/Enemies/ILOSRequester.cs
Normal file
27
Assets/Scripts/Enemies/ILOSRequester.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// BatchLOSSystem 的注册接口(架构 07_EnemyModule §12)。
|
||||
/// 实现此接口的 EnemyBase 子类可以注册到 BatchLOSSystem,
|
||||
/// 以批处理方式接收 LOS(Line of Sight)检测结果。
|
||||
/// </summary>
|
||||
public interface ILOSRequester
|
||||
{
|
||||
/// <summary>射线起点(通常是眼部位置)。</summary>
|
||||
Vector2 LOSOrigin { get; }
|
||||
|
||||
/// <summary>射线终点(通常是玩家位置)。</summary>
|
||||
Vector2 LOSTarget { get; }
|
||||
|
||||
/// <summary>遮挡 LOS 的物理图层。</summary>
|
||||
LayerMask LOSBlockingMask { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 接收 LOS 检测结果。
|
||||
/// <paramref name="hasLineOfSight"/>: true = 有视线,false = 被遮挡。
|
||||
/// </summary>
|
||||
void ReceiveLOSResult(bool hasLineOfSight);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/ILOSRequester.cs.meta
Normal file
11
Assets/Scripts/Enemies/ILOSRequester.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89145f6fbc97f53419fa3ce81fcb6342
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -25,6 +25,9 @@ namespace BaseGames.Enemies
|
||||
/// <summary>当前帧是否在移动(速度 > 0.01 且有有效路径)。</summary>
|
||||
bool IsMoving { get; }
|
||||
|
||||
/// <summary>是否接近平台边缘(脚下或前方无地面时为 true)。</summary>
|
||||
bool IsNearEdge();
|
||||
|
||||
/// <summary>路径寻路失败事件(目标不可达时触发)。</summary>
|
||||
event Action OnNavPathFailed;
|
||||
}
|
||||
@@ -37,6 +40,7 @@ namespace BaseGames.Enemies
|
||||
public bool IsAtDestination() => true;
|
||||
public void SetSpeed(float _) { }
|
||||
public bool IsMoving => false;
|
||||
public bool IsNearEdge() => false;
|
||||
public event Action OnNavPathFailed { add { } remove { } }
|
||||
}
|
||||
}
|
||||
|
||||
72
Assets/Scripts/Enemies/LootResolver.cs
Normal file
72
Assets/Scripts/Enemies/LootResolver.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.World;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 战利品解算器(静态工具类)。
|
||||
/// 根据 <see cref="LootTableSO"/> 和当前难度决定掉落内容。
|
||||
/// </summary>
|
||||
public static class LootResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// 解算并执行战利品掉落。
|
||||
/// <para>保底 Geo:直接加入玩家(通过事件频道)或在 <paramref name="worldPosition"/> 生成拾取物。</para>
|
||||
/// <para>随机物品:加权随机后通过 CollectibleSpawner 实例化拾取物。</para>
|
||||
/// </summary>
|
||||
public static void Resolve(LootTableSO table, Vector2 worldPosition)
|
||||
{
|
||||
if (table == null) return;
|
||||
|
||||
// 保底 Geo
|
||||
int guaranteedGeo = Random.Range(table.GuaranteedGeoMin, table.GuaranteedGeoMax + 1);
|
||||
ApplyDifficultyGeoScale(ref guaranteedGeo);
|
||||
if (guaranteedGeo > 0)
|
||||
CollectibleSpawner.SpawnGeo(worldPosition, guaranteedGeo);
|
||||
|
||||
// 加权随机物品掉落
|
||||
if (table.Entries == null || table.Entries.Length == 0) return;
|
||||
|
||||
var dm = ServiceLocator.GetOrDefault<IDifficultyService>();
|
||||
bool isHard = dm != null &&
|
||||
(int)dm.CurrentLevel >= (int)DifficultyLevel.Hard;
|
||||
|
||||
float totalWeight = 0f;
|
||||
foreach (var entry in table.Entries)
|
||||
{
|
||||
float w = entry.BaseWeight;
|
||||
if (isHard && entry.ScaleWithDifficulty) w *= 1.5f;
|
||||
totalWeight += w;
|
||||
}
|
||||
|
||||
if (totalWeight <= 0f) return;
|
||||
|
||||
float roll = Random.Range(0f, totalWeight);
|
||||
float accum = 0f;
|
||||
foreach (var entry in table.Entries)
|
||||
{
|
||||
float w = entry.BaseWeight;
|
||||
if (isHard && entry.ScaleWithDifficulty) w *= 1.5f;
|
||||
accum += w;
|
||||
if (roll <= accum)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(entry.ItemId))
|
||||
CollectibleSpawner.SpawnItem(worldPosition, entry.ItemId);
|
||||
else if (entry.GeoAmount > 0)
|
||||
CollectibleSpawner.SpawnGeo(worldPosition, entry.GeoAmount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyDifficultyGeoScale(ref int geo)
|
||||
{
|
||||
var dm = ServiceLocator.GetOrDefault<IDifficultyService>();
|
||||
if (dm?.CurrentScaler == null) return;
|
||||
float mult = dm.CurrentScaler.GeoDropMultiplier;
|
||||
geo = Mathf.RoundToInt(geo * mult);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/LootResolver.cs.meta
Normal file
11
Assets/Scripts/Enemies/LootResolver.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e04aa9d1a069e9943a2582cf8af31c67
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
29
Assets/Scripts/Enemies/LootTableSO.cs
Normal file
29
Assets/Scripts/Enemies/LootTableSO.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 战利品表 ScriptableObject。定义敌人死亡后可掉落的物品和保底 Geo 区间。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Enemies/LootTable")]
|
||||
public class LootTableSO : ScriptableObject
|
||||
{
|
||||
[Serializable]
|
||||
public struct LootEntry
|
||||
{
|
||||
public string ItemId;
|
||||
public int GeoAmount;
|
||||
/// <summary>基础权重(难度 Hard 以上会对 ScaleWithDifficulty 项目额外加权)。</summary>
|
||||
public float BaseWeight;
|
||||
public bool ScaleWithDifficulty;
|
||||
}
|
||||
|
||||
[Header("掉落物")]
|
||||
public LootEntry[] Entries;
|
||||
|
||||
[Header("保底 Geo")]
|
||||
public int GuaranteedGeoMin = 1;
|
||||
public int GuaranteedGeoMax = 5;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/LootTableSO.cs.meta
Normal file
11
Assets/Scripts/Enemies/LootTableSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a195219cac0f6ad458c3af11d63a7c83
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -48,6 +48,17 @@ namespace BaseGames.Enemies.Navigation
|
||||
{
|
||||
if (_movement != null) _movement.movementSpeed = speed;
|
||||
}
|
||||
|
||||
public bool IsNearEdge()
|
||||
{
|
||||
// 双射线检测:脚下前方是否有地面
|
||||
if (_navAgent == null) return false;
|
||||
var origin = (Vector2)transform.position;
|
||||
var facing = transform.localScale.x >= 0f ? Vector2.right : Vector2.left;
|
||||
var groundMask = ~0; // 检测所有层;可收窄至 Ground 层
|
||||
bool groundAhead = Physics2D.Raycast(origin + facing * 0.3f, Vector2.down, 0.5f, groundMask);
|
||||
return !groundAhead;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
54
Assets/Scripts/Enemies/RangedEnemy.cs
Normal file
54
Assets/Scripts/Enemies/RangedEnemy.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Pool;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 远程敌人。重写 <see cref="EnemyBase.SpawnProjectile"/> 以从对象池生成抛射物。
|
||||
/// Behavior Designer 任务通过调用 SpawnProjectile(payload) 触发发射。
|
||||
/// </summary>
|
||||
public class RangedEnemy : EnemyBase
|
||||
{
|
||||
[SerializeField] private ProjectileConfigSO _projectileConfig;
|
||||
[SerializeField] private Transform _firePoint;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
Debug.Assert(_projectileConfig != null, $"[RangedEnemy] {name}: 未配置 ProjectileConfigSO。", this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从对象池生成一枚抖射物并朝玩家发射。
|
||||
/// payload 可留空;若传入多重配置键则由子类解析。
|
||||
/// </summary>
|
||||
public override void SpawnProjectile(string payload)
|
||||
{
|
||||
var spawnPos = _firePoint != null ? _firePoint.position : transform.position;
|
||||
var pool = ServiceLocator.GetOrDefault<IObjectPoolService>();
|
||||
if (pool == null)
|
||||
{
|
||||
Debug.LogWarning($"[RangedEnemy] {name}: IObjectPoolService 未就绪,无法生成抛射物。");
|
||||
return;
|
||||
}
|
||||
var go = pool.Spawn(
|
||||
_projectileConfig.PoolKey,
|
||||
spawnPos,
|
||||
Quaternion.identity);
|
||||
|
||||
if (go == null) return;
|
||||
|
||||
var proj = go.GetComponent<Projectile>();
|
||||
if (proj == null) return;
|
||||
|
||||
Vector2 dir = _playerTransform != null
|
||||
? ((Vector2)(_playerTransform.position - spawnPos)).normalized
|
||||
: (Vector2)transform.right;
|
||||
|
||||
var damageInfo = DamageInfo.From(_projectileConfig.DamageSource);
|
||||
proj.Initialize(_projectileConfig, damageInfo, dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/RangedEnemy.cs.meta
Normal file
11
Assets/Scripts/Enemies/RangedEnemy.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d659b7fca1be0848a9b8d35aaa6c62b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Enemies/States.meta
Normal file
8
Assets/Scripts/Enemies/States.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b008076fd3acda4db7895a3c4085159
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
14
Assets/Scripts/Enemies/States/EnemyControlledState.cs
Normal file
14
Assets/Scripts/Enemies/States/EnemyControlledState.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace BaseGames.Enemies.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 受控状态(默认行为)。
|
||||
/// 敌人处于 AI 正常控制下(Behavior Designer 运行中),无特殊 Enter/Exit 动画逻辑。
|
||||
/// </summary>
|
||||
public sealed class EnemyControlledState : IEnemyState
|
||||
{
|
||||
public EnemyStateType StateType => EnemyStateType.Controlled;
|
||||
|
||||
public void Enter(EnemyBase owner) { }
|
||||
public void Exit(EnemyBase owner) { }
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/States/EnemyControlledState.cs.meta
Normal file
11
Assets/Scripts/Enemies/States/EnemyControlledState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 08377192598a73e43a4037ef1ac3e3c8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
18
Assets/Scripts/Enemies/States/EnemyDeadState.cs
Normal file
18
Assets/Scripts/Enemies/States/EnemyDeadState.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace BaseGames.Enemies.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 死亡状态。Enter 仅作为扩展点(实际死亡流程由 EnemyBase.Die() 驱动),
|
||||
/// 防止后续添加死亡特效/逻辑时需要改动 EnemyBase。
|
||||
/// </summary>
|
||||
public sealed class EnemyDeadState : IEnemyState
|
||||
{
|
||||
public EnemyStateType StateType => EnemyStateType.Dead;
|
||||
|
||||
/// <summary>
|
||||
/// 死亡实际逻辑由 EnemyBase.Die() 处理(禁用碰撞体、播放动画、广播事件)。
|
||||
/// 此方法保留为扩展点,子类可重写状态字典条目加入自定义逻辑。
|
||||
/// </summary>
|
||||
public void Enter(EnemyBase owner) { }
|
||||
public void Exit(EnemyBase owner) { }
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/States/EnemyDeadState.cs.meta
Normal file
11
Assets/Scripts/Enemies/States/EnemyDeadState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f0078376e2cfd80419ca7f981c3009ab
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
26
Assets/Scripts/Enemies/States/EnemyHurtState.cs
Normal file
26
Assets/Scripts/Enemies/States/EnemyHurtState.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace BaseGames.Enemies.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 受击状态。播放受击动画,动画结束后自动回到 Controlled。
|
||||
/// 原 EnemyBase.ForceState 中的 Hurt if-else 逻辑已迁移至此。
|
||||
/// </summary>
|
||||
public sealed class EnemyHurtState : IEnemyState
|
||||
{
|
||||
public EnemyStateType StateType => EnemyStateType.Hurt;
|
||||
|
||||
public void Enter(EnemyBase owner)
|
||||
{
|
||||
if (owner.Animancer == null || owner.AnimConfig?.Hurt == null) return;
|
||||
|
||||
var animState = owner.Animancer.Play(owner.AnimConfig.Hurt);
|
||||
animState.Events(owner).OnEnd = () =>
|
||||
{
|
||||
// 只在仍处于 Hurt 时才回 Controlled,避免 Die 时被覆盖
|
||||
if (owner.CurrentState == EnemyStateType.Hurt)
|
||||
owner.ForceState(EnemyStateType.Controlled);
|
||||
};
|
||||
}
|
||||
|
||||
public void Exit(EnemyBase owner) { }
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/States/EnemyHurtState.cs.meta
Normal file
11
Assets/Scripts/Enemies/States/EnemyHurtState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2bc4ca8eb93d1aa4e9c3f0be1c726161
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
25
Assets/Scripts/Enemies/States/EnemyStaggerState.cs
Normal file
25
Assets/Scripts/Enemies/States/EnemyStaggerState.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace BaseGames.Enemies.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 僵直状态(霸体耗尽时触发)。
|
||||
/// 播放 Stagger 动画(若配置),动画结束后自动回到 Controlled。
|
||||
/// </summary>
|
||||
public sealed class EnemyStaggerState : IEnemyState
|
||||
{
|
||||
public EnemyStateType StateType => EnemyStateType.Stagger;
|
||||
|
||||
public void Enter(EnemyBase owner)
|
||||
{
|
||||
if (owner.Animancer == null || owner.AnimConfig?.Stagger == null) return;
|
||||
|
||||
var animState = owner.Animancer.Play(owner.AnimConfig.Stagger);
|
||||
animState.Events(owner).OnEnd = () =>
|
||||
{
|
||||
if (owner.CurrentState == EnemyStateType.Stagger)
|
||||
owner.ForceState(EnemyStateType.Controlled);
|
||||
};
|
||||
}
|
||||
|
||||
public void Exit(EnemyBase owner) { }
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/States/EnemyStaggerState.cs.meta
Normal file
11
Assets/Scripts/Enemies/States/EnemyStaggerState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f2c296d9aa57cfe49aa907367c99f27f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
19
Assets/Scripts/Enemies/States/IEnemyState.cs
Normal file
19
Assets/Scripts/Enemies/States/IEnemyState.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人状态 POCO 接口。
|
||||
/// EnemyBase.ForceState 通过字典分发 Enter/Exit,
|
||||
/// 枚举 EnemyStateType 保持对外 API 不变。
|
||||
/// </summary>
|
||||
public interface IEnemyState
|
||||
{
|
||||
/// <summary>对应的枚举值(用于字典键以及 BD 查询)。</summary>
|
||||
EnemyStateType StateType { get; }
|
||||
|
||||
/// <summary>进入此状态时调用。</summary>
|
||||
void Enter(EnemyBase owner);
|
||||
|
||||
/// <summary>离开此状态时调用。</summary>
|
||||
void Exit(EnemyBase owner);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Enemies/States/IEnemyState.cs.meta
Normal file
11
Assets/Scripts/Enemies/States/IEnemyState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ebc8841ddc5823844b18be2252b1315b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user