From 98fd21ccbaca4055bc50e7dbe17409613c9a256f Mon Sep 17 00:00:00 2001 From: David Lovegrove Date: Wed, 28 Oct 2026 09:34:36 +0000 Subject: [PATCH 1/2] Fixed actions enabled during startup being disabled when entering Play Mode with domain reload disabled and in Player builds. Co-Authored-By: Claude Opus 5.5 --- .../InputActionStatePlayModeTests.cs | 162 ++++++++++++++++++ .../InputActionStatePlayModeTests.cs.meta | 2 + Packages/com.unity.inputsystem/CHANGELOG.md | 1 + .../Editor/InputSystemEditorInitializer.cs | 3 + .../Runtime/Actions/InputActionState.cs | 51 ++++-- 5 files changed, 209 insertions(+), 10 deletions(-) create mode 100644 Assets/Tests/InputSystem.Editor/InputActionStatePlayModeTests.cs create mode 100644 Assets/Tests/InputSystem.Editor/InputActionStatePlayModeTests.cs.meta diff --git a/Assets/Tests/InputSystem.Editor/InputActionStatePlayModeTests.cs b/Assets/Tests/InputSystem.Editor/InputActionStatePlayModeTests.cs new file mode 100644 index 0000000000..af30168514 --- /dev/null +++ b/Assets/Tests/InputSystem.Editor/InputActionStatePlayModeTests.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections; +using NUnit.Framework; +using UnityEditor; +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.TestTools; +using Object = UnityEngine.Object; + +// Enters real play mode so that the engine runs every RuntimeInitializeOnLoadMethod in its own order. +// This deliberately does not derive from InputTestFixture, which replaces the runtime and global state. +internal class InputActionStatePlayModeTests +{ + const string k_TestCategory = "Editor"; + const string k_ArmedKey = "InputSystem.Tests.InputActionStatePlayModeTests.Armed"; + const string k_EnabledAfterSceneLoadKey = "InputSystem.Tests.InputActionStatePlayModeTests.EnabledAfterSceneLoad"; + const string k_SavedOptionsKey = "InputSystem.Tests.InputActionStatePlayModeTests.SavedOptions"; + + static readonly bool[] k_DisableDomainReloadValues = { false, true }; + + static InputAction s_EarlyAction; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + static void EnableActionBeforeSceneLoad() + { + if (!SessionState.GetBool(k_ArmedKey, false)) + return; + + s_EarlyAction = new InputAction(binding: "/space"); + s_EarlyAction.Enable(); + } + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] + static void RecordActionAfterSceneLoad() + { + if (!SessionState.GetBool(k_ArmedKey, false)) + return; + + SessionState.SetBool(k_EnabledAfterSceneLoadKey, s_EarlyAction != null && s_EarlyAction.enabled); + } + + [SetUp] + public void SetUp() + { + // Each domain reload during the test runs SetUp again, so only record the options once. + if (SessionState.GetInt(k_SavedOptionsKey, -1) != -1) + return; + + SessionState.SetInt(k_SavedOptionsKey, (int)EditorSettings.enterPlayModeOptions); + } + + [TearDown] + public void TearDown() + { + if (Application.isPlaying) + return; + + EditorSettings.enterPlayModeOptions = (EnterPlayModeOptions)SessionState.GetInt(k_SavedOptionsKey, 0); + + // Entering play mode saves the modified settings to disk, so save the restored ones too. + AssetDatabase.SaveAssets(); + SessionState.EraseInt(k_SavedOptionsKey); + SessionState.EraseBool(k_ArmedKey); + SessionState.EraseBool(k_EnabledAfterSceneLoadKey); + + s_EarlyAction?.Dispose(); + s_EarlyAction = null; + } + + static void SetDomainReloadDisabled(bool disabled) + { + EditorSettings.enterPlayModeOptions = disabled + ? EnterPlayModeOptions.DisableDomainReload | EnterPlayModeOptions.DisableSceneReload + : EnterPlayModeOptions.None; + } + + static bool HasOnActionChangeCallback(Action callback) + { + var callbacks = InputActionState.s_GlobalState.onActionChange; + for (var i = 0; i < callbacks.length; ++i) + { + if (callbacks[i] == callback) + return true; + } + return false; + } + + [UnityTest] + [Category(k_TestCategory)] + [Description("https://jira.unity3d.com/browse/UUM-151403")] + public IEnumerator Actions_EnabledBeforeSceneLoad_StayEnabled_WhenEnteringPlayMode( + [ValueSource(nameof(k_DisableDomainReloadValues))] bool disableDomainReload) + { + SetDomainReloadDisabled(disableDomainReload); + SessionState.SetBool(k_ArmedKey, true); + + yield return new EnterPlayMode(); + + SessionState.SetBool(k_ArmedKey, false); + Assert.That(SessionState.GetBool(k_EnabledAfterSceneLoadKey, false), Is.True); + + yield return new ExitPlayMode(); + + // Without a domain reload the action object survives, but exiting play mode must still clean up its state. + if (disableDomainReload) + Assert.That(s_EarlyAction.enabled, Is.False); + } + + [UnityTest] + [Category(k_TestCategory)] + [Description("https://jira.unity3d.com/browse/UUM-151403")] + public IEnumerator Actions_OnActionChangeCallbacksAddedInPlayMode_AreRemovedOnExit_WhenDomainReloadIsDisabled() + { + SetDomainReloadDisabled(true); + + Action editModeCallback = (obj, change) => {}; + Action playModeCallback = (obj, change) => {}; + var survivingListener = ScriptableObject.CreateInstance(); + survivingListener.hideFlags = HideFlags.HideAndDontSave; + var destroyedListener = ScriptableObject.CreateInstance(); + destroyedListener.hideFlags = HideFlags.HideAndDontSave; + Action survivingListenerCallback = survivingListener.OnActionChange; + Action destroyedListenerCallback = destroyedListener.OnActionChange; + InputSystem.onActionChange += editModeCallback; + try + { + yield return new EnterPlayMode(); + + Assert.That(HasOnActionChangeCallback(editModeCallback), Is.True); + InputSystem.onActionChange += playModeCallback; + + // Like an editor window opened during play mode, which stays open after it. + InputSystem.onActionChange += survivingListenerCallback; + + // Like a component of a play mode GameObject, which is destroyed on exit. + InputSystem.onActionChange += destroyedListenerCallback; + Object.DestroyImmediate(destroyedListener); + + yield return new ExitPlayMode(); + + Assert.That(HasOnActionChangeCallback(editModeCallback), Is.True); + Assert.That(HasOnActionChangeCallback(playModeCallback), Is.False); + Assert.That(HasOnActionChangeCallback(survivingListenerCallback), Is.True); + Assert.That(HasOnActionChangeCallback(destroyedListenerCallback), Is.False); + } + finally + { + InputSystem.onActionChange -= editModeCallback; + InputSystem.onActionChange -= playModeCallback; + InputSystem.onActionChange -= survivingListenerCallback; + InputSystem.onActionChange -= destroyedListenerCallback; + Object.DestroyImmediate(survivingListener); + } + } + + class ActionChangeListener : ScriptableObject + { + public void OnActionChange(object actionOrMap, InputActionChange change) + { + } + } +} diff --git a/Assets/Tests/InputSystem.Editor/InputActionStatePlayModeTests.cs.meta b/Assets/Tests/InputSystem.Editor/InputActionStatePlayModeTests.cs.meta new file mode 100644 index 0000000000..aa5ca17ee0 --- /dev/null +++ b/Assets/Tests/InputSystem.Editor/InputActionStatePlayModeTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 09ea6c8c372146feba0a15eeb9928da5 diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 8cf9a23eb3..e51ad50c18 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Fixed polling for interaction phases (such as `WasPerformedThisFrame`) being missed during `Update` due to interaction timeouts sometimes occurring during the later before-render period when a before-render device (such as an XR HMD) is added. This means that timeouts and interactions like Hold may now wait to perform until the next frame instead of the earliest chance during before-render. [UUM-147719](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-147719) - Fixed touch input from Unity Remote 5 being ignored in Unity 6000.5 and newer. [UUM-155327](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-155327) +- Fixed actions enabled during startup, for example in an Entities system's `OnCreate`, being disabled when entering Play Mode with domain reload disabled and in Player builds. [UUM-151403](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-151403) ## [1.20.1] - 2026-10-05 diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/InputSystemEditorInitializer.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/InputSystemEditorInitializer.cs index 4500497457..70c63d1fb2 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/InputSystemEditorInitializer.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/InputSystemEditorInitializer.cs @@ -501,6 +501,8 @@ internal static void OnPlayModeChange(PlayModeStateChange change) InputSystem.s_Manager.m_ExitEditModeTime = s_DomainStateManager.exitEditModeTime; InputSystem.s_Manager.m_EnterPlayModeTime = 0; + + InputActionState.RecordCallbacksBeforePlayMode(); break; case PlayModeStateChange.EnteredPlayMode: @@ -524,6 +526,7 @@ internal static void OnPlayModeChange(PlayModeStateChange change) // Nuke all InputActionMapStates. Releases their unmanaged memory. InputActionState.DestroyAllActionMapStates(); + InputActionState.RemoveCallbacksAddedInPlayMode(); // Clear the Action reference from all InputActionReference objects InputActionReference.InvalidateAll(); diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Actions/InputActionState.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Actions/InputActionState.cs index 4325fc1988..7390fbc13a 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Actions/InputActionState.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Actions/InputActionState.cs @@ -4499,25 +4499,56 @@ internal struct GlobalState internal InlinedArray globalList; internal CallbackArray> onActionChange; internal CallbackArray> onActionControlsChanged; +#if UNITY_EDITOR + internal Action[] onActionChangeBeforePlayMode; + internal Action[] onActionControlsChangedBeforePlayMode; +#endif } internal static GlobalState s_GlobalState; - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] - private static void InitializeGlobalActionState() - { #if UNITY_EDITOR - // Appears we shouldn't really reset globals in case the domain reload is enabled. - // This is because in that case, we've just had the whole system init'ed via static ctors - // Moreover, later in GlobalInialize we skip initialization specifically in this case. - if (!(InputSystem.s_IsDomainReloadDisabled?.Invoke() ?? false)) + internal static void RecordCallbacksBeforePlayMode() + { + s_GlobalState.onActionChangeBeforePlayMode = ToArray(ref s_GlobalState.onActionChange); + s_GlobalState.onActionControlsChangedBeforePlayMode = ToArray(ref s_GlobalState.onActionControlsChanged); + } + + // Without a domain reload, callbacks added during play mode would otherwise outlive it. + internal static void RemoveCallbacksAddedInPlayMode() + { + // Nothing was recorded if a domain reload happened since, and that already cleared the callbacks. + if (s_GlobalState.onActionChangeBeforePlayMode == null) return; -#endif - ResetGlobals(); - s_GlobalState = default; + RemovePlayModeCallbacks(ref s_GlobalState.onActionChange, s_GlobalState.onActionChangeBeforePlayMode); + RemovePlayModeCallbacks(ref s_GlobalState.onActionControlsChanged, s_GlobalState.onActionControlsChangedBeforePlayMode); + s_GlobalState.onActionChangeBeforePlayMode = null; + s_GlobalState.onActionControlsChangedBeforePlayMode = null; } + private static TDelegate[] ToArray(ref CallbackArray callbacks) + where TDelegate : Delegate + { + var result = new TDelegate[callbacks.length]; + for (var i = 0; i < callbacks.length; ++i) + result[i] = callbacks[i]; + return result; + } + + private static void RemovePlayModeCallbacks(ref CallbackArray callbacks, TDelegate[] callbacksBeforePlayMode) + where TDelegate : Delegate + { + for (var i = callbacks.length - 1; i >= 0; --i) + { + // Play mode objects are destroyed by now, but editor objects such as open windows survive play mode. + var target = callbacks[i].Target as UnityEngine.Object; + if (Array.IndexOf(callbacksBeforePlayMode, callbacks[i]) < 0 && target == null) + callbacks.RemoveCallback(callbacks[i]); + } + } +#endif + internal static ISavedState SaveAndResetState() { // Save current state From ae4c05801559d7a89c2d3235c7de99567d66365d Mon Sep 17 00:00:00 2001 From: David Lovegrove Date: Wed, 28 Oct 2026 10:15:05 +0000 Subject: [PATCH 2/2] Enabled the enter play mode options in the tests so domain reload is actually disabled on 6000.0, and fixed saving and restoring them. Co-Authored-By: Claude Opus 5.5 --- .../InputActionStatePlayModeTests.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Assets/Tests/InputSystem.Editor/InputActionStatePlayModeTests.cs b/Assets/Tests/InputSystem.Editor/InputActionStatePlayModeTests.cs index af30168514..867b06c35c 100644 --- a/Assets/Tests/InputSystem.Editor/InputActionStatePlayModeTests.cs +++ b/Assets/Tests/InputSystem.Editor/InputActionStatePlayModeTests.cs @@ -15,6 +15,7 @@ internal class InputActionStatePlayModeTests const string k_ArmedKey = "InputSystem.Tests.InputActionStatePlayModeTests.Armed"; const string k_EnabledAfterSceneLoadKey = "InputSystem.Tests.InputActionStatePlayModeTests.EnabledAfterSceneLoad"; const string k_SavedOptionsKey = "InputSystem.Tests.InputActionStatePlayModeTests.SavedOptions"; + const string k_SavedOptionsEnabledKey = "InputSystem.Tests.InputActionStatePlayModeTests.SavedOptionsEnabled"; static readonly bool[] k_DisableDomainReloadValues = { false, true }; @@ -46,7 +47,12 @@ public void SetUp() if (SessionState.GetInt(k_SavedOptionsKey, -1) != -1) return; + // Unity 6000.0 reports no options while they are disabled, so enable them to read the stored ones. + var optionsEnabled = EditorSettings.enterPlayModeOptionsEnabled; + EditorSettings.enterPlayModeOptionsEnabled = true; SessionState.SetInt(k_SavedOptionsKey, (int)EditorSettings.enterPlayModeOptions); + SessionState.SetBool(k_SavedOptionsEnabledKey, optionsEnabled); + EditorSettings.enterPlayModeOptionsEnabled = optionsEnabled; } [TearDown] @@ -55,11 +61,14 @@ public void TearDown() if (Application.isPlaying) return; + EditorSettings.enterPlayModeOptionsEnabled = true; EditorSettings.enterPlayModeOptions = (EnterPlayModeOptions)SessionState.GetInt(k_SavedOptionsKey, 0); + EditorSettings.enterPlayModeOptionsEnabled = SessionState.GetBool(k_SavedOptionsEnabledKey, false); // Entering play mode saves the modified settings to disk, so save the restored ones too. AssetDatabase.SaveAssets(); SessionState.EraseInt(k_SavedOptionsKey); + SessionState.EraseBool(k_SavedOptionsEnabledKey); SessionState.EraseBool(k_ArmedKey); SessionState.EraseBool(k_EnabledAfterSceneLoadKey); @@ -69,6 +78,8 @@ public void TearDown() static void SetDomainReloadDisabled(bool disabled) { + // Unity 6000.0 ignores enterPlayModeOptions unless the options are enabled. + EditorSettings.enterPlayModeOptionsEnabled = true; EditorSettings.enterPlayModeOptions = disabled ? EnterPlayModeOptions.DisableDomainReload | EnterPlayModeOptions.DisableSceneReload : EnterPlayModeOptions.None;