多轮审查和修复

This commit is contained in:
2026-05-12 15:34:08 +08:00
parent f55d2a57c3
commit ebbbb7332e
805 changed files with 838724 additions and 1905 deletions

View 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

View File

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

View 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 ActionBoss 切换阶段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

View File

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

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;
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

View File

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

View 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

View File

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

View 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 ConditionalBoss 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

View File

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

View 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

View File

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

View 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

View File

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

View 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

View File

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

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;
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

View File

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

View 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

View File

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

View File

@@ -1,5 +1,4 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
@@ -8,26 +7,24 @@ namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action移向玩家。
/// OnStart 缓存玩家 TransformOnUpdate 每帧更新导航目标。
/// OnStart 从 EnemyBase.PlayerTransform 获取玩家引用(由 _onPlayerSpawned 事件缓存,
/// 避免 FindWithTag 全场景扫描。OnUpdate 每帧更新导航目标。
/// </summary>
public class BD_MoveToPlayer : Action
{
private EnemyBase _enemy;
private Transform _playerTransform;
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
var playerGO = GameObject.FindWithTag("Player");
if (playerGO != null) _playerTransform = playerGO.transform;
}
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
if (_playerTransform == null) return TaskStatus.Failure;
if (_enemy == null) return TaskStatus.Failure;
if (_enemy.PlayerTransform == null) return TaskStatus.Failure;
_enemy.MoveTo(_playerTransform.position);
_enemy.MoveTo(_enemy.PlayerTransform.position);
_enemy.FacePlayer();
return TaskStatus.Running;
}

View File

@@ -2,16 +2,25 @@
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
using UnityEngine;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action敌人巡逻行为Phase 1 简单实现)
/// 挂在 BT 节点上,持续令敌人向当前朝向移动。
/// BD Action敌人巡逻行为。
/// 持续令敌人向当前朝向移动,遇墙/边缘时自动转向
/// </summary>
public class BD_Patrol : Action
{
[Tooltip("检测地面边缘的向下射线长度")]
public float edgeCheckLength = 1.2f;
[Tooltip("检测障碍物的水平射线长度")]
public float wallCheckLength = 0.4f;
[Tooltip("地面/墙壁 LayerMask")]
public LayerMask groundLayer;
private EnemyBase _enemy;
private float _dir = 1f;
public override void OnStart()
{
@@ -21,8 +30,11 @@ namespace BaseGames.Enemies.AI
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
// Phase 1固定向右巡逻Phase 2 改为往返检测
_enemy.MoveInDirection(1f);
if (ShouldFlip())
_dir = -_dir;
_enemy.MoveInDirection(_dir);
return TaskStatus.Running;
}
@@ -30,6 +42,23 @@ namespace BaseGames.Enemies.AI
{
_enemy?.StopMovement();
}
private bool ShouldFlip()
{
Transform t = _enemy.transform;
Vector2 pos = t.position;
// 前方边缘检测:在脚前方向下射线,若无地面则转向
Vector2 edgeOrigin = pos + Vector2.right * (_dir * 0.3f) + Vector2.down * 0.1f;
bool hasGround = Physics2D.Raycast(edgeOrigin, Vector2.down, edgeCheckLength, groundLayer);
if (!hasGround) return true;
// 前方障碍检测:水平射线,若撞墙则转向
bool hitWall = Physics2D.Raycast(pos, Vector2.right * _dir, wallCheckLength, groundLayer);
if (hitWall) return true;
return false;
}
}
}
#endif

View 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

View File

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

View 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

View File

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

View 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

View File

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

View 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

View File

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

View 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 ActionBoss 专用召唤小兵。
/// 通过 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

View File

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

View 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

View File

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

View 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

View File

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

View 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

View File

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

View 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

View File

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

View 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

View File

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

View File

@@ -9,6 +9,7 @@
"rootNamespace": "BaseGames.Enemies.AI",
"references": [
"BaseGames.Enemies",
"BaseGames.Enemies.Boss.Patterns",
"Opsive.BehaviorDesigner.Runtime"
],
"autoReferenced": true,

View 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);
}
}
}

View File

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

View File

@@ -0,0 +1,27 @@
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// BatchLOSSystem 的注册接口(架构 07_EnemyModule §12
/// 实现此接口的 EnemyBase 子类可以注册到 BatchLOSSystem
/// 以批处理方式接收 LOSLine 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);
}
}

View File

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