perf(editor): 关闭 Domain/Scene Reload 加速进入 Play,并补齐域重载安全

关闭 Domain Reload 后 SO 的 OnEnable/OnDisable 不再于进入/退出 Play 触发、
静态字段不再重置,运行时态会跨 Play 会话残留。补齐重置路径:

- 新增 PlayModeResetHook:编辑器内在"进入 Play 前"统一回调各 SO 的重置,
  复刻 Domain Reload 曾提供的干净起点;运行时构建为空实现、零开销。
- 静态态用 RuntimeInitializeOnLoadMethod 重置:
  ServiceLocator._services / SettingsManager.SettingsChanged /
  EventChainManager.OnChainExecutedInEditor。
- SO 运行时态经 PlayModeResetHook 重置:
  BaseEventChannelSO(粘性值+订阅委托) / VoidBaseEventChannelSO /
  WorldStateRegistry(状态字典+变更事件) / InputReaderSO。
- InputReaderSO 增加解绑追踪:跨会话重建前先解绑旧输入回调,
  避免 InputActionAsset 存活导致同一输入重复绑定、多次触发。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 14:40:37 +08:00
parent 253a8811fa
commit 4189a6210b
9 changed files with 202 additions and 22 deletions

View File

@@ -71,6 +71,9 @@ namespace BaseGames.Input
private InputActionMap _ui;
private bool _isBound;
private InputAction _jumpAction;
// 记录每次绑定对应的解绑动作。关闭 Domain Reload 后 InputActionAsset 跨会话存活,
// 必须在重新绑定前先解绑旧回调,否则同一输入会被绑定多次、触发多次。
private readonly List<Action> _unbinders = new();
private void EnsureInitialized()
{
@@ -105,9 +108,20 @@ namespace BaseGames.Input
{
Debug.Assert(_onPauseRequested != null,
"[InputReaderSO] _onPauseRequested 未赋值,请在 Inspector 中指定 EVT_PauseRequested。", this);
// Reset private state on every OnEnable so stale ScriptableObject
// references from a previous Play session don't cause
// 'Map must be contained in state' errors.
ResetRuntimeState();
// 关闭 Domain Reload 后 OnEnable 不再于每次进入 Play 触发,改由编辑器钩子在进入 Play 前兜住重置。
PlayModeResetHook.Register(ResetRuntimeState);
}
/// <summary>
/// 重置运行时态并重建输入绑定,回到本次 Play 会话的干净起点:
/// 先解绑上一次会话残留在 InputActionAsset 上的回调(避免跨会话重复绑定导致多次触发),
/// 再清空对外事件订阅者,最后重新初始化并绑定。
/// </summary>
private void ResetRuntimeState()
{
UnbindAll();
ClearGameplayEvents();
_gameplay = null;
_ui = null;
_isBound = false;
@@ -115,6 +129,31 @@ namespace BaseGames.Input
EnsureInitialized();
}
/// <summary>解绑本读取器先前注册到 InputActionAsset 上的全部回调。</summary>
private void UnbindAll()
{
foreach (var unbind in _unbinders) unbind();
_unbinders.Clear();
}
/// <summary>清空所有对外事件的订阅者,回到零订阅起点(复刻 Domain Reload 的初始状态)。</summary>
private void ClearGameplayEvents()
{
MoveEvent = null;
JumpStartedEvent = null; JumpCancelledEvent = null;
AttackEvent = null; ParryEvent = null; DashEvent = null;
UseSpringEvent = null;
SwitchTianHunFormEvent = null; SwitchDiHunFormEvent = null; SwitchMingHunFormEvent = null;
SoulSkillEvent = null;
SpiritSkill1StartedEvent = null; SpiritSkill1CancelledEvent = null;
SpiritSkill2StartedEvent = null; SpiritSkill2CancelledEvent = null;
SpellCastEvent = null;
InteractEvent = null; InteractCancelledEvent = null;
PauseEvent = null; NavigateEvent = null;
SubmitEvent = null; CancelEvent = null; PointEvent = null;
CycleMinimapZoomEvent = null; MapCenterEvent = null;
}
private void OnDisable()
{
_gameplay?.Disable();
@@ -245,38 +284,46 @@ namespace BaseGames.Input
_onPauseRequested?.Raise();
}
private static void BindStarted(InputActionMap map, string name, Action callback)
// 以下绑定方法在挂接回调的同时,把对应的解绑动作记入 _unbinders
// 供 UnbindAll 在跨会话重建前清理,避免回调在 InputActionAsset 上累积。
private void BindStarted(InputActionMap map, string name, Action callback)
{
var action = map.FindAction(name, throwIfNotFound: false);
if (action != null)
{
action.started += _ => callback();
}
else
if (action == null)
{
Debug.LogWarning($"[BindStarted] Action '{name}' not found in map");
return;
}
Action<InputAction.CallbackContext> handler = _ => callback();
action.started += handler;
_unbinders.Add(() => action.started -= handler);
}
private static void BindPerformed(InputActionMap map, string name,
private void BindPerformed(InputActionMap map, string name,
Action<InputAction.CallbackContext> callback)
{
var action = map.FindAction(name, throwIfNotFound: false);
if (action != null) action.performed += callback;
if (action == null) return;
action.performed += callback;
_unbinders.Add(() => action.performed -= callback);
}
private static void BindCanceled(InputActionMap map, string name,
private void BindCanceled(InputActionMap map, string name,
Action<InputAction.CallbackContext> callback)
{
var action = map.FindAction(name, throwIfNotFound: false);
if (action != null) action.canceled += callback;
if (action == null) return;
action.canceled += callback;
_unbinders.Add(() => action.canceled -= callback);
}
private static void BindCanceled(InputActionMap map, string name, Action callback)
private void BindCanceled(InputActionMap map, string name, Action callback)
{
var action = map.FindAction(name, throwIfNotFound: false);
if (action != null) action.canceled += _ => callback();
if (action == null) return;
Action<InputAction.CallbackContext> handler = _ => callback();
action.canceled += handler;
_unbinders.Add(() => action.canceled -= handler);
}
// ── Rebinding API (P4-3) ──────────────────────────────────────────────