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。
/// PlayMode 下实时显示移动/巡逻状态,并对"原地不动"给出定位诊断:
/// 模式、策略、当前路点与吸附目标、到达距离、是否在导航图上、目标是否可达。
///
/// 性能:刷新走 EditorApplication.update 定频(~5Hz),与游戏帧率解耦;昂贵查询
/// CanReach 会同步阻塞寻路、_maxSnapDistance 读取)限频 ~2Hz 并缓存——避免每帧
/// 触发一次阻塞式寻路把帧数拖垮。
/// </summary>
[CustomEditor(typeof(EnemyLocomotion))]
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()
{
DrawDefaultInspector();
@@ -27,7 +55,6 @@ namespace BaseGames.Editor
EditorGUILayout.Space(6f);
DrawRuntimeSection();
Repaint(); // 每帧刷新
}
private void DrawRuntimeSection()
@@ -35,6 +62,11 @@ namespace BaseGames.Editor
var loco = (EnemyLocomotion)target;
var enemy = loco.DebugEnemy;
// 是否到了重算昂贵查询的时刻(限频)
double now = EditorApplication.timeSinceStartup;
bool doHeavy = now >= _nextHeavyTime;
if (doHeavy) _nextHeavyTime = now + HeavyInterval;
EditorGUILayout.LabelField("运行时调试", EditorStyles.boldLabel);
// ── 模式 / 策略 ─────────────────────────────────────────────
@@ -62,15 +94,16 @@ namespace BaseGames.Editor
agentMapped ? Green : Red);
Row("Nav 正在寻路/移动", navMoving ? "是" : "否", navMoving ? Green : Gray);
// 身体宽度 / 吸附距离(上下文
// 身体宽度 / 吸附距离(_maxSnapDistance 运行期不变 → 只读一次缓存
var mv = enemy.Movement;
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");
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)
@@ -96,10 +129,20 @@ namespace BaseGames.Editor
Row("到目标距离 / 到达半径", $"{dist:F2} / {loco.DebugArriveRadius:F2}{(arrived ? " ()" : "")}",
arrived ? Green : White);
bool canReach = nav != null && nav.CanReach(goal);
Row("目标可达 (CanReach)", canReach ? "是" : "否 —— 起点/终点不连通", canReach ? Green : Red);
// CanReach 会同步阻塞寻路 → 仅限频重算并缓存
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)
{
EditorGUILayout.Space(4f);