feat(enemy): 巡逻调试 Inspector 加"一键打印报告"按钮(供 AI 诊断)

点击将完整状态以纯文本 Debug.Log 到 Console,可直接复制给 AI:AI决策层(EnemyState/
BrainGraph 当前状态名/LocomotionMode)、移动导航(Nav类型/IsMoving/IsOnLink/HasValidPosition/
PB2d Position/EdgeSafeMargin/_maxSnapDistance)、Waypoints(当前索引/吸附目标/CanReach/逐路点
实时吸附检测 raw→snap 及垂直差)、文末一句根因诊断。编辑期打印静态配置,Play 期打印运行时全量。

实测输出直指根因:Brain='Idle_Disguise' → LocomotionMode=Idle → TickPatrol 不执行。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-23 15:04:53 +08:00
co-authored by Claude Opus 4.8
parent 424d9bbbe0
commit be7d511e45
@@ -1,3 +1,4 @@
using System.Text;
using UnityEditor;
using UnityEngine;
using BaseGames.AI;
@@ -46,10 +47,21 @@ namespace BaseGames.Editor
{
DrawDefaultInspector();
EditorGUILayout.Space(6f);
// 一键打印完整调试报告到 Console(可复制粘贴给 AI 诊断)
var prevBg = GUI.backgroundColor;
GUI.backgroundColor = new Color(0.5f, 0.8f, 1f);
if (GUILayout.Button("📋 打印调试信息(复制给 AI 诊断)", GUILayout.Height(26f)))
{
var loco = (EnemyLocomotion)target;
Debug.Log(BuildReport(loco), loco);
}
GUI.backgroundColor = prevBg;
if (!Application.isPlaying)
{
EditorGUILayout.Space(4f);
EditorGUILayout.HelpBox("进入 Play 模式后此处显示实时巡逻调试信息。", MessageType.None);
EditorGUILayout.HelpBox("进入 Play 模式后此处显示实时巡逻调试信息(按钮在编辑期也可打印静态配置)。", MessageType.None);
return;
}
@@ -57,6 +69,116 @@ namespace BaseGames.Editor
DrawRuntimeSection();
}
// ── 一键调试报告(纯文本,便于复制给 AI)────────────────────────────
private static string BuildReport(EnemyLocomotion loco)
{
var sb = new StringBuilder();
var enemy = loco.DebugEnemy;
var go = loco.gameObject;
sb.AppendLine("========== EnemyLocomotion 调试报告 ==========");
sb.AppendLine($"对象: {go.name} 场景: {go.scene.name} Playing: {Application.isPlaying} t={Time.time:F2}");
// ── AI 决策层 ──
sb.AppendLine("── AI 决策层 ──");
if (enemy != null)
{
string brainState = enemy.Brain != null ? enemy.Brain.CurrentStateName : "(无 EnemyAiBrain)";
sb.AppendLine($"EnemyState(控制态): {enemy.CurrentState}");
sb.AppendLine($"Brain 状态(BrainGraph): {brainState}");
}
else sb.AppendLine("EnemyBase 未解析(编辑期或 Awake 未运行)");
sb.AppendLine($"LocomotionMode: {loco.CurrentMode} (仅 Patrol 时才跑 TickPatrol)");
// ── 移动 / 导航 ──
sb.AppendLine("── 移动/导航 ──");
sb.AppendLine($"PatrolStrategy: {loco.DebugStrategy}");
sb.AppendLine($"Loco.IsMoving: {loco.IsMoving} WanderPausing: {loco.DebugWanderPausing}");
var nav = enemy != null ? enemy.Nav : null;
if (nav != null)
{
sb.AppendLine($"Nav 类型: {nav.GetType().Name} Nav.IsMoving: {nav.IsMoving} IsOnLink: {nav.IsOnLink} LinkType: {nav.CurrentLinkType}");
if (nav is EnemyNavAgent ena && ena.RawNavAgent != null)
{
var raw = ena.RawNavAgent;
sb.AppendLine($"在导航图上(HasValidPosition): {raw.HasValidPosition} IsFollowingAPath: {raw.IsFollowingAPath}");
sb.AppendLine($"PB2d Agent Position: {raw.Position}");
}
}
else sb.AppendLine("Nav: null");
var mv = enemy != null ? enemy.Movement : null;
if (mv != null) sb.AppendLine($"EdgeSafeMargin(身体半宽+偏移): {mv.EdgeSafeMargin:F3}");
sb.AppendLine($"_maxSnapDistance: {ReadMaxSnap(nav)}");
if (enemy != null) sb.AppendLine($"EnemyPos: {enemy.transform.position}");
// ── Waypoints ──
if (loco.DebugStrategy == PatrolStrategy.Waypoints)
{
sb.AppendLine("── Waypoints ──");
var wps = loco.DebugWaypoints;
int cnt = wps != null ? wps.Length : 0;
int idx = loco.DebugWaypointIndex;
sb.AppendLine($"当前索引: {idx} / {(cnt > 0 ? cnt - 1 : 0)} ArriveRadius: {loco.DebugArriveRadius:F2}");
Vector2 goal = loco.DebugResolvedGoal;
sb.AppendLine($"当前吸附目标(Resolved): {goal} ResolveOk: {loco.DebugResolveOk}");
if (enemy != null)
sb.AppendLine($"到目标距离: {Vector2.Distance(enemy.transform.position, goal):F2}" +
(nav != null ? $" CanReach: {nav.CanReach(goal)}" : ""));
if (wps != null)
{
sb.AppendLine("路点逐个吸附检测(点击时实时计算):");
for (int i = 0; i < wps.Length; i++)
{
if (wps[i] == null) { sb.AppendLine($" [{i}] <NULL 引用>"); continue; }
Vector2 rawp = wps[i].position;
string snapStr = "(需 Play 且有 Nav)";
if (nav != null)
{
bool ok = nav.ResolveStandablePoint(rawp, out var snapped);
snapStr = $"snap ok={ok} {snapped} |rawY-snapY|={Mathf.Abs(rawp.y - snapped.y):F2}";
}
sb.AppendLine($" [{i}] {wps[i].name} raw={rawp} -> {snapStr}");
}
}
}
// ── 诊断 ──
sb.AppendLine("── 诊断 ──");
sb.AppendLine(Diagnose(loco, enemy, nav));
sb.AppendLine("=============================================");
return sb.ToString();
}
private static string ReadMaxSnap(IPathAgent nav)
{
if (nav is EnemyNavAgent ena)
{
using var so = new SerializedObject(ena);
var msd = so.FindProperty("_maxSnapDistance");
if (msd != null) return msd.floatValue.ToString("F1");
}
return "(n/a)";
}
// 纯文本诊断(与面板 HelpBox 同一判定逻辑,供报告文末结论)
private static string Diagnose(EnemyLocomotion loco, EnemyBase enemy, IPathAgent nav)
{
if (!Application.isPlaying) return "编辑期:仅静态配置,进入 Play 后可判定运行时问题。";
if (loco.CurrentMode != LocomotionMode.Patrol)
return $"→ 不动主因:LocomotionMode={loco.CurrentMode}(非 Patrol)。AI 尚未进入巡逻态,TickPatrol 不执行。" +
(enemy?.Brain != null ? $" 当前 Brain 状态='{enemy.Brain.CurrentStateName}'——检查该状态是否/何时切到巡逻。" : "");
bool mapped = !(nav is EnemyNavAgent e && e.RawNavAgent != null) || ((EnemyNavAgent)nav).RawNavAgent.HasValidPosition;
if (!mapped) return "→ 敌人不在 NavGraph 上(HasValidPosition=false)RequestMoveTo 被忽略。检查贴地/是否在已 bake 的 NavSurface。";
if (loco.DebugStrategy == PatrolStrategy.Waypoints && !loco.DebugResolveOk)
return "→ 当前路点吸附失败:超出 _maxSnapDistance。调大或把路点摆近平台。";
if (nav != null && !nav.CanReach(loco.DebugResolvedGoal))
return "→ 目标不可达:与吸附目标不连通,或隔着不可穿越 NavLink。";
return "→ 巡逻链路各项正常(若仍不动,检查 EnemyMovement 速度/受阻/刚体约束)。";
}
private void DrawRuntimeSection()
{
var loco = (EnemyLocomotion)target;