refactor(enemy): 消费者切换到 IEnemyNavigator(Wander/Waypoints 去导航图化,调试面板简化)

This commit is contained in:
2026-07-28 11:25:21 +08:00
parent 49627d357d
commit de3c9deff4
4 changed files with 45 additions and 158 deletions
+9 -9
View File
@@ -13,7 +13,7 @@ namespace BaseGames.Enemies
/// 敌人基类(架构 07_EnemyModule §1)。
/// 实现 IDamageable,为 Behavior Designer 任务提供统一虚方法接口。
/// 包含:BD 接口、受击、死亡流程。
/// ⚠️ _nav 字段类型为 IPathAgent(在 BaseGames.Enemies.Navigation 中实现具体类)。
/// ⚠️ _nav 字段类型为 IEnemyNavigator(在 BaseGames.Enemies.Navigation 中实现具体类)。
/// 实现 IPoolable:配合 PooledObject 支持对象池复用,避免频繁 Destroy/Instantiate。
/// </summary>
public class EnemyBase : MonoBehaviour, IDamageable, IPoolable
@@ -60,10 +60,10 @@ namespace BaseGames.Enemies
/// </summary>
[SerializeField] private BaseGames.Core.Events.TransformEventChannelSO _onPlayerSpawned;
// ── 导航代理(IPathAgent;由 EnemyNavAgent 实现)───────────────────
// ── 导航代理(IEnemyNavigator;由 GroundNavigator/FlyingNavigator 实现)─────
// 通过接口引用,避免对 Navigation 程序集的直接依赖。
// 由子类 / Inspector 注入,或者运行时 GetComponent<IPathAgent>() 获取。
protected IPathAgent _nav;
// 由子类 / Inspector 注入,或者运行时 GetComponent<IEnemyNavigator>() 获取。
protected IEnemyNavigator _nav;
// 移动执行器(IEnemyLocomotion;由 EnemyLocomotion 在 Navigation 程序集实现,运行时发现)
protected IEnemyLocomotion _locomotion;
@@ -216,7 +216,7 @@ namespace BaseGames.Enemies
}
// BD 任务访问接口(公共只读属性)────────────────────────────────
public IPathAgent Nav => _nav;
public IEnemyNavigator Nav => _nav;
// 惰性发现兜底:关域重载后 Awake 设的实例字段可能残留为 null(编辑器快速迭代),
// 首次访问时按需补发现,保证编辑器与构建下都不为空。
public IEnemyLocomotion Locomotion => _locomotion ?? (_locomotion = GetComponentInChildren<IEnemyLocomotion>(true));
@@ -256,7 +256,7 @@ namespace BaseGames.Enemies
// ── BD 行为树接口(虚方法)────────────────────────────────────────
public virtual void MoveTo(Vector2 target)
=> _nav?.RequestMoveTo(target);
=> _nav?.MoveTowards(target);
public virtual void MoveInDirection(float dir)
{
@@ -284,7 +284,7 @@ namespace BaseGames.Enemies
public virtual void StopMovement()
{
_nav?.StopNavigation();
_nav?.Stop();
if (_movement != null) _movement.PendingInput.WantStop = true;
}
@@ -547,7 +547,7 @@ namespace BaseGames.Enemies
_stateObjs[EnemyStateType.KnockUp] = new EnemyKnockUpState();
_stateObjs[EnemyStateType.Dead] = new EnemyDeadState();
_nav = GetComponent<IPathAgent>() ?? new NullPathAgent();
_nav = GetComponent<IEnemyNavigator>() ?? new NullEnemyNavigator();
_locomotion = GetComponentInChildren<IEnemyLocomotion>(true);
if (_locomotion == null)
Debug.LogError($"EnemyBase 未找到 EnemyLocomotion 组件:{name}", this);
@@ -771,7 +771,7 @@ namespace BaseGames.Enemies
public virtual void OnDespawn()
{
_abilities.InterruptAll(InterruptReason.Dead);
_nav?.StopNavigation();
_nav?.Stop();
// GO 停用时 EnemyAiBrain.Update 自然停止,无需显式处理。
}
+1 -1
View File
@@ -7,7 +7,7 @@ namespace BaseGames.Enemies
/// <summary>
/// 飞行敌人基类。
///
/// 导航由 <see cref="Navigation.FlyingDirectNavigator"/> 实现(IPathAgent),
/// 导航由 <see cref="Navigation.FlyingNavigator"/> 实现(IEnemyNavigator),
/// AI 行为逻辑由挂载的 Behavior Designer 树驱动。
/// 本类仅负责:
/// <list type="bullet">
@@ -5,7 +5,7 @@ namespace BaseGames.Enemies
{
/// <summary>
/// 敌人移动执行器:唯一的移动/朝向入口。AI 状态与能力经 IEnemyLocomotion
/// 声明意图,本组件每帧把当前模式翻译为对 EnemyBase/IPathAgent 的调用。
/// 声明意图,本组件每帧把当前模式翻译为对 EnemyBase/IEnemyNavigator 的调用。
/// 取代散落的 MoveTo/StopMovement/FacePlayer/FaceTarget 直调与三个协程能力。
/// </summary>
[DisallowMultipleComponent]
@@ -38,9 +38,8 @@ namespace BaseGames.Enemies
private int _wpIndex = -1;
private int _wpDir = 1;
private bool _warnedNoWaypoints;
// 当前路点吸附后的"站得住"目标(宽度由 Nav 层解析,本层不感知身体尺寸
// 当前路点坐标(导航图已移除,直接用原始坐标;走不到会被夹停
private Vector2 _wpResolvedGoal;
private bool _wpResolveOk; // 上次路点是否成功吸附到导航段(调试用)
// Wander 到点停顿状态
private bool _wanderPausing;
private float _wanderPauseTimer;
@@ -141,17 +140,19 @@ namespace BaseGames.Enemies
}
/// <summary>
/// 段内随机游走:随机点限制在当前所站的导航段上(不跨 NavLink,避免崖边夹停/卡 link
/// 到达一个点后原地停顿 [_wanderPauseMin, _wanderPauseMax] 秒(期间播 Idle再挑下一个点。
/// 就近随机游走:随机取的目标点走,**到达或被地形夹停**即视为本次游走结束
/// 停顿 [_wanderPauseMin, _wanderPauseMax] 秒(期间播 Idle再挑下一个点。
/// 走到平台边缘停下转身是预期表现——可达性由移动层夹紧决定,不做地形扫描。
/// </summary>
private void TickWander()
{
if (_enemy.Nav == null) return;
var nav = _enemy.Nav;
if (nav == null) return;
// 移动中:清停顿态,等到达
if (_enemy.Nav.IsMoving) { _wanderPausing = false; return; }
// 仍在推进:清停顿态,等到达/受阻
if (nav.IsMoving) { _wanderPausing = false; return; }
// 到达或尚未出发:先停顿计时,再挑下一个点
// 到达、受阻或尚未出发:先停顿计时,再挑下一个点
if (!_wanderPausing)
{
_wanderPausing = true;
@@ -162,7 +163,7 @@ namespace BaseGames.Enemies
if (_wanderPauseTimer <= 0f)
{
_wanderPausing = false;
_enemy.Nav.WalkToRandomOnSegment();
if (nav.TryPickWanderPoint(out var point)) nav.MoveTowards(point);
}
}
@@ -213,7 +214,7 @@ namespace BaseGames.Enemies
Vector2.Distance(_enemy.transform.position, _wpResolvedGoal) <= _waypointArriveRadius;
// 受阻结算:执行层已把敌人带到最近可达点并停下(或原地停) → 视为本路点完成。
bool obstructedSettled = nav != null && nav.LastMoveObstructed && !nav.IsMoving;
bool obstructedSettled = nav != null && nav.IsBlocked;
if (_wpIndex < 0 || wpArrived || obstructedSettled)
{
@@ -232,31 +233,21 @@ namespace BaseGames.Enemies
AdvanceWaypoint();
SetWaypointGoal();
}
else if (nav != null && !nav.IsMoving && !nav.LastMoveObstructed)
else if (nav != null && !nav.IsMoving && !nav.IsBlocked && !nav.HasArrived)
{
// 瞬态:刚出生尚未映射那一帧的 MoveTo 失败 → 重发(RequestMoveTo 自带 0.25s 防抖)。
// 瞬态:刚出生尚未映射那一帧的 MoveTo 失败 → 重发(MoveTowards 自带防抖)。
// 仅"未受阻"时重发;受阻由上面 obstructedSettled 分支结算,不再无限重发不可达点。
_enemy.MoveTo(_wpResolvedGoal);
nav.MoveTowards(_wpResolvedGoal);
}
break;
}
}
/// <summary>
/// 解析当前路点为"身体站得住的可达点"并出发。宽度处理下沉给 Nav 层(<see cref="IPathAgent.ResolveStandablePoint"/>),
/// 本层只递原始路点坐标、不感知身体尺寸。吸附失败=路点摆得离导航面太远,显式报错暴露根因,不静默兜底。
/// </summary>
/// <summary>取当前路点坐标并出发。路点若摆在崖外/墙后,敌人会走到边缘被夹停 → IsBlocked → 结算推进下一个。</summary>
private void SetWaypointGoal()
{
Vector2 raw = _waypoints[_wpIndex].position;
_wpResolveOk = _enemy.Nav != null && _enemy.Nav.ResolveStandablePoint(raw, out _wpResolvedGoal);
if (!_wpResolveOk)
{
_wpResolvedGoal = raw;
Debug.LogWarning($"[EnemyLocomotion] 路点 '{_waypoints[_wpIndex].name}'(index {_wpIndex}) 无法吸附到任何可行走导航段" +
"(超出搜索半径)。请检查该路点是否摆放在贴近地面/平台的可行走处。", this);
}
_enemy.MoveTo(_wpResolvedGoal);
_wpResolvedGoal = _waypoints[_wpIndex].position;
_enemy.Nav?.MoveTowards(_wpResolvedGoal);
}
private void AdvanceWaypoint()
@@ -280,7 +271,6 @@ namespace BaseGames.Enemies
public Transform[] DebugWaypoints => _waypoints;
public int DebugWaypointIndex => _wpIndex;
public Vector2 DebugResolvedGoal => _wpResolvedGoal;
public bool DebugResolveOk => _wpResolveOk;
public float DebugArriveRadius => _waypointArriveRadius;
public bool DebugWanderPausing => _wanderPausing;
@@ -298,11 +288,11 @@ namespace BaseGames.Enemies
if (next != null) Gizmos.DrawLine(_waypoints[i].position, next.position);
}
// 运行期:当前吸附目标 + 到达半径 + 连线(绿=吸附成功 红=失败回落原始点)
// 运行期:当前目标 + 到达半径 + 连线
if (Application.isPlaying && _wpIndex >= 0)
{
Vector3 from = _enemy != null ? _enemy.transform.position : transform.position;
Gizmos.color = _wpResolveOk ? Color.green : Color.red;
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(_wpResolvedGoal, 0.22f);
Gizmos.DrawLine(from, _wpResolvedGoal);
Gizmos.color = new Color(1f, 0.9f, 0.2f, 0.7f);