From 00ee48f7502866e73f72834bf3c4d0c7a8488add Mon Sep 17 00:00:00 2001 From: Joywayer Date: Thu, 23 Jul 2026 11:57:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(enemy):=20EnemyLocomotion=20=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E5=B7=A1=E9=80=BB=E8=B0=83=E8=AF=95(Inspecto?= =?UTF-8?q?r=20=E9=9D=A2=E6=9D=BF=20+=20Scene=20Gizmos)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 排查"敌人原地不动"用: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) --- .../Scripts/Editor/BaseGames.Editor.asmdef | 1 + .../Editor/Enemies/EnemyLocomotionEditor.cs | 168 ++++++++++++++++++ .../Enemies/EnemyLocomotionEditor.cs.meta | 11 ++ .../Enemies/Navigation/EnemyLocomotion.cs | 42 ++++- 4 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 Assets/_Game/Scripts/Editor/Enemies/EnemyLocomotionEditor.cs create mode 100644 Assets/_Game/Scripts/Editor/Enemies/EnemyLocomotionEditor.cs.meta diff --git a/Assets/_Game/Scripts/Editor/BaseGames.Editor.asmdef b/Assets/_Game/Scripts/Editor/BaseGames.Editor.asmdef index 5da553fd..4a57d278 100644 --- a/Assets/_Game/Scripts/Editor/BaseGames.Editor.asmdef +++ b/Assets/_Game/Scripts/Editor/BaseGames.Editor.asmdef @@ -16,6 +16,7 @@ "BaseGames.Player.States", "BaseGames.Enemies", "BaseGames.Enemies.Navigation", + "BaseGames.AI", "BaseGames.Camera", "BaseGames.World", "BaseGames.UI", diff --git a/Assets/_Game/Scripts/Editor/Enemies/EnemyLocomotionEditor.cs b/Assets/_Game/Scripts/Editor/Enemies/EnemyLocomotionEditor.cs new file mode 100644 index 00000000..26f36fd0 --- /dev/null +++ b/Assets/_Game/Scripts/Editor/Enemies/EnemyLocomotionEditor.cs @@ -0,0 +1,168 @@ +using UnityEditor; +using UnityEngine; +using BaseGames.AI; +using BaseGames.Enemies; +using BaseGames.Enemies.Navigation; + +namespace BaseGames.Editor +{ + /// + /// 自定义 Inspector。 + /// PlayMode 下实时显示移动/巡逻状态,并对"原地不动"给出定位诊断: + /// 模式、策略、当前路点与吸附目标、到达距离、是否在导航图上、目标是否可达。 + /// + [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(); + } + } +} diff --git a/Assets/_Game/Scripts/Editor/Enemies/EnemyLocomotionEditor.cs.meta b/Assets/_Game/Scripts/Editor/Enemies/EnemyLocomotionEditor.cs.meta new file mode 100644 index 00000000..18c0bda4 --- /dev/null +++ b/Assets/_Game/Scripts/Editor/Enemies/EnemyLocomotionEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3c75f9efe0c364f47b03472f71be96b9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs b/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs index 55417163..0b7e4d30 100644 --- a/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs +++ b/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs @@ -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 } }