perf(enemy): 巡逻调试 Inspector 限频,消除每帧阻塞式 CanReach 掉帧

上版 OnInspectorGUI 每帧 Repaint 且每帧调用 NavAgent.CanReach——该 API 会同步阻塞主线程
跑一次完整寻路(其文档自述"not optimal for performance"),选中敌人时每帧一次直接把帧率拖垮。

改为:刷新走 EditorApplication.update 定频(~5Hz)与游戏帧率解耦;CanReach 限频 ~2Hz 并缓存;
_maxSnapDistance 运行期不变只读一次缓存(不再每帧 new SerializedObject)。实测选中敌人 ~53fps 稳定无卡顿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-23 13:15:53 +08:00
co-authored by Claude Opus 4.8
parent 00ee48f750
commit 424d9bbbe0
@@ -10,10 +10,38 @@ namespace BaseGames.Editor
/// <see cref="EnemyLocomotion"/> 自定义 Inspector。 /// <see cref="EnemyLocomotion"/> 自定义 Inspector。
/// PlayMode 下实时显示移动/巡逻状态,并对"原地不动"给出定位诊断: /// PlayMode 下实时显示移动/巡逻状态,并对"原地不动"给出定位诊断:
/// 模式、策略、当前路点与吸附目标、到达距离、是否在导航图上、目标是否可达。 /// 模式、策略、当前路点与吸附目标、到达距离、是否在导航图上、目标是否可达。
///
/// 性能:刷新走 EditorApplication.update 定频(~5Hz),与游戏帧率解耦;昂贵查询
/// CanReach 会同步阻塞寻路、_maxSnapDistance 读取)限频 ~2Hz 并缓存——避免每帧
/// 触发一次阻塞式寻路把帧数拖垮。
/// </summary> /// </summary>
[CustomEditor(typeof(EnemyLocomotion))] [CustomEditor(typeof(EnemyLocomotion))]
public class EnemyLocomotionEditor : UnityEditor.Editor public class EnemyLocomotionEditor : UnityEditor.Editor
{ {
private const double RepaintInterval = 0.2; // 面板刷新间隔(s),与游戏帧率解耦
private const double HeavyInterval = 0.5; // 昂贵查询(CanReach 等)重算间隔(s)
private double _nextRepaintTime;
private double _nextHeavyTime;
// 昂贵查询的缓存(仅每 HeavyInterval 刷新一次)
private bool _cachedCanReach;
private bool _cachedCanReachValid;
private float _cachedMaxSnap = -1f;
private void OnEnable() => EditorApplication.update += ThrottledRepaint;
private void OnDisable() => EditorApplication.update -= ThrottledRepaint;
// 定频请求重绘(不在 OnInspectorGUI 里每帧 Repaint,避免绑定到游戏帧率狂刷)
private void ThrottledRepaint()
{
if (!Application.isPlaying || target == null) return;
double t = EditorApplication.timeSinceStartup;
if (t < _nextRepaintTime) return;
_nextRepaintTime = t + RepaintInterval;
Repaint();
}
public override void OnInspectorGUI() public override void OnInspectorGUI()
{ {
DrawDefaultInspector(); DrawDefaultInspector();
@@ -27,7 +55,6 @@ namespace BaseGames.Editor
EditorGUILayout.Space(6f); EditorGUILayout.Space(6f);
DrawRuntimeSection(); DrawRuntimeSection();
Repaint(); // 每帧刷新
} }
private void DrawRuntimeSection() private void DrawRuntimeSection()
@@ -35,6 +62,11 @@ namespace BaseGames.Editor
var loco = (EnemyLocomotion)target; var loco = (EnemyLocomotion)target;
var enemy = loco.DebugEnemy; var enemy = loco.DebugEnemy;
// 是否到了重算昂贵查询的时刻(限频)
double now = EditorApplication.timeSinceStartup;
bool doHeavy = now >= _nextHeavyTime;
if (doHeavy) _nextHeavyTime = now + HeavyInterval;
EditorGUILayout.LabelField("运行时调试", EditorStyles.boldLabel); EditorGUILayout.LabelField("运行时调试", EditorStyles.boldLabel);
// ── 模式 / 策略 ───────────────────────────────────────────── // ── 模式 / 策略 ─────────────────────────────────────────────
@@ -62,15 +94,16 @@ namespace BaseGames.Editor
agentMapped ? Green : Red); agentMapped ? Green : Red);
Row("Nav 正在寻路/移动", navMoving ? "是" : "否", navMoving ? Green : Gray); Row("Nav 正在寻路/移动", navMoving ? "是" : "否", navMoving ? Green : Gray);
// 身体宽度 / 吸附距离(上下文 // 身体宽度 / 吸附距离(_maxSnapDistance 运行期不变 → 只读一次缓存
var mv = enemy.Movement; var mv = enemy.Movement;
if (mv != null) Row("EdgeSafeMargin(身体半宽+偏移)", mv.EdgeSafeMargin.ToString("F2"), White); if (mv != null) Row("EdgeSafeMargin(身体半宽+偏移)", mv.EdgeSafeMargin.ToString("F2"), White);
if (nav is EnemyNavAgent ena2) if (_cachedMaxSnap < 0f && nav is EnemyNavAgent ena2)
{ {
var so = new SerializedObject(ena2); using var so = new SerializedObject(ena2);
var msd = so.FindProperty("_maxSnapDistance"); var msd = so.FindProperty("_maxSnapDistance");
if (msd != null) Row("_maxSnapDistance", msd.floatValue.ToString("F1"), White); _cachedMaxSnap = msd != null ? msd.floatValue : 0f;
} }
if (_cachedMaxSnap >= 0f) Row("_maxSnapDistance", _cachedMaxSnap.ToString("F1"), White);
// ── 路点巡逻细节 ──────────────────────────────────────────── // ── 路点巡逻细节 ────────────────────────────────────────────
if (loco.DebugStrategy == PatrolStrategy.Waypoints) if (loco.DebugStrategy == PatrolStrategy.Waypoints)
@@ -96,10 +129,20 @@ namespace BaseGames.Editor
Row("到目标距离 / 到达半径", $"{dist:F2} / {loco.DebugArriveRadius:F2}{(arrived ? " ()" : "")}", Row("到目标距离 / 到达半径", $"{dist:F2} / {loco.DebugArriveRadius:F2}{(arrived ? " ()" : "")}",
arrived ? Green : White); arrived ? Green : White);
bool canReach = nav != null && nav.CanReach(goal); // CanReach 会同步阻塞寻路 → 仅限频重算并缓存
Row("目标可达 (CanReach)", canReach ? "是" : "否 —— 起点/终点不连通", canReach ? Green : Red); if (doHeavy && nav != null)
{
_cachedCanReach = nav.CanReach(goal);
_cachedCanReachValid = true;
}
if (_cachedCanReachValid)
Row("目标可达 (CanReach)", _cachedCanReach ? "是" : "否 —— 起点/终点不连通",
_cachedCanReach ? Green : Red);
else
Row("目标可达 (CanReach)", "计算中…", Gray);
DrawDiagnosis(patrolActive, loco.DebugStrategy, agentMapped, loco.DebugResolveOk, canReach, navMoving, arrived); DrawDiagnosis(patrolActive, agentMapped, loco.DebugResolveOk,
_cachedCanReachValid && _cachedCanReach, navMoving, arrived);
} }
} }
@@ -108,7 +151,7 @@ namespace BaseGames.Editor
} }
// ── "原地不动" 定位诊断 ───────────────────────────────────────── // ── "原地不动" 定位诊断 ─────────────────────────────────────────
private static void DrawDiagnosis(bool patrolActive, PatrolStrategy strat, bool mapped, private static void DrawDiagnosis(bool patrolActive, bool mapped,
bool resolveOk, bool canReach, bool navMoving, bool arrived) bool resolveOk, bool canReach, bool navMoving, bool arrived)
{ {
EditorGUILayout.Space(4f); EditorGUILayout.Space(4f);