摄像机区域的架构改动
This commit is contained in:
8
Assets/_Game/Scripts/Enemies/AI.meta
Normal file
8
Assets/_Game/Scripts/Enemies/AI.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 458bf9c7d1eae52438922d9630862656
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
31
Assets/_Game/Scripts/Enemies/AI/BD_Attack.cs
Normal file
31
Assets/_Game/Scripts/Enemies/AI/BD_Attack.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:发动攻击。
|
||||
/// CanAttack() 检查通过后调用 BeginAttack,立即返回 Success。
|
||||
/// </summary>
|
||||
public class BD_Attack : Action
|
||||
{
|
||||
private EnemyBase _enemy;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_enemy = GetComponent<EnemyBase>();
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null || !_enemy.CanAttack())
|
||||
return TaskStatus.Failure;
|
||||
|
||||
_enemy.BeginAttack(AttackType.Melee);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/_Game/Scripts/Enemies/AI/BD_Attack.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/AI/BD_Attack.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6129e08d8bc28a24ebfb02c246e2e744
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
27
Assets/_Game/Scripts/Enemies/AI/BD_CanAttack.cs
Normal file
27
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_CanAttack.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_EnterPhase.cs
Normal file
31
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_EnterPhase.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_FaceTarget.cs
Normal file
32
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_FaceTarget.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_IsGrounded.cs
Normal file
27
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_IsGrounded.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_IsHPBelow.cs
Normal file
32
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_IsHPBelow.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_IsNearEdge.cs
Normal file
28
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_IsNearEdge.cs.meta
Normal file
11
Assets/_Game/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:
|
||||
31
Assets/_Game/Scripts/Enemies/AI/BD_IsPlayerInRange.cs
Normal file
31
Assets/_Game/Scripts/Enemies/AI/BD_IsPlayerInRange.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Conditional:检查玩家是否在指定范围内。
|
||||
/// 成功/失败直接驱动 BT 分支选择(Selector / Sequence 节点)。
|
||||
/// </summary>
|
||||
public class BD_IsPlayerInRange : Conditional
|
||||
{
|
||||
/// <summary>检测范围(Inspector 可配置,默认 6 米)。</summary>
|
||||
public float Range = 6f;
|
||||
|
||||
private EnemyBase _enemy;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_enemy = GetComponent<EnemyBase>();
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null) return TaskStatus.Failure;
|
||||
return _enemy.IsPlayerInRange(Range) ? TaskStatus.Success : TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/_Game/Scripts/Enemies/AI/BD_IsPlayerInRange.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/AI/BD_IsPlayerInRange.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9d176aa20179c6b43a6798432eb073a4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
28
Assets/_Game/Scripts/Enemies/AI/BD_IsPlayerVisible.cs
Normal file
28
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_IsPlayerVisible.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_IsStateMatch.cs
Normal file
43
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_IsStateMatch.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_JumpTo.cs
Normal file
45
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_JumpTo.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_MoveTo.cs
Normal file
42
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_MoveTo.cs.meta
Normal file
11
Assets/_Game/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:
|
||||
38
Assets/_Game/Scripts/Enemies/AI/BD_MoveToPlayer.cs
Normal file
38
Assets/_Game/Scripts/Enemies/AI/BD_MoveToPlayer.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// BD Action:移向玩家。
|
||||
/// OnStart 从 EnemyBase.PlayerTransform 获取玩家引用(由 _onPlayerSpawned 事件缓存,
|
||||
/// 避免 FindWithTag 全场景扫描)。OnUpdate 每帧更新导航目标。
|
||||
/// </summary>
|
||||
public class BD_MoveToPlayer : Action
|
||||
{
|
||||
private EnemyBase _enemy;
|
||||
|
||||
public override void OnStart()
|
||||
{
|
||||
_enemy = GetComponent<EnemyBase>();
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null) return TaskStatus.Failure;
|
||||
if (_enemy.PlayerTransform == null) return TaskStatus.Failure;
|
||||
|
||||
_enemy.MoveTo(_enemy.PlayerTransform.position);
|
||||
_enemy.FacePlayer();
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
public override void OnEnd()
|
||||
{
|
||||
_enemy?.StopMovement();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/_Game/Scripts/Enemies/AI/BD_MoveToPlayer.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/AI/BD_MoveToPlayer.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 67b2f5400ef71aa4fae183cf1ce1e649
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
64
Assets/_Game/Scripts/Enemies/AI/BD_Patrol.cs
Normal file
64
Assets/_Game/Scripts/Enemies/AI/BD_Patrol.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
#if GRAPH_DESIGNER
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||||
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
|
||||
using BaseGames.Enemies;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies.AI
|
||||
{
|
||||
/// <summary>
|
||||
/// 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()
|
||||
{
|
||||
_enemy = GetComponent<EnemyBase>();
|
||||
}
|
||||
|
||||
public override TaskStatus OnUpdate()
|
||||
{
|
||||
if (_enemy == null) return TaskStatus.Failure;
|
||||
|
||||
if (ShouldFlip())
|
||||
_dir = -_dir;
|
||||
|
||||
_enemy.MoveInDirection(_dir);
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
|
||||
public override void OnEnd()
|
||||
{
|
||||
_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
|
||||
11
Assets/_Game/Scripts/Enemies/AI/BD_Patrol.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/AI/BD_Patrol.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b0fd85d7c7ce18f4193bb3b70707d055
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
37
Assets/_Game/Scripts/Enemies/AI/BD_PlayAnimation.cs
Normal file
37
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_PlayAnimation.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_SetAlert.cs
Normal file
29
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_SetAlert.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_SpawnProjectile.cs
Normal file
39
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_SpawnProjectile.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_StopMovement.cs
Normal file
28
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_StopMovement.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_SummonMinions.cs
Normal file
43
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_SummonMinions.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_TelegraphAttack.cs
Normal file
40
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_TelegraphAttack.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_TeleportTo.cs
Normal file
23
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_TeleportTo.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_Wait.cs
Normal file
30
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_Wait.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_WaitForAnimation.cs
Normal file
34
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_WaitForAnimation.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_WaitRandom.cs
Normal file
33
Assets/_Game/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/_Game/Scripts/Enemies/AI/BD_WaitRandom.cs.meta
Normal file
11
Assets/_Game/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:
|
||||
21
Assets/_Game/Scripts/Enemies/AI/BaseGames.Enemies.AI.asmdef
Normal file
21
Assets/_Game/Scripts/Enemies/AI/BaseGames.Enemies.AI.asmdef
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"precompiledReferences": [],
|
||||
"name": "BaseGames.Enemies.AI",
|
||||
"defineConstraints": [],
|
||||
"noEngineReferences": false,
|
||||
"versionDefines": [],
|
||||
"rootNamespace": "BaseGames.Enemies.AI",
|
||||
"references": [
|
||||
"BaseGames.Core",
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Enemies",
|
||||
"BaseGames.Enemies.Boss.Patterns",
|
||||
"Opsive.BehaviorDesigner.Runtime",
|
||||
"Kybernetik.Animancer"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
"includePlatforms": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2f7ab9cf613d0c4fb44cdd94fea551f
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
97
Assets/_Game/Scripts/Enemies/AI/BatchLOSSystem.cs
Normal file
97
Assets/_Game/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/_Game/Scripts/Enemies/AI/BatchLOSSystem.cs.meta
Normal file
11
Assets/_Game/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:
|
||||
25
Assets/_Game/Scripts/Enemies/BaseGames.Enemies.asmdef
Normal file
25
Assets/_Game/Scripts/Enemies/BaseGames.Enemies.asmdef
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"precompiledReferences": [],
|
||||
"name": "BaseGames.Enemies",
|
||||
"defineConstraints": [],
|
||||
"noEngineReferences": false,
|
||||
"versionDefines": [],
|
||||
"rootNamespace": "BaseGames.Enemies",
|
||||
"references": [
|
||||
"BaseGames.Core",
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Combat",
|
||||
"BaseGames.Feedback",
|
||||
"BaseGames.World",
|
||||
"MoreMountains.Tools",
|
||||
"Kybernetik.Animancer",
|
||||
"Opsive.BehaviorDesigner.Runtime",
|
||||
"Unity.Addressables",
|
||||
"Unity.ResourceManager"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
"includePlatforms": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ecc7c40216aa154f81faf49c6d655de
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
32
Assets/_Game/Scripts/Enemies/BodyContactDamage.cs
Normal file
32
Assets/_Game/Scripts/Enemies/BodyContactDamage.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using BaseGames.Combat;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 接触伤害:组件启用时持续激活 HitBox,令敌人对接触到的目标定期造成伤害。
|
||||
/// 适用于无攻击动画的简单障碍物、环境危险或测试场景。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(HitBox))]
|
||||
public class BodyContactDamage : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float _repeatInterval = 0.5f;
|
||||
|
||||
private HitBox _hitBox;
|
||||
private float _timer;
|
||||
|
||||
private void Awake() => _hitBox = GetComponent<HitBox>();
|
||||
private void OnEnable() { _hitBox?.Activate(); _timer = 0f; }
|
||||
private void OnDisable() => _hitBox?.Deactivate();
|
||||
|
||||
private void Update()
|
||||
{
|
||||
_timer += Time.deltaTime;
|
||||
if (_timer >= _repeatInterval)
|
||||
{
|
||||
_timer = 0f;
|
||||
_hitBox.Activate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Enemies/BodyContactDamage.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/BodyContactDamage.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6def12af0589a9545b80eb5accf61bb6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/_Game/Scripts/Enemies/Boss.meta
Normal file
8
Assets/_Game/Scripts/Enemies/Boss.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe0b3fb5ffe55d84a86db3085bd868f1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
33
Assets/_Game/Scripts/Enemies/Boss/AttackPatternSO.cs
Normal file
33
Assets/_Game/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 = "BaseGames/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/_Game/Scripts/Enemies/Boss/AttackPatternSO.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/Boss/BossBase.cs
Normal file
48
Assets/_Game/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/_Game/Scripts/Enemies/Boss/BossBase.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/Boss/BossResourceConfigSO.cs
Normal file
26
Assets/_Game/Scripts/Enemies/Boss/BossResourceConfigSO.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Boss
|
||||
{
|
||||
/// <summary>
|
||||
/// Boss 自身资源(如愤怒值)的配置 ScriptableObject。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 47b7618faa6ceb3458714246d12f9e73
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
184
Assets/_Game/Scripts/Enemies/Boss/BossSkillExecutor.cs
Normal file
184
Assets/_Game/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?.Play();
|
||||
|
||||
yield return GetWFS(window.Duration);
|
||||
|
||||
_weakPointSystem?.SetActive(false, 1f, activateSpecific);
|
||||
window.CloseFeedback?.Play();
|
||||
}
|
||||
}
|
||||
|
||||
// ── 工具 ───────────────────────────────────────────────────────────────
|
||||
|
||||
private bool IsPlayerInRange() =>
|
||||
_playerTransform != null &&
|
||||
Vector2.Distance(transform.position, _playerTransform.position) < 8f;
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Enemies/Boss/BossSkillExecutor.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/Boss/BossSkillSO.cs
Normal file
61
Assets/_Game/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 = "BaseGames/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/_Game/Scripts/Enemies/Boss/BossSkillSO.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/Boss/BossSkillTypes.cs
Normal file
184
Assets/_Game/Scripts/Enemies/Boss/BossSkillTypes.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using BaseGames.Feedback;
|
||||
|
||||
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 SceneFeedback OpenFeedback;
|
||||
public SceneFeedback 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 SceneFeedback 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/_Game/Scripts/Enemies/Boss/BossSkillTypes.cs.meta
Normal file
11
Assets/_Game/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:
|
||||
8
Assets/_Game/Scripts/Enemies/Boss/Patterns.meta
Normal file
8
Assets/_Game/Scripts/Enemies/Boss/Patterns.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d8f5f23ee1dde046b1a7361ac1b6386
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"precompiledReferences": [],
|
||||
"name": "BaseGames.Enemies.Boss.Patterns",
|
||||
"defineConstraints": [],
|
||||
"noEngineReferences": false,
|
||||
"versionDefines": [],
|
||||
"rootNamespace": "BaseGames.Enemies.Boss.Patterns",
|
||||
"references": [
|
||||
"BaseGames.Core",
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Enemies",
|
||||
"BaseGames.Combat"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
"includePlatforms": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8bc3529e552a34a45998814c7cd056e6
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6f4987894dfe1648909b6863c003c31
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
26
Assets/_Game/Scripts/Enemies/Boss/SkillSequenceSO.cs
Normal file
26
Assets/_Game/Scripts/Enemies/Boss/SkillSequenceSO.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Boss
|
||||
{
|
||||
/// <summary>
|
||||
/// 有序攻击序列(一个技能内的多段连段)。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/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/_Game/Scripts/Enemies/Boss/SkillSequenceSO.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/Boss/WeakPointSystem.cs
Normal file
51
Assets/_Game/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/_Game/Scripts/Enemies/Boss/WeakPointSystem.cs.meta
Normal file
11
Assets/_Game/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:
|
||||
44
Assets/_Game/Scripts/Enemies/EnemyAnimationConfigSO.cs
Normal file
44
Assets/_Game/Scripts/Enemies/EnemyAnimationConfigSO.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人动画配置 SO(架构 07_EnemyModule §5)。
|
||||
/// 所有字段为 AnimationClip,由 Animancer 直接播放。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/Enemies/AnimationConfig")]
|
||||
public class EnemyAnimationConfigSO : ScriptableObject
|
||||
{
|
||||
[Header("基础")]
|
||||
public AnimationClip Idle;
|
||||
public AnimationClip Walk;
|
||||
public AnimationClip Run;
|
||||
|
||||
[Header("战斗")]
|
||||
public AnimationClip Attack;
|
||||
|
||||
[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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Enemies/EnemyAnimationConfigSO.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/EnemyAnimationConfigSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f7dd720bca19fcc49b22106fb65f7652
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
353
Assets/_Game/Scripts/Enemies/EnemyBase.cs
Normal file
353
Assets/_Game/Scripts/Enemies/EnemyBase.cs
Normal file
@@ -0,0 +1,353 @@
|
||||
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 任务提供统一虚方法接口。
|
||||
/// 包含:BD 接口、受击、死亡流程。
|
||||
/// ⚠️ _nav 字段类型为 IPathAgent(在 BaseGames.Enemies.Navigation 中实现具体类)。
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
[Header("子组件(Prefab Inspector 绑定)")]
|
||||
[SerializeField] protected EnemyStats _stats;
|
||||
[SerializeField] protected EnemyMovement _movement;
|
||||
[SerializeField] protected EnemyCombat _combat;
|
||||
[SerializeField] protected AnimancerComponent _animancer;
|
||||
[SerializeField] protected EnemyFeedback _feedback;
|
||||
[SerializeField] protected HurtBox _hurtBox;
|
||||
|
||||
[Header("事件频道")]
|
||||
[SerializeField] private BaseGames.Core.Events.StringEventChannelSO _onEnemyDied;
|
||||
/// <summary>
|
||||
/// 玩家生成事件频道(由 PlayerController.Start() 广播)。
|
||||
/// 配置后替代 FindWithTag,避免 N 个敌人同帧全场景标签扫描。
|
||||
/// </summary>
|
||||
[SerializeField] private BaseGames.Core.Events.TransformEventChannelSO _onPlayerSpawned;
|
||||
|
||||
// ── 导航代理(IPathAgent;由 EnemyNavAgent 实现)───────────────────
|
||||
// 通过接口引用,避免对 Navigation 程序集的直接依赖。
|
||||
// 由子类 / Inspector 注入,或者运行时 GetComponent<IPathAgent>() 获取。
|
||||
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 IsAlive => _currentState != EnemyStateType.Dead;
|
||||
public bool IsInvincible => _currentState == EnemyStateType.Dead;
|
||||
public int Defense => _stats != null ? _stats.Defense : 0;
|
||||
|
||||
public void TakeDamage(DamageInfo info)
|
||||
{
|
||||
if (_currentState == EnemyStateType.Dead) return;
|
||||
|
||||
_stats?.TakeDamage(info.FinalDamage);
|
||||
_feedback?.OnHit(info);
|
||||
|
||||
if (_stats != null && _stats.CurrentHP <= 0)
|
||||
{
|
||||
Die();
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据霸体等级选择 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)
|
||||
=> _nav?.RequestMoveTo(target);
|
||||
|
||||
public virtual void MoveInDirection(float dir)
|
||||
=> _movement?.MoveHorizontal(dir);
|
||||
|
||||
public virtual void StopMovement()
|
||||
{
|
||||
_nav?.StopNavigation();
|
||||
_movement?.StopHorizontal();
|
||||
}
|
||||
|
||||
public virtual void BeginAttack(AttackType type)
|
||||
{
|
||||
_combat?.StartAttack(type);
|
||||
_stats?.ResetAttackCooldown();
|
||||
}
|
||||
|
||||
public virtual bool CanAttack()
|
||||
=> _stats != null && _stats.AttackCooldownTimer <= 0f;
|
||||
|
||||
public virtual bool IsPlayerInRange(float range)
|
||||
=> _stats != null && _stats.SqrDistanceToPlayer <= range * range;
|
||||
|
||||
public virtual bool IsPlayerVisible()
|
||||
=> _losResult; // BatchLOSSystem 写入;初始 false(未见玩家)
|
||||
|
||||
public virtual void FacePlayer()
|
||||
{
|
||||
if (_playerTransform != null)
|
||||
_movement?.FaceTarget(_playerTransform.position);
|
||||
}
|
||||
|
||||
public virtual void Knockback(DamageInfo info)
|
||||
{
|
||||
if (info.Flags.HasFlag(DamageFlags.NoKnockback)) return;
|
||||
_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;
|
||||
|
||||
// Enter 新状态
|
||||
if (_stateObjs.TryGetValue(newState, out var next))
|
||||
next.Enter(this);
|
||||
}
|
||||
|
||||
// ── Unity 生命周期 ────────────────────────────────────────────────
|
||||
protected virtual void Awake()
|
||||
{
|
||||
// 初始化 POCO 状态对象(子类可在调用 base.Awake() 后替换字典条目)
|
||||
_stateObjs[EnemyStateType.Controlled] = new EnemyControlledState();
|
||||
_stateObjs[EnemyStateType.Hurt] = new EnemyHurtState();
|
||||
_stateObjs[EnemyStateType.Stagger] = new EnemyStaggerState();
|
||||
_stateObjs[EnemyStateType.Dead] = new EnemyDeadState();
|
||||
|
||||
_nav = GetComponent<IPathAgent>() ?? new NullPathAgent();
|
||||
_poiseSource = GetComponent<IPoiseSource>();
|
||||
|
||||
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.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);
|
||||
}
|
||||
|
||||
// ── 内部 ──────────────────────────────────────────────────────────
|
||||
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()
|
||||
{
|
||||
if (_currentState == EnemyStateType.Dead) return;
|
||||
ForceState(EnemyStateType.Dead);
|
||||
|
||||
// 禁用所有碰撞体
|
||||
foreach (var col in GetComponentsInChildren<Collider2D>())
|
||||
col.enabled = false;
|
||||
|
||||
// 播放死亡动画
|
||||
if (_animancer != null && _animConfig != null && _animConfig.Dead != null)
|
||||
{
|
||||
var state = _animancer.Play(_animConfig.Dead);
|
||||
state.Events(this).OnEnd = () => Destroy(gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject, 1.5f);
|
||||
}
|
||||
|
||||
_feedback?.OnDeath();
|
||||
_onEnemyDied?.Raise(_enemyId);
|
||||
OnDied?.Invoke();
|
||||
}
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (_statsSO == null) return;
|
||||
|
||||
// ── 侦测范围(淡橙圆,始终可见)────────────────────────────
|
||||
Gizmos.color = new Color(1f, 0.6f, 0.1f, 0.18f);
|
||||
HitBox.DrawWireCircle2D(transform.position, _statsSO.DetectRange);
|
||||
|
||||
// ── 攻击范围(淡红圆)───────────────────────────────────────
|
||||
Gizmos.color = new Color(1f, 0.15f, 0.15f, 0.22f);
|
||||
HitBox.DrawWireCircle2D(transform.position, _statsSO.AttackRange);
|
||||
|
||||
// ── 运行时:LOS 连线 ────────────────────────────────────────
|
||||
if (!Application.isPlaying || _playerTransform == null) return;
|
||||
|
||||
Vector3 eyeWorld = transform.position + new Vector3(_statsSO.EyeOffset.x, _statsSO.EyeOffset.y, 0f);
|
||||
Vector3 playerPos = _playerTransform.position;
|
||||
float sqrDist = (playerPos - transform.position).sqrMagnitude;
|
||||
bool inRange = sqrDist <= _statsSO.DetectRange * _statsSO.DetectRange;
|
||||
|
||||
// 眼睛位置小圆点(金黄)
|
||||
Gizmos.color = new Color(1f, 0.9f, 0.2f, 0.85f);
|
||||
HitBox.DrawWireCircle2D(eyeWorld, 0.07f, 8);
|
||||
|
||||
if (inRange || _losResult)
|
||||
{
|
||||
// 有 LOS → 橙色实线;在范围内但遮挡 → 灰色虚感(透明度低)
|
||||
Gizmos.color = _losResult
|
||||
? new Color(1f, 0.5f, 0f, 0.85f)
|
||||
: new Color(0.6f, 0.6f, 0.6f, 0.25f);
|
||||
Gizmos.DrawLine(eyeWorld, playerPos);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (_statsSO == null) return;
|
||||
|
||||
// 选中时加亮范围圆
|
||||
Gizmos.color = new Color(1f, 0.6f, 0.1f, 0.6f);
|
||||
HitBox.DrawWireCircle2D(transform.position, _statsSO.DetectRange);
|
||||
|
||||
Gizmos.color = new Color(1f, 0.2f, 0.2f, 0.7f);
|
||||
HitBox.DrawWireCircle2D(transform.position, _statsSO.AttackRange);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// ── 枚举(架构 07 §1)────────────────────────────────────────────────
|
||||
public enum EnemyStateType { Controlled, Hurt, Stagger, Dead }
|
||||
public enum AttackType { Melee, Ranged, Special }
|
||||
}
|
||||
11
Assets/_Game/Scripts/Enemies/EnemyBase.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/EnemyBase.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a2dbfbcc31a4c34cbd3ac893f02e07d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
38
Assets/_Game/Scripts/Enemies/EnemyCombat.cs
Normal file
38
Assets/_Game/Scripts/Enemies/EnemyCombat.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人战斗组件(架构 07_EnemyModule §4)。
|
||||
/// HitBox 按 AttackType 索引管理,动画事件触发 HitBox 开关。
|
||||
/// </summary>
|
||||
public class EnemyCombat : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private HitBox[] _hitBoxes; // Inspector 按 AttackType 索引绑定
|
||||
|
||||
public void StartAttack(AttackType type)
|
||||
{
|
||||
int idx = (int)type;
|
||||
EnableHitBox(idx);
|
||||
}
|
||||
|
||||
public void EnableHitBox(int index)
|
||||
{
|
||||
if (_hitBoxes == null || index >= _hitBoxes.Length) return;
|
||||
_hitBoxes[index]?.Activate();
|
||||
}
|
||||
|
||||
public void DisableHitBox(int index)
|
||||
{
|
||||
if (_hitBoxes == null || index >= _hitBoxes.Length) return;
|
||||
_hitBoxes[index]?.Deactivate();
|
||||
}
|
||||
|
||||
public void DisableAllHitBoxes()
|
||||
{
|
||||
if (_hitBoxes == null) return;
|
||||
foreach (var hb in _hitBoxes) hb?.Deactivate();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Enemies/EnemyCombat.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/EnemyCombat.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b092975777446a24ba295c6d30470935
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
63
Assets/_Game/Scripts/Enemies/EnemyFeedback.cs
Normal file
63
Assets/_Game/Scripts/Enemies/EnemyFeedback.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using UnityEngine;
|
||||
using MoreMountains.Feedbacks;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Feedback;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人反馈播放器:实现 IFeedbackPlayer,将行为语义映射到 MMF_Player 实例。
|
||||
/// 同时保留 OnHit/OnDeath 语义方法供 EnemyBase 直接调用(转发给接口方法)。
|
||||
/// </summary>
|
||||
public class EnemyFeedback : MonoBehaviour, IFeedbackPlayer
|
||||
{
|
||||
[Header("命中反馈")]
|
||||
[SerializeField] private MMF_Player _onHitLight;
|
||||
[SerializeField] private MMF_Player _onHitMedium;
|
||||
[SerializeField] private MMF_Player _onHitHeavy;
|
||||
|
||||
[Header("受伤 / 死亡反馈")]
|
||||
[SerializeField] private MMF_Player _onTakeHit;
|
||||
[SerializeField] private MMF_Player _onDeath;
|
||||
|
||||
// ── IFeedbackPlayer ──────────────────────────────────────────────────────
|
||||
public void PlayHit(HitWeight weight)
|
||||
{
|
||||
var player = weight switch
|
||||
{
|
||||
HitWeight.Light => _onHitLight,
|
||||
HitWeight.Medium => _onHitMedium,
|
||||
HitWeight.Heavy => _onHitHeavy,
|
||||
_ => _onHitLight,
|
||||
};
|
||||
player?.PlayFeedbacks();
|
||||
}
|
||||
|
||||
public void PlayTakeHit() => _onTakeHit?.PlayFeedbacks();
|
||||
public void PlayDeath() => _onDeath?.PlayFeedbacks();
|
||||
|
||||
// 以下方法对敌人无意义,提供空实现保持接口完整
|
||||
public void PlayParrySuccess() { }
|
||||
public void PlayHeal() { }
|
||||
public void PlayLandImpact() { }
|
||||
public void PlayAttackWhoosh() { }
|
||||
public void PlayJumpLaunch() { }
|
||||
public void PlayFootstep() { }
|
||||
public void TriggerPreset(string presetId) { }
|
||||
public void PlaySFXById(string sfxId) { }
|
||||
|
||||
// ── EnemyBase 语义方法 ────────────────────────────────────────────
|
||||
/// <summary>受到伤害时调用(由 EnemyBase 触发)。</summary>
|
||||
public void OnHit(DamageInfo info)
|
||||
{
|
||||
PlayTakeHit();
|
||||
}
|
||||
|
||||
/// <summary>死亡时调用(由 EnemyBase 触发)。</summary>
|
||||
public void OnDeath()
|
||||
{
|
||||
PlayDeath();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
Assets/_Game/Scripts/Enemies/EnemyFeedback.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/EnemyFeedback.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9892874f77e34964092168ab0642a47c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
142
Assets/_Game/Scripts/Enemies/EnemyMovement.cs
Normal file
142
Assets/_Game/Scripts/Enemies/EnemyMovement.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人移动组件(架构 07_EnemyModule §3)。
|
||||
/// 实现:水平移动、面向目标、击退。
|
||||
/// ⚠️ 使用 Rigidbody2D.velocity(Unity 2022 LTS)。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Rigidbody2D))]
|
||||
public class EnemyMovement : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private EnemyStatsSO _config;
|
||||
[SerializeField] private SpriteRenderer _spriteRenderer;
|
||||
|
||||
private Rigidbody2D _rb;
|
||||
private int _facingDir = 1;
|
||||
|
||||
public bool IsGrounded { get; private set; }
|
||||
|
||||
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)
|
||||
{
|
||||
float speed = _config.WalkSpeed;
|
||||
_rb.velocity = new Vector2(dir * speed, _rb.velocity.y);
|
||||
UpdateFacing(dir);
|
||||
}
|
||||
|
||||
/// <summary>显式指定速度(BD 追击任务调用)。</summary>
|
||||
public void MoveWithSpeed(float dir, float speed)
|
||||
{
|
||||
_rb.velocity = new Vector2(dir * speed, _rb.velocity.y);
|
||||
UpdateFacing(dir);
|
||||
}
|
||||
|
||||
public void FaceTarget(Vector2 targetPos)
|
||||
{
|
||||
float dir = targetPos.x < transform.position.x ? -1f : 1f;
|
||||
UpdateFacing(dir);
|
||||
}
|
||||
|
||||
public void ApplyKnockback(Vector2 dir, float force)
|
||||
{
|
||||
_rb.velocity = dir.normalized * force;
|
||||
}
|
||||
|
||||
public void StopHorizontal()
|
||||
{
|
||||
_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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
// ── 1. 敌人物理轮廓(珊瑚红,区别于玩家绿色)────────────────
|
||||
Gizmos.color = new Color(1f, 0.45f, 0.35f, 0.65f);
|
||||
foreach (var col in GetComponents<Collider2D>())
|
||||
{
|
||||
if (col.isTrigger) continue;
|
||||
BaseGames.Combat.HitBox.DrawCollider2DWire(col);
|
||||
}
|
||||
|
||||
// ── 2. 朝向箭头(橙色)──────────────────────────────────────
|
||||
Vector3 center = transform.position;
|
||||
DrawArrow2D(center, center + new Vector3(_facingDir * 0.5f, 0f, 0f),
|
||||
new Color(1f, 0.6f, 0.1f, 0.9f));
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
// 运行时:青色箭头显示速度向量(选中时)
|
||||
if (!Application.isPlaying || _rb == null) return;
|
||||
Vector2 vel = _rb.velocity;
|
||||
if (vel.sqrMagnitude < 0.01f) return;
|
||||
DrawArrow2D(transform.position, transform.position + (Vector3)(vel * 0.12f),
|
||||
new Color(0.2f, 0.9f, 1f, 0.9f), 0.1f);
|
||||
#endif
|
||||
}
|
||||
|
||||
// 在 Gizmos 空间绘制带箭头的 2D 有向线段
|
||||
private static void DrawArrow2D(Vector3 from, Vector3 to, Color color, float headLen = 0.15f)
|
||||
{
|
||||
Vector3 dir = to - from;
|
||||
if (dir.sqrMagnitude < 0.0001f) return;
|
||||
dir = dir.normalized;
|
||||
Gizmos.color = color;
|
||||
Gizmos.DrawLine(from, to);
|
||||
float cos = 0.8192f, sin = 0.5736f; // cos/sin 35°
|
||||
float bx = -dir.x, by = -dir.y;
|
||||
Vector3 wing1 = new Vector3(bx * cos - by * sin, bx * sin + by * cos, 0f) * headLen;
|
||||
Vector3 wing2 = new Vector3(bx * cos + by * sin, -bx * sin + by * cos, 0f) * headLen;
|
||||
Gizmos.DrawLine(to, to + wing1);
|
||||
Gizmos.DrawLine(to, to + wing2);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Enemies/EnemyMovement.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/EnemyMovement.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20bd45717dc17a94581eee24814fe60c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
40
Assets/_Game/Scripts/Enemies/EnemyPoiseComponent.cs
Normal file
40
Assets/_Game/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/_Game/Scripts/Enemies/EnemyPoiseComponent.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/EnemyQuotaManager.cs
Normal file
104
Assets/_Game/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/_Game/Scripts/Enemies/EnemyQuotaManager.cs.meta
Normal file
11
Assets/_Game/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:
|
||||
83
Assets/_Game/Scripts/Enemies/EnemyStats.cs
Normal file
83
Assets/_Game/Scripts/Enemies/EnemyStats.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
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 更新(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;
|
||||
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)
|
||||
{
|
||||
CurrentHP = Mathf.Max(0, CurrentHP - amount);
|
||||
}
|
||||
|
||||
public void TickAttackCooldown(float dt)
|
||||
{
|
||||
if (AttackCooldownTimer > 0f)
|
||||
AttackCooldownTimer -= dt;
|
||||
}
|
||||
|
||||
public void ResetAttackCooldown()
|
||||
{
|
||||
AttackCooldownTimer = _config.AttackCooldown;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Enemies/EnemyStats.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/EnemyStats.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 48bc7c82cd2c1df4ba7103160db48a11
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
37
Assets/_Game/Scripts/Enemies/EnemyStatsSO.cs
Normal file
37
Assets/_Game/Scripts/Enemies/EnemyStatsSO.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人属性配置 SO(架构 07_EnemyModule §2)。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/Enemies/EnemyStats")]
|
||||
public class EnemyStatsSO : ScriptableObject
|
||||
{
|
||||
[Header("生命")]
|
||||
public int MaxHP = 50;
|
||||
|
||||
[Header("防御")]
|
||||
public int Defense = 0;
|
||||
|
||||
[Header("移动")]
|
||||
public float WalkSpeed = 2f;
|
||||
public float RunSpeed = 4f;
|
||||
|
||||
[Header("战斗")]
|
||||
public int AttackDamage = 10;
|
||||
public float AttackRange = 1.5f;
|
||||
public float AttackCooldown = 1f;
|
||||
public float DetectRange = 6f;
|
||||
|
||||
[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
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Enemies/EnemyStatsSO.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/EnemyStatsSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed4391dfa14c0304c8932f1ef9f8ce63
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
70
Assets/_Game/Scripts/Enemies/FlyingEnemy.cs
Normal file
70
Assets/_Game/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/_Game/Scripts/Enemies/FlyingEnemy.cs.meta
Normal file
11
Assets/_Game/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/_Game/Scripts/Enemies/ILOSRequester.cs
Normal file
27
Assets/_Game/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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user