feat(enemy): EnemyLocomotion 运行时巡逻调试(Inspector 面板 + Scene Gizmos)
排查"敌人原地不动"用:PlayMode 下自定义 Inspector 实时显示 模式/策略/是否移动/是否在导航图上/ 当前路点与吸附目标/到目标距离vs到达半径/目标是否可达,并按优先级给出定位诊断(模式非Patrol|不在 NavGraph|吸附失败|不可达|已寻路未动)。Scene 视图 Gizmos 画出全部路点连线 + 当前吸附目标(绿/红)+到达半径。 EnemyLocomotion 加 #if UNITY_EDITOR 只读调试访问器 + OnDrawGizmosSelected;记录 _wpResolveOk。 Editor 程序集补 BaseGames.AI 引用(PatrolStrategy/LocomotionMode 所在)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
"BaseGames.Player.States",
|
||||
"BaseGames.Enemies",
|
||||
"BaseGames.Enemies.Navigation",
|
||||
"BaseGames.AI",
|
||||
"BaseGames.Camera",
|
||||
"BaseGames.World",
|
||||
"BaseGames.UI",
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using BaseGames.AI;
|
||||
using BaseGames.Enemies;
|
||||
using BaseGames.Enemies.Navigation;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="EnemyLocomotion"/> 自定义 Inspector。
|
||||
/// PlayMode 下实时显示移动/巡逻状态,并对"原地不动"给出定位诊断:
|
||||
/// 模式、策略、当前路点与吸附目标、到达距离、是否在导航图上、目标是否可达。
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(EnemyLocomotion))]
|
||||
public class EnemyLocomotionEditor : UnityEditor.Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
EditorGUILayout.Space(4f);
|
||||
EditorGUILayout.HelpBox("进入 Play 模式后此处显示实时巡逻调试信息。", MessageType.None);
|
||||
return;
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(6f);
|
||||
DrawRuntimeSection();
|
||||
Repaint(); // 每帧刷新
|
||||
}
|
||||
|
||||
private void DrawRuntimeSection()
|
||||
{
|
||||
var loco = (EnemyLocomotion)target;
|
||||
var enemy = loco.DebugEnemy;
|
||||
|
||||
EditorGUILayout.LabelField("运行时调试", EditorStyles.boldLabel);
|
||||
|
||||
// ── 模式 / 策略 ─────────────────────────────────────────────
|
||||
var mode = loco.CurrentMode;
|
||||
bool patrolActive = mode == LocomotionMode.Patrol;
|
||||
Row("当前模式 (Mode)", mode.ToString(),
|
||||
patrolActive ? Green : (mode == LocomotionMode.Idle ? Gray : White));
|
||||
Row("巡逻策略 (Strategy)", loco.DebugStrategy.ToString(), White);
|
||||
Row("正在移动 (IsMoving)", loco.IsMoving ? "是" : "否", loco.IsMoving ? Green : Gray);
|
||||
|
||||
if (enemy == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox("EnemyBase 未解析(_enemy=null)。", MessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 导航代理状态 ────────────────────────────────────────────
|
||||
var nav = enemy.Nav;
|
||||
bool agentMapped = true; // 非 PB2d 代理默认视为有效
|
||||
bool navMoving = nav != null && nav.IsMoving;
|
||||
if (nav is EnemyNavAgent ena && ena.RawNavAgent != null)
|
||||
agentMapped = ena.RawNavAgent.HasValidPosition;
|
||||
|
||||
Row("在导航图上 (Mapped)", agentMapped ? "是" : "否 —— 敌人不在 NavGraph 上",
|
||||
agentMapped ? Green : Red);
|
||||
Row("Nav 正在寻路/移动", navMoving ? "是" : "否", navMoving ? Green : Gray);
|
||||
|
||||
// 身体宽度 / 吸附距离(上下文)
|
||||
var mv = enemy.Movement;
|
||||
if (mv != null) Row("EdgeSafeMargin(身体半宽+偏移)", mv.EdgeSafeMargin.ToString("F2"), White);
|
||||
if (nav is EnemyNavAgent ena2)
|
||||
{
|
||||
var so = new SerializedObject(ena2);
|
||||
var msd = so.FindProperty("_maxSnapDistance");
|
||||
if (msd != null) Row("_maxSnapDistance", msd.floatValue.ToString("F1"), White);
|
||||
}
|
||||
|
||||
// ── 路点巡逻细节 ────────────────────────────────────────────
|
||||
if (loco.DebugStrategy == PatrolStrategy.Waypoints)
|
||||
{
|
||||
EditorGUILayout.Space(4f);
|
||||
EditorGUILayout.LabelField("Waypoints", EditorStyles.miniBoldLabel);
|
||||
|
||||
var wps = loco.DebugWaypoints;
|
||||
int idx = loco.DebugWaypointIndex;
|
||||
int cnt = wps != null ? wps.Length : 0;
|
||||
Row("当前路点索引", cnt > 0 ? $"{idx} / {cnt - 1}" : "无路点", cnt > 0 ? White : Red);
|
||||
|
||||
if (wps != null && idx >= 0 && idx < cnt && wps[idx] != null)
|
||||
{
|
||||
Vector2 raw = wps[idx].position;
|
||||
Vector2 goal = loco.DebugResolvedGoal;
|
||||
Row("原始路点", $"{wps[idx].name} {raw}", White);
|
||||
Row("吸附目标 (Resolved)", loco.DebugResolveOk ? goal.ToString() : $"{goal} (吸附失败,回落原始点)",
|
||||
loco.DebugResolveOk ? Green : Red);
|
||||
|
||||
float dist = Vector2.Distance(enemy.transform.position, goal);
|
||||
bool arrived = dist <= loco.DebugArriveRadius;
|
||||
Row("到目标距离 / 到达半径", $"{dist:F2} / {loco.DebugArriveRadius:F2}{(arrived ? " (已到达)" : "")}",
|
||||
arrived ? Green : White);
|
||||
|
||||
bool canReach = nav != null && nav.CanReach(goal);
|
||||
Row("目标可达 (CanReach)", canReach ? "是" : "否 —— 起点/终点不连通", canReach ? Green : Red);
|
||||
|
||||
DrawDiagnosis(patrolActive, loco.DebugStrategy, agentMapped, loco.DebugResolveOk, canReach, navMoving, arrived);
|
||||
}
|
||||
}
|
||||
|
||||
if (GUILayout.Button("在 Hierarchy 中定位敌人", EditorStyles.miniButton))
|
||||
Selection.activeGameObject = enemy.gameObject;
|
||||
}
|
||||
|
||||
// ── "原地不动" 定位诊断 ─────────────────────────────────────────
|
||||
private static void DrawDiagnosis(bool patrolActive, PatrolStrategy strat, bool mapped,
|
||||
bool resolveOk, bool canReach, bool navMoving, bool arrived)
|
||||
{
|
||||
EditorGUILayout.Space(4f);
|
||||
string msg; MessageType type;
|
||||
|
||||
if (!patrolActive)
|
||||
{
|
||||
msg = "模式不是 Patrol —— AI 尚未进入巡逻态(如伪装/待机),TickPatrol 不会执行,故不动。" +
|
||||
"需触发 AI 切到巡逻状态。";
|
||||
type = MessageType.Warning;
|
||||
}
|
||||
else if (!mapped)
|
||||
{
|
||||
msg = "敌人不在导航图上(HasValidPosition=false)—— RequestMoveTo/UpdatePath 被直接忽略。" +
|
||||
"检查:碰撞体底部是否贴地(原点=脚底)、是否站在已 bake 的 NavSurface 上、PointMappingDistance(默认0.2m)。";
|
||||
type = MessageType.Error;
|
||||
}
|
||||
else if (!resolveOk)
|
||||
{
|
||||
msg = "路点吸附失败 —— 目标离所有烘焙段都超过 _maxSnapDistance。调大 _maxSnapDistance 或把路点摆近平台。";
|
||||
type = MessageType.Error;
|
||||
}
|
||||
else if (!canReach)
|
||||
{
|
||||
msg = "目标不可达 —— 起点与吸附目标不在同一连通区,或中间隔着不可穿越的 NavLink(跳/落/爬)。";
|
||||
type = MessageType.Error;
|
||||
}
|
||||
else if (!navMoving && !arrived)
|
||||
{
|
||||
msg = "已寻路成功但没有移动 —— 检查 EnemyMovement 速度/受阻(WouldBlockAhead 夹停)、刚体约束、是否被其他状态覆盖输入。";
|
||||
type = MessageType.Warning;
|
||||
}
|
||||
else
|
||||
{
|
||||
msg = "巡逻链路正常。";
|
||||
type = MessageType.Info;
|
||||
}
|
||||
EditorGUILayout.HelpBox(msg, type);
|
||||
}
|
||||
|
||||
// ── 绘制辅助 ────────────────────────────────────────────────────
|
||||
private static readonly Color Green = new Color(0.4f, 0.9f, 0.4f);
|
||||
private static readonly Color Red = new Color(1f, 0.5f, 0.4f);
|
||||
private static readonly Color Gray = new Color(0.7f, 0.7f, 0.7f);
|
||||
private static readonly Color White = Color.white;
|
||||
|
||||
private static void Row(string label, string value, Color valueColor)
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField(label, GUILayout.Width(200f));
|
||||
var prev = GUI.color; GUI.color = valueColor;
|
||||
EditorGUILayout.LabelField(value, EditorStyles.boldLabel);
|
||||
GUI.color = prev;
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c75f9efe0c364f47b03472f71be96b9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -40,6 +40,7 @@ namespace BaseGames.Enemies
|
||||
private bool _warnedNoWaypoints;
|
||||
// 当前路点吸附后的"站得住"目标(宽度由 Nav 层解析,本层不感知身体尺寸)
|
||||
private Vector2 _wpResolvedGoal;
|
||||
private bool _wpResolveOk; // 上次路点是否成功吸附到导航段(调试用)
|
||||
// Wander 到点停顿状态
|
||||
private bool _wanderPausing;
|
||||
private float _wanderPauseTimer;
|
||||
@@ -214,7 +215,8 @@ namespace BaseGames.Enemies
|
||||
private void SetWaypointGoal()
|
||||
{
|
||||
Vector2 raw = _waypoints[_wpIndex].position;
|
||||
if (_enemy.Nav == null || !_enemy.Nav.ResolveStandablePoint(raw, out _wpResolvedGoal))
|
||||
_wpResolveOk = _enemy.Nav != null && _enemy.Nav.ResolveStandablePoint(raw, out _wpResolvedGoal);
|
||||
if (!_wpResolveOk)
|
||||
{
|
||||
_wpResolvedGoal = raw;
|
||||
Debug.LogWarning($"[EnemyLocomotion] 路点 '{_waypoints[_wpIndex].name}'(index {_wpIndex}) 无法吸附到任何可行走导航段" +
|
||||
@@ -236,5 +238,43 @@ namespace BaseGames.Enemies
|
||||
_wpIndex = (_wpIndex + 1) % _waypoints.Length;
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// ── 调试只读(供自定义 Inspector / Gizmos,仅编辑器)────────────────────────
|
||||
public EnemyBase DebugEnemy => _enemy;
|
||||
public PatrolStrategy DebugStrategy => _patrolStrategy;
|
||||
public Transform[] DebugWaypoints => _waypoints;
|
||||
public int DebugWaypointIndex => _wpIndex;
|
||||
public Vector2 DebugResolvedGoal => _wpResolvedGoal;
|
||||
public bool DebugResolveOk => _wpResolveOk;
|
||||
public float DebugArriveRadius => _waypointArriveRadius;
|
||||
public bool DebugWanderPausing => _wanderPausing;
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (_patrolStrategy != PatrolStrategy.Waypoints || _waypoints == null) return;
|
||||
|
||||
// 所有路点 + 连线(编辑期即可见,用于摆点)
|
||||
Gizmos.color = new Color(0.3f, 0.8f, 1f);
|
||||
for (int i = 0; i < _waypoints.Length; i++)
|
||||
{
|
||||
if (_waypoints[i] == null) continue;
|
||||
Gizmos.DrawWireSphere(_waypoints[i].position, 0.15f);
|
||||
var next = _waypoints[(i + 1) % _waypoints.Length];
|
||||
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.DrawWireSphere(_wpResolvedGoal, 0.22f);
|
||||
Gizmos.DrawLine(from, _wpResolvedGoal);
|
||||
Gizmos.color = new Color(1f, 0.9f, 0.2f, 0.7f);
|
||||
Gizmos.DrawWireSphere(_wpResolvedGoal, _waypointArriveRadius);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user