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