docs(enemy): 敌人寻路受阻处理实现计划(6任务,编译门+Play验证)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,509 @@
|
||||
# 敌人寻路受阻处理 实现计划(Waypoints 巡逻 + MoveTo 一次性目标)
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 让走 PathBerserker2d 寻路的移动在目标被障碍挡住时「寻路到最近可到达点」而非对着障碍空推,同时覆盖「障碍已进图(寻路失败)」与「障碍未进图(物理卡死)」两种成因。
|
||||
|
||||
**Architecture:** 三层——PB2D `NavAgent` 暴露既有的最近可达点;`EnemyNavAgent` 执行层做卡死检测 + 统一受阻处理 + best-effort 改道(可复用);`EnemyLocomotion` 意图层只负责 Waypoints「受阻/到达即结算推进」。设计文档见 `Docs_Dev/superpowers/specs/2026-07-23-enemy-pathfind-obstruction-handling-design.md`。
|
||||
|
||||
**Tech Stack:** Unity C#、PathBerserker2d(vendored 源码可改)、Unity MCP(编译/Play 验证)。
|
||||
|
||||
**测试策略(重要,TDD 适配):** 本改动全部为 Unity 引擎耦合的运行时逻辑(NavGraph/物理/协程),项目无单元测试工程。因此**不写 NUnit 单测**,改用两道验证门:① 每个代码任务后用 `unity_get_compilation_errors` 确认零编译错误;② 全部完成后在 Play 模式经 Unity MCP 做行为验证(Task 6)。任务顺序已排成**每一步都能独立编译**。
|
||||
|
||||
> **Unity MCP 前置:** 首个 MCP 调用会自动发现实例;若多开需先 `unity_select_instance`。所有 MCP 调用走 `unity_*` 工具,勿直连 HTTP 桥。
|
||||
|
||||
---
|
||||
|
||||
## Task 1: PB2D `NavAgent` 暴露最近可达点
|
||||
|
||||
**Files:**
|
||||
- Modify: `Assets/PathBerserker2d/Scripts/PathBerserker2d/NavAgent/NavAgent.cs`
|
||||
|
||||
- [ ] **Step 1: 加私有字段**
|
||||
|
||||
在字段区 `private bool stopRequested;`(约 `:366`)之后新增一行:
|
||||
|
||||
```csharp
|
||||
private bool stopRequested;
|
||||
// [项目新增] 上次寻路失败(NoPathFromStartToGoal)时算出的最近可达点,供执行层"寻路到最近可到达点"读取。
|
||||
private NavSegmentPositionPointer _lastClosestReachable = NavSegmentPositionPointer.Invalid;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 加公共读取方法**
|
||||
|
||||
在 `CanReach(Vector2 goal)` 方法结束(`return pr.Status == PathRequest.RequestState.Finished; }`,约 `:838`)之后新增:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// After the most recent failed path request (NoPathFromStartToGoal), returns the closest
|
||||
/// reachable world position found towards the goal. Returns false if none is available.
|
||||
/// </summary>
|
||||
public bool TryGetClosestReachablePosition(out Vector2 worldPos)
|
||||
{
|
||||
if (!_lastClosestReachable.IsInvalid())
|
||||
{
|
||||
worldPos = _lastClosestReachable.Position;
|
||||
return true;
|
||||
}
|
||||
worldPos = Vector2.zero;
|
||||
return false;
|
||||
}
|
||||
|
||||
// [项目新增] 记录失败请求的最近可达点后再广播寻路失败,供执行层读取。仅 NoPath 失败才有最近点。
|
||||
private void RaiseFailedToFindPath(PathRequest failedRequest)
|
||||
{
|
||||
_lastClosestReachable = (failedRequest != null
|
||||
&& failedRequest.FailReason == PathRequest.RequestFailReason.NoPathFromStartToGoal)
|
||||
? failedRequest.closestReachablePosition
|
||||
: NavSegmentPositionPointer.Invalid;
|
||||
OnFailedToFindPath?.Invoke(this);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 替换 `HandlePathRequest` 里两处失败广播**
|
||||
|
||||
第一处——`Failed` 分支 `!allowCloseEnoughPath` 内(约 `:956`):
|
||||
|
||||
```csharp
|
||||
OnFailedToFindPath?.Invoke(this);
|
||||
currentPathRequest.Reset();
|
||||
```
|
||||
改为:
|
||||
```csharp
|
||||
RaiseFailedToFindPath(currentPathRequest);
|
||||
currentPathRequest.Reset();
|
||||
```
|
||||
|
||||
第二处——`Failed` 分支 `allowCloseEnoughPath` 内(约 `:967`):
|
||||
|
||||
```csharp
|
||||
OnFailedToFindPath?.Invoke(this);
|
||||
}
|
||||
break;
|
||||
case PathRequest.RequestState.Finished:
|
||||
```
|
||||
改为:
|
||||
```csharp
|
||||
RaiseFailedToFindPath(currentPathRequest);
|
||||
}
|
||||
break;
|
||||
case PathRequest.RequestState.Finished:
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 替换 `Repath` 里的失败广播**
|
||||
|
||||
`Repath()` 的 `Failed` 分支(约 `:927`):
|
||||
|
||||
```csharp
|
||||
Stop();
|
||||
OnFailedToFindPath?.Invoke(this);
|
||||
}
|
||||
|
||||
repathPathRequest.Reset();
|
||||
```
|
||||
改为:
|
||||
```csharp
|
||||
Stop();
|
||||
RaiseFailedToFindPath(repathPathRequest);
|
||||
}
|
||||
|
||||
repathPathRequest.Reset();
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 编译验证**
|
||||
|
||||
调用 `unity_get_compilation_errors`。
|
||||
预期:无错误。(`NavSegmentPositionPointer.Position`/`.IsInvalid()`/`.Invalid` 已在本文件使用过,均可访问。)
|
||||
|
||||
- [ ] **Step 6: 提交**
|
||||
|
||||
```bash
|
||||
git add Assets/PathBerserker2d/Scripts/PathBerserker2d/NavAgent/NavAgent.cs
|
||||
git commit -m "feat(nav): NavAgent 暴露寻路失败时的最近可达点 TryGetClosestReachablePosition"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: `EnemyNavAgent` 卡死检测 + 统一受阻处理 + best-effort 改道
|
||||
|
||||
**Files:**
|
||||
- Modify: `Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs`
|
||||
|
||||
> 说明:本任务先把两个新成员做成**具体 public 方法/属性**(`LastMoveObstructed`、`TryGetClosestReachable`)。Task 4 才把它们加进 `IPathAgent` 接口——届时本类已满足接口,无需再改。
|
||||
|
||||
- [ ] **Step 1: 加序列化调参字段**
|
||||
|
||||
在 `_maxSnapDistance` 字段块(约 `:42`)之后、`// 重寻路防抖状态` 之前新增:
|
||||
|
||||
```csharp
|
||||
[Header("受阻检测(寻路移动无进展即判被挡)")]
|
||||
[Tooltip("卡死检测窗口时长(s):段上移动时,此时长内朝子目标推进小于阈值即判受阻")]
|
||||
[SerializeField] private float _stuckWindowSeconds = 0.5f;
|
||||
[Tooltip("卡死推进阈值(m):窗口内朝子目标的距离缩小不足此值即判卡死")]
|
||||
[SerializeField] private float _stuckMinProgress = 0.05f;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 加受阻/卡死运行时状态字段**
|
||||
|
||||
在 `// 连接段状态缓存` 区(`_currentLinkType` 等,约 `:73`)之前或之后新增:
|
||||
|
||||
```csharp
|
||||
// 受阻状态(供意图层结算)
|
||||
private bool _lastMoveObstructed;
|
||||
private bool _redirectedToClosest; // 本轮已改道到最近点,避免每帧重复下发
|
||||
private bool _obstructionSignal; // 受阻信号(寻路失败/卡死),延后到 FixedUpdate 统一处理
|
||||
// 卡死检测窗口
|
||||
private float _stuckWinTimer;
|
||||
private float _stuckWinStartDist;
|
||||
private bool _stuckWinInit;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 加公共状态成员 + 最近点查询**
|
||||
|
||||
在 `public Vector2 CurrentLinkEnd => _currentLinkEnd;`(约 `:54`)之后新增:
|
||||
|
||||
```csharp
|
||||
/// <summary>上次寻路移动是否因不可达/被挡而受阻(走到最近点或原地停,而非到达原目标)。新目标 RequestMoveTo 后复位。</summary>
|
||||
public bool LastMoveObstructed => _lastMoveObstructed;
|
||||
|
||||
/// <summary>取上次寻路失败时算出的最近可达世界点。无则返回 false。</summary>
|
||||
public bool TryGetClosestReachable(out Vector2 worldPos)
|
||||
{
|
||||
if (_navAgent != null) return _navAgent.TryGetClosestReachablePosition(out worldPos);
|
||||
worldPos = Vector2.zero;
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: `RequestMoveTo` 新目标复位受阻态**
|
||||
|
||||
将 `RequestMoveTo`(约 `:145-159`)改为(新增 3 行复位):
|
||||
|
||||
```csharp
|
||||
public void RequestMoveTo(Vector2 target)
|
||||
{
|
||||
if (_navAgent == null) return;
|
||||
if (_hasPathGoal
|
||||
&& Vector2.Distance(target, _lastPathGoal) < _repathTargetThreshold
|
||||
&& Time.time - _lastPathTime < _repathMinInterval)
|
||||
return;
|
||||
|
||||
_lastPathGoal = target;
|
||||
_hasPathGoal = true;
|
||||
_lastPathTime = Time.time;
|
||||
_lastMoveObstructed = false; // 新目标 → 复位受阻态
|
||||
_redirectedToClosest = false;
|
||||
ResetStuckWindow();
|
||||
_navAgent.UpdatePath(target);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: `HandlePathFailed` 仅置信号(不在回调内重入寻路)**
|
||||
|
||||
将 `private void HandlePathFailed(NavAgent _) => OnNavPathFailed?.Invoke();`(约 `:251`)改为:
|
||||
|
||||
```csharp
|
||||
private void HandlePathFailed(NavAgent _)
|
||||
{
|
||||
_obstructionSignal = true; // 延后到 FixedUpdate 统一处理,避免在 NavAgent 失败回调内重入 UpdatePath
|
||||
OnNavPathFailed?.Invoke();
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: `FixedUpdate` 接入卡死检测 + 受阻处理**
|
||||
|
||||
将 `FixedUpdate`(约 `:260-285`)整体替换为:
|
||||
|
||||
```csharp
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (_enemyMovement == null || _navAgent == null) return;
|
||||
|
||||
bool onSegment = _navAgent.IsMovingOnSegment;
|
||||
_enemyMovement.NavDriving = onSegment; // 状态标记(供调试/上层判断"是否导航中")
|
||||
|
||||
TickStuckDetection(onSegment);
|
||||
|
||||
if (onSegment)
|
||||
{
|
||||
// 物理原生:朝子目标水平移动,由 EnemyMovement 施加 velocity、刚体重力保持贴地
|
||||
float dx = _navAgent.PathSubGoal.x - transform.position.x;
|
||||
_enemyMovement.PendingInput.MoveDir = Mathf.Abs(dx) > 0.05f ? Mathf.Sign(dx) : 0f;
|
||||
_enemyMovement.PendingInput.MoveSpeed = _movement != null ? _movement.movementSpeed : 0f;
|
||||
|
||||
// 段推进:到达当前子目标 → 推进到下一段
|
||||
if (_navAgent.HasReachedCurrentSubGoal(0.25f))
|
||||
_navAgent.CompleteSegmentTraversal();
|
||||
}
|
||||
else if (_wasNavOnSegment && !_navAgent.IsFollowingAPath)
|
||||
{
|
||||
// 导航刚结束(到达目标 / 路径失败)→ 停下水平移动
|
||||
_enemyMovement.PendingInput.WantStop = true;
|
||||
}
|
||||
|
||||
// 受阻信号统一处理(寻路失败/卡死):此处不在 NavAgent 回调内,UpdatePath 安全
|
||||
if (_obstructionSignal)
|
||||
{
|
||||
_obstructionSignal = false;
|
||||
ProcessObstruction();
|
||||
}
|
||||
|
||||
_wasNavOnSegment = onSegment;
|
||||
}
|
||||
|
||||
// 卡死检测:段上移动时,滑动窗口内朝子目标推进不足阈值 → 判受阻(覆盖"障碍未进图"的物理空推)。
|
||||
private void TickStuckDetection(bool onSegment)
|
||||
{
|
||||
if (!onSegment) { _stuckWinInit = false; return; }
|
||||
|
||||
float dist = Mathf.Abs(_navAgent.PathSubGoal.x - transform.position.x);
|
||||
if (!_stuckWinInit)
|
||||
{
|
||||
_stuckWinInit = true;
|
||||
_stuckWinTimer = 0f;
|
||||
_stuckWinStartDist = dist;
|
||||
return;
|
||||
}
|
||||
_stuckWinTimer += Time.fixedDeltaTime;
|
||||
if (_stuckWinTimer >= _stuckWindowSeconds)
|
||||
{
|
||||
if (_stuckWinStartDist - dist < _stuckMinProgress)
|
||||
_obstructionSignal = true; // 窗口内几乎没靠近子目标 → 卡死
|
||||
_stuckWinTimer = 0f;
|
||||
_stuckWinStartDist = dist;
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetStuckWindow() => _stuckWinInit = false;
|
||||
|
||||
// 统一受阻处理:尽力改道到最近可达点(走到障碍前),拿不到更近点则停下,交由意图层结算。
|
||||
private void ProcessObstruction()
|
||||
{
|
||||
_lastMoveObstructed = true;
|
||||
|
||||
if (!_redirectedToClosest
|
||||
&& _navAgent.TryGetClosestReachablePosition(out var closest)
|
||||
&& Vector2.Distance(transform.position, closest) > _repathTargetThreshold)
|
||||
{
|
||||
_redirectedToClosest = true;
|
||||
ResetStuckWindow();
|
||||
_navAgent.UpdatePath(closest); // 走向最近可达点
|
||||
return;
|
||||
}
|
||||
|
||||
// 无更近点 / 已在最近点附近(含"障碍未进图"无 closest 数据)→ 停下
|
||||
_navAgent.Stop();
|
||||
_enemyMovement.PendingInput.WantStop = true;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 7: 编译验证**
|
||||
|
||||
调用 `unity_get_compilation_errors`。
|
||||
预期:无错误。(`_movement`/`_navAgent`/`PathSubGoal`/`IsMovingOnSegment`/`HasReachedCurrentSubGoal`/`UpdatePath`/`Stop` 均已存在。)
|
||||
|
||||
- [ ] **Step 8: 提交**
|
||||
|
||||
```bash
|
||||
git add Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs
|
||||
git commit -m "feat(enemy): EnemyNavAgent 卡死检测+统一受阻处理+best-effort 改道到最近可达点"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: `FlyingDirectNavigator` 实现新成员(返回 false)
|
||||
|
||||
**Files:**
|
||||
- Modify: `Assets/_Game/Scripts/Enemies/Navigation/FlyingDirectNavigator.cs`
|
||||
|
||||
- [ ] **Step 1: 加两个成员**
|
||||
|
||||
在 IPathAgent 属性区 `public Vector2 CurrentLinkEnd => Vector2.zero;`(约 `:67`)之后新增:
|
||||
|
||||
```csharp
|
||||
// 飞行单位直飞、始终可达,无"受阻/最近可达点"概念。
|
||||
public bool LastMoveObstructed => false;
|
||||
public bool TryGetClosestReachable(out Vector2 worldPos) { worldPos = Vector2.zero; return false; }
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 编译验证**
|
||||
|
||||
调用 `unity_get_compilation_errors`。预期:无错误。
|
||||
|
||||
- [ ] **Step 3: 提交**
|
||||
|
||||
```bash
|
||||
git add Assets/_Game/Scripts/Enemies/Navigation/FlyingDirectNavigator.cs
|
||||
git commit -m "feat(enemy): FlyingDirectNavigator 实现受阻/最近可达点成员(飞行单位恒 false)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: `IPathAgent` 加接口成员 + `NullPathAgent` 实现
|
||||
|
||||
**Files:**
|
||||
- Modify: `Assets/_Game/Scripts/Enemies/IPathAgent.cs`
|
||||
|
||||
> 此时 `EnemyNavAgent`(Task 2)与 `FlyingDirectNavigator`(Task 3)已具备这两个成员,加进接口后即满足。
|
||||
|
||||
- [ ] **Step 1: 接口新增成员**
|
||||
|
||||
在接口内 `bool ResolveStandablePoint(Vector2 target, out Vector2 standable);`(约 `:78`)之后新增:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 上次寻路移动是否因目标不可达/被挡而受阻(走到最近可达点或原地停下,而非到达原目标)。
|
||||
/// 供意图层(Locomotion)判断本次移动应否视为结算/推进。对新目标 RequestMoveTo 后复位。
|
||||
/// </summary>
|
||||
bool LastMoveObstructed { get; }
|
||||
|
||||
/// <summary>取上次寻路失败时算出的最近可达世界点(尽力贴近目标的点)。无则返回 false。</summary>
|
||||
bool TryGetClosestReachable(out Vector2 worldPos);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `NullPathAgent` 实现**
|
||||
|
||||
在 `NullPathAgent` 内 `public bool ResolveStandablePoint(...) { standable = target; return false; }`(约 `:110`)之后新增:
|
||||
|
||||
```csharp
|
||||
public bool LastMoveObstructed => false;
|
||||
public bool TryGetClosestReachable(out Vector2 worldPos) { worldPos = Vector2.zero; return false; }
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 编译验证**
|
||||
|
||||
调用 `unity_get_compilation_errors`。
|
||||
预期:无错误(三个实现者 EnemyNavAgent/FlyingDirectNavigator/NullPathAgent 均已具备成员)。
|
||||
|
||||
- [ ] **Step 4: 提交**
|
||||
|
||||
```bash
|
||||
git add Assets/_Game/Scripts/Enemies/IPathAgent.cs
|
||||
git commit -m "feat(enemy): IPathAgent 增加 LastMoveObstructed/TryGetClosestReachable 契约"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: `EnemyLocomotion` Waypoints 受阻/到达结算,删除无限重发
|
||||
|
||||
**Files:**
|
||||
- Modify: `Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs`
|
||||
|
||||
- [ ] **Step 1: 加路点停顿状态字段**
|
||||
|
||||
在 `// Wander 到点停顿状态`(`_wanderPausing`/`_wanderPauseTimer`,约 `:45-46`)之后新增:
|
||||
|
||||
```csharp
|
||||
// Waypoints 受阻结算停顿状态
|
||||
private bool _wpPausing;
|
||||
private float _wpPauseTimer;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 切模式时复位路点停顿**
|
||||
|
||||
`SetMode` 内 `_wanderPausing = false;`(约 `:63`)之后新增一行:
|
||||
|
||||
```csharp
|
||||
_wanderPausing = false; // 切换模式清 Wander 停顿态
|
||||
_wpPausing = false; // 同时清 Waypoints 受阻停顿态
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 替换 Waypoints 分支**
|
||||
|
||||
将 `case PatrolStrategy.Waypoints:` 整段(约 `:187-214`,从 `case PatrolStrategy.Waypoints:` 到该 case 的 `break;`)替换为:
|
||||
|
||||
```csharp
|
||||
case PatrolStrategy.Waypoints:
|
||||
if (_waypoints == null || _waypoints.Length == 0)
|
||||
{
|
||||
if (!_warnedNoWaypoints)
|
||||
{
|
||||
Debug.LogWarning("[EnemyLocomotion] PatrolStrategy=Waypoints 但未配置 _waypoints,无法巡逻。", this);
|
||||
_warnedNoWaypoints = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
var nav = _enemy.Nav;
|
||||
|
||||
// 到达判定对"吸附后的可达点"做(原始路点可能摆在空中/崖外,永远进不了到达半径)。
|
||||
bool wpArrived = _wpIndex >= 0 &&
|
||||
Vector2.Distance(_enemy.transform.position, _wpResolvedGoal) <= _waypointArriveRadius;
|
||||
|
||||
// 受阻结算:执行层已把敌人带到最近可达点并停下(或原地停) → 视为本路点完成。
|
||||
bool obstructedSettled = nav != null && nav.LastMoveObstructed && !nav.IsMoving;
|
||||
|
||||
if (_wpIndex < 0 || wpArrived || obstructedSettled)
|
||||
{
|
||||
// 受阻结算走短暂停顿(复用 Wander 停顿参数);正常到达/首个点立即推进。
|
||||
if (obstructedSettled && !_wpPausing)
|
||||
{
|
||||
_wpPausing = true;
|
||||
_wpPauseTimer = Random.Range(_wanderPauseMin, Mathf.Max(_wanderPauseMin, _wanderPauseMax));
|
||||
}
|
||||
if (_wpPausing)
|
||||
{
|
||||
_wpPauseTimer -= Time.deltaTime;
|
||||
if (_wpPauseTimer > 0f) break; // 停顿中,原地待机
|
||||
_wpPausing = false;
|
||||
}
|
||||
AdvanceWaypoint();
|
||||
SetWaypointGoal();
|
||||
}
|
||||
else if (nav != null && !nav.IsMoving && !nav.LastMoveObstructed)
|
||||
{
|
||||
// 瞬态:刚出生尚未映射那一帧的 MoveTo 失败 → 重发(RequestMoveTo 自带 0.25s 防抖)。
|
||||
// 仅"未受阻"时重发;受阻由上面 obstructedSettled 分支结算,不再无限重发不可达点。
|
||||
_enemy.MoveTo(_wpResolvedGoal);
|
||||
}
|
||||
break;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 编译验证**
|
||||
|
||||
调用 `unity_get_compilation_errors`。
|
||||
预期:无错误(`nav.LastMoveObstructed` 来自 Task 4 接口;`Random.Range`/`_wanderPauseMin/Max` 已存在)。
|
||||
|
||||
- [ ] **Step 5: 提交**
|
||||
|
||||
```bash
|
||||
git add Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs
|
||||
git commit -m "feat(enemy): Waypoints 受阻/到达即结算推进,删除对不可达点的无限重发"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Play 模式行为验证(经 Unity MCP)
|
||||
|
||||
**Files:** 无(验证任务)
|
||||
|
||||
- [ ] **Step 1: 打开测试场景**
|
||||
|
||||
`unity_scene_open` 打开 `Assets/_Game/Scenes/Testings/TestRoomA.unity`。确认场景内有 NavSurface + 一个 Waypoints 巡逻敌人(`EnemyLocomotion.PatrolStrategy=Waypoints`,至少 2 个路点)。若无,用脚手架 `SceneObjectPlacerTool` 放置敌人并配路点(遵守 CLAUDE.md §2 脚手架规范)。
|
||||
|
||||
- [ ] **Step 2: 成因 B 验证——裸障碍**
|
||||
|
||||
在两个路点之间放一个挡路的 Collider(高度≥敌人 Height,横跨路径)。`unity_play_mode` 进入 Play。观察(`unity_screenshot_game` 连续截图或 `unity_editor_state` 读位置):
|
||||
预期:敌人走到障碍前 → 停顿约 `_wanderPauseMin~Max` 秒 → 推进到下一个路点继续巡逻。**不再对着障碍空推**。
|
||||
|
||||
- [ ] **Step 3: 回归——无障碍巡逻**
|
||||
|
||||
移除障碍,重进 Play。
|
||||
预期:Waypoints 巡逻在各路点间正常往返,行为与改动前一致(正常到达不停顿、立即推进)。
|
||||
|
||||
- [ ] **Step 4: 记录结果**
|
||||
|
||||
把验证结论(成因 B 通过 / 回归通过)追加到设计文档末尾或开发笔记。成因 A(障碍已进图寻路失败)待后续 Tier-1 软障碍/重烘焙就绪后复测——本次不阻塞。
|
||||
|
||||
- [ ] **Step 5: 退出 Play**
|
||||
|
||||
`unity_play_mode` 退出 Play 模式。
|
||||
|
||||
---
|
||||
|
||||
## 自检记录(写计划时已核对)
|
||||
|
||||
- **Spec 覆盖**:设计 §3.1→Task1;§3.2→Task2;§3.4→Task3+Task4;§3.3→Task5;§7→Task6。全覆盖。
|
||||
- **占位符**:无 TBD/TODO,每个代码步含完整代码。
|
||||
- **类型一致**:`TryGetClosestReachablePosition`(NavAgent) / `TryGetClosestReachable`(IPathAgent 实现) / `LastMoveObstructed` 三处命名在各任务间一致;`_obstructionSignal`/`ResetStuckWindow`/`ProcessObstruction` 定义与调用一致。
|
||||
- **可编译顺序**:先在实现类(Task2/3)加具体成员,再加接口(Task4),消费方(Task5)最后——每步均可编译。
|
||||
Reference in New Issue
Block a user