feat: Implement Room Streaming System

- Add RoomStreamingManager to manage room loading and unloading based on player proximity.
- Create StreamingBudgetConfigSO for memory and performance budgeting of the streaming system.
- Introduce TransitionDirector to handle seamless and atmospheric fade transitions between rooms.
- Develop WorldGraph to represent room connectivity and facilitate neighbor queries and distance calculations.
- Implement RoomNode and RoomEdge classes to structure room data and connections.
This commit is contained in:
2026-05-23 19:10:29 +08:00
parent 81c326af53
commit a1b4e629aa
165 changed files with 7904 additions and 313 deletions

View File

@@ -0,0 +1,37 @@
#if GRAPH_DESIGNER
using System;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// 指定 BD Task 在编辑器任务面板中显示的名称。
/// 本版本 BehaviorDesigner 不内置此特性,由项目自行定义以保持代码可读性与前向兼容性。
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class TaskNameAttribute : Attribute
{
public string Name { get; }
public TaskNameAttribute(string name) => Name = name;
}
/// <summary>
/// 指定 BD Task 在编辑器任务面板中所属分类(路径形式,如 "BaseGames/Enemy/Combat")。
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class TaskCategoryAttribute : Attribute
{
public string Category { get; }
public TaskCategoryAttribute(string category) => Category = category;
}
/// <summary>
/// 为 BD Task 提供编辑器 Tooltip 描述文本。
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class TaskDescriptionAttribute : Attribute
{
public string Description { get; }
public TaskDescriptionAttribute(string description) => Description = description;
}
}
#endif

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
@@ -9,14 +9,14 @@ namespace BaseGames.Enemies.AI
/// BD Action发动攻击。
/// CanAttack() 检查通过后调用 BeginAttack立即返回 Success。
/// </summary>
[TaskName("Attack")]
[TaskCategory("BaseGames/Enemy/Combat")]
[TaskDescription("执行近战攻击(单段或连击序列)")]
public class BD_Attack : Action
{
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{

View File

@@ -0,0 +1,54 @@
#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启动 Boss 阶段过渡演出(无敌帧 + 可选定格),等待过渡完成后返回 Success。
///
/// 返回 Running过渡演出进行中BossBase.IsPhaseTransitioning = true
/// 返回 Success过渡完成已切换到目标阶段。
/// 返回 FailureBossBase 组件不存在。
///
/// 典型 BT 用法:
/// Sequence [ BD_IsHPBelow(0.5) → BD_BossPhaseTransition(Phase=1, Duration=1.5) → ... ]
/// </summary>
[TaskName("Boss Phase Transition")]
[TaskCategory("BaseGames/Enemy/Boss")]
[TaskDescription("触发 Boss 阶段过渡演出(无敌 + 动画 + 广播事件)")]
public class BD_BossPhaseTransition : Action
{
[Tooltip("切换到的目标阶段索引")]
[SerializeField] private int m_TargetPhase = 1;
[Tooltip("无敌帧 + 过渡演出持续时间(秒)")]
[SerializeField, Min(0f)] private float m_InvincibleDuration = 1.5f;
private BossBase _boss;
private bool _started;
public override void OnAwake() => _boss = GetComponent<BossBase>();
public override void OnStart()
{
_started = false;
}
public override TaskStatus OnUpdate()
{
if (_boss == null) return TaskStatus.Failure;
if (!_started)
{
_boss.BeginPhaseTransition(m_TargetPhase, m_InvincibleDuration);
_started = true;
}
return _boss.IsPhaseTransitioning ? TaskStatus.Running : TaskStatus.Success;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e86991de79acdb3498c9187ea96a8c3c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,39 @@
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using UnityEngine;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action向半径内其他敌人广播警戒Group Alert
///
/// 返回 Success广播完成不保证命中任何敌人
///
/// 典型用法:在 BD_SetAiPhase(Chase) 之后立即调用此节点,
/// 使发现玩家的敌人将警报传播给周围同伴,实现群体索敌效果。
///
/// 半径优先使用 m_OverrideRadius若为 0 则读取 EnemyStatsSO.AlertBroadcastRadius。
/// </summary>
[TaskName("Broadcast Alert")]
[TaskCategory("BaseGames/Enemy/Perception")]
[TaskDescription("向附近的友方广播警报,使其进入 Alert 状态")]
public class BD_BroadcastAlert : Action
{
[Tooltip("广播半径m0 = 使用 EnemyStatsSO.AlertBroadcastRadius")]
[SerializeField, Min(0f)] private float m_OverrideRadius = 0f;
private EnemyBase _enemy;
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
float r = m_OverrideRadius > 0f ? m_OverrideRadius : (_enemy.StatsSO?.AlertBroadcastRadius ?? 0f);
_enemy.AlertNearby(r);
return TaskStatus.Success;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 93ffcd4596213bf4d8499aa565712213
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies;
@@ -8,14 +8,14 @@ namespace BaseGames.Enemies.AI
/// <summary>
/// BD Conditional攻击冷却是否完毕可以发动攻击
/// </summary>
[TaskName("Can Attack?")]
[TaskCategory("BaseGames/Enemy/Combat")]
[TaskDescription("检查当前是否满足近战攻击条件(距离 + 视线)")]
public class BD_CanAttack : Conditional
{
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{

View File

@@ -0,0 +1,35 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// 条件:目标坐标从当前位置是否可到达(调用 NavAgent.CanReach无法寻路时 Failure
/// 用于 Boss 阶段切换或小怪决策能否追击到跨平台目标。
/// </summary>
[TaskName("Can Reach Target?")]
[TaskCategory("BaseGames/Enemy/Perception")]
[TaskDescription("检查 NavAgent 是否可以规划到目标的有效路径")]
public sealed class BD_CanReachTarget : Conditional
{
[Tooltip("检查目标(留空则使用玩家位置)")]
[SerializeField] private Transform m_Target;
private EnemyBase _enemy;
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{
if (_enemy?.Nav == null) return TaskStatus.Failure;
Vector2 pos = m_Target != null
? (Vector2)m_Target.position
: _enemy.PlayerTransform != null ? (Vector2)_enemy.PlayerTransform.position : Vector2.zero;
return _enemy.Nav.CanReach(pos) ? TaskStatus.Success : TaskStatus.Failure;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 436d3fd564880ae46a899b347f9a3494
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,71 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies.Abilities;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// 条件abilityId 当前可用(冷却完毕且未在运行)。
/// 可选开启范围、视线、脚踏地面等调度提示检查;
/// 仅当所有启用的条件均满足时才返回 Success。
/// </summary>
[TaskName("Can Use Ability?")]
[TaskCategory("BaseGames/Enemy/Combat")]
[TaskDescription("检查能力是否存在且冷却就绪,支持范围/视线/地面等调度提示检查")]
public sealed class BD_CanUseAbility : Conditional
{
[Tooltip("能力 ScriptableObject优先级高于下方字符串 ID")]
[SerializeField] private EnemyAbilitySO m_AbilitySO;
[Tooltip("能力 ID当 AbilitySO 未赋值时使用)")]
[SerializeField] private string m_AbilityId = "";
[Header("调度提示(可选)")]
[Tooltip("启用后,检查玩家是否在 AbilitySO.preferredRange 范围内")]
[SerializeField] private bool m_CheckRange = false;
[Tooltip("启用后,检查 AbilitySO.requiresLineOfSight 是否满足视线条件")]
[SerializeField] private bool m_CheckLineOfSight = false;
[Tooltip("启用后,检查 AbilitySO.requiresGrounded 是否满足落地条件")]
[SerializeField] private bool m_CheckGrounded = false;
private EnemyBase _enemy;
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
string id = m_AbilitySO != null ? m_AbilitySO.abilityId : m_AbilityId;
var ab = _enemy.Abilities.Get(id);
if (ab == null || !ab.CanUse) return TaskStatus.Failure;
// ── 调度提示检查(均可在 Inspector 独立开关)──────────────────────────
var config = ab.Config;
if (config != null)
{
if (m_CheckGrounded && config.requiresGrounded && !(_enemy.Movement?.IsGrounded ?? false))
return TaskStatus.Failure;
if (m_CheckLineOfSight && config.requiresLineOfSight && !_enemy.IsPlayerVisible())
return TaskStatus.Failure;
if (m_CheckRange && _enemy.Stats != null)
{
float sqrDist = _enemy.Stats.SqrDistanceToPlayer;
float minSqr = config.preferredMinRange * config.preferredMinRange;
float maxSqr = config.preferredMaxRange * config.preferredMaxRange;
if (sqrDist < minSqr || sqrDist > maxSqr)
return TaskStatus.Failure;
}
}
return TaskStatus.Success;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3fbc6b3634e77bf40bfeb918c7e45e5f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,41 @@
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using UnityEngine;
using BaseGames.Boss;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Conditional检查 Boss 技能是否冷却就绪。
/// 支持拖拽 BossSkillSO 或直接填写 skillId 字符串SO 优先)。
/// </summary>
[TaskName("Can Use Boss Skill?")]
[TaskCategory("BaseGames/Enemy/Boss")]
[TaskDescription("检查指定 Boss 技能是否在当前阶段可用且冷却就绪")]
public class BD_CanUseBossSkill : Conditional
{
[SerializeField] private BossSkillSO m_SkillSO;
[SerializeField] private string m_SkillId;
private BossBase _boss;
private BossSkillExecutor _executor;
public override void OnAwake()
{
_boss = GetComponent<BossBase>();
_executor = GetComponentInChildren<BossSkillExecutor>();
}
public override TaskStatus OnUpdate()
{
if (_executor == null) return TaskStatus.Failure;
string id = m_SkillSO != null ? m_SkillSO.skillId : m_SkillId;
if (string.IsNullOrEmpty(id)) return TaskStatus.Failure;
return _executor.CanUseSkill(id) ? TaskStatus.Success : TaskStatus.Failure;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bb7c971f5193f174189337814487bd7d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,140 @@
#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追击玩家三阶段视线丢失模型
///
/// <list type="bullet">
/// <item><b>Tracking</b>:持有视线,以奔跑速度追击并更新 LastKnownPlayerPosition。</item>
/// <item><b>Searching</b>:视线丢失超过 LostSightBuffer~0.3s)后进入。速度降低,向最后可见位置行进;若恢复视线即回到 Tracking。</item>
/// <item><b>Lost</b>Searching 持续超过 LoseLinkTimeout → 返回 Failure让 BD 树切入 BD_InvestigateLastKnown。</item>
/// </list>
///
/// 短暂遮挡(如绕过柱子)在 LostSightBuffer 内不会改变速度,避免追击手感割裂。
/// </summary>
[TaskName("Chase Player")]
[TaskCategory("BaseGames/Enemy/Movement")]
[TaskDescription("三段式追击Tracking→Searching→Lost丢失视线后进入缓冲期")]
public sealed class BD_ChasePlayer : Action
{
[Header("距离限制")]
[Tooltip("追击距离上限m0 = 使用 EnemyStatsSO.MaxChaseDistance")]
[SerializeField] [Min(0f)] private float m_MaxChaseDistance = 0f;
[Header("视线丢失")]
[Tooltip("视线丢失判定超时s0 = 使用 EnemyStatsSO.LoseLinkTimeout")]
[SerializeField] [Min(0f)] private float m_LoseLinkTimeout = 0f;
[Tooltip("视线刚丢失后的缓冲期s此期间保持追击速度短暂遮挡不触发搜索模式")]
[SerializeField] [Min(0f)] private float m_LostSightBuffer = 0.3f;
[Header("路径规划")]
[Tooltip("路径重规划阈值m玩家移动超过此距离后才重新规划避免每帧请求")]
[SerializeField] [Min(0.1f)] private float m_ReplanThreshold = 1.5f;
[Header("搜索阶段")]
[Tooltip("Searching 阶段速度倍率(相对于行走速度;<1 = 减速搜索)")]
[SerializeField] [UnityEngine.Range(0.3f, 1.5f)] private float m_SearchSpeedMultiplier = 0.7f;
private enum ChaseSubState { Tracking, Searching }
private EnemyBase _enemy;
private float _losTimer;
private Vector2 _lastReplanPos;
private ChaseSubState _subState;
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
public override void OnStart()
{
_enemy.SetAiPhase(AiPhase.Chase);
_losTimer = 0f;
_lastReplanPos = Vector2.positiveInfinity;
_subState = ChaseSubState.Tracking;
float runSpeed = _enemy.Stats?.RunSpeed ?? 4f;
_enemy.Nav?.SetSpeed(runSpeed);
var ac = _enemy.AnimConfig;
if (_enemy.Animancer != null && ac?.Run != null)
_enemy.Animancer.Play(ac.Run);
}
public override TaskStatus OnUpdate()
{
if (_enemy == null || _enemy.PlayerTransform == null)
return TaskStatus.Failure;
float maxDist = m_MaxChaseDistance > 0f ? m_MaxChaseDistance : (_enemy.Stats?.MaxChaseDistance ?? 15f);
float loseTime = m_LoseLinkTimeout > 0f ? m_LoseLinkTimeout : (_enemy.Stats?.LoseLinkTimeout ?? 2f);
// 超出最大追击距离 → 放弃追击
if (_enemy.Stats != null && _enemy.Stats.SqrDistanceToPlayer > maxDist * maxDist)
return TaskStatus.Failure;
Vector2 playerPos = _enemy.PlayerTransform.position;
if (_enemy.IsPlayerVisible())
{
// 视线恢复Searching → Tracking恢复奔跑速度
if (_subState == ChaseSubState.Searching)
{
_subState = ChaseSubState.Tracking;
_enemy.Nav?.SetSpeed(_enemy.Stats?.RunSpeed ?? 4f);
}
_losTimer = 0f;
_enemy.LastKnownPlayerPosition = playerPos;
}
else
{
_losTimer += Time.deltaTime;
if (_subState == ChaseSubState.Tracking)
{
// 缓冲期结束 → 切入 Searching减速并向最后可见位置行进
if (_losTimer >= m_LostSightBuffer)
{
_subState = ChaseSubState.Searching;
float searchSpeed = (_enemy.Stats?.WalkSpeed ?? 2f) * m_SearchSpeedMultiplier;
_enemy.Nav?.SetSpeed(searchSpeed);
_enemy.MoveTo(_enemy.LastKnownPlayerPosition);
_lastReplanPos = _enemy.LastKnownPlayerPosition;
}
}
else // Searching
{
if (_losTimer >= loseTime)
return TaskStatus.Failure; // 搜索超时 → 进入 BD_InvestigateLastKnown
}
}
// Tracking 阶段按阈值重规划路径
if (_subState == ChaseSubState.Tracking)
{
float sqrReplan = m_ReplanThreshold * m_ReplanThreshold;
if ((playerPos - _lastReplanPos).sqrMagnitude > sqrReplan)
{
_enemy.MoveTo(playerPos);
_lastReplanPos = playerPos;
}
}
_enemy.FacePlayer();
return TaskStatus.Running;
}
public override void OnEnd()
{
float walkSpeed = _enemy?.Stats?.WalkSpeed ?? 2f;
_enemy?.Nav?.SetSpeed(walkSpeed);
_enemy?.StopMovement();
_subState = ChaseSubState.Tracking; // 重置子状态,防止下次激活时以错误状态重入
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a7ecfbdf1fee6a141a18c72d36fcfbf2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
@@ -9,22 +9,26 @@ namespace BaseGames.Enemies.AI
/// BD ActionBoss 切换阶段Phase
/// 调用 BossBase.EnterPhase(PhaseIndex),单帧完成,返回 Success。
/// </summary>
[TaskName("Enter Boss Phase")]
[TaskCategory("BaseGames/Enemy/Boss")]
[TaskDescription("立即切换 Boss 到目标阶段序号(单帧完成)")]
public class BD_EnterPhase : Action
{
[UnityEngine.SerializeField] private int m_PhaseIndex = 1;
private BossBase _boss;
public override void OnAwake() => _boss = GetComponent<BossBase>();
public override void OnStart()
{
_boss = GetComponent<BossBase>();
// 阶段切换是单帧操作,在 OnStart 完成OnUpdate 仅汇报结果
_boss?.EnterPhase(m_PhaseIndex);
}
public override TaskStatus OnUpdate()
{
if (_boss == null) return TaskStatus.Failure;
_boss.EnterPhase(m_PhaseIndex);
return TaskStatus.Success;
return _boss == null ? TaskStatus.Failure : TaskStatus.Success;
}
}
}

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
@@ -9,21 +9,18 @@ namespace BaseGames.Enemies.AI
/// <summary>
/// BD Action立即朝向玩家单帧完成返回 Success。
/// </summary>
[TaskName("Face Target")]
[TaskCategory("BaseGames/Enemy/Animation")]
[TaskDescription("立即朝向目标 Transform 或玩家,单帧返回 Success")]
public class BD_FaceTarget : Action
{
private EnemyBase _enemy;
private Transform _player;
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
var go = GameObject.FindWithTag("Player");
_player = go != null ? go.transform : null;
}
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{
if (_enemy == null || _player == null) return TaskStatus.Failure;
if (_enemy == null) return TaskStatus.Failure;
_enemy.FacePlayer();
return TaskStatus.Success;
}

View File

@@ -0,0 +1,32 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies.Abilities;
namespace BaseGames.Enemies.AI
{
/// <summary>动作:中断指定能力。</summary>
[TaskName("Interrupt Ability")]
[TaskCategory("BaseGames/Enemy/Combat")]
[TaskDescription("立即中断指定能力或全部能力")]
public sealed class BD_InterruptAbility : Action
{
[SerializeField] private string m_AbilityId = "";
[SerializeField] private InterruptReason m_Reason = InterruptReason.ExternalRequest;
private EnemyBase _enemy;
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
var ab = _enemy.Abilities.Get(m_AbilityId);
if (ab == null) return TaskStatus.Failure;
ab.Interrupt(m_Reason);
return TaskStatus.Success;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3db7ca634cc20574dba7f11ab018b23a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,184 @@
#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多步搜查Navigate → LookAround → RandomWalk × N
///
/// <list type="bullet">
/// <item><b>Navigate</b>:导航至最后可见位置。</item>
/// <item><b>LookAround</b>:到达后原地环顾(播放 Look 动画,持续 LookAroundDuration。</item>
/// <item><b>WalkRandom</b>:向搜查半径内的随机点移动,重复 RandomStepCount 次。</item>
/// <item>任意阶段重新发现玩家 → 返回 FailureBD 树重入追击。</item>
/// <item>所有步骤完成仍未发现 → 返回 SuccessBD 树归位/恢复巡逻。</item>
/// </list>
/// </summary>
[TaskName("Investigate Last Known Pos")]
[TaskCategory("BaseGames/Enemy/Movement")]
[TaskDescription("多步搜查:导航到最后已知位置 → 环顾四周 → 随机游走 N 次")]
public sealed class BD_InvestigateLastKnown : Action
{
[Tooltip("到达搜查点后环顾的时长s")]
[SerializeField] private float m_LookAroundDuration = 0.8f;
[Tooltip("随机行走步数(完成环顾后随机游荡的次数)")]
[SerializeField] [Min(0)] private int m_RandomStepCount = 2;
[Tooltip("随机行走半径m")]
[SerializeField] private float m_SearchRadius = 2.5f;
[Tooltip("到达目标点的判定半径m")]
[SerializeField] private float m_ArriveRadius = 0.6f;
private enum InvestigateSubStep { Navigate, LookAround, WalkRandom }
private EnemyBase _enemy;
private InvestigateSubStep _step;
private float _stepTimer;
private int _stepsRemaining;
private Vector2 _randomTarget;
private bool _pathFailed;
private bool _subscribed;
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
public override void OnStart()
{
_enemy.SetAiPhase(AiPhase.Investigate);
_step = InvestigateSubStep.Navigate;
_stepTimer = 0f;
_stepsRemaining = m_RandomStepCount;
_pathFailed = false;
var ac = _enemy.AnimConfig;
if (_enemy.Animancer != null)
{
var clip = ac?.Investigate ?? ac?.Walk;
if (clip != null) _enemy.Animancer.Play(clip);
}
if (!_subscribed && _enemy.Nav != null)
{
_enemy.Nav.OnNavPathFailed += HandlePathFailed;
_subscribed = true;
}
_enemy.MoveTo(_enemy.LastKnownPlayerPosition);
}
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
if (_enemy.IsPlayerVisible()) return TaskStatus.Failure;
switch (_step)
{
case InvestigateSubStep.Navigate: return UpdateNavigate();
case InvestigateSubStep.LookAround: return UpdateLookAround();
case InvestigateSubStep.WalkRandom: return UpdateWalkRandom();
default: return TaskStatus.Success;
}
}
public override void OnEnd()
{
if (_subscribed && _enemy?.Nav != null)
{
_enemy.Nav.OnNavPathFailed -= HandlePathFailed;
_subscribed = false;
}
_enemy?.StopMovement();
}
// ── 阶段逻辑 ────────────────────────────────────────────────────
private TaskStatus UpdateNavigate()
{
Vector2 self = _enemy.transform.position;
bool arrived = _pathFailed ||
(self - _enemy.LastKnownPlayerPosition).sqrMagnitude <= m_ArriveRadius * m_ArriveRadius;
if (!arrived) return TaskStatus.Running;
_enemy.StopMovement();
EnterLookAround();
return TaskStatus.Running;
}
private TaskStatus UpdateLookAround()
{
_stepTimer += Time.deltaTime;
if (_stepTimer < m_LookAroundDuration) return TaskStatus.Running;
if (_stepsRemaining > 0)
EnterRandomWalk();
else
return TaskStatus.Success;
return TaskStatus.Running;
}
private TaskStatus UpdateWalkRandom()
{
Vector2 self = _enemy.transform.position;
bool arrived = _pathFailed ||
(self - _randomTarget).sqrMagnitude <= m_ArriveRadius * m_ArriveRadius;
if (!arrived) return TaskStatus.Running;
_enemy.StopMovement();
_stepsRemaining--;
_pathFailed = false;
if (_stepsRemaining > 0)
EnterRandomWalk();
else
return TaskStatus.Success;
return TaskStatus.Running;
}
// ── 辅助方法 ────────────────────────────────────────────────────
private void EnterLookAround()
{
_step = InvestigateSubStep.LookAround;
_stepTimer = 0f;
var ac = _enemy.AnimConfig;
if (_enemy.Animancer != null)
{
var clip = ac?.Investigate ?? ac?.Idle;
if (clip != null) _enemy.Animancer.Play(clip);
}
}
private void EnterRandomWalk()
{
_step = InvestigateSubStep.WalkRandom;
_pathFailed = false;
Vector2 origin = _enemy.LastKnownPlayerPosition;
// 横板地形中只在水平方向随机偏移,保留 origin.y
// PathBerserker2d 寻路负责处理跨平台的纵向路径规划。
float dir = Random.value > 0.5f ? 1f : -1f;
float dist = Random.Range(0.5f, m_SearchRadius);
_randomTarget = new Vector2(origin.x + dir * dist, origin.y);
_enemy.MoveTo(_randomTarget);
var ac = _enemy.AnimConfig;
if (_enemy.Animancer != null)
{
var clip = ac?.Walk;
if (clip != null) _enemy.Animancer.Play(clip);
}
}
private void HandlePathFailed() => _pathFailed = true;
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2e43db9982a49864e95812919b0efd10
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,34 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies.Abilities;
namespace BaseGames.Enemies.AI
{
/// <summary>条件abilityId 当前是否正在运行。</summary>
[TaskName("Is Ability Running?")]
[TaskCategory("BaseGames/Enemy/Combat")]
[TaskDescription("检查指定能力当前是否正在执行中")]
public sealed class BD_IsAbilityRunning : Conditional
{
[Tooltip("能力 ScriptableObject优先级高于下方字符串 ID")]
[SerializeField] private EnemyAbilitySO m_AbilitySO;
[Tooltip("能力 ID当 AbilitySO 未赋值时使用)")]
[SerializeField] private string m_AbilityId = "";
private EnemyBase _enemy;
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
string id = m_AbilitySO != null ? m_AbilitySO.abilityId : m_AbilityId;
var ab = _enemy.Abilities.Get(id);
return ab != null && ab.IsRunning ? TaskStatus.Success : TaskStatus.Failure;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 254e8e3d6b56229498e94e24e7e53393
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,36 @@
#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当前 AI 行为阶段是否与目标匹配。
///
/// 常用于 Decorator Conditional Abort 或 Sequence 头部保护子树,
/// 例如只有在 <see cref="AiPhase.Patrol"/> 阶段时才执行巡逻序列。
/// </summary>
[TaskName("Is Ai Phase?")]
[TaskCategory("BaseGames/Enemy/State")]
[TaskDescription("检查当前 AI 行为阶段是否与目标枚举值匹配")]
public sealed class BD_IsAiPhase : Conditional
{
[Tooltip("目标 AI 行为阶段")]
[SerializeField] private AiPhase m_Phase = AiPhase.Patrol;
private EnemyBase _enemy;
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
return _enemy.CurrentAiPhase == m_Phase
? TaskStatus.Success
: TaskStatus.Failure;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c5d65c8544c21a146a5f30140acdb5ce
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies;
@@ -8,14 +8,14 @@ namespace BaseGames.Enemies.AI
/// <summary>
/// BD Conditional敌人是否处于地面。
/// </summary>
[TaskName("Is Grounded?")]
[TaskCategory("BaseGames/Enemy/State")]
[TaskDescription("检查敌人是否接地(适用于触发跳跃或落地逻辑的条件保护)")]
public class BD_IsGrounded : Conditional
{
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies;
@@ -9,22 +9,24 @@ namespace BaseGames.Enemies.AI
/// BD ConditionalBoss HP 是否低于阈值(用于触发阶段切换)。
/// HPThreshold 为 0~1 归一化比例(如 0.5 = HP ≤ 50%)。
/// </summary>
[TaskName("Is HP Below Threshold?")]
[TaskCategory("BaseGames/Enemy/State")]
[TaskDescription("检查当前 HP 是否低于指定百分比阈值")]
public class BD_IsHPBelow : Conditional
{
[UnityEngine.Range(0f, 1f)]
[UnityEngine.SerializeField] private float m_HPThreshold = 0.5f;
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override void OnAwake() => _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;
float maxHP = UnityEngine.Mathf.Max(1f, _enemy.Stats.MaxHP);
float ratio = (float)_enemy.Stats.CurrentHP / maxHP;
return ratio <= m_HPThreshold ? TaskStatus.Success : TaskStatus.Failure;
}
}

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies;
@@ -9,14 +9,14 @@ namespace BaseGames.Enemies.AI
/// BD Conditional敌人是否靠近平台边缘。
/// 通过 EnemyNavAgent.IsNearEdge() 检测(双射线检测脚下/前方地面)。
/// </summary>
[TaskName("Is Near Edge?")]
[TaskCategory("BaseGames/Enemy/State")]
[TaskDescription("检查前方是否有悬崖边缘(基于 SensorToolkit 或 Raycast")]
public class BD_IsNearEdge : Conditional
{
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{

View File

@@ -0,0 +1,35 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// 条件NavAgent 当前是否正在穿越连接段(跳跃/下落/爬梯/传送等)。
/// 可选过滤具体连接类型(填 None = 任意类型均匹配)。
/// </summary>
[TaskName("Is On NavLink?")]
[TaskCategory("BaseGames/Enemy/State")]
[TaskDescription("检查 NavAgent 当前是否正在穿越 NavLink如跳跃传送门")]
public sealed class BD_IsOnNavLink : Conditional
{
[Tooltip("填 None 匹配任意类型;填具体类型则只在该类型时 Success。")]
[SerializeField] private NavLinkType m_FilterType = NavLinkType.None;
private EnemyBase _enemy;
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{
if (_enemy?.Nav == null) return TaskStatus.Failure;
if (!_enemy.Nav.IsOnLink) return TaskStatus.Failure;
if (m_FilterType != NavLinkType.None && _enemy.Nav.CurrentLinkType != m_FilterType)
return TaskStatus.Failure;
return TaskStatus.Success;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 44406bb7e06320746950682adf59f59c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies;
@@ -9,6 +9,9 @@ namespace BaseGames.Enemies.AI
/// BD Conditional检查玩家是否在指定范围内。
/// 成功/失败直接驱动 BT 分支选择Selector / Sequence 节点)。
/// </summary>
[TaskName("Is Player In Range?")]
[TaskCategory("BaseGames/Enemy/Perception")]
[TaskDescription("检查玩家是否在指定距离内")]
public class BD_IsPlayerInRange : Conditional
{
/// <summary>检测范围Inspector 可配置,默认 6 米)。</summary>
@@ -16,10 +19,7 @@ namespace BaseGames.Enemies.AI
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies;
@@ -9,14 +9,14 @@ namespace BaseGames.Enemies.AI
/// BD Conditional玩家是否可见LOS 检测)。
/// 读取 EnemyBase._losResult 字段(由 BatchLOSSystem 每帧写入,或降级 3 帧节流 Raycast
/// </summary>
[TaskName("Is Player Visible?")]
[TaskCategory("BaseGames/Enemy/Perception")]
[TaskDescription("检查是否有视线到达玩家(通过 EnemyBase.HasLineOfSight")]
public class BD_IsPlayerVisible : Conditional
{
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{

View File

@@ -0,0 +1,41 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies.Perception;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// 条件EnemySensorHub 中名为 slotName 的 Sensor 是否检测到目标。
/// 若 m_Target 为空则使用 EnemyBase.PlayerTransform。
/// </summary>
[TaskName("Is Sensor Detecting?")]
[TaskCategory("BaseGames/Enemy/Perception")]
[TaskDescription("检查 EnemySensorHub 中指定 Sensor 槽是否检测到目标")]
public sealed class BD_IsSensorDetecting : Conditional
{
[SerializeField] private string m_SlotName = "aggro";
[SerializeField] private bool m_AnyTarget = false;
private EnemySensorHub _hub;
private EnemyBase _enemy;
public override void OnAwake()
{
_hub = gameObject.GetComponent<EnemySensorHub>();
_enemy = gameObject.GetComponent<EnemyBase>();
}
public override TaskStatus OnUpdate()
{
if (_hub == null) return TaskStatus.Failure;
if (m_AnyTarget)
return _hub.HasAnyDetection(m_SlotName) ? TaskStatus.Success : TaskStatus.Failure;
var tgt = _enemy != null ? _enemy.PlayerTransform : null;
if (tgt == null) return TaskStatus.Failure;
return _hub.IsDetecting(m_SlotName, tgt.gameObject) ? TaskStatus.Success : TaskStatus.Failure;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a1015b63dbb3da4aa877d7e898b394a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
@@ -7,36 +7,23 @@ using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Conditional敌人当前 EnemyStateType 是否与目标状态名称匹配。
/// TargetStateName 直接输入枚举名称字符串Controlled / Hurt / Stagger / Dead
/// 枚举值顺序变化时 BD 图不会静默失效。
/// BD Conditional敌人当前 EnemyStateType 是否与目标状态匹配。
/// </summary>
[TaskName("Is State Match?")]
[TaskCategory("BaseGames/Enemy/State")]
[TaskDescription("检查当前物理/战斗状态是否与目标枚举值匹配")]
public class BD_IsStateMatch : Conditional
{
/// <summary>目标状态名称Controlled / Hurt / Stagger / Dead。</summary>
[SerializeField] private string m_TargetStateName = "Controlled";
[SerializeField] private EnemyStateType m_TargetState = EnemyStateType.Controlled;
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override void OnAwake() => _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;
return _enemy.CurrentState == m_TargetState ? TaskStatus.Success : TaskStatus.Failure;
}
}
}

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
@@ -10,6 +10,9 @@ namespace BaseGames.Enemies.AI
/// BD Action跳跃至目标坐标抛物线跳跃
/// 调用 EnemyBase.JumpTo待落地IsGrounded后返回 Success。
/// </summary>
[TaskName("Jump To")]
[TaskCategory("BaseGames/Enemy/Movement")]
[TaskDescription("执行跳跃动作到达目标高度或 NavLink 对接点")]
public class BD_JumpTo : Action
{
[SerializeField] private Vector2 m_Target;
@@ -17,9 +20,10 @@ namespace BaseGames.Enemies.AI
private EnemyBase _enemy;
private bool _jumped;
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
_jumped = false;
}

View File

@@ -0,0 +1,96 @@
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using UnityEngine;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action战斗站位控制。
/// 在 [m_PreferredMinDist, m_PreferredMaxDist] 范围内保持与玩家的水平距离:
/// - 距离过近 → 后退
/// - 距离过远 → 靠近
/// - 在范围内 → 停止移动,持续朝向玩家
/// 始终返回 Running上层 BT 通过 Abort 或条件终止该 Task。
/// </summary>
[TaskName("Maintain Combat Distance")]
[TaskCategory("BaseGames/Enemy/Combat")]
[TaskDescription("在战斗中维持与目标的理想距离范围")]
public class BD_MaintainCombatDistance : Action
{
[Tooltip("期望最小距离m小于此值开始后退")]
[SerializeField] private float m_PreferredMinDist = 2f;
[Tooltip("期望最大距离m大于此值开始靠近")]
[SerializeField] private float m_PreferredMaxDist = 4f;
[Tooltip("后退速度m/s默认使用 WalkSpeed")]
[SerializeField] private float m_BackpedaleSpeed = 0f;
[Tooltip("每帧重新规划路径的阈值m玩家移动超过此距离才重规划降低频率")]
[SerializeField] private float m_ReplanThreshold = 0.5f;
private EnemyBase _enemy;
private Vector2 _lastPlayerPos;
private float _sqrMin;
private float _sqrMax;
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override void OnStart()
{
_sqrMin = m_PreferredMinDist * m_PreferredMinDist;
_sqrMax = m_PreferredMaxDist * m_PreferredMaxDist;
_lastPlayerPos = Vector2.positiveInfinity;
}
public override TaskStatus OnUpdate()
{
if (_enemy == null || _enemy.PlayerTransform == null)
return TaskStatus.Failure;
if (_enemy.Stats == null)
return TaskStatus.Failure;
_enemy.FacePlayer();
float sqrDist = _enemy.Stats.SqrDistanceToPlayer;
if (sqrDist < _sqrMin)
{
// 距离过近 → 后退(向远离玩家方向移动)
_enemy.Nav?.StopNavigation();
Vector2 toPlayer = ((Vector2)_enemy.PlayerTransform.position - (Vector2)_enemy.transform.position).normalized;
float backDir = -Mathf.Sign(toPlayer.x);
float speed = m_BackpedaleSpeed > 0f ? m_BackpedaleSpeed : _enemy.Stats.WalkSpeed;
_enemy.Movement?.MoveWithSpeed(backDir, speed);
}
else if (sqrDist > _sqrMax)
{
// 距离过远 → 靠近
Vector2 playerPos = _enemy.PlayerTransform.position;
float moved = ((Vector2)playerPos - _lastPlayerPos).sqrMagnitude;
if (moved > m_ReplanThreshold * m_ReplanThreshold)
{
_lastPlayerPos = playerPos;
_enemy.MoveTo(playerPos);
}
}
else
{
// 在最优范围内 → 停止导航,原地保持朝向
_enemy.Nav?.StopNavigation();
_enemy.Movement?.StopHorizontal();
}
return TaskStatus.Running;
}
public override void OnEnd()
{
_enemy?.Movement?.StopHorizontal();
_enemy?.Nav?.StopNavigation();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 480875a7bad333140a3c46e637eb7bc6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
@@ -10,16 +10,16 @@ namespace BaseGames.Enemies.AI
/// BD Action移动到指定世界坐标 Target。
/// 到达目标IsAtDestination后返回 Success。
/// </summary>
[TaskName("Move To")]
[TaskCategory("BaseGames/Enemy/Movement")]
[TaskDescription("导航到目标 Transform 或世界坐标点;到达返回 Success")]
public class BD_MoveTo : Action
{
[SerializeField] private Vector2 m_Target;
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{

View File

@@ -0,0 +1,58 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// 动作:移动到目标并等待 OnGoalReached 事件(事件驱动,替代轮询版 BD_MoveTo
/// OnGoalReached 触发后返回 Success路径失败返回 Failure。
/// </summary>
[TaskName("Move To And Wait")]
[TaskCategory("BaseGames/Enemy/Movement")]
[TaskDescription("导航到目标点,到达后原地等待指定时长,然后返回 Success")]
public sealed class BD_MoveToAndWait : Action
{
[SerializeField] private Transform m_Target;
private EnemyBase _enemy;
private bool _reached;
private bool _failed;
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
public override void OnStart()
{
_reached = _failed = false;
if (_enemy?.Nav == null) { _failed = true; return; }
_enemy.Nav.OnGoalReached += HandleReached;
_enemy.Nav.OnNavPathFailed += HandleFailed;
var pos = m_Target != null ? (Vector2)m_Target.position
: _enemy.PlayerTransform != null ? (Vector2)_enemy.PlayerTransform.position
: (Vector2)transform.position;
_enemy.Nav.RequestMoveTo(pos);
}
public override TaskStatus OnUpdate()
{
if (_failed) return TaskStatus.Failure;
if (_reached) return TaskStatus.Success;
return TaskStatus.Running;
}
public override void OnEnd()
{
if (_enemy?.Nav != null)
{
_enemy.Nav.OnGoalReached -= HandleReached;
_enemy.Nav.OnNavPathFailed -= HandleFailed;
}
}
private void HandleReached() => _reached = true;
private void HandleFailed() => _failed = true;
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: df51ca671ebccea47a45c1e1bca3caa3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
@@ -10,14 +10,14 @@ namespace BaseGames.Enemies.AI
/// OnStart 从 EnemyBase.PlayerTransform 获取玩家引用(由 _onPlayerSpawned 事件缓存,
/// 避免 FindWithTag 全场景扫描。OnUpdate 每帧更新导航目标。
/// </summary>
[TaskName("Move To Player")]
[TaskCategory("BaseGames/Enemy/Movement")]
[TaskDescription("持续向玩家方向移动,失去视线或超出追踪距离返回 Failure")]
public class BD_MoveToPlayer : Action
{
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{

View File

@@ -0,0 +1,30 @@
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Conditional Task检查敌人本次 BT Tick 内是否发生了弹反事件。
///
/// 返回 <c>Success</c> 时同时清除标志(每次弹反只触发一次响应分支)。
/// 典型用法:在 BD 树根部优先分支放置此条件,触发时执行受击硬直 / 反制动画等子树。
/// </summary>
[TaskName("On Parried?")]
[TaskCategory("BaseGames/Enemy/Utility")]
[TaskDescription("检查上一次攻击是否被玩家格挡(用于 Boss 反制逻辑)")]
public class BD_OnParried : Conditional
{
private EnemyBase _enemy;
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
return _enemy.ConsumeParryEvent() ? TaskStatus.Success : TaskStatus.Failure;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fdae429711fc46d41a7b025eed43784f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,32 +1,57 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
using UnityEngine;
using BaseGames.Enemies.Perception;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action敌人巡逻行为
/// 持续令敌人向当前朝向移动,遇墙/边缘时自动转向。
/// BD Action来回踱步巡逻——持续向当前方向移动,遇墙或悬崖时自动翻转方向
///
/// 若需要按预设路点顺序巡逻,请使用 <see cref="BD_PatrolWaypoints"/>(支持 Transform 引用和内联坐标)。
///
/// 转向检测优先级:
/// <list type="number">
/// <item>EnemySensorHub "wall_ahead" / "ledge" 槽SensorToolkit已配置时使用</item>
/// <item>Physics2D Raycast 兜底Prefab 未配置 Sensor 时自动启用)</item>
/// </list>
/// </summary>
[TaskName("Patrol (Pace)")]
[TaskCategory("BaseGames/Enemy/Movement")]
[TaskDescription("来回踱步巡逻遇墙或悬崖自动翻转方向SensorToolkit 优先)")]
public class BD_Patrol : Action
{
[Tooltip("检测地面边缘的向下射线长度")]
[Tooltip("(兜底)检测地面边缘的向下射线长度m")]
public float edgeCheckLength = 1.2f;
[Tooltip("检测障碍物的水平射线长度")]
[Tooltip("(兜底)检测障碍物的水平射线长度m")]
public float wallCheckLength = 0.4f;
[Tooltip("地面/墙壁 LayerMask")]
[Tooltip("兜底边缘检测射线起点相对角色的前向偏移m")]
public float edgeCheckFwdOffset = 0.3f;
[Tooltip("兜底边缘检测射线起点相对角色的向下偏移m")]
public float edgeCheckDownOffset = 0.1f;
[Tooltip("(兜底)地面/墙壁 LayerMask")]
public LayerMask groundLayer;
private EnemyBase _enemy;
private float _dir = 1f;
private EnemyBase _enemy;
private EnemySensorHub _hub;
private float _dir = 1f;
public override void OnStart()
// 缓存SensorHub 中对应槽位是否已配置Awake 时查询一次,避免每帧 Dictionary 查找)
private bool _hasWallSensor;
private bool _hasEdgeSensor;
public override void OnAwake()
{
_enemy = GetComponent<EnemyBase>();
_enemy = GetComponent<EnemyBase>();
_hub = GetComponent<EnemySensorHub>();
_hasWallSensor = _hub != null && _hub.Get(SensorSlotNames.WallAhead) != null;
_hasEdgeSensor = _hub != null && _hub.Get(SensorSlotNames.Ledge) != null;
}
public override void OnStart() => _enemy?.SetAiPhase(AiPhase.Patrol);
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
@@ -38,27 +63,33 @@ namespace BaseGames.Enemies.AI
return TaskStatus.Running;
}
public override void OnEnd()
{
_enemy?.StopMovement();
}
public override void OnEnd() => _enemy?.StopMovement();
private bool ShouldFlip()
{
Transform t = _enemy.transform;
Vector2 pos = t.position;
if (_hub != null)
{
// 有传感器配置:用传感器结果,完全跳过 Raycast
if (_hasWallSensor || _hasEdgeSensor)
{
bool wallHit = _hasWallSensor && _hub.HasAnyDetection(SensorSlotNames.WallAhead);
bool edgeHit = _hasEdgeSensor && _hub.HasAnyDetection(SensorSlotNames.Ledge);
return wallHit || edgeHit;
}
}
// 前方边缘检测:在脚前方向下射线,若无地面则转向
Vector2 edgeOrigin = pos + Vector2.right * (_dir * 0.3f) + Vector2.down * 0.1f;
// Raycast 兜底:仅在未配置 Sensor 时执行
Transform t = _enemy.transform;
Vector2 pos = t.position;
Vector2 edgeOrigin = pos + Vector2.right * (_dir * edgeCheckFwdOffset) + Vector2.down * edgeCheckDownOffset;
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;
return hitWall;
}
}
}
#endif

View File

@@ -0,0 +1,185 @@
#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按预设路点顺序巡逻支持 Transform 引用或内联 Vector2 坐标两种模式)。
///
/// <list type="bullet">
/// <item><b>m_WaypointsTransform[]</b>:拖入场景中的路点对象;适合动态路点(可在运行时移动)。</item>
/// <item><b>m_InlineWaypointsVector2[]</b>:直接填写世界坐标;无需在场景中放置对象,编辑器中以 Gizmo 可视化路径。</item>
/// <item>两者同时设置时 m_Waypoints 优先。</item>
/// </list>
///
/// 到达最后一个路点后可循环Loop或往返PingPong
/// 与 PathBerserker2d 集成:通过 IPathAgent.RequestMoveTo 导航,支持跨平台跳跃 NavLink。
/// </summary>
[TaskName("Patrol (Waypoints)")]
[TaskCategory("BaseGames/Enemy/Movement")]
[TaskDescription("按预设路点顺序巡逻;支持 Transform 引用或内联 Vector2 坐标,可循环或折返")]
public sealed class BD_PatrolWaypoints : Action
{
[Tooltip("路点列表(世界空间 Transform与 m_InlineWaypoints 同时设置时此项优先")]
[SerializeField] private Transform[] m_Waypoints;
[Tooltip("内联路点坐标(世界空间 Vector2m_Waypoints 为空时使用;在 Scene 视图中以绿色 Gizmo 可视化")]
[SerializeField] private Vector2[] m_InlineWaypoints;
[Tooltip("到达路点的判定半径m")]
[SerializeField] private float m_ArriveRadius = 0.3f;
[Tooltip("true = 往返; false = 循环")]
[SerializeField] private bool m_PingPong = false;
[Tooltip("每个路点到达后等待时长s")]
[SerializeField] private float m_WaitAtWaypoint = 0f;
private EnemyBase _enemy;
private int _index = 0;
private int _dir = 1;
private float _waitTimer = 0f;
private bool _waiting = false;
// ── 统一路点访问 ────────────────────────────────────────────────────
private int WaypointCount =>
m_Waypoints != null && m_Waypoints.Length > 0
? m_Waypoints.Length
: m_InlineWaypoints?.Length ?? 0;
private Vector2 GetWaypoint(int index)
{
if (m_Waypoints != null && m_Waypoints.Length > 0)
{
var t = m_Waypoints[index];
return t != null ? (Vector2)t.position : (Vector2)transform.position;
}
return m_InlineWaypoints != null && index < m_InlineWaypoints.Length
? m_InlineWaypoints[index]
: (Vector2)transform.position;
}
// ── BD 生命周期 ─────────────────────────────────────────────────────
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
public override void OnStart()
{
if (WaypointCount == 0) return;
_waiting = false;
_waitTimer = 0f;
_enemy?.SetAiPhase(AiPhase.Patrol);
RequestCurrent();
}
public override TaskStatus OnUpdate()
{
if (_enemy == null || WaypointCount == 0)
return TaskStatus.Failure;
if (_waiting)
{
_waitTimer -= Time.deltaTime;
if (_waitTimer > 0f) return TaskStatus.Running;
_waiting = false;
Advance();
RequestCurrent();
}
else
{
Vector2 wp = GetWaypoint(_index);
float sqrDist = ((Vector2)_enemy.transform.position - wp).sqrMagnitude;
if (sqrDist <= m_ArriveRadius * m_ArriveRadius)
{
if (m_WaitAtWaypoint > 0f)
{
_waiting = true;
_waitTimer = m_WaitAtWaypoint;
_enemy.StopMovement();
}
else
{
Advance();
RequestCurrent();
}
}
else
{
_enemy.MoveTo(wp);
_enemy.Movement?.FaceTarget(wp);
}
}
return TaskStatus.Running;
}
public override void OnEnd() => _enemy?.StopMovement();
// ── 内部辅助 ────────────────────────────────────────────────────────
private void RequestCurrent()
{
if (WaypointCount == 0) return;
_enemy.MoveTo(GetWaypoint(_index));
}
private void Advance()
{
if (WaypointCount <= 1) return;
if (m_PingPong)
{
_index += _dir;
if (_index >= WaypointCount) { _index = WaypointCount - 2; _dir = -1; }
else if (_index < 0) { _index = 1; _dir = 1; }
}
else
{
_index = (_index + 1) % WaypointCount;
}
}
// ── Gizmo 可视化(仅 m_InlineWaypoints 模式)────────────────────────
#if UNITY_EDITOR
private new void OnDrawGizmos()
{
if (m_InlineWaypoints == null || m_InlineWaypoints.Length < 1) return;
// m_Waypoints 存在时不绘制,避免与场景路点重叠
if (m_Waypoints != null && m_Waypoints.Length > 0) return;
Gizmos.color = new Color(0.2f, 0.85f, 0.2f, 0.8f);
for (int i = 0; i < m_InlineWaypoints.Length; i++)
{
Gizmos.DrawWireSphere(m_InlineWaypoints[i], m_ArriveRadius);
if (i < m_InlineWaypoints.Length - 1)
Gizmos.DrawLine(m_InlineWaypoints[i], m_InlineWaypoints[i + 1]);
}
// PingPong 时也画返回线Loop 时画首尾连接线
if (m_InlineWaypoints.Length >= 2)
{
if (m_PingPong)
{
// 往返:虚线效果(半透明线)
Gizmos.color = new Color(0.2f, 0.85f, 0.2f, 0.35f);
Gizmos.DrawLine(m_InlineWaypoints[m_InlineWaypoints.Length - 1], m_InlineWaypoints[0]);
}
else
{
// 循环:画首尾连接
Gizmos.color = new Color(0.2f, 0.85f, 0.2f, 0.5f);
Gizmos.DrawLine(m_InlineWaypoints[m_InlineWaypoints.Length - 1], m_InlineWaypoints[0]);
}
}
// 编辑器标签(路点序号)
UnityEditor.Handles.color = new Color(0.2f, 0.85f, 0.2f, 1f);
for (int i = 0; i < m_InlineWaypoints.Length; i++)
UnityEditor.Handles.Label(m_InlineWaypoints[i] + Vector2.up * 0.25f, $"WP{i}");
}
#endif
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e7a2cb1e940ff92499a0ebb9d3063e21
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
@@ -10,6 +10,9 @@ namespace BaseGames.Enemies.AI
/// BD Action通过 AnimationClip 名称播放 Animancer 动画,立即返回 Success。
/// ClipName 需与 EnemyAnimationConfigSO 中字段名一致。
/// </summary>
[TaskName("Play Animation")]
[TaskCategory("BaseGames/Enemy/Animation")]
[TaskDescription("通过 Animancer 播放指定动画 Clip支持等待动画结束后返回")]
public class BD_PlayAnimation : Action
{
/// <summary>EnemyAnimationConfigSO 中的 AnimationClip 字段名(如 "Attack_Melee")。</summary>
@@ -17,10 +20,7 @@ namespace BaseGames.Enemies.AI
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{

View File

@@ -0,0 +1,103 @@
#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归位到出生点<see cref="EnemyBase.HomePosition"/>)。
///
/// <list type="bullet">
/// <item>切换 <see cref="AiPhase.ReturnHome"/>,使用行走速度导航回初始位置。</item>
/// <item>到达(距离 ≤ homeRadius或路径失败时切换 <see cref="AiPhase.Idle"/>,返回 Success。</item>
/// <item>路径失败也返回 Success避免 BD 树卡死;上层节点继续恢复巡逻。</item>
/// </list>
///
/// 典型 BD 用法:
/// <code>
/// Sequence
/// BD_InvestigateLastKnown → Success
/// BD_ReturnToHome → Success
/// BD_SetAiPhase(Patrol)
/// </code>
/// </summary>
[TaskName("Return To Home")]
[TaskCategory("BaseGames/Enemy/Movement")]
[TaskDescription("导航回出生点;到达后切换 Idle 并返回 Success")]
public sealed class BD_ReturnToHome : Action
{
[Tooltip("到达判定半径m0 = 使用 EnemyStatsSO.HomeRadius")]
[SerializeField] private float m_ArriveRadius = 0f;
private EnemyBase _enemy;
private bool _reached;
private bool _pathFailed;
private bool _subscribed;
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
public override void OnStart()
{
_enemy.SetAiPhase(AiPhase.ReturnHome);
_reached = false;
_pathFailed = false;
if (!_subscribed && _enemy.Nav != null)
{
_enemy.Nav.OnGoalReached += HandleReached;
_enemy.Nav.OnNavPathFailed += HandleFailed;
_subscribed = true;
}
// 切换为行走速度
float walkSpeed = _enemy.Stats?.WalkSpeed ?? 2f;
_enemy.Nav?.SetSpeed(walkSpeed);
// 播放行走动画
var ac = _enemy.AnimConfig;
if (_enemy.Animancer != null && ac?.Walk != null)
_enemy.Animancer.Play(ac.Walk);
_enemy.MoveTo(_enemy.HomePosition);
}
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
if (_pathFailed) return CompleteReturn();
if (_reached) return CompleteReturn();
// 兜底距离判断(事件可能因帧序问题延迟一帧)
float radius = m_ArriveRadius > 0f ? m_ArriveRadius : (_enemy.Stats?.HomeRadius ?? 0.5f);
float sqr = ((Vector2)_enemy.transform.position - _enemy.HomePosition).sqrMagnitude;
if (sqr <= radius * radius)
return CompleteReturn();
return TaskStatus.Running;
}
public override void OnEnd()
{
if (_subscribed && _enemy?.Nav != null)
{
_enemy.Nav.OnGoalReached -= HandleReached;
_enemy.Nav.OnNavPathFailed -= HandleFailed;
_subscribed = false;
}
}
private TaskStatus CompleteReturn()
{
_enemy.StopMovement();
_enemy.SetAiPhase(AiPhase.Idle);
return TaskStatus.Success;
}
private void HandleReached() => _reached = true;
private void HandleFailed() => _pathFailed = true;
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0555fce4edd236847b580a2b436bd22f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,34 @@
#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设置 AI 行为阶段(<see cref="AiPhase"/>)。
/// 单帧完成,返回 Success。常用于行为树中作为阶段过渡的标记节点
/// 例如在 Selector 分支开头标记当前进入了哪个阶段。
/// </summary>
[TaskName("Set Ai Phase")]
[TaskCategory("BaseGames/Enemy/State")]
[TaskDescription("立即切换 AI 行为阶段AiPhase单帧返回 Success")]
public sealed class BD_SetAiPhase : Action
{
[Tooltip("目标 AI 行为阶段")]
[SerializeField] private AiPhase m_Phase = AiPhase.Idle;
private EnemyBase _enemy;
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
_enemy.SetAiPhase(m_Phase);
return TaskStatus.Success;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6f70a0ea7dc70bb4cbf300d19eacfba0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
@@ -8,14 +8,14 @@ namespace BaseGames.Enemies.AI
/// <summary>
/// BD Action设置警觉状态并通知 EnemyBase 调整 BT Tick 频率。
/// </summary>
[TaskName("Set Alert Tick Rate")]
[TaskCategory("BaseGames/Enemy/Perception")]
[TaskDescription("切换行为树 Tick 频率:警觉时高频,巡逻时低频")]
public class BD_SetAlert : Action
{
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{

View File

@@ -0,0 +1,45 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
using BaseGames.Enemies.Abilities;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action强制切换敌人到指定 EnemyStateType并中断所有正在执行的能力。
/// 单帧完成,立即返回 Success。
///
/// 典型用法:
/// - 进入 Stagger 状态后重置为 Idle替代手动停止动画
/// - 技能打断后显式回到 Controlled 状态
/// </summary>
[TaskName("Set State")]
[TaskCategory("BaseGames/Enemy/State")]
[TaskDescription("强制切换敌人物理/战斗状态ForceState")]
public class BD_SetState : Action
{
[Tooltip("切换到的目标状态")]
[SerializeField] private EnemyStateType m_TargetState = EnemyStateType.Controlled;
[Tooltip("切换状态时同时中断所有正在执行的能力")]
[SerializeField] private bool m_InterruptAbilities = true;
private EnemyBase _enemy;
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
if (m_InterruptAbilities)
_enemy.Abilities?.InterruptAll(InterruptReason.ExternalRequest);
_enemy.ForceState(m_TargetState);
return TaskStatus.Success;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d3406f00433d44e449c24b4340b0c49a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
@@ -11,6 +11,9 @@ namespace BaseGames.Enemies.AI
/// BD Action在敌人当前位置生成弹射物。
/// ProjectileKey 为 Addressable 键,通过 GlobalObjectPool 实例化。
/// </summary>
[TaskName("Spawn Projectile")]
[TaskCategory("BaseGames/Enemy/Utility")]
[TaskDescription("在指定位置生成投射物并赋予初速度")]
public class BD_SpawnProjectile : Action
{
[SerializeField] private Vector2 m_Direction = Vector2.right;

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
@@ -8,14 +8,14 @@ namespace BaseGames.Enemies.AI
/// <summary>
/// BD Action立即停止移动单帧完成返回 Success。
/// </summary>
[TaskName("Stop Movement")]
[TaskCategory("BaseGames/Enemy/Movement")]
[TaskDescription("立即停止 NavAgent 移动,单帧返回 Success")]
public class BD_StopMovement : Action
{
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override TaskStatus OnUpdate()
{

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
@@ -12,28 +12,40 @@ namespace BaseGames.Enemies.AI
/// BD ActionBoss 专用召唤小兵。
/// 通过 GlobalObjectPool 在 Boss 周围生成 MinionPrefabKey 对应的敌人。
/// </summary>
[TaskName("Summon Minions")]
[TaskCategory("BaseGames/Enemy/Boss")]
[TaskDescription("在指定位置生成小怪预制体(支持延迟和数量配置)")]
public class BD_SummonMinions : Action
{
[Header("召唤配置")]
[Tooltip("对象池 Key对应 PoolRegistry 中注册的小怪预制体。")]
[SerializeField] private string m_MinionPrefabKey = "";
[Tooltip("单次召唤的小兵数量。")]
[Min(1)]
[SerializeField] private int m_Count = 2;
[Tooltip("小兵生成位置距 Boss 中心的横向散开半径m。")]
[Min(0.1f)]
[SerializeField] private float m_SpawnRadius = 3f;
// 延迟缓存:首次调用时解析,避免每帧服务定位开销
private IObjectPoolService _pool;
public override TaskStatus OnUpdate()
{
if (string.IsNullOrEmpty(m_MinionPrefabKey)) return TaskStatus.Failure;
var pool = ServiceLocator.GetOrDefault<IObjectPoolService>();
if (pool == null) return TaskStatus.Failure;
_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(
float angleRad = (i / (float)m_Count) * Mathf.PI * 2f;
var offset = new Vector2(Mathf.Cos(angleRad), 0f) * m_SpawnRadius;
var spawnPos = new Vector3(
transform.position.x + offset.x,
transform.position.y + offset.y,
transform.position.y,
0f);
pool.Spawn(m_MinionPrefabKey, spawnPos, Quaternion.identity);
_pool.Spawn(m_MinionPrefabKey, spawnPos, Quaternion.identity);
}
return TaskStatus.Success;

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
@@ -10,6 +10,9 @@ namespace BaseGames.Enemies.AI
/// BD Action触发攻击预警TelegraphSystem等待 Duration 秒后返回 Success。
/// 对应架构 07_EnemyModule §8 BD_TelegraphAttack与 TelegraphSystem 协作。
/// </summary>
[TaskName("Telegraph Attack")]
[TaskCategory("BaseGames/Enemy/Combat")]
[TaskDescription("播放攻击前摇动画并等待结束后返回 Success")]
public class BD_TelegraphAttack : Action
{
[SerializeField] private float m_Duration = 1f;
@@ -17,15 +20,18 @@ namespace BaseGames.Enemies.AI
private TelegraphSystem _telegraph;
private float _elapsed;
private Coroutine _telegraphCoroutine;
public override void OnAwake() => _telegraph = GetComponent<TelegraphSystem>();
public override void OnStart()
{
_telegraph = GetComponent<TelegraphSystem>();
_elapsed = 0f;
_elapsed = 0f;
_telegraphCoroutine = null;
if (_telegraph != null && !string.IsNullOrEmpty(m_VfxKey))
{
_telegraph.StartCoroutine(
_telegraphCoroutine = _telegraph.StartCoroutine(
_telegraph.ShowTelegraph(m_VfxKey, m_Duration, transform.position));
}
}
@@ -35,6 +41,16 @@ namespace BaseGames.Enemies.AI
_elapsed += Time.deltaTime;
return _elapsed >= m_Duration ? TaskStatus.Success : TaskStatus.Running;
}
public override void OnEnd()
{
// BD 任务被 Abort 时停止仍在播放的预警协程,避免孤立 VFX
if (_telegraphCoroutine != null)
{
_telegraph?.StopCoroutine(_telegraphCoroutine);
_telegraphCoroutine = null;
}
}
}
}
#endif

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
@@ -9,6 +9,9 @@ namespace BaseGames.Enemies.AI
/// BD Action瞬移传送到目标坐标Boss 专用。
/// 单帧直接修改 transform.position不使用导航。
/// </summary>
[TaskName("Teleport To")]
[TaskCategory("BaseGames/Enemy/Movement")]
[TaskDescription("瞬移到目标位置(配合粒子/闪烁能力)")]
public class BD_TeleportTo : Action
{
[SerializeField] private Vector2 m_Target;

View File

@@ -0,0 +1,58 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies.Abilities;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// 通用触发能力。OnStart 调用 ability.Execute()OnUpdate 等待结束。
/// 失败条件:能力不存在、能力 CanUse=false 或 Execute 返回 false。
/// </summary>
[TaskName("Use Ability")]
[TaskCategory("BaseGames/Enemy/Combat")]
[TaskDescription("触发指定能力(拖拽 EnemyAbilitySO 或填写 ID等待执行结束")]
public sealed class BD_UseAbility : Action
{
[Tooltip("可直接拖拽 EnemyAbilitySO 资产(推荐),或填写裸字符串 ID 作为兜底")]
[SerializeField] private EnemyAbilitySO m_AbilitySO;
[SerializeField] private string m_AbilityId = "";
private EnemyBase _enemy;
private EnemyAbilityBase _ability;
private bool _startedSuccessfully;
public override void OnAwake()
{
_enemy = gameObject.GetComponent<EnemyBase>();
}
public override void OnStart()
{
_startedSuccessfully = false;
if (_enemy == null) return;
// SO 拖拽优先;裸字符串兜底
string id = m_AbilitySO != null ? m_AbilitySO.abilityId : m_AbilityId;
if (string.IsNullOrEmpty(id)) return;
_ability = _enemy.Abilities.Get(id);
if (_ability == null) return;
_startedSuccessfully = _ability.Execute();
}
public override TaskStatus OnUpdate()
{
if (!_startedSuccessfully || _ability == null) return TaskStatus.Failure;
return _ability.IsRunning ? TaskStatus.Running : TaskStatus.Success;
}
public override void OnEnd()
{
_ability = null;
_startedSuccessfully = false;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4865a86627f84a54292736e6dc2b9e15
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,53 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Boss;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// 触发 Boss 技能。OnStart 调用 BossBase.UseBossSkill(skillId)OnUpdate 等待技能结束。
/// 失败条件:非 Boss 敌人、技能 ID 无效、BossSkillExecutor 未就绪或技能执行中。
/// </summary>
[TaskName("Use Boss Skill")]
[TaskCategory("BaseGames/Enemy/Boss")]
[TaskDescription("通过技能 ID 执行 Boss 指定技能")]
public sealed class BD_UseBossSkill : Action
{
[SerializeField] private string m_SkillId = "";
[Tooltip("可选:直接拖拽 BossSkillSO 资产以替代裸字符串(优先于 m_SkillId")]
[SerializeField] private BossSkillSO m_SkillSO;
private BossBase _boss;
private bool _startedSuccessfully;
public override void OnAwake()
{
_boss = gameObject.GetComponent<BossBase>();
}
public override void OnStart()
{
_startedSuccessfully = false;
if (_boss == null) return;
string id = m_SkillSO != null ? m_SkillSO.skillId : m_SkillId;
if (string.IsNullOrEmpty(id)) return;
_startedSuccessfully = _boss.UseBossSkill(id);
}
public override TaskStatus OnUpdate()
{
if (!_startedSuccessfully || _boss == null) return TaskStatus.Failure;
return _boss.IsBossSkillExecuting ? TaskStatus.Running : TaskStatus.Success;
}
public override void OnEnd()
{
_startedSuccessfully = false;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8f86542f8dede80438ab28ec682758b6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,54 @@
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using UnityEngine;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action在当前阶段可用且冷却就绪的技能中按 weight 加权随机选择一个技能并执行。
///
/// 返回 Running技能正在执行。
/// 返回 Success技能执行完成。
/// 返回 Failure无可用技能或执行器忙。
///
/// 用法:在 Boss BT 的 Combat 阶段,替代 BD_UseBossSkill固定 ID实现随机化技能组合。
/// 每个 BossSkillSO 设置 weight 字段控制出现概率:越大越常见。
/// </summary>
[TaskName("Use Boss Skill (Weighted)")]
[TaskCategory("BaseGames/Enemy/Boss")]
[TaskDescription("加权随机选择并执行可用技能;对上一次使用的技能施加权重惩罚")]
public class BD_UseBossSkillWeighted : Action
{
[Tooltip("等待技能执行完成后才返回 Success。关闭后执行开始即返回 SuccessBT 继续运行其他节点。")]
[SerializeField] private bool m_WaitForCompletion = true;
private BossBase _boss;
private bool _started;
public override void OnAwake() => _boss = GetComponent<BossBase>();
public override void OnStart()
{
_started = false;
}
public override TaskStatus OnUpdate()
{
if (_boss == null) return TaskStatus.Failure;
if (!_started)
{
bool ok = _boss.UseBossSkillWeighted();
if (!ok) return TaskStatus.Failure;
_started = true;
if (!m_WaitForCompletion) return TaskStatus.Success;
}
// 等待技能执行完成
return _boss.IsBossSkillExecuting ? TaskStatus.Running : TaskStatus.Success;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5cf18a1d81f80c646947ea0475c31108
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
@@ -9,6 +9,9 @@ namespace BaseGames.Enemies.AI
/// BD Action等待固定 Duration 秒后返回 Success。
/// 适用于攻击前摇、冷却间隔等固定等待。
/// </summary>
[TaskName("Wait")]
[TaskCategory("BaseGames/Enemy/Utility")]
[TaskDescription("等待固定 Duration 秒后返回 Success")]
public class BD_Wait : Action
{
[UnityEngine.SerializeField] private float m_Duration = 1f;

View File

@@ -1,4 +1,5 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
@@ -8,23 +9,36 @@ namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action等待当前 Animancer 动画播放完毕再返回 Success。
/// 通过 EnemyBase.IsAnimationComplete 轮询(Animancer CurrentState.NormalizedTime >= 1)。
/// 通过 Animancer CurrentState.NormalizedTime >= 1 轮询;
/// 若动画为循环动画NormalizedTime 每帧 wrap 到 0超时后强制返回 Success防止永久挂死。
/// </summary>
[TaskName("Wait For Animation")]
[TaskCategory("BaseGames/Enemy/Animation")]
[TaskDescription("等待 Animancer 当前动画播放完毕后返回 Success。循环动画请设置超时时间。")]
public class BD_WaitForAnimation : Action
{
[Tooltip("安全超时(秒)。若动画是循环型,超时后自动返回 Success。0 = 不启用超时(仅适合非循环动画)。")]
[SerializeField, Min(0f)] private float m_Timeout = 5f;
private EnemyBase _enemy;
private float _deadline;
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
_deadline = m_Timeout > 0f ? Time.time + m_Timeout : float.MaxValue;
}
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
// 超时保护:防止循环动画导致永久阻塞
if (Time.time >= _deadline) return TaskStatus.Success;
var state = _enemy.Animancer?.States?.Current;
if (state == null) return TaskStatus.Success; // 无动画直接继续
if (state == null) return TaskStatus.Success; // 无动画直接继续
if (state.NormalizedTime >= 1f) return TaskStatus.Success;
return TaskStatus.Running;

View File

@@ -1,4 +1,4 @@
#if GRAPH_DESIGNER
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
@@ -9,6 +9,9 @@ namespace BaseGames.Enemies.AI
/// BD Action在 [Min, Max] 范围内随机等待后返回 Success。
/// 适用于巡逻间隔、随机攻击延迟等自然行为。
/// </summary>
[TaskName("Wait (Random)")]
[TaskCategory("BaseGames/Enemy/Utility")]
[TaskDescription("在 Min~Max 范围内随机等待后返回 Success")]
public class BD_WaitRandom : Action
{
[UnityEngine.SerializeField] private float m_Min = 0.5f;

View File

@@ -0,0 +1,68 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
using BaseGames.Enemies.Abilities;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action等待指定能力执行结束后返回 Success。
/// 适用于需要同步等待某个由外部触发的能力(而非本节点触发)的场景。
///
/// 返回 Running能力正在执行。
/// 返回 Success能力执行完毕IsRunning = false
/// 返回 Failure能力不存在或超时保护触发。
///
/// 典型用法Sequence [ BD_UseAbility(A) → BD_WaitUntilAbilityEnd(B) ] 等待 B 被 A 内部链式触发后结束。
/// </summary>
[TaskName("Wait Until Ability End")]
[TaskCategory("BaseGames/Enemy/Combat")]
[TaskDescription("等待指定能力执行完成后返回 Success")]
public sealed class BD_WaitUntilAbilityEnd : Action
{
[Tooltip("可直接拖拽 EnemyAbilitySO 资产(推荐),或填写裸字符串 ID 作为兜底")]
[SerializeField] private EnemyAbilitySO m_AbilitySO;
[SerializeField] private string m_AbilityId = "";
[Tooltip("超时保护0 = 永不超时")]
[SerializeField, Min(0f)] private float m_Timeout = 5f;
private EnemyBase _enemy;
private EnemyAbilityBase _ability;
private float _deadline;
public override void OnAwake() => _enemy = GetComponent<EnemyBase>();
public override void OnStart()
{
_ability = null;
_deadline = m_Timeout > 0f ? Time.time + m_Timeout : float.MaxValue;
if (_enemy == null) return;
string id = m_AbilitySO != null ? m_AbilitySO.abilityId : m_AbilityId;
if (!string.IsNullOrEmpty(id))
_ability = _enemy.Abilities?.Get(id);
}
public override TaskStatus OnUpdate()
{
if (_ability == null) return TaskStatus.Failure;
if (Time.time >= _deadline)
{
Debug.LogWarning($"[BD_WaitUntilAbilityEnd] 能力 '{_ability.Config?.abilityId}' 超时,强制中断", gameObject);
// 强制中断仍在运行的能力,防止 HitBox/动画协程泄漏
if (_ability.IsRunning)
_ability.Interrupt(InterruptReason.ExternalRequest);
return TaskStatus.Failure;
}
return _ability.IsRunning ? TaskStatus.Running : TaskStatus.Success;
}
public override void OnEnd() => _ability = null;
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9311a809097042f4b8b5de9681d51d0d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,44 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// 动作:随机游走(调用 NavAgent.SetRandomDestination
/// 到达目的地后立即选下一个随机点,持续 Running。
/// 常用于巡逻 fallback 或无路点的小怪。
/// </summary>
[TaskName("Walk Random")]
[TaskCategory("BaseGames/Enemy/Movement")]
[TaskDescription("随机游走:持续选取随机目的地导航(常用于待机巡逻兜底)")]
public sealed class BD_WalkRandom : Action
{
[Tooltip("尝试选取随机点的次数")]
[SerializeField] private int m_RetryCount = 5;
private EnemyBase _enemy;
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
public override void OnStart() => TryWalk();
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
if (_enemy.Nav.IsAtDestination()) TryWalk();
return TaskStatus.Running;
}
public override void OnEnd() => _enemy?.StopMovement();
private void TryWalk()
{
for (int i = 0; i < m_RetryCount; i++)
if (_enemy.Nav.WalkToRandom()) break;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a55d2b270a2c1d34a9b08771feedba62
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -7,13 +7,14 @@
"noEngineReferences": false,
"versionDefines": [],
"rootNamespace": "BaseGames.Enemies.AI",
"references": [
"references": [
"BaseGames.Core",
"BaseGames.Core.Events",
"BaseGames.Enemies",
"BaseGames.Enemies.Boss.Patterns",
"Opsive.BehaviorDesigner.Runtime",
"Kybernetik.Animancer"
"Kybernetik.Animancer",
"Micosmo.SensorToolkit"
],
"autoReferenced": true,
"overrideReferences": false,