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:
@@ -26,9 +26,24 @@ namespace BaseGames.Core.Events
|
||||
#endif
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
ResetRuntimeState();
|
||||
// 关闭 Domain Reload 后 OnEnable 不再于每次进入 Play 触发,改由编辑器钩子在进入 Play 前兜住重置。
|
||||
PlayModeResetHook.Register(ResetRuntimeState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置运行时态:清空粘性缓存值与订阅委托,回到"零订阅、无缓存"的干净起点
|
||||
/// (复刻 Domain Reload 每次进入 Play 提供的初始状态)。
|
||||
/// </summary>
|
||||
private void ResetRuntimeState()
|
||||
{
|
||||
_hasLastValue = false;
|
||||
_lastValue = default;
|
||||
_onEventRaisedBacking = null;
|
||||
#if UNITY_EDITOR
|
||||
_subscriberCount = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
public event Action<T> OnEventRaised
|
||||
@@ -90,6 +105,22 @@ namespace BaseGames.Core.Events
|
||||
private int _subscriberCount;
|
||||
#endif
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
ResetRuntimeState();
|
||||
// 关闭 Domain Reload 后 OnEnable 不再于每次进入 Play 触发,改由编辑器钩子在进入 Play 前兜住重置。
|
||||
PlayModeResetHook.Register(ResetRuntimeState);
|
||||
}
|
||||
|
||||
/// <summary>重置运行时态:清空订阅委托,回到"零订阅"的干净起点。</summary>
|
||||
private void ResetRuntimeState()
|
||||
{
|
||||
_onEventRaisedBacking = null;
|
||||
#if UNITY_EDITOR
|
||||
_subscriberCount = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
public event Action OnEventRaised
|
||||
{
|
||||
add
|
||||
|
||||
56
Assets/_Game/Scripts/Core/Events/PlayModeResetHook.cs
Normal file
56
Assets/_Game/Scripts/Core/Events/PlayModeResetHook.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace BaseGames.Core.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// 关闭 Domain Reload(Project Settings → Editor → Enter Play Mode Settings)后,
|
||||
/// ScriptableObject 资产不再于进入/退出 Play 时被卸载重载,其 OnEnable/OnDisable 也就不再触发,
|
||||
/// 导致 SO 的运行时态(粘性缓存值、订阅委托、运行时字典等)在多次 Play 会话间残留。
|
||||
///
|
||||
/// 本工具提供一条可靠的"进入 Play 前重置"通道,复刻 Domain Reload 曾提供的"干净起点":
|
||||
/// 持有运行时态的 SO 在首次 OnEnable(编辑器加载资产时仍会触发一次)调用 <see cref="Register"/>
|
||||
/// 登记自身的重置方法;此后每次"即将进入 Play"由编辑器事件统一回调全部重置方法。
|
||||
///
|
||||
/// 运行时构建(非编辑器)下为空实现、零开销;构建中没有 Domain Reload 概念,
|
||||
/// SO 的 OnEnable 仍照常负责重置,无需本工具介入。
|
||||
/// </summary>
|
||||
public static class PlayModeResetHook
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
// 以委托相等性去重:同一 SO 实例的同一重置方法多次 Register 只保留一份
|
||||
// (Delegate 的 Equals/GetHashCode 比较 Target 实例与方法)。
|
||||
private static readonly HashSet<Action> _resets = new();
|
||||
private static bool _subscribed;
|
||||
|
||||
/// <summary>登记一个"进入 Play 前"执行的重置回调(仅编辑器有效)。</summary>
|
||||
public static void Register(Action reset)
|
||||
{
|
||||
if (reset == null) return;
|
||||
_resets.Add(reset);
|
||||
if (_subscribed) return;
|
||||
_subscribed = true;
|
||||
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
||||
}
|
||||
|
||||
private static void OnPlayModeStateChanged(PlayModeStateChange change)
|
||||
{
|
||||
// 仅在"退出编辑态、即将进入 Play"的瞬间重置,使后续 Awake 执行时各 SO 处于干净起点。
|
||||
if (change != PlayModeStateChange.ExitingEditMode) return;
|
||||
foreach (var reset in _resets)
|
||||
{
|
||||
// SO 资产被卸载后其托管对象可能已失效(Unity 重载 == null),跳过避免无谓调用。
|
||||
if (reset.Target is UnityEngine.Object uo && uo == null) continue;
|
||||
try { reset(); }
|
||||
catch (Exception e) { UnityEngine.Debug.LogException(e); } // 暴露错误而非静默吞掉
|
||||
}
|
||||
}
|
||||
#else
|
||||
/// <summary>运行时构建:空实现。SO 的 OnEnable 在构建中照常重置运行时态。</summary>
|
||||
public static void Register(Action reset) { }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Core/Events/PlayModeResetHook.cs.meta
Normal file
11
Assets/_Game/Scripts/Core/Events/PlayModeResetHook.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5cebf2bf42383c74a86dd7138932143c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -12,6 +12,13 @@ namespace BaseGames.Core
|
||||
{
|
||||
private static readonly Dictionary<Type, object> _services = new();
|
||||
|
||||
/// <summary>
|
||||
/// 关闭 Domain Reload 后静态字段不再于进入 Play 时重置,
|
||||
/// 残留的服务注册会指向上一次运行已销毁的对象。此钩子在任何 Awake 之前清空注册表。
|
||||
/// </summary>
|
||||
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ResetOnEnterPlayMode() => _services.Clear();
|
||||
|
||||
/// <summary>以接口类型 TInterface 注册实现 impl。</summary>
|
||||
public static void Register<TInterface>(TInterface impl)
|
||||
=> _services[typeof(TInterface)] = impl;
|
||||
|
||||
@@ -27,6 +27,13 @@ namespace BaseGames.Core
|
||||
/// <summary>设置变更后触发(用于 UIScaleApplier、色盲滤镜、Camera Shake 等订阅)。</summary>
|
||||
public static event Action<GlobalSettingsData> SettingsChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 关闭 Domain Reload 后静态事件不再于进入 Play 时清空,残留订阅者会指向上一次运行已销毁的对象。
|
||||
/// 此钩子在任何 Awake 之前将其重置为 null。
|
||||
/// </summary>
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ResetStaticState() => SettingsChanged = null;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ServiceLocator.Register<ISettingsService>(this);
|
||||
|
||||
Reference in New Issue
Block a user