diff --git a/Assets/Tests/InputSystem/CoreTests_Actions.cs b/Assets/Tests/InputSystem/CoreTests_Actions.cs index ba26bc52b6..bdedf0f288 100644 --- a/Assets/Tests/InputSystem/CoreTests_Actions.cs +++ b/Assets/Tests/InputSystem/CoreTests_Actions.cs @@ -5321,56 +5321,68 @@ public class ModificationCases : IEnumerable [Preserve] public ModificationCases() {} + private static readonly Modification[] ModificationAppliesToSingleActionMap = + { + Modification.AddBinding, + Modification.RemoveBinding, + Modification.ModifyBinding, + Modification.ApplyBindingOverride, + Modification.AddAction, + Modification.RemoveAction, + Modification.ChangeBindingMask, + Modification.AddDevice, + Modification.RemoveDevice, + Modification.AddDeviceGlobally, + Modification.RemoveDeviceGlobally, + // Excludes: AddMap, RemoveMap + }; + + private static readonly Modification[] ModificationAppliesToSingletonAction = + { + Modification.AddBinding, + Modification.RemoveBinding, + Modification.ModifyBinding, + Modification.ApplyBindingOverride, + Modification.AddDeviceGlobally, + Modification.RemoveDeviceGlobally, + }; + public IEnumerator GetEnumerator() { - bool ModificationAppliesToSingletonAction(Modification modification) + // NOTE: This executes *outside* of our test fixture during test discovery. + + // We cannot directly create the InputAction objects within GetEnumerator() because the underlying + // asset object might be invalid by the time the tests are actually run. + // + // That is, NUnit TestCases are generated once when the Assembly is loaded and will persist until it's unloaded, + // meaning they'll never be recreated without a Domain Reload. However, since InputActionAsset is a ScriptableObject, + // it could be deleted or otherwise invalidated between test case creation and actual test execution. + // + // So, instead we'll create a delegate to create the Actions object as the parameter for each test case, allowing + // the test case to create an Actions object itself when it actually runs. { - switch (modification) + var actionsFromAsset = new Func(() => new DefaultInputActions().asset); + foreach (var value in Enum.GetValues(typeof(Modification))) { - case Modification.AddBinding: - case Modification.RemoveBinding: - case Modification.ModifyBinding: - case Modification.ApplyBindingOverride: - case Modification.AddDeviceGlobally: - case Modification.RemoveDeviceGlobally: - return true; + yield return new TestCaseData(value, actionsFromAsset); } - return false; } - bool ModificationAppliesToSingleActionMap(Modification modification) { - switch (modification) + var actionMap = new Func(CreateMap); + foreach (var value in Enum.GetValues(typeof(Modification))) { - case Modification.AddMap: - case Modification.RemoveMap: - return false; + if (ModificationAppliesToSingleActionMap.Contains((Modification)value)) + yield return new TestCaseData(value, actionMap); } - return true; } - // NOTE: This executes *outside* of our test fixture during test discovery. - - // Creates a matrix of all permutations of Modifications combined with assets, maps, and singleton actions. - foreach (var func in new Func[] { () => new DefaultInputActions().asset, CreateMap, CreateSingletonAction }) { + var singletonMap = new Func(CreateSingletonAction); foreach (var value in Enum.GetValues(typeof(Modification))) { - var actions = func(); - if (actions is InputActionMap map) - { - if (map.m_SingletonAction != null) - { - if (!ModificationAppliesToSingletonAction((Modification)value)) - continue; - } - else if (!ModificationAppliesToSingleActionMap((Modification)value)) - { - continue; - } - } - - yield return new TestCaseData(value, actions); + if (ModificationAppliesToSingletonAction.Contains((Modification)value)) + yield return new TestCaseData(value, singletonMap); } } } @@ -5401,14 +5413,14 @@ private InputActionMap CreateSingletonAction() [Test] [Category("Actions")] [TestCaseSource(typeof(ModificationCases))] - public void Actions_CanHandleModification(Modification modification, IInputActionCollection2 actions) + public void Actions_CanHandleModification(Modification modification, Func getActions) { #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS // Exclude project-wide actions from this test InputSystem.actions?.Disable(); InputActionState.DestroyAllActionMapStates(); // Required for `onActionChange` to report correct number of changes #endif - + var actions = getActions(); var gamepad = InputSystem.AddDevice(); if (modification == Modification.AddDevice || modification == Modification.RemoveDevice) @@ -6138,12 +6150,12 @@ public void Actions_AddingSameProcessorTwice_DoesntImpactUIHideState() InputSystem.RegisterProcessor(); Assert.That(InputSystem.TryGetProcessor("ConstantFloat1Test"), Is.Not.EqualTo(null)); - bool hide = InputSystem.s_Manager.processors.ShouldHideInUI("ConstantFloat1Test"); + bool hide = InputSystem.manager.processors.ShouldHideInUI("ConstantFloat1Test"); Assert.That(hide, Is.EqualTo(false)); InputSystem.RegisterProcessor(); // Check we haven't caused this to alias with itself and cause it to be hidden in the UI - hide = InputSystem.s_Manager.processors.ShouldHideInUI("ConstantFloat1Test"); + hide = InputSystem.manager.processors.ShouldHideInUI("ConstantFloat1Test"); Assert.That(hide, Is.EqualTo(false)); } @@ -6789,7 +6801,7 @@ public void Actions_RegisteringExistingInteractionUnderNewName_CreatesAlias() { InputSystem.RegisterInteraction("TestTest"); - Assert.That(InputSystem.s_Manager.interactions.aliases.Contains(new InternedString("TestTest"))); + Assert.That(InputSystem.manager.interactions.aliases.Contains(new InternedString("TestTest"))); } #endif // UNITY_EDITOR @@ -9081,7 +9093,7 @@ public void Actions_RegisteringExistingCompositeUnderNewName_CreatesAlias() { InputSystem.RegisterBindingComposite("TestTest"); - Assert.That(InputSystem.s_Manager.composites.aliases.Contains(new InternedString("TestTest"))); + Assert.That(InputSystem.manager.composites.aliases.Contains(new InternedString("TestTest"))); } #endif // UNITY_EDITOR @@ -11426,7 +11438,7 @@ public void Actions_DisablingAllActions_RemovesAllTheirStateMonitors() // Not the most elegant test as we reach into internals here but with the // current API, it's not possible to enumerate monitors from outside. - Assert.That(InputSystem.s_Manager.m_StateChangeMonitors, + Assert.That(InputSystem.manager.m_StateChangeMonitors, Has.All.Matches( (InputManager.StateChangeMonitorsForDevice x) => x.memoryRegions.All(r => r.sizeInBits == 0))); } @@ -12446,7 +12458,7 @@ public void Actions_CompositeBindingResetWhenResetDeviceCalledWhileExecutingActi // Disable the Keyboard while action is being performed. // This simulates an "OnFocusLost" event occurring while processing the Action, e.g. when switching primary displays or moving the main window actionPerformed = true; - InputSystem.s_Manager.EnableOrDisableDevice(keyboard.device, false, InputManager.DeviceDisableScope.TemporaryWhilePlayerIsInBackground); + InputSystem.manager.EnableOrDisableDevice(keyboard.device, false, InputManager.DeviceDisableScope.TemporaryWhilePlayerIsInBackground); }; map.Enable(); @@ -12459,7 +12471,7 @@ public void Actions_CompositeBindingResetWhenResetDeviceCalledWhileExecutingActi Assert.IsTrue(actionPerformed); // Re enable the Keyboard (before keys are released) and execute Action again - InputSystem.s_Manager.EnableOrDisableDevice(keyboard.device, true, InputManager.DeviceDisableScope.TemporaryWhilePlayerIsInBackground); + InputSystem.manager.EnableOrDisableDevice(keyboard.device, true, InputManager.DeviceDisableScope.TemporaryWhilePlayerIsInBackground); actionPerformed = false; Release(keyboard.leftShiftKey); @@ -12478,7 +12490,7 @@ public void Actions_CompositeBindingResetWhenResetDeviceCalledWhileExecutingActi Release(keyboard.f1Key); // Re enable the Keyboard (after keys are released) and execute Action one more time - InputSystem.s_Manager.EnableOrDisableDevice(keyboard.device, true, InputManager.DeviceDisableScope.TemporaryWhilePlayerIsInBackground); + InputSystem.manager.EnableOrDisableDevice(keyboard.device, true, InputManager.DeviceDisableScope.TemporaryWhilePlayerIsInBackground); Press(keyboard.leftCtrlKey); Press(keyboard.leftShiftKey); @@ -12492,7 +12504,7 @@ public void Actions_CompositeBindingResetWhenResetDeviceCalledWhileExecutingActi Press(keyboard.f1Key); // Re enable the Keyboard (before keys are released) and verify Action isn't triggered when Key pressed first - InputSystem.s_Manager.EnableOrDisableDevice(keyboard.device, true, InputManager.DeviceDisableScope.TemporaryWhilePlayerIsInBackground); + InputSystem.manager.EnableOrDisableDevice(keyboard.device, true, InputManager.DeviceDisableScope.TemporaryWhilePlayerIsInBackground); Press(keyboard.f1Key); Press(keyboard.leftCtrlKey); diff --git a/Assets/Tests/InputSystem/CoreTests_Analytics.cs b/Assets/Tests/InputSystem/CoreTests_Analytics.cs index e9892b3064..a24dddae1c 100644 --- a/Assets/Tests/InputSystem/CoreTests_Analytics.cs +++ b/Assets/Tests/InputSystem/CoreTests_Analytics.cs @@ -409,7 +409,6 @@ public void Analytics_ShouldReportBuildAnalytics_WhenNotHavingSettingsAsset() { CollectAnalytics(InputBuildAnalytic.kEventName); - var storedSettings = InputSystem.s_Manager.settings; InputSettings defaultSettings = null; try @@ -470,7 +469,6 @@ public void Analytics_ShouldReportBuildAnalytics_WhenNotHavingSettingsAsset() } finally { - InputSystem.s_Manager.settings = storedSettings; if (defaultSettings != null) Object.DestroyImmediate(defaultSettings); } @@ -482,7 +480,6 @@ public void Analytics_ShouldReportBuildAnalytics_WhenHavingSettingsAssetWithCust { CollectAnalytics(InputBuildAnalytic.kEventName); - var storedSettings = InputSystem.s_Manager.settings; InputSettings customSettings = null; try @@ -576,7 +573,6 @@ public void Analytics_ShouldReportBuildAnalytics_WhenHavingSettingsAssetWithCust } finally { - InputSystem.s_Manager.settings = storedSettings; if (customSettings != null) Object.DestroyImmediate(customSettings); } diff --git a/Assets/Tests/InputSystem/CoreTests_Devices.cs b/Assets/Tests/InputSystem/CoreTests_Devices.cs index bc4ec5a82b..1b5dd6aaf5 100644 --- a/Assets/Tests/InputSystem/CoreTests_Devices.cs +++ b/Assets/Tests/InputSystem/CoreTests_Devices.cs @@ -572,11 +572,11 @@ public void Devices_AddingDeviceThatUsesBeforeRenderUpdates_CausesBeforeRenderUp InputSystem.RegisterLayout(deviceJson); - Assert.That(InputSystem.s_Manager.updateMask & InputUpdateType.BeforeRender, Is.EqualTo((InputUpdateType)0)); + Assert.That(InputSystem.manager.updateMask & InputUpdateType.BeforeRender, Is.EqualTo((InputUpdateType)0)); InputSystem.AddDevice("CustomGamepad"); - Assert.That(InputSystem.s_Manager.updateMask & InputUpdateType.BeforeRender, Is.EqualTo(InputUpdateType.BeforeRender)); + Assert.That(InputSystem.manager.updateMask & InputUpdateType.BeforeRender, Is.EqualTo(InputUpdateType.BeforeRender)); } [Test] @@ -596,15 +596,15 @@ public void Devices_RemovingLastDeviceThatUsesBeforeRenderUpdates_CausesBeforeRe var device1 = InputSystem.AddDevice("CustomGamepad"); var device2 = InputSystem.AddDevice("CustomGamepad"); - Assert.That(InputSystem.s_Manager.updateMask & InputUpdateType.BeforeRender, Is.EqualTo(InputUpdateType.BeforeRender)); + Assert.That(InputSystem.manager.updateMask & InputUpdateType.BeforeRender, Is.EqualTo(InputUpdateType.BeforeRender)); InputSystem.RemoveDevice(device1); - Assert.That(InputSystem.s_Manager.updateMask & InputUpdateType.BeforeRender, Is.EqualTo(InputUpdateType.BeforeRender)); + Assert.That(InputSystem.manager.updateMask & InputUpdateType.BeforeRender, Is.EqualTo(InputUpdateType.BeforeRender)); InputSystem.RemoveDevice(device2); - Assert.That(InputSystem.s_Manager.updateMask & InputUpdateType.BeforeRender, Is.EqualTo((InputUpdateType)0)); + Assert.That(InputSystem.manager.updateMask & InputUpdateType.BeforeRender, Is.EqualTo((InputUpdateType)0)); } private class TestDeviceReceivingAddAndRemoveNotification : Mouse diff --git a/Assets/Tests/InputSystem/CoreTests_Editor.cs b/Assets/Tests/InputSystem/CoreTests_Editor.cs index 0a74266c6b..ac197352a0 100644 --- a/Assets/Tests/InputSystem/CoreTests_Editor.cs +++ b/Assets/Tests/InputSystem/CoreTests_Editor.cs @@ -147,11 +147,11 @@ public void Editor_CanSaveAndRestoreState() }.ToJson()); InputSystem.Update(); - InputSystem.SaveAndReset(); + m_StateManager.SaveAndReset(false, null); Assert.That(InputSystem.devices, Has.Count.EqualTo(0)); - InputSystem.Restore(); + m_StateManager.Restore(); Assert.That(InputSystem.devices, Has.Exactly(1).With.Property("layout").EqualTo("MyDevice").And.TypeOf()); @@ -165,6 +165,7 @@ public void Editor_CanSaveAndRestoreState() Assert.That(unsupportedDevices[0].interfaceName, Is.EqualTo("Test")); } +#if !ENABLE_CORECLR // onFindLayoutForDevice allows dynamically injecting new layouts into the system that // are custom-tailored at runtime for the discovered device. Make sure that our domain // reload can restore these. @@ -195,12 +196,12 @@ public void Editor_DomainReload_CanRestoreDevicesBuiltWithDynamicallyGeneratedLa Assert.That(InputSystem.devices, Has.Exactly(1).TypeOf()); - InputSystem.SaveAndReset(); + m_StateManager.SaveAndReset(false, null); Assert.That(InputSystem.devices, Is.Empty); - var state = InputSystem.GetSavedState(); - var manager = InputSystem.s_Manager; + var state = m_StateManager.GetSavedState(); + var manager = InputSystem.manager; manager.m_SavedAvailableDevices = state.managerState.availableDevices; manager.m_SavedDeviceStates = state.managerState.devices; @@ -209,7 +210,7 @@ public void Editor_DomainReload_CanRestoreDevicesBuiltWithDynamicallyGeneratedLa Assert.That(InputSystem.devices, Has.Exactly(1).TypeOf()); - InputSystem.Restore(); + m_StateManager.Restore(); } [Test] @@ -219,7 +220,7 @@ public void Editor_DomainReload_PreservesUsagesOnDevices() var device = InputSystem.AddDevice(); InputSystem.SetDeviceUsage(device, CommonUsages.LeftHand); - SimulateDomainReload(); + InputSystem.TestHook_SimulateDomainReload(runtime); var newDevice = InputSystem.devices[0]; @@ -239,7 +240,7 @@ public void Editor_DomainReload_PreservesEnabledState() Assert.That(device.enabled, Is.False); - SimulateDomainReload(); + InputSystem.TestHook_SimulateDomainReload(runtime); var newDevice = InputSystem.devices[0]; @@ -252,7 +253,7 @@ public void Editor_DomainReload_InputSystemInitializationCausesDevicesToBeRecrea { InputSystem.AddDevice(); - SimulateDomainReload(); + InputSystem.TestHook_SimulateDomainReload(runtime); Assert.That(InputSystem.devices, Has.Count.EqualTo(1)); Assert.That(InputSystem.devices[0], Is.TypeOf()); @@ -289,7 +290,7 @@ public void Editor_DomainReload_CustomDevicesAreRestoredAsLayoutsBecomeAvailable InputSystem.RegisterLayout(kLayout); InputSystem.AddDevice("CustomDevice"); - SimulateDomainReload(); + InputSystem.TestHook_SimulateDomainReload(runtime); Assert.That(InputSystem.devices, Is.Empty); @@ -310,7 +311,7 @@ public void Editor_DomainReload_RetainsUnsupportedDevices() }); InputSystem.Update(); - SimulateDomainReload(); + InputSystem.TestHook_SimulateDomainReload(runtime); Assert.That(InputSystem.GetUnsupportedDevices(), Has.Count.EqualTo(1)); Assert.That(InputSystem.GetUnsupportedDevices()[0].interfaceName, Is.EqualTo("SomethingUnknown")); @@ -346,11 +347,13 @@ public void Editor_DomainReload_CanRemoveDevicesDuringDomainReload() Assert.That(InputSystem.devices[0], Is.AssignableTo()); } +#endif // !ENABLE_CORECLR + [Test] [Category("Editor")] public void Editor_RestoringStateWillCleanUpEventHooks() { - InputSystem.SaveAndReset(); + m_StateManager.SaveAndReset(false, null); var receivedOnEvent = 0; var receivedOnDeviceChange = 0; @@ -358,7 +361,7 @@ public void Editor_RestoringStateWillCleanUpEventHooks() InputSystem.onEvent += (e, d) => ++ receivedOnEvent; InputSystem.onDeviceChange += (c, d) => ++ receivedOnDeviceChange; - InputSystem.Restore(); + m_StateManager.Restore(); var device = InputSystem.AddDevice("Gamepad"); InputSystem.QueueStateEvent(device, new GamepadState()); @@ -375,8 +378,8 @@ public void Editor_RestoringStateWillRestoreObjectsOfLayoutBuilder() var builder = new TestLayoutBuilder {layoutToLoad = "Gamepad"}; InputSystem.RegisterLayoutBuilder(() => builder.DoIt(), "TestLayout"); - InputSystem.SaveAndReset(); - InputSystem.Restore(); + m_StateManager.SaveAndReset(false, null); + m_StateManager.Restore(); var device = InputSystem.AddDevice("TestLayout"); @@ -2504,7 +2507,7 @@ public void TODO_Editor_SettingsModifiedInPlayMode_AreRestoredWhenReEnteringEdit [Category("Editor")] public void Editor_AlwaysKeepsEditorUpdatesEnabled() { - Assert.That(InputSystem.s_Manager.updateMask & InputUpdateType.Editor, Is.EqualTo(InputUpdateType.Editor)); + Assert.That(InputSystem.manager.updateMask & InputUpdateType.Editor, Is.EqualTo(InputUpdateType.Editor)); } [Test] @@ -2926,15 +2929,15 @@ public void Editor_LeavingPlayMode_DestroysAllActionStates() action.Enable(); Assert.That(InputActionState.s_GlobalState.globalList.length, Is.EqualTo(1)); - Assert.That(InputSystem.s_Manager.m_StateChangeMonitors.Length, Is.GreaterThan(0)); - Assert.That(InputSystem.s_Manager.m_StateChangeMonitors[0].count, Is.EqualTo(1)); + Assert.That(InputSystem.manager.m_StateChangeMonitors.Length, Is.GreaterThan(0)); + Assert.That(InputSystem.manager.m_StateChangeMonitors[0].count, Is.EqualTo(1)); // Exit play mode. InputSystem.OnPlayModeChange(PlayModeStateChange.ExitingPlayMode); InputSystem.OnPlayModeChange(PlayModeStateChange.EnteredEditMode); Assert.That(InputActionState.s_GlobalState.globalList.length, Is.Zero); - Assert.That(InputSystem.s_Manager.m_StateChangeMonitors[0].listeners[0].control, Is.Null); // Won't get removed, just cleared. + Assert.That(InputSystem.manager.m_StateChangeMonitors[0].listeners[0].control, Is.Null); // Won't get removed, just cleared. } [Test] diff --git a/Assets/Tests/InputSystem/CoreTests_Events.cs b/Assets/Tests/InputSystem/CoreTests_Events.cs index 6addd65598..df3367f98e 100644 --- a/Assets/Tests/InputSystem/CoreTests_Events.cs +++ b/Assets/Tests/InputSystem/CoreTests_Events.cs @@ -431,7 +431,7 @@ public void Events_CanSwitchToFullyManualUpdates() #if UNITY_EDITOR // Edit mode updates shouldn't have been disabled in editor. - Assert.That(InputSystem.s_Manager.updateMask & InputUpdateType.Editor, Is.Not.Zero); + Assert.That(InputSystem.manager.updateMask & InputUpdateType.Editor, Is.Not.Zero); #endif InputSystem.QueueStateEvent(mouse, new MouseState().WithButton(MouseButton.Left)); @@ -458,8 +458,8 @@ public void Events_CanSwitchToProcessingInFixedUpdates() Assert.That(InputSystem.settings.updateMode, Is.EqualTo(InputSettings.UpdateMode.ProcessEventsInFixedUpdate)); Assert.That(receivedOnChange, Is.True); - Assert.That(InputSystem.s_Manager.updateMask & InputUpdateType.Fixed, Is.EqualTo(InputUpdateType.Fixed)); - Assert.That(InputSystem.s_Manager.updateMask & InputUpdateType.Dynamic, Is.EqualTo(InputUpdateType.None)); + Assert.That(InputSystem.manager.updateMask & InputUpdateType.Fixed, Is.EqualTo(InputUpdateType.Fixed)); + Assert.That(InputSystem.manager.updateMask & InputUpdateType.Dynamic, Is.EqualTo(InputUpdateType.None)); InputSystem.QueueStateEvent(mouse, new MouseState().WithButton(MouseButton.Left)); runtime.currentTimeForFixedUpdate += Time.fixedDeltaTime; @@ -475,19 +475,19 @@ public void Events_CanSwitchToProcessingInFixedUpdates() [Category("Events")] public void Events_ShouldRunUpdate_AppliesUpdateMask() { - InputSystem.s_Manager.updateMask = InputUpdateType.Dynamic; + InputSystem.manager.updateMask = InputUpdateType.Dynamic; Assert.That(runtime.onShouldRunUpdate.Invoke(InputUpdateType.Dynamic)); Assert.That(!runtime.onShouldRunUpdate.Invoke(InputUpdateType.Fixed)); Assert.That(!runtime.onShouldRunUpdate.Invoke(InputUpdateType.Manual)); - InputSystem.s_Manager.updateMask = InputUpdateType.Manual; + InputSystem.manager.updateMask = InputUpdateType.Manual; Assert.That(!runtime.onShouldRunUpdate.Invoke(InputUpdateType.Dynamic)); Assert.That(!runtime.onShouldRunUpdate.Invoke(InputUpdateType.Fixed)); Assert.That(runtime.onShouldRunUpdate.Invoke(InputUpdateType.Manual)); - InputSystem.s_Manager.updateMask = InputUpdateType.Default; + InputSystem.manager.updateMask = InputUpdateType.Default; Assert.That(runtime.onShouldRunUpdate.Invoke(InputUpdateType.Dynamic)); Assert.That(runtime.onShouldRunUpdate.Invoke(InputUpdateType.Fixed)); diff --git a/Assets/Tests/InputSystem/CoreTests_Remoting.cs b/Assets/Tests/InputSystem/CoreTests_Remoting.cs index e8eb988e0a..e44f3db236 100644 --- a/Assets/Tests/InputSystem/CoreTests_Remoting.cs +++ b/Assets/Tests/InputSystem/CoreTests_Remoting.cs @@ -297,7 +297,7 @@ public void Remote_CanConnectInputSystemsOverEditorPlayerConnection() connectionToPlayer.Bind(fakeEditorConnection, true); // Bind a local remote on the player side. - var local = new InputRemoting(InputSystem.s_Manager); + var local = new InputRemoting(InputSystem.manager); local.Subscribe(connectionToEditor); connectionToEditor.Subscribe(local); @@ -477,17 +477,13 @@ private class FakeRemote : IDisposable public FakeRemote() { runtime = new InputTestRuntime(); - var manager = new InputManager(); - manager.m_Settings = ScriptableObject.CreateInstance(); - manager.InitializeData(); - manager.InstallRuntime(runtime); - manager.ApplySettings(); + var manager = InputManager.CreateAndInitialize(runtime, null, true); - local = new InputRemoting(InputSystem.s_Manager); + local = new InputRemoting(InputSystem.manager); remote = new InputRemoting(manager); var remoteInstaller = new GlobalsInstallerObserver(manager); - var localInstaller = new GlobalsInstallerObserver(InputSystem.s_Manager); + var localInstaller = new GlobalsInstallerObserver(InputSystem.manager); // The installers will ensure the globals environment is prepared right before // the receiver processes the message. There are some static fields, such as @@ -505,14 +501,12 @@ public FakeRemote() public void SwitchToRemoteState() { - InputSystem.s_Manager = remoteManager; - InputStateBuffers.SwitchTo(remoteManager.m_StateBuffers, remoteManager.defaultUpdateType); + InputSystem.TestHook_SwitchToDifferentInputManager(remoteManager); } public void SwitchToLocalState() { - InputSystem.s_Manager = localManager; - InputStateBuffers.SwitchTo(localManager.m_StateBuffers, localManager.defaultUpdateType); + InputSystem.TestHook_SwitchToDifferentInputManager(localManager); } public void Dispose() @@ -524,8 +518,8 @@ public void Dispose() } if (remoteManager != null) { - Object.Destroy(remoteManager.m_Settings); - remoteManager.Destroy(); + Object.Destroy(remoteManager.settings); + remoteManager.Dispose(); } } } diff --git a/Assets/Tests/InputSystem/CoreTests_State.cs b/Assets/Tests/InputSystem/CoreTests_State.cs index 8bc38bf728..12c185d9ab 100644 --- a/Assets/Tests/InputSystem/CoreTests_State.cs +++ b/Assets/Tests/InputSystem/CoreTests_State.cs @@ -1235,7 +1235,7 @@ public void State_FixedUpdatesAreDisabledByDefault() { Assert.That(InputSystem.settings.updateMode, Is.EqualTo(InputSettings.UpdateMode.ProcessEventsInDynamicUpdate)); Assert.That(runtime.onShouldRunUpdate(InputUpdateType.Fixed), Is.False); - Assert.That(InputSystem.s_Manager.updateMask & InputUpdateType.Fixed, Is.EqualTo(InputUpdateType.None)); + Assert.That(InputSystem.manager.updateMask & InputUpdateType.Fixed, Is.EqualTo(InputUpdateType.None)); } [Test] diff --git a/Assets/Tests/InputSystem/Plugins/HIDTests.cs b/Assets/Tests/InputSystem/Plugins/HIDTests.cs index a36018f22b..4b9559160c 100644 --- a/Assets/Tests/InputSystem/Plugins/HIDTests.cs +++ b/Assets/Tests/InputSystem/Plugins/HIDTests.cs @@ -708,8 +708,8 @@ public void Devices_HIDDescriptorSurvivesReload() }.ToJson()); InputSystem.Update(); - InputSystem.SaveAndReset(); - InputSystem.Restore(); + m_StateManager.SaveAndReset(false, null); + m_StateManager.Restore(); var hid = (HID)InputSystem.devices.First(x => x is HID); diff --git a/Assets/Tests/InputSystem/Plugins/InputForUITests.cs b/Assets/Tests/InputSystem/Plugins/InputForUITests.cs index 94662f7515..d9c921cc6a 100644 --- a/Assets/Tests/InputSystem/Plugins/InputForUITests.cs +++ b/Assets/Tests/InputSystem/Plugins/InputForUITests.cs @@ -64,7 +64,7 @@ public override void TearDown() EventProvider.ClearMockProvider(); m_InputForUIEvents.Clear(); - InputSystem.s_Manager.actions = storedActions; + InputSystem.manager.actions = storedActions; #if UNITY_EDITOR if (File.Exists(kAssetPath)) @@ -197,7 +197,7 @@ public void UIActionNavigation_FiresUINavigationEvents_FromInputsGamepadJoystick // Remove the project-wide actions asset in play mode and player. // It will call InputSystem.onActionChange and re-set InputSystemProvider.actionAsset // This the case where no project-wide actions asset is available in the project. - InputSystem.s_Manager.actions = null; + InputSystem.manager.actions = null; } Update(); @@ -271,7 +271,7 @@ public void UIActionSubmit_FiresUISubmitEvents_FromInputsGamepadJoystickAndKeybo Update(); if (!useProjectWideActionsAsset) { - InputSystem.s_Manager.actions = null; + InputSystem.manager.actions = null; } Update(); @@ -308,7 +308,7 @@ public void UIActionCancel_FiresUICancelEvents_FromInputsGamepadAndKeyboard(bool Update(); if (!useProjectWideActionsAsset) { - InputSystem.s_Manager.actions = null; + InputSystem.manager.actions = null; } Update(); @@ -338,7 +338,7 @@ public void UIActionPoint_FiresUIPointEvents_FromInputsMousePenAndTouch(bool use Update(); if (!useProjectWideActionsAsset) { - InputSystem.s_Manager.actions = null; + InputSystem.manager.actions = null; } Update(); @@ -390,7 +390,7 @@ public void UIActionClick_FiresUIClickEvents_FromInputsMousePenAndTouch(bool use Update(); if (!useProjectWideActionsAsset) { - InputSystem.s_Manager.actions = null; + InputSystem.manager.actions = null; } Update(); @@ -477,7 +477,7 @@ public void UIActionScroll_FiresUIScrollEvents_FromInputMouse(bool useProjectWid Update(); if (!useProjectWideActionsAsset) { - InputSystem.s_Manager.actions = null; + InputSystem.manager.actions = null; } Update(); @@ -532,7 +532,7 @@ public void UIActionScroll_ReceivesNormalizedScrollWheelDelta(float scrollWheelD public void DefaultActions_ShouldNotGenerateAnyVerificationWarnings(bool useProjectWideActions) { if (!useProjectWideActions) - InputSystem.s_Manager.actions = null; + InputSystem.manager.actions = null; Update(); LogAssert.NoUnexpectedReceived(); } @@ -545,7 +545,7 @@ public void ActionsWithoutUIMap_ShouldGenerateWarnings() var asset = ProjectWideActionsAsset.CreateDefaultAssetAtPath(kAssetPath); asset.RemoveActionMap(asset.FindActionMap("UI", throwIfNotFound: true)); - InputSystem.s_Manager.actions = asset; + InputSystem.manager.actions = asset; Update(); var link = EditorHelpers.GetHyperlink(kAssetPath); @@ -581,7 +581,7 @@ public void ActionMapWithNonExistentRequiredAction_ShouldGenerateWarning(string var action = asset.FindAction(actionPath); action.Rename("Other"); - InputSystem.s_Manager.actions = asset; + InputSystem.manager.actions = asset; Update(); //var link = AssetDatabase.GetAssetPath()//EditorHelpers.GetHyperlink(kAssetPath); @@ -624,7 +624,7 @@ public void ActionMapWithUnboundRequiredAction_ShouldGenerateWarning(string acti asset.AddActionMap(newMap); - InputSystem.s_Manager.actions = asset; + InputSystem.manager.actions = asset; Update(); LogAssert.Expect(LogType.Warning, new Regex($"^InputAction with path '{actionPath}' in asset \"{kAssetPath}\" do not have any configured bindings.")); @@ -649,7 +649,7 @@ public void ActionWithUnexpectedActionType_ShouldGenerateWarning(string actionPa var expectedType = action.type; action.m_Type = unexpectedType; // change directly via internals for now - InputSystem.s_Manager.actions = asset; + InputSystem.manager.actions = asset; Update(); LogAssert.Expect(LogType.Warning, @@ -675,7 +675,7 @@ public void ActionWithDifferentExpectedControlType_ShouldGenerateWarning(string var expectedControlType = action.expectedControlType; action.expectedControlType = unexpectedControlType; - InputSystem.s_Manager.actions = asset; + InputSystem.manager.actions = asset; Update(); LogAssert.Expect(LogType.Warning, diff --git a/Assets/Tests/InputSystem/Plugins/SteamTests.cs b/Assets/Tests/InputSystem/Plugins/SteamTests.cs index 09a1039bc1..2298dfc71a 100644 --- a/Assets/Tests/InputSystem/Plugins/SteamTests.cs +++ b/Assets/Tests/InputSystem/Plugins/SteamTests.cs @@ -36,13 +36,7 @@ public override void Setup() public override void TearDown() { base.TearDown(); - m_SteamAPI = null; - - SteamSupport.s_API = null; - SteamSupport.s_InputDevices = null; - SteamSupport.s_ConnectedControllers = null; - SteamSupport.s_InputDeviceCount = 0; - SteamSupport.s_HooksInstalled = false; + SteamSupport.Shutdown(); } [Test] diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionAsset.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionAsset.cs index 598ce1cd2b..23b7aafd57 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionAsset.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionAsset.cs @@ -869,7 +869,7 @@ IEnumerator IEnumerable.GetEnumerator() internal void MarkAsDirty() { #if UNITY_EDITOR - InputSystem.TrackDirtyInputActionAsset(this); + DirtyAssetTracker.TrackDirtyInputActionAsset(this); #endif } diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionMap.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionMap.cs index 8918757d89..3a81b55ab3 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionMap.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionMap.cs @@ -318,7 +318,8 @@ public event Action actionTriggered /// public InputActionMap() { - s_NeedToResolveBindings = true; + if (InputSystem.manager != null) + InputSystem.manager.bindingsNeedResolving = true; } /// @@ -810,9 +811,6 @@ private enum Flags BindingsForEachActionInitialized = 1 << 3, } - internal static int s_DeferBindingResolution; - internal static bool s_NeedToResolveBindings; - internal struct DeviceArray { private bool m_HaveValue; @@ -1195,9 +1193,6 @@ internal bool LazyResolveBindings(bool fullResolve) m_ControlsForEachAction = null; controlsForEachActionInitialized = false; - // Indicate that there is at least one action map that has a change - s_NeedToResolveBindings = true; - // If we haven't had to resolve bindings yet, we can wait until when we // actually have to. if (m_State == null) @@ -1211,7 +1206,10 @@ internal bool LazyResolveBindings(bool fullResolve) needToResolveBindings = true; bindingResolutionNeedsFullReResolve |= fullResolve; - if (s_DeferBindingResolution > 0) + // Indicate that there is at least one action map that has a change + InputSystem.manager.bindingsNeedResolving = true; + + if (InputSystem.manager.areDeferredBindingsToResolve) return false; // Have to do it straight away. @@ -1230,7 +1228,7 @@ internal bool ResolveBindingsIfNecessary() { if (m_State != null && m_State.isProcessingControlStateChange) { - Debug.Assert(s_DeferBindingResolution > 0, "While processing control state changes, binding resolution should be suppressed"); + Debug.Assert(InputSystem.manager.areDeferredBindingsToResolve, "While processing control state changes, binding resolution should be suppressed"); return false; } @@ -1988,7 +1986,8 @@ public void OnBeforeSerialize() public void OnAfterDeserialize() { // Indicate that there is at least one action map that has a change - s_NeedToResolveBindings = true; + if (InputSystem.manager != null) + InputSystem.manager.bindingsNeedResolving = true; m_State = null; m_MapIndexInState = InputActionState.kInvalidIndex; diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionRebindingExtensions.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionRebindingExtensions.cs index 23ca02c342..f677b4633e 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionRebindingExtensions.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionRebindingExtensions.cs @@ -2779,39 +2779,73 @@ public static RebindingOperation PerformInteractiveRebinding(this InputAction ac return rebind; } + internal static DeferBindingResolutionContext DeferBindingResolution() + { + return InputSystem.manager.DeferBindingResolution(); + } + } + + internal sealed class DeferBindingResolutionContext : IDisposable + { + public int deferredCount => m_DeferredCount; + + public void Acquire() + { + ++m_DeferredCount; + } + + public void Release() + { + if (m_DeferredCount > 0 && --m_DeferredCount == 0) + ExecuteDeferredResolutionOfBindings(); + } + /// - /// Temporarily suspend immediate re-resolution of bindings. + /// Allows usage within using() blocks, i.e. we need a "Release" method to match "Acquire", but we also want + /// to implement IDisposable so instance are automatically cleaned up when exiting a using() block. /// - /// - /// When changing control setups, it may take multiple steps to get to the final setup but each individual - /// step may trigger bindings to be resolved again in order to update controls on actions (see ). - /// Using this struct, this can be avoided and binding resolution can be deferred to after the whole operation - /// is complete and the final binding setup is in place. - /// - internal static DeferBindingResolutionWrapper DeferBindingResolution() + public void Dispose() { - if (s_DeferBindingResolutionWrapper == null) - s_DeferBindingResolutionWrapper = new DeferBindingResolutionWrapper(); - s_DeferBindingResolutionWrapper.Acquire(); - return s_DeferBindingResolutionWrapper; + Release(); } - private static DeferBindingResolutionWrapper s_DeferBindingResolutionWrapper; - - internal class DeferBindingResolutionWrapper : IDisposable + private void ExecuteDeferredResolutionOfBindings() { - public void Acquire() + ++m_DeferredCount; + try { - ++InputActionMap.s_DeferBindingResolution; - } + if (bindingsNeedResolving) + { + ref var globalList = ref InputActionState.s_GlobalState.globalList; - public void Dispose() + for (var i = 0; i < globalList.length; ++i) + { + var handle = globalList[i]; + + var state = handle.IsAllocated ? (InputActionState)handle.Target : null; + if (state == null) + { + // Stale entry in the list. State has already been reclaimed by GC. Remove it. + if (handle.IsAllocated) + globalList[i].Free(); + globalList.RemoveAtWithCapacity(i); + --i; + continue; + } + + for (var n = 0; n < state.totalMapCount; ++n) + state.maps[n].ResolveBindingsIfNecessary(); + } + bindingsNeedResolving = false; + } + } + finally { - if (InputActionMap.s_DeferBindingResolution > 0) - --InputActionMap.s_DeferBindingResolution; - if (InputActionMap.s_DeferBindingResolution == 0) - InputActionState.DeferredResolutionOfBindings(); + --m_DeferredCount; } } + + private int m_DeferredCount; + public bool bindingsNeedResolving; // TODO Revisit: Was previously static, but based on changes by Alex+Tim might no longer be needed?! } } diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs index cd17fdd942..cef41c7e19 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs @@ -1129,7 +1129,7 @@ private void EnableControls(int mapIndex, int controlStartIndex, int numControls "Control start index out of range"); Debug.Assert(controlStartIndex + numControls <= totalControlCount, "Control range out of bounds"); - var manager = InputSystem.s_Manager; + var manager = InputSystem.manager; for (var i = 0; i < numControls; ++i) { var controlIndex = controlStartIndex + i; @@ -1158,7 +1158,7 @@ private void DisableControls(int mapIndex, int controlStartIndex, int numControl "Control start index out of range"); Debug.Assert(controlStartIndex + numControls <= totalControlCount, "Control range out of bounds"); - var manager = InputSystem.s_Manager; + var manager = InputSystem.manager; for (var i = 0; i < numControls; ++i) { var controlIndex = controlStartIndex + i; @@ -1234,7 +1234,7 @@ private void HookOnBeforeUpdate() if (m_OnBeforeUpdateDelegate == null) m_OnBeforeUpdateDelegate = OnBeforeInitialUpdate; - InputSystem.s_Manager.onBeforeUpdate += m_OnBeforeUpdateDelegate; + InputSystem.manager.onBeforeUpdate += m_OnBeforeUpdateDelegate; m_OnBeforeUpdateHooked = true; } @@ -1243,7 +1243,7 @@ private void UnhookOnBeforeUpdate() if (!m_OnBeforeUpdateHooked) return; - InputSystem.s_Manager.onBeforeUpdate -= m_OnBeforeUpdateDelegate; + InputSystem.manager.onBeforeUpdate -= m_OnBeforeUpdateDelegate; m_OnBeforeUpdateHooked = false; } @@ -1276,7 +1276,7 @@ private void OnBeforeInitialUpdate() // Go through all binding states and for every binding that needs an initial state check, // go through all bound controls and for each one that isn't in its default state, pretend // that the control just got actuated. - var manager = InputSystem.s_Manager; + var manager = InputSystem.manager; for (var bindingIndex = 0; bindingIndex < totalBindingCount; ++bindingIndex) { ref var bindingState = ref bindingStates[bindingIndex]; @@ -2113,7 +2113,7 @@ internal void StartTimeout(float seconds, ref TriggerState trigger) Debug.Assert(trigger.controlIndex >= 0 && trigger.controlIndex < totalControlCount, "Control index out of range"); Debug.Assert(trigger.interactionIndex >= 0 && trigger.interactionIndex < totalInteractionCount, "Interaction index out of range"); - var manager = InputSystem.s_Manager; + var manager = InputSystem.manager; var currentTime = trigger.time; var control = controls[trigger.controlIndex]; var interactionIndex = trigger.interactionIndex; @@ -2142,7 +2142,7 @@ private void StopTimeout(int interactionIndex) ref var interactionState = ref interactionStates[interactionIndex]; - var manager = InputSystem.s_Manager; + var manager = InputSystem.manager; manager.RemoveStateChangeMonitorTimeout(this, interactionState.timerMonitorIndex, interactionIndex); // Update state. @@ -4266,6 +4266,13 @@ internal struct GlobalState internal static GlobalState s_GlobalState; + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void InitializeGlobalActionState() + { + ResetGlobals(); + s_GlobalState = default; + } + internal static ISavedState SaveAndResetState() { // Save current state @@ -4515,40 +4522,6 @@ internal static void OnDeviceChange(InputDevice device, InputDeviceChange change } } - internal static void DeferredResolutionOfBindings() - { - ++InputActionMap.s_DeferBindingResolution; - try - { - if (InputActionMap.s_NeedToResolveBindings) - { - for (var i = 0; i < s_GlobalState.globalList.length; ++i) - { - var handle = s_GlobalState.globalList[i]; - - var state = handle.IsAllocated ? (InputActionState)handle.Target : null; - if (state == null) - { - // Stale entry in the list. State has already been reclaimed by GC. Remove it. - if (handle.IsAllocated) - s_GlobalState.globalList[i].Free(); - s_GlobalState.globalList.RemoveAtWithCapacity(i); - --i; - continue; - } - - for (var n = 0; n < state.totalMapCount; ++n) - state.maps[n].ResolveBindingsIfNecessary(); - } - InputActionMap.s_NeedToResolveBindings = false; - } - } - finally - { - --InputActionMap.s_DeferBindingResolution; - } - } - internal static void DisableAllActions() { for (var i = 0; i < s_GlobalState.globalList.length; ++i) diff --git a/Packages/com.unity.inputsystem/InputSystem/Controls/InputControl.cs b/Packages/com.unity.inputsystem/InputSystem/Controls/InputControl.cs index 1f5ec6adec..462b6e5ae6 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Controls/InputControl.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Controls/InputControl.cs @@ -929,7 +929,7 @@ public void ApplyParameterChanges() private void SetOptimizedControlDataType() { // setting check need to be inline so we clear optimizations if setting is disabled after the fact - m_OptimizedControlDataType = InputSystem.s_Manager.optimizedControlsFeatureEnabled + m_OptimizedControlDataType = InputSystem.manager.optimizedControlsFeatureEnabled ? CalculateOptimizedControlDataType() : (FourCC)InputStateBlock.kFormatInvalid; } @@ -957,7 +957,7 @@ internal void SetOptimizedControlDataTypeRecursively() [Conditional("UNITY_EDITOR")] internal void EnsureOptimizationTypeHasNotChanged() { - if (!InputSystem.s_Manager.optimizedControlsFeatureEnabled) + if (!InputSystem.manager.optimizedControlsFeatureEnabled) return; var currentOptimizedControlDataType = CalculateOptimizedControlDataType(); @@ -1182,7 +1182,7 @@ public ref readonly TValue value if ( // if feature is disabled we re-evaluate every call - !InputSystem.s_Manager.readValueCachingFeatureEnabled + !InputSystem.manager.readValueCachingFeatureEnabled // if cached value is stale we re-evaluate and clear the flag || m_CachedValueIsStale // if a processor in stack needs to be re-evaluated, but unprocessedValue is still can be cached @@ -1193,7 +1193,7 @@ public ref readonly TValue value m_CachedValueIsStale = false; } #if DEBUG - else if (InputSystem.s_Manager.paranoidReadValueCachingChecksEnabled) + else if (InputSystem.manager.paranoidReadValueCachingChecksEnabled) { var oldUnprocessedValue = m_UnprocessedCachedValue; var newUnprocessedValue = unprocessedValue; @@ -1249,7 +1249,7 @@ internal unsafe ref readonly TValue unprocessedValue if ( // if feature is disabled we re-evaluate every call - !InputSystem.s_Manager.readValueCachingFeatureEnabled + !InputSystem.manager.readValueCachingFeatureEnabled // if cached value is stale we re-evaluate and clear the flag || m_UnprocessedCachedValueIsStale ) @@ -1258,7 +1258,7 @@ internal unsafe ref readonly TValue unprocessedValue m_UnprocessedCachedValueIsStale = false; } #if DEBUG - else if (InputSystem.s_Manager.paranoidReadValueCachingChecksEnabled) + else if (InputSystem.manager.paranoidReadValueCachingChecksEnabled) { var currentUnprocessedValue = ReadUnprocessedValueFromState(currentStatePtr); if (CompareValue(ref currentUnprocessedValue, ref m_UnprocessedCachedValue)) diff --git a/Packages/com.unity.inputsystem/InputSystem/Controls/InputControlLayout.cs b/Packages/com.unity.inputsystem/InputSystem/Controls/InputControlLayout.cs index 501378afdc..ad8923044f 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Controls/InputControlLayout.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Controls/InputControlLayout.cs @@ -109,7 +109,7 @@ public delegate string InputDeviceFindControlLayoutDelegate(ref InputDeviceDescr /// public class InputControlLayout { - private static InternedString s_DefaultVariant = new InternedString("Default"); + private static readonly InternedString s_DefaultVariant = new InternedString("Default"); public static InternedString DefaultVariant => s_DefaultVariant; public const string VariantSeparator = ";"; diff --git a/Packages/com.unity.inputsystem/InputSystem/Devices/InputDevice.cs b/Packages/com.unity.inputsystem/InputSystem/Devices/InputDevice.cs index f2b469bc73..01459b17b9 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Devices/InputDevice.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Devices/InputDevice.cs @@ -570,7 +570,7 @@ public unsafe long ExecuteCommand(ref TCommand command) var commandPtr = (InputDeviceCommand*)UnsafeUtility.AddressOf(ref command); // Give callbacks first shot. - var manager = InputSystem.s_Manager; + var manager = InputSystem.manager; manager.m_DeviceCommandCallbacks.LockForChanges(); for (var i = 0; i < manager.m_DeviceCommandCallbacks.length; ++i) { diff --git a/Packages/com.unity.inputsystem/InputSystem/Devices/Touchscreen.cs b/Packages/com.unity.inputsystem/InputSystem/Devices/Touchscreen.cs index 69735b1abe..532c1a4483 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Devices/Touchscreen.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Devices/Touchscreen.cs @@ -539,6 +539,14 @@ protected TouchControl[] touchControlArray /// Current touch screen. public new static Touchscreen current { get; internal set; } + /// + /// The current global settings for Touchscreen devices. + /// + /// + /// These are cached values taken from . + /// + internal static TouchscreenSettings settings { get; set; } + /// public override void MakeCurrent() { @@ -628,14 +636,14 @@ protected override void FinishSetup() // that to do so we would have to add another record to keep track of timestamps for each touch. And // since we know the maximum time that a tap can take, we have a reasonable estimate for when a prior // tap must have ended. - if (touchStatePtr->tapCount > 0 && InputState.currentTime >= touchStatePtr->startTime + s_TapTime + s_TapDelayTime) + if (touchStatePtr->tapCount > 0 && InputState.currentTime >= touchStatePtr->startTime + settings.tapTime + settings.tapDelayTime) InputState.Change(touches[i].tapCount, (byte)0); } var primaryTouchState = (TouchState*)((byte*)statePtr + stateBlock.byteOffset); if (primaryTouchState->delta != default) InputState.Change(primaryTouch.delta, Vector2.zero); - if (primaryTouchState->tapCount > 0 && InputState.currentTime >= primaryTouchState->startTime + s_TapTime + s_TapDelayTime) + if (primaryTouchState->tapCount > 0 && InputState.currentTime >= primaryTouchState->startTime + settings.tapTime + settings.tapDelayTime) InputState.Change(primaryTouch.tapCount, (byte)0); k_TouchscreenUpdateMarker.End(); @@ -724,11 +732,11 @@ protected override void FinishSetup() // Detect taps. var isTap = newTouchState.isNoneEndedOrCanceled && - (eventPtr.time - newTouchState.startTime) <= s_TapTime && + (eventPtr.time - newTouchState.startTime) <= settings.tapTime && ////REVIEW: this only takes the final delta to start position into account, not the delta over the lifetime of the //// touch; is this robust enough or do we need to make sure that we never move more than the tap radius //// over the entire lifetime of the touch? - (newTouchState.position - newTouchState.startPosition).sqrMagnitude <= s_TapRadiusSquared; + (newTouchState.position - newTouchState.startPosition).sqrMagnitude <= settings.tapRadiusSquared; if (isTap) newTouchState.tapCount = (byte)(currentTouchState[i].tapCount + 1); else @@ -1048,8 +1056,16 @@ private static void TriggerTap(TouchControl control, ref TouchState state, Input state.isTapRelease = false; } - internal static float s_TapTime; - internal static float s_TapDelayTime; - internal static float s_TapRadiusSquared; + private static TouchscreenSettings s_Settings; + } + + /// + /// Cached settings retrieved from . + /// + internal struct TouchscreenSettings + { + public float tapTime; + public float tapDelayTime; + public float tapRadiusSquared; } } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputActionsEditorSessionAnalytic.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputActionsEditorSessionAnalytic.cs index a966aa18a0..3b6686e078 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputActionsEditorSessionAnalytic.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputActionsEditorSessionAnalytic.cs @@ -223,7 +223,7 @@ private bool ImplicitFocus() private double m_FocusStart; private double m_SessionStart; - private static IInputRuntime runtime => InputSystem.s_Manager.m_Runtime; + private static IInputRuntime runtime => InputSystem.manager.runtime; private bool hasFocus => !double.IsNaN(m_FocusStart); private bool hasSession => !double.IsNaN(m_SessionStart); // Returns current time since startup. Note that IInputRuntime explicitly defines in interface that diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputBuildAnalytic.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputBuildAnalytic.cs index 40bb709409..09bb5d8ffb 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputBuildAnalytic.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Analytics/InputBuildAnalytic.cs @@ -392,7 +392,7 @@ internal class ReportProcessor : IPostprocessBuildWithReport public void OnPostprocessBuild(BuildReport report) { - InputSystem.s_Manager?.m_Runtime?.SendAnalytic(new InputBuildAnalytic(report)); + InputSystem.manager?.runtime?.SendAnalytic(new InputBuildAnalytic(report)); } } } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetEditor/InputActionPropertiesView.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetEditor/InputActionPropertiesView.cs index 093283868f..d6bf5e4759 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetEditor/InputActionPropertiesView.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetEditor/InputActionPropertiesView.cs @@ -81,7 +81,7 @@ protected override void DrawGeneralProperties() private void BuildControlTypeList() { var types = new List(); - var allLayouts = InputSystem.s_Manager.m_Layouts; + var allLayouts = InputSystem.manager.m_Layouts; foreach (var layoutName in allLayouts.layoutTypes.Keys) { if (EditorInputControlLayoutCache.TryGetLayout(layoutName).hideInUI) diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/ControlPicker/InputControlPickerDropdown.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/ControlPicker/InputControlPickerDropdown.cs index 6609f622f5..4468acd77a 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/ControlPicker/InputControlPickerDropdown.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/ControlPicker/InputControlPickerDropdown.cs @@ -47,7 +47,7 @@ public void SetExpectedControlLayout(string expectedControlLayout) m_ExpectedControlType = typeof(InputDevice); else m_ExpectedControlType = !string.IsNullOrEmpty(expectedControlLayout) - ? InputSystem.s_Manager.m_Layouts.GetControlTypeForLayout(new InternedString(expectedControlLayout)) + ? InputSystem.manager.m_Layouts.GetControlTypeForLayout(new InternedString(expectedControlLayout)) : null; // If the layout is for a device, automatically switch to device @@ -421,7 +421,7 @@ private bool LayoutMatchesExpectedControlLayoutFilter(string layout) if (m_ExpectedControlType == null) return true; - var layoutType = InputSystem.s_Manager.m_Layouts.GetControlTypeForLayout(new InternedString(layout)); + var layoutType = InputSystem.manager.m_Layouts.GetControlTypeForLayout(new InternedString(layout)); return m_ExpectedControlType.IsAssignableFrom(layoutType); } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDebuggerWindow.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDebuggerWindow.cs index 6086f7d605..a98a3dd7a6 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDebuggerWindow.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDebuggerWindow.cs @@ -212,9 +212,9 @@ private static void ResetDevice(InputDevice device, bool hard) { var playerUpdateType = InputDeviceDebuggerWindow.DetermineUpdateTypeToShow(device); var currentUpdateType = InputState.currentUpdateType; - InputStateBuffers.SwitchTo(InputSystem.s_Manager.m_StateBuffers, playerUpdateType); + InputStateBuffers.SwitchTo(InputSystem.manager.m_StateBuffers, playerUpdateType); InputSystem.ResetDevice(device, alsoResetDontResetControls: hard); - InputStateBuffers.SwitchTo(InputSystem.s_Manager.m_StateBuffers, currentUpdateType); + InputStateBuffers.SwitchTo(InputSystem.manager.m_StateBuffers, currentUpdateType); } private static void ToggleAddDevicesNotSupportedByProject() @@ -225,15 +225,15 @@ private static void ToggleAddDevicesNotSupportedByProject() private void ToggleDiagnosticMode() { - if (InputSystem.s_Manager.m_Diagnostics != null) + if (InputSystem.manager.m_Diagnostics != null) { - InputSystem.s_Manager.m_Diagnostics = null; + InputSystem.manager.m_Diagnostics = null; } else { if (m_Diagnostics == null) m_Diagnostics = new InputDiagnostics(); - InputSystem.s_Manager.m_Diagnostics = m_Diagnostics; + InputSystem.manager.m_Diagnostics = m_Diagnostics; } } @@ -311,7 +311,7 @@ private void DrawToolbarGUI() menu.AddItem(Contents.addDevicesNotSupportedByProjectContent, InputEditorUserSettings.addDevicesNotSupportedByProject, ToggleAddDevicesNotSupportedByProject); - menu.AddItem(Contents.diagnosticsModeContent, InputSystem.s_Manager.m_Diagnostics != null, + menu.AddItem(Contents.diagnosticsModeContent, InputSystem.manager.m_Diagnostics != null, ToggleDiagnosticMode); menu.AddItem(Contents.touchSimulationContent, InputEditorUserSettings.simulateTouch, ToggleTouchSimulation); diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDeviceDebuggerWindow.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDeviceDebuggerWindow.cs index bda67adbf8..b79720417b 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDeviceDebuggerWindow.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDeviceDebuggerWindow.cs @@ -358,9 +358,9 @@ private void RefreshControlTreeValues() m_InputUpdateTypeShownInControlTree = DetermineUpdateTypeToShow(m_Device); var currentUpdateType = InputState.currentUpdateType; - InputStateBuffers.SwitchTo(InputSystem.s_Manager.m_StateBuffers, m_InputUpdateTypeShownInControlTree); + InputStateBuffers.SwitchTo(InputSystem.manager.m_StateBuffers, m_InputUpdateTypeShownInControlTree); m_ControlTree.RefreshControlValues(); - InputStateBuffers.SwitchTo(InputSystem.s_Manager.m_StateBuffers, currentUpdateType); + InputStateBuffers.SwitchTo(InputSystem.manager.m_StateBuffers, currentUpdateType); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "device", Justification = "Keep this for future implementation")] diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/EditorInputControlLayoutCache.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/EditorInputControlLayoutCache.cs index 81dac4b311..7800d5ea06 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/EditorInputControlLayoutCache.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/EditorInputControlLayoutCache.cs @@ -246,7 +246,7 @@ internal static void Clear() // If our layout data is outdated, rescan all the layouts in the system. private static void Refresh() { - var manager = InputSystem.s_Manager; + var manager = InputSystem.manager; if (manager.m_LayoutRegistrationVersion == s_LayoutRegistrationVersion) return; diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/InputStateWindow.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/InputStateWindow.cs index 8376792d48..5f996a3610 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/InputStateWindow.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/InputStateWindow.cs @@ -152,7 +152,7 @@ private unsafe void PollBuffersFromControl(InputControl control, bool selectBuff private static unsafe void* TryGetDeviceState(InputDevice device, BufferSelector selector) { - var manager = InputSystem.s_Manager; + var manager = InputSystem.manager; var deviceIndex = device.m_DeviceIndex; switch (selector) diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputEditorUserSettings.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputEditorUserSettings.cs index d2d3527cdb..108f3a36d1 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputEditorUserSettings.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputEditorUserSettings.cs @@ -87,7 +87,7 @@ internal struct SerializedState private static void OnChange() { Save(); - InputSystem.s_Manager.ApplySettings(); + InputSystem.manager.ApplySettings(); } internal static void Load() diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Views/Selectors.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Views/Selectors.cs index 90ecf92f5c..3559418da1 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Views/Selectors.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Views/Selectors.cs @@ -277,7 +277,7 @@ public static IEnumerable GetCompositePartOptions(string bindingName, st public static IEnumerable BuildControlTypeList(InputActionType selectedActionType) { - var allLayouts = InputSystem.s_Manager.m_Layouts; + var allLayouts = InputSystem.manager.m_Layouts; // "Any" is always in first position (index 0) yield return "Any"; diff --git a/Packages/com.unity.inputsystem/InputSystem/Events/InputEventListener.cs b/Packages/com.unity.inputsystem/InputSystem/Events/InputEventListener.cs index 6ffb0bbfdd..d9f1354738 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Events/InputEventListener.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Events/InputEventListener.cs @@ -57,8 +57,8 @@ public struct InputEventListener : IObservable { if (callback == null) throw new ArgumentNullException(nameof(callback)); - lock (InputSystem.s_Manager) - InputSystem.s_Manager.onEvent += callback; + lock (InputSystem.manager) + InputSystem.manager.onEvent += callback; return default; } @@ -82,8 +82,8 @@ public struct InputEventListener : IObservable { if (callback == null) throw new ArgumentNullException(nameof(callback)); - lock (InputSystem.s_Manager) - InputSystem.s_Manager.onEvent -= callback; + lock (InputSystem.manager) + InputSystem.manager.onEvent -= callback; return default; } @@ -110,7 +110,7 @@ public IDisposable Subscribe(IObserver observer) s_ObserverState = new ObserverState(); if (s_ObserverState.observers.length == 0) - InputSystem.s_Manager.onEvent += s_ObserverState.onEventDelegate; + InputSystem.manager.onEvent += s_ObserverState.onEventDelegate; s_ObserverState.observers.AppendWithCapacity(observer); return new DisposableObserver { observer = observer }; @@ -142,7 +142,7 @@ public void Dispose() if (index >= 0) s_ObserverState.observers.RemoveAtWithCapacity(index); if (s_ObserverState.observers.length == 0) - InputSystem.s_Manager.onEvent -= s_ObserverState.onEventDelegate; + InputSystem.manager.onEvent -= s_ObserverState.onEventDelegate; } } } diff --git a/Packages/com.unity.inputsystem/InputSystem/InputAnalytics.cs b/Packages/com.unity.inputsystem/InputSystem/InputAnalytics.cs index 49027e7e2a..f1adaf9213 100644 --- a/Packages/com.unity.inputsystem/InputSystem/InputAnalytics.cs +++ b/Packages/com.unity.inputsystem/InputSystem/InputAnalytics.cs @@ -55,17 +55,17 @@ public interface IInputAnalytic public static void Initialize(InputManager manager) { - Debug.Assert(manager.m_Runtime != null); + Debug.Assert(manager.runtime != null); } public static void OnStartup(InputManager manager) { - manager.m_Runtime.SendAnalytic(new StartupEventAnalytic(manager)); + manager.runtime.SendAnalytic(new StartupEventAnalytic(manager)); } public static void OnShutdown(InputManager manager) { - manager.m_Runtime.SendAnalytic(new ShutdownEventDataAnalytic(manager)); + manager.runtime.SendAnalytic(new ShutdownEventDataAnalytic(manager)); } /// @@ -278,7 +278,7 @@ internal static class AnalyticExtensions { internal static void Send(this TSource analytic) where TSource : InputAnalytics.IInputAnalytic { - InputSystem.s_Manager?.m_Runtime?.SendAnalytic(analytic); + InputSystem.manager?.runtime?.SendAnalytic(analytic); } } } diff --git a/Packages/com.unity.inputsystem/InputSystem/InputManager.cs b/Packages/com.unity.inputsystem/InputSystem/InputManager.cs index 10e3c17fb8..2f152695cc 100644 --- a/Packages/com.unity.inputsystem/InputSystem/InputManager.cs +++ b/Packages/com.unity.inputsystem/InputSystem/InputManager.cs @@ -18,6 +18,8 @@ #if UNITY_EDITOR using UnityEngine.InputSystem.Editor; +using UnityEngine.Tilemaps; + #endif #if UNITY_EDITOR @@ -57,13 +59,92 @@ namespace UnityEngine.InputSystem /// /// Manages devices, layouts, and event processing. /// - internal partial class InputManager + internal partial class InputManager : IDisposable { + private InputManager() {} + + public static InputManager CreateAndInitialize(IInputRuntime runtime, InputSettings settings, bool fakeManagerForRemotingTests = false) + { + var newInstance = new InputManager(); + + // Not directly used by InputManager, but we need a single instance that's used in a variety of places without a static field + newInstance.m_DeferBindingResolutionContext = new DeferBindingResolutionContext(); + + // If settings object wasn't provided, create a temporary settings object for now + if (settings == null) + { + settings = ScriptableObject.CreateInstance(); + settings.hideFlags = HideFlags.HideAndDontSave; + } + newInstance.m_Settings = settings; + + #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + newInstance.InitializeActions(); + #endif // UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + + newInstance.InitializeData(); + newInstance.InstallRuntime(runtime); + + // For remoting tests, we need to create a "fake manager" that simulates a remote endpoint. + // In this case don't install globals as this will corrupt the "local" manager state. + if (!fakeManagerForRemotingTests) + newInstance.InstallGlobals(); + + newInstance.ApplySettings(); + + #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + newInstance.ApplyActions(); + #endif + + newInstance.bindingsNeedResolving = true; + return newInstance; + } + + #region Dispose implementation + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // Notify devices are being removed but don't actually removed them; no point when disposing + for (var i = 0; i < m_DevicesCount; ++i) + m_Devices[i].NotifyRemoved(); + + m_StateBuffers.FreeAll(); + UninstallGlobals(); + + // If we're still holding the "temporary" settings object make sure to delete it + if (m_Settings != null && m_Settings.hideFlags == HideFlags.HideAndDontSave) + Object.DestroyImmediate(m_Settings); + + // Project-wide Actions are never temporary so we do not destroy them. + } + + disposedValue = true; + } + } + + ~InputManager() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private bool disposedValue; + #endregion + public ReadOnlyArray devices => new ReadOnlyArray(m_Devices, 0, m_DevicesCount); public TypeTable processors => m_Processors; public TypeTable interactions => m_Interactions; public TypeTable composites => m_Composites; + internal IInputRuntime runtime => m_Runtime; static readonly ProfilerMarker k_InputUpdateProfilerMarker = new ProfilerMarker("InputUpdate"); static readonly ProfilerMarker k_InputTryFindMatchingControllerMarker = new ProfilerMarker("InputSystem.TryFindMatchingControlLayout"); @@ -109,7 +190,6 @@ public InputSettings settings { get { - Debug.Assert(m_Settings != null); return m_Settings; } set @@ -117,6 +197,10 @@ public InputSettings settings if (value == null) throw new ArgumentNullException(nameof(value)); + // Delete the "temporary" settings if necessary + if (m_Settings != null && m_Settings.hideFlags == HideFlags.HideAndDontSave) + ScriptableObject.DestroyImmediate(m_Settings); + if (m_Settings == value) return; @@ -341,9 +425,6 @@ internal bool ShouldDrawWarningIconForBinding(string bindingPath) "InputSystem.ShouldDrawWarningIconForBinding"); } -#endif // UNITY_EDITOR - -#if UNITY_EDITOR private bool m_RunPlayerUpdatesInEditMode; /// @@ -358,7 +439,8 @@ public bool runPlayerUpdatesInEditMode get => m_RunPlayerUpdatesInEditMode; set => m_RunPlayerUpdatesInEditMode = value; } -#endif + +#endif // UNITY_EDITOR private bool gameIsPlaying => #if UNITY_EDITOR @@ -1803,55 +1885,15 @@ public void Update(InputUpdateType updateType) m_Runtime.Update(updateType); } - internal void Initialize(IInputRuntime runtime, InputSettings settings) - { - Debug.Assert(settings != null); - - m_Settings = settings; - -#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS - InitializeActions(); -#endif // UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS - InitializeData(); - InstallRuntime(runtime); - InstallGlobals(); - - ApplySettings(); - #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS - ApplyActions(); - #endif - } - - internal void Destroy() - { - // There isn't really much of a point in removing devices but we still - // want to clear out any global state they may be keeping. So just tell - // the devices that they got removed without actually removing them. - for (var i = 0; i < m_DevicesCount; ++i) - m_Devices[i].NotifyRemoved(); - - // Free all state memory. - m_StateBuffers.FreeAll(); - - // Uninstall globals. - UninstallGlobals(); - - // Destroy settings if they are temporary. - if (m_Settings != null && m_Settings.hideFlags == HideFlags.HideAndDontSave) - Object.DestroyImmediate(m_Settings); - - // Project-wide Actions are never temporary so we do not destroy them. - } - -#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS // Initialize project-wide actions: // - In editor (edit mode or play-mode) we always use the editor build preferences persisted setting. // - In player build we always attempt to find a preloaded asset. private void InitializeActions() { -#if UNITY_EDITOR + #if UNITY_EDITOR m_Actions = ProjectWideActionsBuildProvider.actionsToIncludeInPlayerBuild; -#else + #else m_Actions = null; var candidates = Resources.FindObjectsOfTypeAll(); foreach (var candidate in candidates) @@ -1862,10 +1904,10 @@ private void InitializeActions() break; } } -#endif // UNITY_EDITOR + #endif // UNITY_EDITOR } -#endif // UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + #endif // UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS internal void InitializeData() { @@ -1881,10 +1923,10 @@ internal void InitializeData() // can manually turn off one of them to optimize operation. m_UpdateMask = InputUpdateType.Dynamic | InputUpdateType.Fixed; m_HasFocus = Application.isFocused; -#if UNITY_EDITOR + #if UNITY_EDITOR m_EditorIsActive = true; m_UpdateMask |= InputUpdateType.Editor; -#endif + #endif m_ScrollDeltaBehavior = InputSettings.ScrollDeltaBehavior.UniformAcrossAllPlatforms; @@ -1992,15 +2034,15 @@ void RegisterCustomTypes(Type[] types) continue; if (typeof(InputProcessor).IsAssignableFrom(type)) { - InputSystem.RegisterProcessor(type); + RegisterProcessor(type); } else if (typeof(IInputInteraction).IsAssignableFrom(type)) { - InputSystem.RegisterInteraction(type); + RegisterInteraction(type); } else if (typeof(InputBindingComposite).IsAssignableFrom(type)) { - InputSystem.RegisterBindingComposite(type, null); + RegisterBindingComposite(type, null); } } } @@ -2038,6 +2080,47 @@ void RegisterCustomTypes() k_InputRegisterCustomTypesMarker.End(); } + private static string GetRegisteredTypeDefaultName(Type type, string name, string suffix) + { + // Default name to name of type without suffix. + if (!string.IsNullOrEmpty(name)) + return name; + name = type.Name; + if (name.EndsWith(suffix)) + name = name.Substring(0, name.Length - suffix.Length); + return name; + } + + internal void RegisterProcessor(Type type, string name = null) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + name = GetRegisteredTypeDefaultName(type, name, "Processor"); + + // Flush out any precompiled layout depending on the processor. + var precompiledLayouts = m_Layouts.precompiledLayouts; + foreach (var key in new List(precompiledLayouts.Keys)) // Need to keep key list stable while iterating; ToList() for some reason not available with .NET Standard 2.0 on Mono. + { + if (StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(precompiledLayouts[key].metadata, name, ';')) + m_Layouts.precompiledLayouts.Remove(key); + } + + processors.AddTypeRegistration(name, type); + } + + public void RegisterInteraction(Type type, string name = null) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + name = GetRegisteredTypeDefaultName(type, name, "Interaction"); + interactions.AddTypeRegistration(name, type); + } + + public void RegisterBindingComposite(Type type, string name = null) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + name = GetRegisteredTypeDefaultName(type, name, "Composite"); + composites.AddTypeRegistration(name, type); + } + internal void InstallRuntime(IInputRuntime runtime) { if (m_Runtime != null) @@ -2130,6 +2213,30 @@ internal void UninstallGlobals() } } + /// + /// Acquires a temporary "lock" to suspend immediate re-resolution of bindings. + /// + /// + /// When changing control setups, it may take multiple steps to get to the final setup but each individual + /// step may trigger bindings to be resolved again in order to update controls on actions (see ). + /// Using Acquire/Release semantics via the returned context object, binding resolution can be deferred until the entire operation + /// is complete and the final binding setup is in place. + /// + /// NOTE: Returned DeferBindingResolutionContext object is used globally for all ActionMaps. + /// + internal DeferBindingResolutionContext DeferBindingResolution() + { + m_DeferBindingResolutionContext.Acquire(); + return m_DeferBindingResolutionContext; + } + + internal bool areDeferredBindingsToResolve => m_DeferBindingResolutionContext.deferredCount > 0; + public bool bindingsNeedResolving + { + get => m_DeferBindingResolutionContext.bindingsNeedResolving; + set => m_DeferBindingResolutionContext.bindingsNeedResolving = value; + } + [Serializable] internal struct AvailableDevice { @@ -2210,9 +2317,9 @@ internal struct AvailableDevice private bool m_HaveSentStartupAnalytics; #endif - internal IInputRuntime m_Runtime; - internal InputMetrics m_Metrics; - internal InputSettings m_Settings; + private IInputRuntime m_Runtime; + private InputMetrics m_Metrics; + private InputSettings m_Settings; // Extract as booleans (from m_Settings) because feature check is in the hot path @@ -2248,6 +2355,8 @@ internal bool paranoidReadValueCachingChecksEnabled internal IInputDiagnostics m_Diagnostics; #endif + private DeferBindingResolutionContext m_DeferBindingResolutionContext; + ////REVIEW: Make it so that device names *always* have a number appended? (i.e. Gamepad1, Gamepad2, etc. instead of Gamepad, Gamepad1, etc) private void MakeDeviceNameUnique(InputDevice device) @@ -2633,13 +2742,13 @@ private void InstallBeforeUpdateHookIfNecessary() private void RestoreDevicesAfterDomainReloadIfNecessary() { - #if UNITY_EDITOR + #if UNITY_EDITOR && !ENABLE_CORECLR if (m_SavedDeviceStates != null) RestoreDevicesAfterDomainReload(); #endif } -#if UNITY_EDITOR + #if UNITY_EDITOR private void SyncAllDevicesWhenEditorIsActivated() { var isActive = m_Runtime.isEditorActive; @@ -2672,7 +2781,7 @@ internal void SyncAllDevicesAfterEnteringPlayMode() SyncAllDevices(); } -#endif + #endif // UNITY_EDITOR private void WarnAboutDevicesFailingToRecreateAfterDomainReload() { @@ -2692,7 +2801,7 @@ private void WarnAboutDevicesFailingToRecreateAfterDomainReload() // At this point, we throw the device states away and forget about // what we had before the domain reload. m_SavedDeviceStates = null; - #endif + #endif // UNITY_EDITOR } private void OnBeforeUpdate(InputUpdateType updateType) @@ -2736,6 +2845,8 @@ private void OnBeforeUpdate(InputUpdateType updateType) /// internal void ApplySettings() { + Debug.Assert(m_Settings != null); + // Sync update mask. var newUpdateMask = InputUpdateType.Editor; if ((m_UpdateMask & InputUpdateType.BeforeRender) != 0) @@ -2825,10 +2936,14 @@ internal void ApplySettings() m_ParanoidReadValueCachingChecksEnabled = m_Settings.IsFeatureEnabled((InputFeatureNames.kParanoidReadValueCachingChecks)); } - // Cache some values. - Touchscreen.s_TapTime = settings.defaultTapTime; - Touchscreen.s_TapDelayTime = settings.multiTapDelayTime; - Touchscreen.s_TapRadiusSquared = settings.tapRadius * settings.tapRadius; + // Cache Touch specific settings to Touchscreen + Touchscreen.settings = new TouchscreenSettings + { + tapTime = settings.defaultTapTime, + tapDelayTime = settings.multiTapDelayTime, + tapRadiusSquared = settings.tapRadius * settings.tapRadius + }; + // Extra clamp here as we can't tell what we're getting from serialized data. ButtonControl.s_GlobalDefaultButtonPressPoint = Mathf.Clamp(settings.defaultButtonPressPoint, ButtonControl.kMinButtonPressPoint, float.MaxValue); ButtonControl.s_GlobalDefaultButtonReleaseThreshold = settings.buttonReleaseThreshold; @@ -3002,7 +3117,7 @@ internal void OnFocusChanged(bool focus) m_HasFocus = focus; } -#if UNITY_EDITOR + #if UNITY_EDITOR internal void LeavePlayMode() { // Reenable all devices and reset their play mode state. @@ -3030,7 +3145,7 @@ private void OnPlayerLoopInitialization() InputStateBuffers.SwitchTo(m_StateBuffers, InputUpdate.s_LatestUpdateType); } -#endif + #endif // UNITY_EDITOR internal bool ShouldRunUpdate(InputUpdateType updateType) { @@ -3041,7 +3156,7 @@ internal bool ShouldRunUpdate(InputUpdateType updateType) var mask = m_UpdateMask; -#if UNITY_EDITOR + #if UNITY_EDITOR // If the player isn't running, the only thing we run is editor updates, except if // explicitly overriden via `runUpdatesInEditMode`. // NOTE: This means that in edit mode (outside of play mode) we *never* switch to player @@ -3050,7 +3165,7 @@ internal bool ShouldRunUpdate(InputUpdateType updateType) // it will see gamepad inputs going to the editor and respond to them. if (!gameIsPlaying && updateType != InputUpdateType.Editor && !runPlayerUpdatesInEditMode) return false; -#endif + #endif // UNITY_EDITOR return (updateType & mask) != 0; } @@ -3148,21 +3263,21 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev // Figure out if we can just flush the buffer and early out. var canFlushBuffer = false -#if UNITY_EDITOR + #if UNITY_EDITOR // If out of focus and runInBackground is off and ExactlyAsInPlayer is on, discard input. || (!gameHasFocus && m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView && (!m_Runtime.runInBackground || m_Settings.backgroundBehavior == InputSettings.BackgroundBehavior.ResetAndDisableAllDevices)) -#else + #else || (!gameHasFocus && !m_Runtime.runInBackground) -#endif + #endif ; var canEarlyOut = // Early out if there's no events to process. eventBuffer.eventCount == 0 || canFlushBuffer -#if UNITY_EDITOR + #if UNITY_EDITOR // If we're in the background and not supposed to process events in this update (but somehow // still ended up here), we're done. || ((!gameHasFocus || gameShouldGetInputRegardlessOfFocus) && @@ -3173,11 +3288,11 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev // When the game is playing and has focus, we never process input in editor updates. All we // do is just switch to editor state buffers and then exit. || (gameIsPlaying && gameHasFocus && updateType == InputUpdateType.Editor)) -#endif + #endif ; -#if UNITY_EDITOR + #if UNITY_EDITOR var dropStatusEvents = false; if (!gameIsPlaying && gameShouldGetInputRegardlessOfFocus && (eventBuffer.sizeInBytes > (100 * 1024))) { @@ -3186,7 +3301,7 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev canEarlyOut = false; dropStatusEvents = true; } -#endif + #endif if (canEarlyOut) { @@ -3250,7 +3365,7 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev var currentEventTimeInternal = currentEventReadPtr->internalTime; var currentEventType = currentEventReadPtr->type; -#if UNITY_EDITOR + #if UNITY_EDITOR if (dropStatusEvents) { // If the type here is a status event, ask advance not to leave the event in the buffer. Otherwise, leave it there. @@ -3261,7 +3376,7 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev continue; } -#endif + #endif // In the editor, we discard all input events that occur in-between exiting edit mode and having // entered play mode as otherwise we'll spill a bunch of UI events that have occurred while the @@ -3272,19 +3387,19 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev // here such as throwing partial touches away and then letting the rest of a touch go through. // Could be that ultimately we need to issue a full reset of all devices at the beginning of // play mode in the editor. -#if UNITY_EDITOR + #if UNITY_EDITOR if ((currentEventType == StateEvent.Type || currentEventType == DeltaStateEvent.Type) && (updateType & InputUpdateType.Editor) == 0 && - InputSystem.s_SystemObject.exitEditModeTime > 0 && - currentEventTimeInternal >= InputSystem.s_SystemObject.exitEditModeTime && - (currentEventTimeInternal < InputSystem.s_SystemObject.enterPlayModeTime || - InputSystem.s_SystemObject.enterPlayModeTime == 0)) + InputSystem.domainStateManager.exitEditModeTime > 0 && + currentEventTimeInternal >= InputSystem.domainStateManager.exitEditModeTime && + (currentEventTimeInternal < InputSystem.domainStateManager.enterPlayModeTime || + InputSystem.domainStateManager.enterPlayModeTime == 0)) { m_InputEventStream.Advance(false); continue; } -#endif + #endif // If we're timeslicing, check if the event time is within limits. if (timesliceEvents && currentEventTimeInternal >= currentTime) @@ -3298,10 +3413,10 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev device = TryGetDeviceById(currentEventReadPtr->deviceId); if (device == null) { -#if UNITY_EDITOR + #if UNITY_EDITOR ////TODO: see if this is a device we haven't created and if so, just ignore m_Diagnostics?.OnCannotFindDeviceForEvent(new InputEventPtr(currentEventReadPtr)); -#endif + #endif m_InputEventStream.Advance(false); continue; @@ -3309,7 +3424,7 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev // In the editor, we may need to bump events from editor updates into player updates // and vice versa. -#if UNITY_EDITOR + #if UNITY_EDITOR if (isPlaying && !gameHasFocus) { if (m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode @@ -3340,7 +3455,7 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev } } } -#endif + #endif // UNITY_EDITOR // If device is disabled, we let the event through only in certain cases. // Removal and configuration change events should always be processed. @@ -3350,12 +3465,12 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev (device.m_DeviceFlags & (InputDevice.DeviceFlags.DisabledInRuntime | InputDevice.DeviceFlags.DisabledWhileInBackground)) != 0) { -#if UNITY_EDITOR + #if UNITY_EDITOR // If the device is disabled in the backend, getting events for them // is something that indicates a problem in the backend so diagnose. if ((device.m_DeviceFlags & InputDevice.DeviceFlags.DisabledInRuntime) != 0) m_Diagnostics?.OnEventForDisabledDevice(currentEventReadPtr, device); -#endif + #endif m_InputEventStream.Advance(false); continue; @@ -3427,17 +3542,17 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev // Give the device a chance to do something with data before we propagate it to event listeners. if (device.hasEventPreProcessor) { -#if UNITY_EDITOR + #if UNITY_EDITOR var eventSizeBeforePreProcessor = currentEventReadPtr->sizeInBytes; -#endif + #endif var shouldProcess = ((IEventPreProcessor)device).PreProcessEvent(currentEventReadPtr); -#if UNITY_EDITOR + #if UNITY_EDITOR if (currentEventReadPtr->sizeInBytes > eventSizeBeforePreProcessor) { k_InputUpdateProfilerMarker.End(); throw new AccessViolationException($"'{device}'.PreProcessEvent tries to grow an event from {eventSizeBeforePreProcessor} bytes to {currentEventReadPtr->sizeInBytes} bytes, this will potentially corrupt events after the current event and/or cause out-of-bounds memory access."); } -#endif + #endif if (!shouldProcess) { // Skip event if PreProcessEvent considers it to be irrelevant. @@ -3489,7 +3604,7 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev if (currentEventTimeInternal < device.m_LastUpdateTimeInternal && !(deviceIsStateCallbackReceiver && device.stateBlock.format != eventPtr.stateFormat)) { -#if UNITY_EDITOR + #if UNITY_EDITOR m_Diagnostics?.OnEventTimestampOutdated(new InputEventPtr(currentEventReadPtr), device); #elif UNITY_ANDROID // Android keyboards can send events out of order: Holding down a key will send multiple @@ -3520,9 +3635,9 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev // If the state format doesn't match, ignore the event. if (device.stateBlock.format != eventPtr.stateFormat) { -#if UNITY_EDITOR + #if UNITY_EDITOR m_Diagnostics?.OnEventFormatMismatch(currentEventReadPtr, device); -#endif + #endif break; } @@ -3538,9 +3653,9 @@ private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer ev // Only events should. If running play mode updates in editor, we want to defer to the play mode // callbacks to set the last update time to avoid dropping events only processed by the editor state. if (device.m_LastUpdateTimeInternal <= eventPtr.internalTime -#if UNITY_EDITOR + #if UNITY_EDITOR && !(updateType == InputUpdateType.Editor && runPlayerUpdatesInEditMode) -#endif + #endif ) device.m_LastUpdateTimeInternal = eventPtr.internalTime; @@ -3705,7 +3820,7 @@ private void ResetCurrentProcessedEventBytesForDevices() [Conditional("UNITY_EDITOR")] void CheckAllDevicesOptimizedControlsHaveValidState() { - if (!InputSystem.s_Manager.m_OptimizedControlsFeatureEnabled) + if (!InputSystem.manager.m_OptimizedControlsFeatureEnabled) return; foreach (var device in devices) @@ -3862,7 +3977,7 @@ internal unsafe bool UpdateState(InputDevice device, InputUpdateType updateType, { // Update the pressed/not pressed state of all buttons that have changed this update // With enough ButtonControls being checked, it's faster to find out which have actually changed rather than test all. - if (InputSystem.s_Manager.m_ReadValueCachingFeatureEnabled || device.m_UseCachePathForButtonPresses) + if (InputSystem.manager.m_ReadValueCachingFeatureEnabled || device.m_UseCachePathForButtonPresses) { foreach (var button in device.m_UpdatedButtons) { @@ -3939,7 +4054,7 @@ private unsafe void WriteStateChange(InputStateBuffers.DoubleBuffers buffers, in // If we have enough ButtonControls being checked for wasPressedThisFrame/wasReleasedThisFrame, // use this path to find out which have actually changed here. - if (InputSystem.s_Manager.m_ReadValueCachingFeatureEnabled || m_Devices[deviceIndex].m_UseCachePathForButtonPresses) + if (InputSystem.manager.m_ReadValueCachingFeatureEnabled || m_Devices[deviceIndex].m_UseCachePathForButtonPresses) { // if the buffers have just been flipped, and we're doing a full state update, then the state from the // previous update is now in the back buffer, and we should be comparing to that when checking what @@ -3969,7 +4084,7 @@ private bool FlipBuffersForDeviceIfNecessary(InputDevice device, InputUpdateType return false; } -#if UNITY_EDITOR + #if UNITY_EDITOR ////REVIEW: should this use the editor update ticks as quasi-frame-boundaries? // Updates go to the editor only if the game isn't playing or does not have focus. // Otherwise we fall through to the logic that flips for the *next* dynamic and @@ -3983,7 +4098,7 @@ private bool FlipBuffersForDeviceIfNecessary(InputDevice device, InputUpdateType m_StateBuffers.m_EditorStateBuffers.SwapBuffers(device.m_DeviceIndex); return true; } -#endif + #endif // Flip buffers if we haven't already for this frame. if (device.m_CurrentUpdateStepCount != InputUpdate.s_UpdateStepCount) @@ -4001,7 +4116,7 @@ private bool FlipBuffersForDeviceIfNecessary(InputDevice device, InputUpdateType // Stuff everything that we want to survive a domain reload into // a m_SerializedState. -#if UNITY_EDITOR || DEVELOPMENT_BUILD + #if UNITY_EDITOR || DEVELOPMENT_BUILD [Serializable] internal struct DeviceState { @@ -4120,8 +4235,16 @@ internal void RestoreStateWithoutDevices(SerializedState state) m_Metrics = state.metrics; m_PollingFrequency = state.pollingFrequency; - if (m_Settings != null) + // Cached settings might be null if the ScriptableObject was destroyed; create new default instance in this case. + if (state.settings == null) + { + state.settings = ScriptableObject.CreateInstance(); + state.settings.hideFlags = HideFlags.HideAndDontSave; // Hide from the project Hierarchy and Scene + } + + if (m_Settings != null && m_Settings != state.settings) Object.DestroyImmediate(m_Settings); + m_Settings = state.settings; #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS @@ -4144,6 +4267,7 @@ internal void RestoreStateWithoutDevices(SerializedState state) internal DeviceState[] m_SavedDeviceStates; internal AvailableDevice[] m_SavedAvailableDevices; +#if !ENABLE_CORECLR /// /// Recreate devices based on the devices we had before a domain reload. /// @@ -4227,6 +4351,30 @@ internal void RestoreDevicesAfterDomainReload() k_InputRestoreDevicesAfterReloadMarker.End(); } + /// + /// Notifies all devices of removal to better cleanup data when using SimulateDomainReload test hook + /// + /// + /// Devices maintain their own list of Devices within static fields, updated via NotifyAdded and NotifyRemoved overrides. + /// These fields are reset during a real DR, but not so when we "simulate" causing them to report incorrect values when + /// queried via direct APIs, e.g. Gamepad.all. So, to mitigate this we'll call NotifyRemove during this scenario. + /// + internal void TestHook_RemoveDevicesForSimulatedDomainReload() + { + if (m_Devices == null) + return; + + foreach (var device in m_Devices) + { + if (device == null) + break; + + device.NotifyRemoved(); + } + } + +#endif // !ENABLE_CORECLR + // We have two general types of devices we need to care about when recreating devices // after domain reloads: // diff --git a/Packages/com.unity.inputsystem/InputSystem/InputSettings.cs b/Packages/com.unity.inputsystem/InputSystem/InputSettings.cs index 4ee37718d9..437368cb53 100644 --- a/Packages/com.unity.inputsystem/InputSystem/InputSettings.cs +++ b/Packages/com.unity.inputsystem/InputSystem/InputSettings.cs @@ -790,7 +790,7 @@ internal bool IsFeatureEnabled(string featureName) internal void OnChange() { if (InputSystem.settings == this) - InputSystem.s_Manager.ApplySettings(); + InputSystem.manager.ApplySettings(); } internal const int s_OldUnsupportedFixedAndDynamicUpdateSetting = 0; diff --git a/Packages/com.unity.inputsystem/InputSystem/InputSystem.cs b/Packages/com.unity.inputsystem/InputSystem/InputSystem.cs index 2acd8b9856..7e7b914aa0 100644 --- a/Packages/com.unity.inputsystem/InputSystem/InputSystem.cs +++ b/Packages/com.unity.inputsystem/InputSystem/InputSystem.cs @@ -80,13 +80,22 @@ namespace UnityEngine.InputSystem #if UNITY_EDITOR [InitializeOnLoad] #endif - public static partial class InputSystem { #if UNITY_EDITOR static readonly ProfilerMarker k_InputInitializeInEditorMarker = new ProfilerMarker("InputSystem.InitializeInEditor"); #endif - static readonly ProfilerMarker k_InputResetMarker = new ProfilerMarker("InputSystem.Reset"); + + static InputSystem() + { + GlobalInitialize(true); + } + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void RuntimeInitialize() + { + GlobalInitialize(false); + } #region Layouts @@ -877,26 +886,7 @@ public static bool IsFirstLayoutBasedOnSecond(string firstLayoutName, string sec /// public static void RegisterProcessor(Type type, string name = null) { - if (type == null) - throw new ArgumentNullException(nameof(type)); - - // Default name to name of type without Processor suffix. - if (string.IsNullOrEmpty(name)) - { - name = type.Name; - if (name.EndsWith("Processor")) - name = name.Substring(0, name.Length - "Processor".Length); - } - - // Flush out any precompiled layout depending on the processor. - var precompiledLayouts = s_Manager.m_Layouts.precompiledLayouts; - foreach (var key in new List(precompiledLayouts.Keys)) // Need to keep key list stable while iterating; ToList() for some reason not available with .NET Standard 2.0 on Mono. - { - if (StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(precompiledLayouts[key].metadata, name, ';')) - s_Manager.m_Layouts.precompiledLayouts.Remove(key); - } - - s_Manager.processors.AddTypeRegistration(name, type); + s_Manager.RegisterProcessor(type, name); } /// @@ -2900,7 +2890,7 @@ public static InputSettings settings if (value == null) throw new ArgumentNullException(nameof(value)); - if (s_Manager.m_Settings == value) + if (s_Manager.settings == value) return; // In the editor, we keep track of the settings asset through EditorBuildSettings. @@ -3105,9 +3095,8 @@ public static InputActionAsset actions var current = s_Manager.actions; if (ReferenceEquals(current, value)) return; - var valueIsNotNull = value != null; -#if UNITY_EDITOR + #if UNITY_EDITOR // Do not allow assigning non-persistent assets (pure in-memory objects) if (valueIsNotNull && !EditorUtility.IsPersistent(value)) throw new ArgumentException($"Assigning a non-persistent {nameof(InputActionAsset)} to this property is not allowed. The assigned asset need to be persisted on disc inside the /Assets folder."); @@ -3115,7 +3104,7 @@ public static InputActionAsset actions // Track reference to enable including it in built Players, note that it will discard any non-persisted // object reference ProjectWideActionsBuildProvider.actionsToIncludeInPlayerBuild = value; -#endif // UNITY_EDITOR + #endif // UNITY_EDITOR // Update underlying value s_Manager.actions = value; @@ -3138,7 +3127,6 @@ public static event Action onActionsChange add => s_Manager.onActionsChange += value; remove => s_Manager.onActionsChange -= value; } - #endif // UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS /// @@ -3242,17 +3230,7 @@ public static event Action onActionChange /// public static void RegisterInteraction(Type type, string name = null) { - if (type == null) - throw new ArgumentNullException(nameof(type)); - - if (string.IsNullOrEmpty(name)) - { - name = type.Name; - if (name.EndsWith("Interaction")) - name = name.Substring(0, name.Length - "Interaction".Length); - } - - s_Manager.interactions.AddTypeRegistration(name, type); + s_Manager.RegisterInteraction(type, name); } /// @@ -3311,17 +3289,7 @@ public static IEnumerable ListInteractions() /// public static void RegisterBindingComposite(Type type, string name) { - if (type == null) - throw new ArgumentNullException(nameof(type)); - - if (string.IsNullOrEmpty(name)) - { - name = type.Name; - if (name.EndsWith("Composite")) - name = name.Substring(0, name.Length - "Composite".Length); - } - - s_Manager.composites.AddTypeRegistration(name, type); + s_Manager.RegisterBindingComposite(type, name); } /// @@ -3425,8 +3393,8 @@ public static int ListEnabledActions(List actions) /// The boolean value to set to public static bool runInBackground { - get => s_Manager.m_Runtime.runInBackground; - set => s_Manager.m_Runtime.runInBackground = value; + get => s_Manager.runtime.runInBackground; + set => s_Manager.runtime.runInBackground = value; } #if UNITY_INPUT_SYSTEM_PLATFORM_SCROLL_DELTA @@ -3442,13 +3410,15 @@ public static bool runInBackground /// Up-to-date metrics on input system activity. public static InputMetrics metrics => s_Manager.metrics; - internal static InputManager s_Manager; - internal static InputRemoting s_Remote; + internal static InputManager manager => s_Manager; + private static InputManager s_Manager; + private static InputRemoting s_Remote; #if DEVELOPMENT_BUILD || UNITY_EDITOR - internal static RemoteInputPlayerConnection s_RemoteConnection; + private static RemoteInputPlayerConnection s_RemoteConnection; + internal static RemoteInputPlayerConnection remoteConnection => s_RemoteConnection; - private static void SetUpRemoting() + internal static void SetUpRemoting() { Debug.Assert(s_Manager != null); @@ -3484,46 +3454,55 @@ private static void SetUpRemotingInternal() #if !UNITY_EDITOR private static bool ShouldEnableRemoting() { -#if UNITY_INCLUDE_TESTS - var isRunningTests = true; -#else - var isRunningTests = false; -#endif - if (isRunningTests) - return false; // Don't remote while running tests. + #if UNITY_INCLUDE_TESTS + return false; // Don't remote while running tests. + #endif + return true; } - #endif + #endif //!UNITY_EDITOR #endif // DEVELOPMENT_BUILD || UNITY_EDITOR // The rest here is internal stuff to manage singletons, survive domain reloads, // and to support the reset ability for tests. - static InputSystem() + + private static bool IsDomainReloadDisabledForPlayMode() { - #if UNITY_EDITOR - InitializeInEditor(); - #else - InitializeInPlayer(); + #if UNITY_EDITOR && !ENABLE_CORECLR + if (!EditorSettings.enterPlayModeOptionsEnabled || (EditorSettings.enterPlayModeOptions & EnterPlayModeOptions.DisableDomainReload) == 0) + return false; #endif + return true; } - ////FIXME: Unity is not calling this method if it's inside an #if block that is not - //// visible to the editor; that shouldn't be the case - [RuntimeInitializeOnLoadMethod(loadType: RuntimeInitializeLoadType.SubsystemRegistration)] - private static void RunInitializeInPlayer() + private static void GlobalInitialize(bool calledFromCtor) { - // We're using this method just to make sure the class constructor is called - // so we don't need any code in here. When the engine calls this method, the - // class constructor will be run if it hasn't been run already. + // This method is called twice: once from the static ctor and again from RuntimeInitialize(). + // We handle the calls differently for the Editor and Player. - // IL2CPP has a bug that causes the class constructor to not be run when - // the RuntimeInitializeOnLoadMethod is invoked. So we need an explicit check - // here until that is fixed (case 1014293). - #if !UNITY_EDITOR - if (s_Manager == null) - InitializeInPlayer(); - #endif +#if UNITY_EDITOR + // If Domain Reloads are enabled, InputSystem is initialized via the ctor and we can ignore + // the second call from "Runtime", otherwise (DRs are disabled) the ctor isn't fired, so we + // must initialize via the Runtime call. + + if (calledFromCtor || IsDomainReloadDisabledForPlayMode()) + { + InitializeInEditor(calledFromCtor); + } +#else + // In the Player, simply initialize InputSystem from the ctor and then execute the initial update + // from the second call. This saves us from needing another RuntimeInitializeOnLoad attribute. + + if (calledFromCtor) + { + InitializeInPlayer(null, true); + } + else + { + RunInitialUpdate(); + } +#endif // UNITY_EDITOR } // Initialization is triggered by accessing InputSystem. Some parts (like InputActions) @@ -3534,56 +3513,77 @@ internal static void EnsureInitialized() } #if UNITY_EDITOR - internal static InputSystemObject s_SystemObject; - internal static void InitializeInEditor(IInputRuntime runtime = null) + // ISX-1860 - #ifdef out Domain Reload specific functionality from CoreCLR + private static InputSystemStateManager s_DomainStateManager; + internal static InputSystemStateManager domainStateManager => s_DomainStateManager; + + internal static void InitializeInEditor(bool calledFromCtor, IInputRuntime runtime = null) { k_InputInitializeInEditorMarker.Begin(); - Reset(runtime: runtime); + bool globalReset = calledFromCtor || !IsDomainReloadDisabledForPlayMode(); - var existingSystemObjects = Resources.FindObjectsOfTypeAll(); - if (existingSystemObjects != null && existingSystemObjects.Length > 0) + // We must initialize a new InputManager object first thing since other parts + // of the init flow depend on it. + if (globalReset) { - ////FIXME: does not preserve action map state + if (s_Manager != null) + s_Manager.Dispose(); - // We're coming back out of a domain reload. We're restoring part of the - // InputManager state here but we're still waiting from layout registrations - // that happen during domain initialization. + // Settings object should get set by an actual InputSettings asset. + s_Manager = InputManager.CreateAndInitialize(runtime ?? NativeInputRuntime.instance, null); + s_Manager.runtime.onPlayModeChanged = OnPlayModeChange; + s_Manager.runtime.onProjectChange = OnProjectChange; - s_SystemObject = existingSystemObjects[0]; - s_Manager.RestoreStateWithoutDevices(s_SystemObject.systemState.managerState); - InputDebuggerWindow.ReviveAfterDomainReload(); + InputEditorUserSettings.s_Settings = new InputEditorUserSettings.SerializedState(); - // Restore remoting state. - s_RemoteConnection = s_SystemObject.systemState.remoteConnection; - SetUpRemoting(); - s_Remote.RestoreState(s_SystemObject.systemState.remotingState, s_Manager); + #if !UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION + InputSystem.PerformDefaultPluginInitialization(); + #endif + } - // Get manager to restore devices on first input update. By that time we - // should have all (possibly updated) layout information in place. - s_Manager.m_SavedDeviceStates = s_SystemObject.systemState.managerState.devices; - s_Manager.m_SavedAvailableDevices = s_SystemObject.systemState.managerState.availableDevices; + var existingSystemStateManagers = Resources.FindObjectsOfTypeAll(); + if (existingSystemStateManagers != null && existingSystemStateManagers.Length > 0) + { + if (globalReset) + { + ////FIXME: does not preserve action map state + + // If we're coming back out of a domain reload. We're restoring part of the + // InputManager state here but we're still waiting from layout registrations + // that happen during domain initialization. + + s_DomainStateManager = existingSystemStateManagers[0]; + s_Manager.RestoreStateWithoutDevices(s_DomainStateManager.systemState.managerState); + InputDebuggerWindow.ReviveAfterDomainReload(); - // Restore editor settings. - InputEditorUserSettings.s_Settings = s_SystemObject.systemState.userSettings; + // Restore remoting state. + s_RemoteConnection = s_DomainStateManager.systemState.remoteConnection; + SetUpRemoting(); + s_Remote.RestoreState(s_DomainStateManager.systemState.remotingState, s_Manager); - // Get rid of saved state. - s_SystemObject.systemState = new State(); + // Get s_Manager to restore devices on first input update. By that time we + // should have all (possibly updated) layout information in place. + s_Manager.m_SavedDeviceStates = s_DomainStateManager.systemState.managerState.devices; + s_Manager.m_SavedAvailableDevices = s_DomainStateManager.systemState.managerState.availableDevices; + + // Restore editor settings. + InputEditorUserSettings.s_Settings = s_DomainStateManager.systemState.userSettings; + + // Get rid of saved state. + s_DomainStateManager.systemState = new InputSystemState(); + } } else { - s_SystemObject = ScriptableObject.CreateInstance(); - s_SystemObject.hideFlags = HideFlags.HideAndDontSave; + s_DomainStateManager = ScriptableObject.CreateInstance(); + s_DomainStateManager.hideFlags = HideFlags.HideAndDontSave; // See if we have a remembered settings object. - if (EditorBuildSettings.TryGetConfigObject(InputSettingsProvider.kEditorBuildSettingsConfigKey, - out InputSettings settingsAsset)) + if (EditorBuildSettings.TryGetConfigObject(InputSettingsProvider.kEditorBuildSettingsConfigKey, out InputSettings settingsAsset)) { - if (s_Manager.m_Settings.hideFlags == HideFlags.HideAndDontSave) - ScriptableObject.DestroyImmediate(s_Manager.m_Settings); - s_Manager.m_Settings = settingsAsset; - s_Manager.ApplySettings(); + s_Manager.settings = settingsAsset; } #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS @@ -3599,17 +3599,15 @@ internal static void InitializeInEditor(IInputRuntime runtime = null) } Debug.Assert(settings != null); - #if UNITY_EDITOR Debug.Assert(EditorUtility.InstanceIDToObject(settings.GetInstanceID()) != null, "InputSettings has lost its native object"); - #endif // If native backends for new input system aren't enabled, ask user whether we should // enable them (requires restart). We only ask once per session and don't ask when // running in batch mode. - if (!s_SystemObject.newInputBackendsCheckedAsEnabled && + if (!s_DomainStateManager.newInputBackendsCheckedAsEnabled && !EditorPlayerSettingHelpers.newSystemBackendsEnabled && - !s_Manager.m_Runtime.isInBatchMode) + !s_Manager.runtime.isInBatchMode) { const string dialogText = "This project is using the new input system package but the native platform backends for the new input system are not enabled in the player settings. " + "This means that no input from native devices will come through." + @@ -3621,13 +3619,13 @@ internal static void InitializeInEditor(IInputRuntime runtime = null) EditorHelpers.RestartEditorAndRecompileScripts(); } } - s_SystemObject.newInputBackendsCheckedAsEnabled = true; + s_DomainStateManager.newInputBackendsCheckedAsEnabled = true; -#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS // Make sure project wide input actions are enabled. // Note that this will always fail if entering play-mode within editor since not yet in play-mode. EnableActions(); -#endif + #endif RunInitialUpdate(); @@ -3641,15 +3639,15 @@ internal static void OnPlayModeChange(PlayModeStateChange change) switch (change) { case PlayModeStateChange.ExitingEditMode: - s_SystemObject.settings = JsonUtility.ToJson(settings); - s_SystemObject.exitEditModeTime = InputRuntime.s_Instance.currentTime; - s_SystemObject.enterPlayModeTime = 0; + s_DomainStateManager.settings = JsonUtility.ToJson(settings); + s_DomainStateManager.exitEditModeTime = InputRuntime.s_Instance.currentTime; + s_DomainStateManager.enterPlayModeTime = 0; // InputSystem.actions is not setup yet break; case PlayModeStateChange.EnteredPlayMode: - s_SystemObject.enterPlayModeTime = InputRuntime.s_Instance.currentTime; + s_DomainStateManager.enterPlayModeTime = InputRuntime.s_Instance.currentTime; s_Manager.SyncAllDevicesAfterEnteringPlayMode(); #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS EnableActions(); @@ -3664,9 +3662,9 @@ internal static void OnPlayModeChange(PlayModeStateChange change) ////REVIEW: is there any other cleanup work we want to before? should we automatically nuke //// InputDevices that have been created with AddDevice<> during play mode? case PlayModeStateChange.EnteredEditMode: -#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS DisableActions(false); -#endif + #endif // Nuke all InputUsers. InputUser.ResetGlobals(); @@ -3678,34 +3676,20 @@ internal static void OnPlayModeChange(PlayModeStateChange change) InputActionReference.ResetCachedAction(); // Restore settings. - if (!string.IsNullOrEmpty(s_SystemObject.settings)) + if (!string.IsNullOrEmpty(s_DomainStateManager.settings)) { - JsonUtility.FromJsonOverwrite(s_SystemObject.settings, settings); - s_SystemObject.settings = null; + JsonUtility.FromJsonOverwrite(s_DomainStateManager.settings, settings); + s_DomainStateManager.settings = null; settings.OnChange(); } - // reload input action assets marked as dirty from disk - if (s_TrackedDirtyAssets == null) - return; - - foreach (var assetGuid in s_TrackedDirtyAssets) - { - var assetPath = AssetDatabase.GUIDToAssetPath(assetGuid); - - if (string.IsNullOrEmpty(assetPath)) - continue; - - AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate); - } - - s_TrackedDirtyAssets.Clear(); - + // reload input assets marked as dirty from disk + DirtyAssetTracker.ReloadDirtyAssets(); break; } } - private static void OnProjectChange() + internal static void OnProjectChange() { ////TODO: use dirty count to find whether settings have actually changed // May have added, removed, moved, or renamed settings asset. Force a refresh @@ -3714,9 +3698,7 @@ private static void OnProjectChange() // Also, if the asset holding our current settings got deleted, switch back to a // temporary settings object. - // NOTE: We access m_Settings directly here to make sure we're not running into asserts - // from the settings getter checking it has a valid object. - if (EditorUtility.InstanceIDToObject(s_Manager.m_Settings.GetInstanceID()) == null) + if (EditorUtility.InstanceIDToObject(s_Manager.settings.GetInstanceID()) == null) { var newSettings = ScriptableObject.CreateInstance(); newSettings.hideFlags = HideFlags.HideAndDontSave; @@ -3724,59 +3706,63 @@ private static void OnProjectChange() } } - private static HashSet s_TrackedDirtyAssets; - - /// - /// Keep track of InputActionAsset assets that you want to re-load on exiting Play mode. This is useful because - /// some user actions, such as adding a new input binding at runtime, change the in-memory representation of the - /// input action asset and those changes survive when exiting Play mode. If you re-open an Input - /// Action Asset in the Editor that has been changed this way, you see the new bindings that have been added - /// during Play mode which you might not typically want to happen. - /// - /// You can avoid this by force re-loading from disk any asset that has been marked as dirty. - /// - /// - internal static void TrackDirtyInputActionAsset(InputActionAsset asset) +#else // UNITY_EDITOR + internal static void InitializeInPlayer(IInputRuntime runtime, bool loadSettingsAsset) { - if (s_TrackedDirtyAssets == null) - s_TrackedDirtyAssets = new HashSet(); - - if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(asset, out string assetGuid, out long _) == false) - return; + InputSettings settings = null; - s_TrackedDirtyAssets.Add(assetGuid); - } - -#else - private static void InitializeInPlayer(IInputRuntime runtime = null, InputSettings settings = null) - { - if (settings == null) - settings = Resources.FindObjectsOfTypeAll().FirstOrDefault() ?? ScriptableObject.CreateInstance(); + if (loadSettingsAsset) + settings = Resources.FindObjectsOfTypeAll().FirstOrDefault(); // No domain reloads in the player so we don't need to look for existing // instances. - s_Manager = new InputManager(); - s_Manager.Initialize(runtime ?? NativeInputRuntime.instance, settings); + s_Manager = InputManager.CreateAndInitialize(runtime ?? NativeInputRuntime.instance, settings); -#if !UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION + #if !UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION PerformDefaultPluginInitialization(); -#endif + #endif // Automatically enable remoting in development players. -#if DEVELOPMENT_BUILD + #if DEVELOPMENT_BUILD if (ShouldEnableRemoting()) SetUpRemoting(); -#endif + #endif -#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS // && !UNITY_INCLUDE_TESTS + #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS // This is the point where we initialise project-wide actions for the Player EnableActions(); -#endif + #endif } #endif // UNITY_EDITOR - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] +#if UNITY_INCLUDE_TESTS + // + // We cannot define UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS within the Test-Framework assembly, and + // so this hook is needed; it's called from InputTestStateManager.Reset(). + // + internal static void TestHook_DisableActions() + { + #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + // Note that in a test setup we might enter reset with project-wide actions already enabled but the + // reset itself has pushed the action system state on the state stack. To avoid action state memory + // problems we disable actions here and also request asset to be marked dirty and reimported. + DisableActions(triggerSetupChanged: true); + if (s_Manager != null) + s_Manager.actions = null; + #endif // UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + } + + internal static void TestHook_EnableActions() + { + // Note this is too early for editor ! actions is not setup yet. + #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + EnableActions(); + #endif + } + +#endif // UNITY_INCLUDE_TESTS + private static void RunInitialUpdate() { // Request an initial Update so that user methods such as Start and Awake @@ -3790,8 +3776,24 @@ private static void RunInitialUpdate() } #if !UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION - private static void PerformDefaultPluginInitialization() + + #if UNITY_EDITOR + // Plug-ins must only be initialized once, since many of them use static fields. + // When Domain Reloads are disabled, we must guard against this method being called a second time. + private static bool s_PluginsInitialized = false; + #endif + + internal static void PerformDefaultPluginInitialization() { + #if UNITY_EDITOR + if (s_PluginsInitialized) + { + Debug.Assert(false, "Attempted to re-initialize InputSystem Plugins!"); + return; + } + s_PluginsInitialized = true; + #endif + UISupport.Initialize(); #if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WSA || UNITY_ANDROID || UNITY_IOS || UNITY_TVOS || UNITY_VISIONOS @@ -3848,216 +3850,5 @@ private static void PerformDefaultPluginInitialization() } #endif // UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION - - // For testing, we want the ability to push/pop system state even in the player. - // However, we don't want it in release players. -#if DEVELOPMENT_BUILD || UNITY_EDITOR - /// - /// Return the input system to its default state. - /// - private static void Reset(bool enableRemoting = false, IInputRuntime runtime = null) - { - k_InputResetMarker.Begin(); - -#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS - // Note that in a test setup we might enter reset with project-wide actions already enabled but the - // reset itself has pushed the action system state on the state stack. To avoid action state memory - // problems we disable actions here and also request asset to be marked dirty and reimported. - DisableActions(triggerSetupChanged: true); - if (s_Manager != null) - s_Manager.actions = null; -#endif // UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS - - // Some devices keep globals. Get rid of them by pretending the devices - // are removed. - if (s_Manager != null) - { - foreach (var device in s_Manager.devices) - device.NotifyRemoved(); - - s_Manager.UninstallGlobals(); - } - - // Create temporary settings. In the tests, this is all we need. But outside of tests,d - // this should get replaced with an actual InputSettings asset. - var settings = ScriptableObject.CreateInstance(); - settings.hideFlags = HideFlags.HideAndDontSave; - - #if UNITY_EDITOR - s_Manager = new InputManager(); - s_Manager.Initialize( - runtime: runtime ?? NativeInputRuntime.instance, - settings: settings); - - s_Manager.m_Runtime.onPlayModeChanged = OnPlayModeChange; - s_Manager.m_Runtime.onProjectChange = OnProjectChange; - - InputEditorUserSettings.s_Settings = new InputEditorUserSettings.SerializedState(); - - if (enableRemoting) - SetUpRemoting(); - - #if !UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION - PerformDefaultPluginInitialization(); - #endif - - #else - InitializeInPlayer(runtime, settings); - #endif - - Mouse.s_PlatformMouseDevice = null; - - InputEventListener.s_ObserverState = default; - InputUser.ResetGlobals(); - EnhancedTouchSupport.Reset(); - - // This is the point where we initialise project-wide actions for the Editor, Editor Tests and Player Tests. - // Note this is too early for editor ! actions is not setup yet. - #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS - EnableActions(); - #endif - - k_InputResetMarker.End(); - } - - /// - /// Destroy the current setup of the input system. - /// - /// - /// NOTE: This also de-allocates data we're keeping in unmanaged memory! - /// - private static void Destroy() - { - // NOTE: Does not destroy InputSystemObject. We want to destroy input system - // state repeatedly during tests but we want to not create InputSystemObject - // over and over. - s_Manager.Destroy(); - if (s_RemoteConnection != null) - Object.DestroyImmediate(s_RemoteConnection); - #if UNITY_EDITOR - EditorInputControlLayoutCache.Clear(); - InputDeviceDebuggerWindow.s_OnToolbarGUIActions.Clear(); - InputEditorUserSettings.s_Settings = new InputEditorUserSettings.SerializedState(); - #endif - - s_Manager = null; - s_RemoteConnection = null; - s_Remote = null; - } - - /// - /// Snapshot of the state used by the input system. - /// - /// - /// Can be taken across domain reloads. - /// - [Serializable] - internal struct State - { - [NonSerialized] public InputManager manager; - [NonSerialized] public InputRemoting remote; - [SerializeField] public RemoteInputPlayerConnection remoteConnection; - [SerializeField] public InputManager.SerializedState managerState; - [SerializeField] public InputRemoting.SerializedState remotingState; - #if UNITY_EDITOR - [SerializeField] public InputEditorUserSettings.SerializedState userSettings; - [SerializeField] public string systemObject; - #endif - ////TODO: make these saved states capable of surviving domain reloads - [NonSerialized] public ISavedState inputActionState; - [NonSerialized] public ISavedState touchState; - [NonSerialized] public ISavedState inputUserState; - } - - private static Stack s_SavedStateStack; - - internal static State GetSavedState() - { - return s_SavedStateStack.Peek(); - } - - /// - /// Push the current state of the input system onto a stack and - /// reset the system to its default state. - /// - /// - /// The save stack is not able to survive domain reloads. It is intended solely - /// for use in tests. - /// - internal static void SaveAndReset(bool enableRemoting = false, IInputRuntime runtime = null) - { - if (s_SavedStateStack == null) - s_SavedStateStack = new Stack(); - - ////FIXME: does not preserve global state in InputActionState - ////TODO: preserve InputUser state - ////TODO: preserve EnhancedTouchSupport state - - s_SavedStateStack.Push(new State - { - manager = s_Manager, - remote = s_Remote, - remoteConnection = s_RemoteConnection, - managerState = s_Manager.SaveState(), - remotingState = s_Remote?.SaveState() ?? new InputRemoting.SerializedState(), - #if UNITY_EDITOR - userSettings = InputEditorUserSettings.s_Settings, - systemObject = JsonUtility.ToJson(s_SystemObject), - #endif - inputActionState = InputActionState.SaveAndResetState(), - touchState = EnhancedTouch.Touch.SaveAndResetState(), - inputUserState = InputUser.SaveAndResetState() - }); - - Reset(enableRemoting, runtime ?? InputRuntime.s_Instance); // Keep current runtime. - } - - ////FIXME: this method doesn't restore things like InputDeviceDebuggerWindow.onToolbarGUI - /// - /// Restore the state of the system from the last state pushed with . - /// - internal static void Restore() - { - Debug.Assert(s_SavedStateStack != null && s_SavedStateStack.Count > 0); - - // Load back previous state. - var state = s_SavedStateStack.Pop(); - - state.inputUserState.StaticDisposeCurrentState(); - state.touchState.StaticDisposeCurrentState(); - state.inputActionState.StaticDisposeCurrentState(); - - // Nuke what we have. - Destroy(); - - state.inputUserState.RestoreSavedState(); - state.touchState.RestoreSavedState(); - state.inputActionState.RestoreSavedState(); - - s_Manager = state.manager; - s_Remote = state.remote; - s_RemoteConnection = state.remoteConnection; - - InputUpdate.Restore(state.managerState.updateState); - - s_Manager.InstallRuntime(s_Manager.m_Runtime); - s_Manager.InstallGlobals(); - s_Manager.ApplySettings(); - - #if UNITY_EDITOR - InputEditorUserSettings.s_Settings = state.userSettings; - JsonUtility.FromJsonOverwrite(state.systemObject, s_SystemObject); - #endif - - // Get devices that keep global lists (like Gamepad) to re-initialize them - // by pretending the devices have been added. - foreach (var device in devices) - { - device.NotifyAdded(); - device.MakeCurrent(); - } - } - -#endif } } diff --git a/Packages/com.unity.inputsystem/InputSystem/InputSystemObject.cs b/Packages/com.unity.inputsystem/InputSystem/InputSystemObject.cs deleted file mode 100644 index 4bb0ddc027..0000000000 --- a/Packages/com.unity.inputsystem/InputSystem/InputSystemObject.cs +++ /dev/null @@ -1,37 +0,0 @@ -#if UNITY_EDITOR -using UnityEngine.InputSystem.Editor; - -namespace UnityEngine.InputSystem -{ - /// - /// A hidden, internal object we put in the editor to bundle input system state - /// and help us survive domain reloads. - /// - /// - /// Player doesn't need this stuff because there's no domain reloads to survive. - /// - internal class InputSystemObject : ScriptableObject, ISerializationCallbackReceiver - { - [SerializeField] public InputSystem.State systemState; - [SerializeField] public bool newInputBackendsCheckedAsEnabled; - [SerializeField] public string settings; - [SerializeField] public double exitEditModeTime; - [SerializeField] public double enterPlayModeTime; - - public void OnBeforeSerialize() - { - // Save current system state. - systemState.manager = InputSystem.s_Manager; - systemState.remote = InputSystem.s_Remote; - systemState.remoteConnection = InputSystem.s_RemoteConnection; - systemState.managerState = InputSystem.s_Manager.SaveState(); - systemState.remotingState = InputSystem.s_Remote.SaveState(); - systemState.userSettings = InputEditorUserSettings.s_Settings; - } - - public void OnAfterDeserialize() - { - } - } -} -#endif // UNITY_EDITOR diff --git a/Packages/com.unity.inputsystem/InputSystem/InputSystemObject.cs.meta b/Packages/com.unity.inputsystem/InputSystem/InputSystemObject.cs.meta deleted file mode 100644 index 0a9818e38c..0000000000 --- a/Packages/com.unity.inputsystem/InputSystem/InputSystemObject.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 5cdce2bffd1e49bda08b3db54a031207 -timeCreated: 1506825901 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/InputSystemStateManager.cs b/Packages/com.unity.inputsystem/InputSystem/InputSystemStateManager.cs new file mode 100644 index 0000000000..0c2decc009 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/InputSystemStateManager.cs @@ -0,0 +1,99 @@ +using System; +using UnityEngine.InputSystem; +using UnityEngine; +#if UNITY_EDITOR +using UnityEngine.InputSystem.Editor; +#endif +using UnityEngine.InputSystem.Utilities; + +namespace UnityEngine.InputSystem +{ +#if UNITY_EDITOR || DEVELOPMENT_BUILD + /// + /// Snapshot of the state used by the input system. + /// + /// + /// Can be taken across domain reloads. + /// + [Serializable] + internal struct InputSystemState + { + [NonSerialized] public InputManager manager; + [NonSerialized] public InputRemoting remote; + [SerializeField] public RemoteInputPlayerConnection remoteConnection; + [SerializeField] public InputManager.SerializedState managerState; + [SerializeField] public InputRemoting.SerializedState remotingState; +#if UNITY_EDITOR + [SerializeField] public InputEditorUserSettings.SerializedState userSettings; + [SerializeField] public string systemObject; +#endif + ////TODO: make these saved states capable of surviving domain reloads + [NonSerialized] public ISavedState inputActionState; + [NonSerialized] public ISavedState touchState; + [NonSerialized] public ISavedState inputUserState; + } + + // ISX-1860 - #ifdef out Domain Reload specific functionality from CoreCLR +#if UNITY_EDITOR + /// + /// A hidden, internal object we put in the editor to bundle input system state + /// and help us survive domain reloads. + /// + /// + /// Player doesn't need this stuff because there's no domain reloads to survive, and + /// also doesn't have domain reloads. + /// + internal class InputSystemStateManager : ScriptableObject, ISerializationCallbackReceiver + { + /// + /// References the "core" input state that must survive domain reloads. + /// + [SerializeField] public InputSystemState systemState; + + /// + /// Triggers Editor restart when enabling NewInput back-ends. + /// + [SerializeField] public bool newInputBackendsCheckedAsEnabled; + + /// + /// Saves and restores InputSettings across domain reloads + /// + /// + /// InputSettings are serialized to JSON which this string holds. + /// + [SerializeField] public string settings; + + /// + /// Timestamp retrieved from InputRuntime.currentTime when exiting EditMode. + /// + /// + /// All input events occurring between exiting EditMode and entering PlayMode are discarded. + /// + [SerializeField] public double exitEditModeTime; + + /// + /// Timestamp retrieved from InputRuntime.currentTime when entering PlayMode. + /// + /// + /// All input events occurring between exiting EditMode and entering PlayMode are discarded. + /// + [SerializeField] public double enterPlayModeTime; + + public void OnBeforeSerialize() + { + // Save current system state. + systemState.manager = InputSystem.manager; + systemState.remote = InputSystem.remoting; + systemState.remoteConnection = InputSystem.remoteConnection; + systemState.managerState = InputSystem.manager.SaveState(); + systemState.remotingState = InputSystem.remoting.SaveState(); + systemState.userSettings = InputEditorUserSettings.s_Settings; + } + + public void OnAfterDeserialize() + { + } + } +#endif // UNITY_EDITOR +#endif // UNITY_EDITOR || DEVELOPMENT_BUILD +} diff --git a/Packages/com.unity.inputsystem/InputSystem/InputSystemStateManager.cs.meta b/Packages/com.unity.inputsystem/InputSystem/InputSystemStateManager.cs.meta new file mode 100644 index 0000000000..532aad0114 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/InputSystemStateManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3a1038634f66b98469a174a3e1a85190 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/InputSystemTestHooks.cs b/Packages/com.unity.inputsystem/InputSystem/InputSystemTestHooks.cs new file mode 100644 index 0000000000..69274b2d08 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/InputSystemTestHooks.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +#if UNITY_EDITOR +using UnityEngine.InputSystem.Editor; +#endif +using UnityEngine.InputSystem.LowLevel; + +#if UNITY_EDITOR || UNITY_INCLUDE_TESTS + +namespace UnityEngine.InputSystem +{ + /// + /// Extension of class to provide test-specific functionality + /// + public static partial class InputSystem + { +#if UNITY_EDITOR + internal static void TestHook_InitializeForPlayModeTests(bool enableRemoting, IInputRuntime runtime) + { + s_Manager = InputManager.CreateAndInitialize(runtime, null); + + s_Manager.runtime.onPlayModeChanged = InputSystem.OnPlayModeChange; + s_Manager.runtime.onProjectChange = InputSystem.OnProjectChange; + + InputEditorUserSettings.s_Settings = new InputEditorUserSettings.SerializedState(); + + if (enableRemoting) + InputSystem.SetUpRemoting(); + +#if !UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION + // Reset the flag so can re-initialize Plugins between tests. + InputSystem.s_PluginsInitialized = false; + InputSystem.PerformDefaultPluginInitialization(); +#endif + } + +#if !ENABLE_CORECLR + internal static void TestHook_SimulateDomainReload(IInputRuntime runtime) + { + // This quite invasive goes into InputSystem internals. Unfortunately, we + // have no proper way of simulating domain reloads ATM. So we directly call various + // internal methods here in a sequence similar to what we'd get during a domain reload. + // Since we're faking it, pass 'true' for calledFromCtor param. + + // Need to notify devices of removal so their static fields are cleaned up + InputSystem.s_Manager.TestHook_RemoveDevicesForSimulatedDomainReload(); + + InputSystem.s_DomainStateManager.OnBeforeSerialize(); + InputSystem.s_DomainStateManager = null; + InputSystem.s_Manager = null; // Do NOT Dispose()! The native memory cannot be freed as it's reference by saved state + InputSystem.s_PluginsInitialized = false; + InputSystem.InitializeInEditor(true, runtime); + } + +#endif +#endif // UNITY_EDITOR + + /// + /// Destroy the current setup of the input system. + /// + /// + /// NOTE: This also de-allocates data we're keeping in unmanaged memory! + /// + internal static void TestHook_DestroyAndReset() + { + // NOTE: Does not destroy InputSystemObject. We want to destroy input system + // state repeatedly during tests but we want to not create InputSystemObject + // over and over. + InputSystem.manager.Dispose(); + if (InputSystem.s_RemoteConnection != null) + Object.DestroyImmediate(InputSystem.s_RemoteConnection); + +#if UNITY_EDITOR + EditorInputControlLayoutCache.Clear(); + InputDeviceDebuggerWindow.s_OnToolbarGUIActions.Clear(); + InputEditorUserSettings.s_Settings = new InputEditorUserSettings.SerializedState(); +#endif + + InputSystem.s_Manager = null; + InputSystem.s_RemoteConnection = null; + InputSystem.s_Remote = null; + } + + internal static void TestHook_RestoreFromSavedState(InputSystemState savedState) + { + s_Manager = savedState.manager; + s_Remote = savedState.remote; + s_RemoteConnection = savedState.remoteConnection; + } + + internal static void TestHook_SwitchToDifferentInputManager(InputManager otherManager) + { + s_Manager = otherManager; + InputStateBuffers.SwitchTo(otherManager.m_StateBuffers, otherManager.defaultUpdateType); + } + } +} + +#endif // UNITY_EDITOR || UNITY_INCLUDE_TESTS diff --git a/Packages/com.unity.inputsystem/InputSystem/InputSystemTestHooks.cs.meta b/Packages/com.unity.inputsystem/InputSystem/InputSystemTestHooks.cs.meta new file mode 100644 index 0000000000..83ac0bf722 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/InputSystemTestHooks.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1f95d379362f6c74ca1eb6368642199f \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/NativeInputRuntime.cs b/Packages/com.unity.inputsystem/InputSystem/NativeInputRuntime.cs index f82d5fa8b3..12294dc6b8 100644 --- a/Packages/com.unity.inputsystem/InputSystem/NativeInputRuntime.cs +++ b/Packages/com.unity.inputsystem/InputSystem/NativeInputRuntime.cs @@ -20,7 +20,26 @@ namespace UnityEngine.InputSystem.LowLevel /// internal class NativeInputRuntime : IInputRuntime { - public static readonly NativeInputRuntime instance = new NativeInputRuntime(); + private static NativeInputRuntime s_Instance; + + // Private ctor exists to enforce Singleton pattern + private NativeInputRuntime() {} + + /// + /// Employ the Singleton pattern for this class and initialize a new instance on first use. + /// + /// + /// This property is typically used to initialize InputManager and isn't used afterwards, i.e. there's + /// no perf impact to the null check. + /// + public static NativeInputRuntime instance + { + get + { + s_Instance ??= new NativeInputRuntime(); + return s_Instance; + } + } public int AllocateDeviceId() { diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/DualShock/DualShockGamepadHID.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/DualShock/DualShockGamepadHID.cs index 3cb6a963ef..245ee38d31 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/DualShock/DualShockGamepadHID.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/DualShock/DualShockGamepadHID.cs @@ -585,7 +585,7 @@ public unsafe void OnStateEvent(InputEventPtr eventPtr) || newState->buttons2 != currentState->buttons2; if (!actuated) - InputSystem.s_Manager.DontMakeCurrentlyUpdatingDeviceCurrent(); + InputSystem.manager.DontMakeCurrentlyUpdatingDeviceCurrent(); } InputState.Change(this, eventPtr); @@ -675,8 +675,8 @@ public DualSenseHIDInputReport ToHIDInputReport() [StructLayout(LayoutKind.Explicit)] internal struct DualSenseHIDMinimalInputReport { - public static int ExpectedSize1 = 10; - public static int ExpectedSize2 = 78; + public const int ExpectedSize1 = 10; + public const int ExpectedSize2 = 78; [FieldOffset(0)] public byte reportId; [FieldOffset(1)] public byte leftStickX; @@ -920,7 +920,7 @@ public unsafe void OnStateEvent(InputEventPtr eventPtr) || newState->buttons3 != currentState->buttons3; if (!actuatedOrChanged) - InputSystem.s_Manager.DontMakeCurrentlyUpdatingDeviceCurrent(); + InputSystem.manager.DontMakeCurrentlyUpdatingDeviceCurrent(); } InputState.Change(this, eventPtr); diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/EnhancedTouch/EnhancedTouchSupport.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/EnhancedTouch/EnhancedTouchSupport.cs index 92437b307f..b3bdc45b9d 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/EnhancedTouch/EnhancedTouchSupport.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/EnhancedTouch/EnhancedTouchSupport.cs @@ -63,10 +63,7 @@ public static class EnhancedTouchSupport /// Whether enhanced touch support is currently enabled. /// /// True if EnhancedTouch support has been enabled. - public static bool enabled => s_Enabled > 0; - - private static int s_Enabled; - private static InputSettings.UpdateMode s_UpdateMode; + public static bool enabled => Touch.s_GlobalState.enhancedTouchEnabled > 0; /// /// Enable enhanced touch support. @@ -82,8 +79,8 @@ public static class EnhancedTouchSupport /// public static void Enable() { - ++s_Enabled; - if (s_Enabled > 1) + ++Touch.s_GlobalState.enhancedTouchEnabled; + if (Touch.s_GlobalState.enhancedTouchEnabled > 1) return; InputSystem.onDeviceChange += OnDeviceChange; @@ -107,8 +104,8 @@ public static void Disable() { if (!enabled) return; - --s_Enabled; - if (s_Enabled > 0) + --Touch.s_GlobalState.enhancedTouchEnabled; + if (Touch.s_GlobalState.enhancedTouchEnabled > 0) return; InputSystem.onDeviceChange -= OnDeviceChange; @@ -131,7 +128,7 @@ internal static void Reset() Touch.s_GlobalState.editorState.Destroy(); Touch.s_GlobalState.editorState = default; #endif - s_Enabled = 0; + Touch.s_GlobalState.enhancedTouchEnabled = 0; } private static void SetUpState() @@ -141,7 +138,7 @@ private static void SetUpState() Touch.s_GlobalState.editorState.updateMask = InputUpdateType.Editor; #endif - s_UpdateMode = InputSystem.settings.updateMode; + Touch.s_GlobalState.enhancedTouchUpdateMode = InputSystem.settings.updateMode; foreach (var device in InputSystem.devices) OnDeviceChange(device, InputDeviceChange.Added); @@ -186,7 +183,7 @@ private static void OnDeviceChange(InputDevice device, InputDeviceChange change) private static void OnSettingsChange() { var currentUpdateMode = InputSystem.settings.updateMode; - if (s_UpdateMode == currentUpdateMode) + if (Touch.s_GlobalState.enhancedTouchUpdateMode == currentUpdateMode) return; TearDownState(); SetUpState(); diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/EnhancedTouch/Touch.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/EnhancedTouch/Touch.cs index 096f8c9659..169f3d34ed 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/EnhancedTouch/Touch.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/EnhancedTouch/Touch.cs @@ -587,18 +587,24 @@ internal struct GlobalState internal CallbackArray> onFingerMove; internal CallbackArray> onFingerUp; + // Used by EnhancedTouchSupport but placed here to consolidate static fields + internal int enhancedTouchEnabled; + internal InputSettings.UpdateMode enhancedTouchUpdateMode; + internal FingerAndTouchState playerState; #if UNITY_EDITOR internal FingerAndTouchState editorState; #endif } - private static GlobalState CreateGlobalState() - { // Convenient method since parameterized construction is default - return new GlobalState { historyLengthPerFinger = 64 }; - } + internal static GlobalState s_GlobalState; - internal static GlobalState s_GlobalState = CreateGlobalState(); + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void InitializeGlobalTouchState() + { + // Touch GlobalState doesn't require Dispose operations + s_GlobalState = new GlobalState { historyLengthPerFinger = 64 }; + } internal static ISavedState SaveAndResetState() { @@ -609,7 +615,7 @@ internal static ISavedState SaveAndResetState() () => { /* currently nothing to dispose */ }); // Reset global state - s_GlobalState = CreateGlobalState(); + InitializeGlobalTouchState(); return savedState; } diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/EnhancedTouch/TouchSimulation.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/EnhancedTouch/TouchSimulation.cs index 108e83e5a4..2d99d80fd7 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/EnhancedTouch/TouchSimulation.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/EnhancedTouch/TouchSimulation.cs @@ -317,6 +317,20 @@ private unsafe void UpdateTouch(int touchIndex, int pointerIndex, TouchPhase pha else { touch.touchId = m_TouchIds[touchIndex]; + /*touch.touchId = oldTouchState->touchId; // TODO: Follow-up develop: touch.touchId = m_TouchIds[touchIndex]; + touch.isPrimaryTouch = m_PrimaryTouchIndex == touchIndex; + touch.delta = position - oldTouchState->position; + touch.startPosition = oldTouchState->startPosition; + touch.startTime = oldTouchState->startTime; + touch.tapCount = oldTouchState->tapCount; + + if (phase == TouchPhase.Ended) + { + touch.isTap = time - oldTouchState->startTime <= Touchscreen.settings.tapTime && + (position - oldTouchState->startPosition).sqrMagnitude <= Touchscreen.settings.tapRadiusSquared; + if (touch.isTap) + ++touch.tapCount; + }*/ } //NOTE: Processing these events still happen in the current frame. diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/HID/HIDSupport.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/HID/HIDSupport.cs index 29341e26d8..842d96bb42 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/HID/HIDSupport.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/HID/HIDSupport.cs @@ -94,7 +94,7 @@ public static ReadOnlyArray supportedHIDUsages s_SupportedHIDUsages = value.ToArray(); // Add HIDs we now support. - InputSystem.s_Manager.AddAvailableDevicesThatAreNowRecognized(); + InputSystem.manager.AddAvailableDevicesThatAreNowRecognized(); // Remove HIDs we no longer support. for (var i = 0; i < InputSystem.devices.Count; ++i) diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInput.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInput.cs index df23a4ff96..b9fab3607c 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInput.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInput.cs @@ -800,7 +800,7 @@ public ReadOnlyArray devices /// /// /// - public static ReadOnlyArray all => new ReadOnlyArray(s_AllActivePlayers, 0, s_AllActivePlayersCount); + public static ReadOnlyArray all => new ReadOnlyArray(s_GlobalState.allActivePlayers, 0, s_GlobalState.allActivePlayersCount); /// /// Whether PlayerInput operates in single-player mode. @@ -814,7 +814,7 @@ public ReadOnlyArray devices /// /// public static bool isSinglePlayer => - s_AllActivePlayersCount <= 1 && + s_GlobalState.allActivePlayersCount <= 1 && (PlayerInputManager.instance == null || !PlayerInputManager.instance.joiningEnabled); /// @@ -996,9 +996,9 @@ public void SwitchCurrentActionMap(string mapNameOrId) /// public static PlayerInput GetPlayerByIndex(int playerIndex) { - for (var i = 0; i < s_AllActivePlayersCount; ++i) - if (s_AllActivePlayers[i].playerIndex == playerIndex) - return s_AllActivePlayers[i]; + for (var i = 0; i < s_GlobalState.allActivePlayersCount; ++i) + if (s_GlobalState.allActivePlayers[i].playerIndex == playerIndex) + return s_GlobalState.allActivePlayers[i]; return null; } @@ -1022,10 +1022,10 @@ public static PlayerInput FindFirstPairedToDevice(InputDevice device) if (device == null) throw new ArgumentNullException(nameof(device)); - for (var i = 0; i < s_AllActivePlayersCount; ++i) + for (var i = 0; i < s_GlobalState.allActivePlayersCount; ++i) { - if (ReadOnlyArrayExtensions.ContainsReference(s_AllActivePlayers[i].devices, device)) - return s_AllActivePlayers[i]; + if (ReadOnlyArrayExtensions.ContainsReference(s_GlobalState.allActivePlayers[i].devices, device)) + return s_GlobalState.allActivePlayers[i]; } return null; @@ -1051,11 +1051,11 @@ public static PlayerInput Instantiate(GameObject prefab, int playerIndex = -1, s throw new ArgumentNullException(nameof(prefab)); // Set initialization data. - s_InitPlayerIndex = playerIndex; - s_InitSplitScreenIndex = splitScreenIndex; - s_InitControlScheme = controlScheme; + s_GlobalState.initPlayerIndex = playerIndex; + s_GlobalState.initSplitScreenIndex = splitScreenIndex; + s_GlobalState.initControlScheme = controlScheme; if (pairWithDevice != null) - ArrayHelpers.AppendWithCapacity(ref s_InitPairWithDevices, ref s_InitPairWithDevicesCount, pairWithDevice); + ArrayHelpers.AppendWithCapacity(ref s_GlobalState.initPairWithDevices, ref s_GlobalState.initPairWithDevicesCount, pairWithDevice); return DoInstantiate(prefab); } @@ -1083,13 +1083,13 @@ public static PlayerInput Instantiate(GameObject prefab, int playerIndex = -1, s throw new ArgumentNullException(nameof(prefab)); // Set initialization data. - s_InitPlayerIndex = playerIndex; - s_InitSplitScreenIndex = splitScreenIndex; - s_InitControlScheme = controlScheme; + s_GlobalState.initPlayerIndex = playerIndex; + s_GlobalState.initSplitScreenIndex = splitScreenIndex; + s_GlobalState.initControlScheme = controlScheme; if (pairWithDevices != null) { for (var i = 0; i < pairWithDevices.Length; ++i) - ArrayHelpers.AppendWithCapacity(ref s_InitPairWithDevices, ref s_InitPairWithDevicesCount, pairWithDevices[i]); + ArrayHelpers.AppendWithCapacity(ref s_GlobalState.initPairWithDevices, ref s_GlobalState.initPairWithDevicesCount, pairWithDevices[i]); } return DoInstantiate(prefab); @@ -1097,7 +1097,7 @@ public static PlayerInput Instantiate(GameObject prefab, int playerIndex = -1, s private static PlayerInput DoInstantiate(GameObject prefab) { - var destroyIfDeviceSetupUnsuccessful = s_DestroyIfDeviceSetupUnsuccessful; + var destroyIfDeviceSetupUnsuccessful = s_GlobalState.destroyIfDeviceSetupUnsuccessful; GameObject instance; try @@ -1108,13 +1108,13 @@ private static PlayerInput DoInstantiate(GameObject prefab) finally { // Reset init data. - s_InitPairWithDevicesCount = 0; - if (s_InitPairWithDevices != null) - Array.Clear(s_InitPairWithDevices, 0, s_InitPairWithDevicesCount); - s_InitControlScheme = null; - s_InitPlayerIndex = -1; - s_InitSplitScreenIndex = -1; - s_DestroyIfDeviceSetupUnsuccessful = false; + s_GlobalState.initPairWithDevicesCount = 0; + if (s_GlobalState.initPairWithDevices != null) + Array.Clear(s_GlobalState.initPairWithDevices, 0, s_GlobalState.initPairWithDevicesCount); + s_GlobalState.initControlScheme = null; + s_GlobalState.initPlayerIndex = -1; + s_GlobalState.initSplitScreenIndex = -1; + s_GlobalState.destroyIfDeviceSetupUnsuccessful = false; } var playerInput = instance.GetComponentInChildren(); @@ -1180,18 +1180,41 @@ private static PlayerInput DoInstantiate(GameObject prefab) [NonSerialized] private Action m_DeviceChangeDelegate; [NonSerialized] private bool m_OnDeviceChangeHooked; - internal static int s_AllActivePlayersCount; - internal static PlayerInput[] s_AllActivePlayers; - private static Action s_UserChangeDelegate; + /// + /// Holds global (static) Player data + /// + internal struct GlobalState + { + public int allActivePlayersCount; + public PlayerInput[] allActivePlayers; + public Action userChangeDelegate; + + // The following information is used when the next PlayerInput component is enabled. + + public int initPairWithDevicesCount; + public InputDevice[] initPairWithDevices; + public int initPlayerIndex; + public int initSplitScreenIndex; + public string initControlScheme; + public bool destroyIfDeviceSetupUnsuccessful; + } + private static GlobalState s_GlobalState; - // The following information is used when the next PlayerInput component is enabled. + // For sanity purposes, GlobalState is private with properties accessing specific fields + internal static int allActivePlayersCount => s_GlobalState.allActivePlayersCount; + internal static PlayerInput[] allActivePlayers => s_GlobalState.allActivePlayers; + internal static bool destroyIfDeviceSetupUnsuccessful { get; set; } - private static int s_InitPairWithDevicesCount; - private static InputDevice[] s_InitPairWithDevices; - private static int s_InitPlayerIndex = -1; - private static int s_InitSplitScreenIndex = -1; - private static string s_InitControlScheme; - internal static bool s_DestroyIfDeviceSetupUnsuccessful; + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void InitializeGlobalPlayerState() + { + // Touch GlobalState doesn't require Dispose operations + s_GlobalState = new PlayerInput.GlobalState + { + initPlayerIndex = -1, + initSplitScreenIndex = -1 + }; + } private void InitializeActions() { @@ -1202,8 +1225,8 @@ private void InitializeActions() // Check if we need to duplicate our actions by looking at all other players. If any // has the same actions, duplicate. - for (var i = 0; i < s_AllActivePlayersCount; ++i) - if (s_AllActivePlayers[i].m_Actions == m_Actions && s_AllActivePlayers[i] != this) + for (var i = 0; i < s_GlobalState.allActivePlayersCount; ++i) + if (s_GlobalState.allActivePlayers[i].m_Actions == m_Actions && s_GlobalState.allActivePlayers[i] != this) { var oldActions = m_Actions; m_Actions = Instantiate(m_Actions); @@ -1394,10 +1417,10 @@ private void AssignUserAndDevices() { // If we have devices we are meant to pair with, do so. Otherwise, don't // do anything as we don't know what kind of input to look for. - if (s_InitPairWithDevicesCount > 0) + if (s_GlobalState.initPairWithDevicesCount > 0) { - for (var i = 0; i < s_InitPairWithDevicesCount; ++i) - m_InputUser = InputUser.PerformPairingWithDevice(s_InitPairWithDevices[i], m_InputUser); + for (var i = 0; i < s_GlobalState.initPairWithDevicesCount; ++i) + m_InputUser = InputUser.PerformPairingWithDevice(s_GlobalState.initPairWithDevices[i], m_InputUser); } else { @@ -1411,15 +1434,15 @@ private void AssignUserAndDevices() // If we have control schemes, try to find the one we should use. if (m_Actions.controlSchemes.Count > 0) { - if (!string.IsNullOrEmpty(s_InitControlScheme)) + if (!string.IsNullOrEmpty(s_GlobalState.initControlScheme)) { // We've been given a control scheme to initialize this. Try that one and // that one only. Might mean we end up with missing devices. - var controlScheme = m_Actions.FindControlScheme(s_InitControlScheme); + var controlScheme = m_Actions.FindControlScheme(s_GlobalState.initControlScheme); if (controlScheme == null) { - Debug.LogError($"No control scheme '{s_InitControlScheme}' in '{m_Actions}'", this); + Debug.LogError($"No control scheme '{s_GlobalState.initControlScheme}' in '{m_Actions}'", this); } else { @@ -1443,13 +1466,13 @@ private void AssignUserAndDevices() // If we did not end up with a usable scheme by now but we've been given devices to pair with, // search for a control scheme matching the given devices. - if (s_InitPairWithDevicesCount > 0 && (!m_InputUser.valid || m_InputUser.controlScheme == null)) + if (s_GlobalState.initPairWithDevicesCount > 0 && (!m_InputUser.valid || m_InputUser.controlScheme == null)) { // The devices we've been given may not be all the devices required to satisfy a given control scheme so we // want to pick any one control scheme that is the best match for the devices we have regardless of whether // we'll need additional devices. TryToActivateControlScheme will take care of that. var controlScheme = InputControlScheme.FindControlSchemeForDevices( - new ReadOnlyArray(s_InitPairWithDevices, 0, s_InitPairWithDevicesCount), m_Actions.controlSchemes, + new ReadOnlyArray(s_GlobalState.initPairWithDevices, 0, s_GlobalState.initPairWithDevicesCount), m_Actions.controlSchemes, allowUnsuccesfulMatch: true); if (controlScheme != null) TryToActivateControlScheme(controlScheme.Value); @@ -1457,7 +1480,7 @@ private void AssignUserAndDevices() // If we don't have a working control scheme by now and we haven't been instructed to use // one specific control scheme, try each one in the asset one after the other until we // either find one we can use or run out of options. - else if ((!m_InputUser.valid || m_InputUser.controlScheme == null) && string.IsNullOrEmpty(s_InitControlScheme)) + else if ((!m_InputUser.valid || m_InputUser.controlScheme == null) && string.IsNullOrEmpty(s_GlobalState.initControlScheme)) { using (var availableDevices = InputUser.GetUnpairedInputDevices()) { @@ -1483,10 +1506,10 @@ private void AssignUserAndDevices() // device is present that matches the binding and that isn't used by any other player, we'll // pair to the player. - if (s_InitPairWithDevicesCount > 0) + if (s_GlobalState.initPairWithDevicesCount > 0) { - for (var i = 0; i < s_InitPairWithDevicesCount; ++i) - m_InputUser = InputUser.PerformPairingWithDevice(s_InitPairWithDevices[i], m_InputUser); + for (var i = 0; i < s_GlobalState.initPairWithDevicesCount; ++i) + m_InputUser = InputUser.PerformPairingWithDevice(s_GlobalState.initPairWithDevices[i], m_InputUser); } else { @@ -1539,7 +1562,7 @@ private bool TryToActivateControlScheme(InputControlScheme controlScheme) ////FIXME: this will fall apart if account management is involved and a user needs to log in on device first // Pair any devices we may have been given. - if (s_InitPairWithDevicesCount > 0) + if (s_GlobalState.initPairWithDevicesCount > 0) { ////REVIEW: should AndPairRemainingDevices() require that there is at least one existing //// device paired to the user that is usable with the given control scheme? @@ -1549,17 +1572,17 @@ private bool TryToActivateControlScheme(InputControlScheme controlScheme) // we have the player grab all the devices in s_InitPairWithDevices along with a control // scheme that fits none of them and then AndPairRemainingDevices() supplying the devices // actually needed by the control scheme. - for (var i = 0; i < s_InitPairWithDevicesCount; ++i) + for (var i = 0; i < s_GlobalState.initPairWithDevicesCount; ++i) { - var device = s_InitPairWithDevices[i]; + var device = s_GlobalState.initPairWithDevices[i]; if (!controlScheme.SupportsDevice(device)) return false; } // We're good. Give the devices to the user. - for (var i = 0; i < s_InitPairWithDevicesCount; ++i) + for (var i = 0; i < s_GlobalState.initPairWithDevicesCount; ++i) { - var device = s_InitPairWithDevices[i]; + var device = s_GlobalState.initPairWithDevices[i]; m_InputUser = InputUser.PerformPairingWithDevice(device, m_InputUser); } } @@ -1580,16 +1603,16 @@ private bool TryToActivateControlScheme(InputControlScheme controlScheme) private void AssignPlayerIndex() { - if (s_InitPlayerIndex != -1) - m_PlayerIndex = s_InitPlayerIndex; + if (s_GlobalState.initPlayerIndex != -1) + m_PlayerIndex = s_GlobalState.initPlayerIndex; else { var minPlayerIndex = int.MaxValue; var maxPlayerIndex = int.MinValue; - for (var i = 0; i < s_AllActivePlayersCount; ++i) + for (var i = 0; i < s_GlobalState.allActivePlayersCount; ++i) { - var playerIndex = s_AllActivePlayers[i].playerIndex; + var playerIndex = s_GlobalState.allActivePlayers[i].playerIndex; minPlayerIndex = Math.Min(minPlayerIndex, playerIndex); maxPlayerIndex = Math.Max(maxPlayerIndex, playerIndex); } @@ -1619,7 +1642,7 @@ private void AssignPlayerIndex() } } - #if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS +#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS void Reset() { // Set default actions to project wide actions. @@ -1627,7 +1650,7 @@ void Reset() // TODO Need to monitor changes? } - #endif +#endif private void OnEnable() { @@ -1642,23 +1665,23 @@ private void OnEnable() } // Split-screen index defaults to player index. - if (s_InitSplitScreenIndex >= 0) + if (s_GlobalState.initSplitScreenIndex >= 0) m_SplitScreenIndex = splitScreenIndex; else m_SplitScreenIndex = playerIndex; // Add to global list and sort it by player index. - ArrayHelpers.AppendWithCapacity(ref s_AllActivePlayers, ref s_AllActivePlayersCount, this); - for (var i = 1; i < s_AllActivePlayersCount; ++i) - for (var j = i; j > 0 && s_AllActivePlayers[j - 1].playerIndex > s_AllActivePlayers[j].playerIndex; --j) - s_AllActivePlayers.SwapElements(j, j - 1); + ArrayHelpers.AppendWithCapacity(ref s_GlobalState.allActivePlayers, ref s_GlobalState.allActivePlayersCount, this); + for (var i = 1; i < s_GlobalState.allActivePlayersCount; ++i) + for (var j = i; j > 0 && s_GlobalState.allActivePlayers[j - 1].playerIndex > s_GlobalState.allActivePlayers[j].playerIndex; --j) + s_GlobalState.allActivePlayers.SwapElements(j, j - 1); // If it's the first player, hook into user change notifications. - if (s_AllActivePlayersCount == 1) + if (s_GlobalState.allActivePlayersCount == 1) { - if (s_UserChangeDelegate == null) - s_UserChangeDelegate = OnUserChange; - InputUser.onChange += s_UserChangeDelegate; + if (s_GlobalState.userChangeDelegate == null) + s_GlobalState.userChangeDelegate = OnUserChange; + InputUser.onChange += s_GlobalState.userChangeDelegate; } // In single player, set up for automatic device switching. @@ -1731,13 +1754,13 @@ private void OnDisable() m_Enabled = false; // Remove from global list. - var index = ArrayHelpers.IndexOfReference(s_AllActivePlayers, this, s_AllActivePlayersCount); + var index = ArrayHelpers.IndexOfReference(s_GlobalState.allActivePlayers, this, s_GlobalState.allActivePlayersCount); if (index != -1) - ArrayHelpers.EraseAtWithCapacity(s_AllActivePlayers, ref s_AllActivePlayersCount, index); + ArrayHelpers.EraseAtWithCapacity(s_GlobalState.allActivePlayers, ref s_GlobalState.allActivePlayersCount, index); // Unhook from change notifications if we're the last player. - if (s_AllActivePlayersCount == 0 && s_UserChangeDelegate != null) - InputUser.onChange -= s_UserChangeDelegate; + if (s_GlobalState.allActivePlayersCount == 0 && s_GlobalState.userChangeDelegate != null) + InputUser.onChange -= s_GlobalState.userChangeDelegate; StopListeningForUnpairedDeviceActivity(); StopListeningForDeviceChanges(); @@ -1839,9 +1862,9 @@ private static void OnUserChange(InputUser user, InputUserChange change, InputDe { case InputUserChange.DeviceLost: case InputUserChange.DeviceRegained: - for (var i = 0; i < s_AllActivePlayersCount; ++i) + for (var i = 0; i < s_GlobalState.allActivePlayersCount; ++i) { - var player = s_AllActivePlayers[i]; + var player = s_GlobalState.allActivePlayers[i]; if (player.m_InputUser == user) { if (change == InputUserChange.DeviceLost) @@ -1853,9 +1876,9 @@ private static void OnUserChange(InputUser user, InputUserChange change, InputDe break; case InputUserChange.ControlsChanged: - for (var i = 0; i < s_AllActivePlayersCount; ++i) + for (var i = 0; i < s_GlobalState.allActivePlayersCount; ++i) { - var player = s_AllActivePlayers[i]; + var player = s_GlobalState.allActivePlayers[i]; if (player.m_InputUser == user) player.HandleControlsChanged(); } diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInputManager.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInputManager.cs index 2c2c557766..196c1f1de9 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInputManager.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInputManager.cs @@ -129,7 +129,7 @@ public bool splitScreen /// /// This count corresponds to all instances that are currently enabled. /// - public int playerCount => PlayerInput.s_AllActivePlayersCount; + public int playerCount => PlayerInput.allActivePlayersCount; ////FIXME: this needs to be settable /// @@ -432,7 +432,7 @@ public PlayerInput JoinPlayer(int playerIndex = -1, int splitScreenIndex = -1, s if (!CheckIfPlayerCanJoin(playerIndex)) return null; - PlayerInput.s_DestroyIfDeviceSetupUnsuccessful = true; + PlayerInput.destroyIfDeviceSetupUnsuccessful = true; return PlayerInput.Instantiate(m_PlayerPrefab, playerIndex: playerIndex, splitScreenIndex: splitScreenIndex, controlScheme: controlScheme, pairWithDevice: pairWithDevice); } @@ -458,7 +458,7 @@ public PlayerInput JoinPlayer(int playerIndex = -1, int splitScreenIndex = -1, s if (!CheckIfPlayerCanJoin(playerIndex)) return null; - PlayerInput.s_DestroyIfDeviceSetupUnsuccessful = true; + PlayerInput.destroyIfDeviceSetupUnsuccessful = true; return PlayerInput.Instantiate(m_PlayerPrefab, playerIndex: playerIndex, splitScreenIndex: splitScreenIndex, controlScheme: controlScheme, pairWithDevices: pairWithDevices); } @@ -508,12 +508,12 @@ private bool CheckIfPlayerCanJoin(int playerIndex = -1) // If we have a player index, make sure it's unique. if (playerIndex != -1) { - for (var i = 0; i < PlayerInput.s_AllActivePlayersCount; ++i) - if (PlayerInput.s_AllActivePlayers[i].playerIndex == playerIndex) + for (var i = 0; i < PlayerInput.allActivePlayersCount; ++i) + if (PlayerInput.allActivePlayers[i].playerIndex == playerIndex) { Debug.LogError( - $"Player index #{playerIndex} is already taken by player {PlayerInput.s_AllActivePlayers[i]}", - PlayerInput.s_AllActivePlayers[i]); + $"Player index #{playerIndex} is already taken by player {PlayerInput.allActivePlayers[i]}", + PlayerInput.allActivePlayers[i]); return false; } } @@ -565,8 +565,8 @@ private void OnEnable() } // Join all players already in the game. - for (var i = 0; i < PlayerInput.s_AllActivePlayersCount; ++i) - NotifyPlayerJoined(PlayerInput.s_AllActivePlayers[i]); + for (var i = 0; i < PlayerInput.allActivePlayersCount; ++i) + NotifyPlayerJoined(PlayerInput.allActivePlayers[i]); if (m_AllowJoining) EnableJoining(); diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/Steam/SteamSupport.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/Steam/SteamSupport.cs index 357b65676e..c84dee3b92 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/Steam/SteamSupport.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/Steam/SteamSupport.cs @@ -27,7 +27,7 @@ public static ISteamControllerAPI api set { s_API = value; - InstallHooks(s_API != null); + InstallControllerUpdateHooks(s_API != null); } } @@ -38,11 +38,19 @@ internal static ISteamControllerAPI GetAPIAndRequireItToBeSet() return s_API; } - internal static SteamHandle[] s_ConnectedControllers; - internal static SteamController[] s_InputDevices; - internal static int s_InputDeviceCount; - internal static bool s_HooksInstalled; - internal static ISteamControllerAPI s_API; + /// + /// Returns if the controller Update event handlers have been set or not. + /// + /// + /// The s_ConnectedControllers array is allocated in response to setting event handlers and + /// so it can double as our "is installed" flag. + /// + private static bool updateHooksInstalled => s_ConnectedControllers != null; + + private static SteamHandle[] s_ConnectedControllers; + private static SteamController[] s_InputDevices; + private static int s_InputDeviceCount; + private static ISteamControllerAPI s_API; private const int STEAM_CONTROLLER_MAX_COUNT = 16; @@ -54,22 +62,35 @@ public static void Initialize() // We use this as a base layout. InputSystem.RegisterLayout(); - if (api != null) - InstallHooks(true); + InstallControllerUpdateHooks(s_API != null); } - private static void InstallHooks(bool state) + /// + /// Disable Steam controller API support and reset the state. + /// + internal static void Shutdown() { - Debug.Assert(api != null); - if (state && !s_HooksInstalled) + InstallControllerUpdateHooks(false); + + s_API = null; + s_InputDevices = null; + s_InputDeviceCount = 0; + } + + private static void InstallControllerUpdateHooks(bool state) + { + Debug.Assert(api != null || !state); + if (state && !updateHooksInstalled) { + s_ConnectedControllers = new SteamHandle[STEAM_CONTROLLER_MAX_COUNT]; InputSystem.onBeforeUpdate += OnUpdate; InputSystem.onActionChange += OnActionChange; } - else if (!state && s_HooksInstalled) + else if (!state && updateHooksInstalled) { InputSystem.onBeforeUpdate -= OnUpdate; InputSystem.onActionChange -= OnActionChange; + s_ConnectedControllers = null; } } @@ -125,8 +146,6 @@ private static void OnUpdate() api.RunFrame(); // Check if we have any new controllers have appeared. - if (s_ConnectedControllers == null) - s_ConnectedControllers = new SteamHandle[STEAM_CONTROLLER_MAX_COUNT]; var numConnectedControllers = api.GetConnectedControllers(s_ConnectedControllers); for (var i = 0; i < numConnectedControllers; ++i) { diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/Switch/SwitchProControllerHID.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/Switch/SwitchProControllerHID.cs index 6a619301da..97704c9347 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/Switch/SwitchProControllerHID.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/Switch/SwitchProControllerHID.cs @@ -253,7 +253,7 @@ public unsafe void OnStateEvent(InputEventPtr eventPtr) || newState->buttons2 != currentState->buttons2; if (!actuated) - InputSystem.s_Manager.DontMakeCurrentlyUpdatingDeviceCurrent(); + InputSystem.manager.DontMakeCurrentlyUpdatingDeviceCurrent(); } InputState.Change(this, eventPtr); diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs index c65c6a6700..153d6f977e 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs @@ -1446,7 +1446,8 @@ public InputActionReference trackedDevicePosition public void AssignDefaultActions() { - if (defaultActions == null) + // Without Domain Reloads, the InputActionAsset could be "null" even if defaultActions is valid + if (defaultActions == null || defaultActions.asset == null) { defaultActions = new DefaultInputActions(); } diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModuleEditor.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModuleEditor.cs index a160f2c9c0..e3dc4688d1 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModuleEditor.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModuleEditor.cs @@ -13,6 +13,7 @@ namespace UnityEngine.InputSystem.UI.Editor [InitializeOnLoad] internal class InputSystemUIInputModuleEditor : UnityEditor.Editor { + // ISX-1966 - It's unclear if this initializer will work correctly with CoreCLR and needs to be investigated. static InputSystemUIInputModuleEditor() { #if UNITY_6000_0_OR_NEWER && ENABLE_INPUT_SYSTEM diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/Users/InputUser.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/Users/InputUser.cs index 02518240b7..f59f46bfd2 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/Users/InputUser.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/Users/InputUser.cs @@ -1876,6 +1876,13 @@ private struct GlobalState private static GlobalState s_GlobalState; + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void InitializeGlobalUserState() + { + ResetGlobals(); + s_GlobalState = default; + } + internal static ISavedState SaveAndResetState() { // Save current state and provide an opaque interface to restore it diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/iOS/iOSSupport.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/iOS/iOSSupport.cs index 981bac4fad..d2975cf469 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/iOS/iOSSupport.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/iOS/iOSSupport.cs @@ -53,7 +53,7 @@ public static void Initialize() InputSystem.RegisterLayout(); // Don't add devices for InputTestRuntime // TODO: Maybe there should be a better place for adding device from C# - if (InputSystem.s_Manager.m_Runtime is NativeInputRuntime) + if (InputSystem.manager.runtime is NativeInputRuntime) { if (iOSStepCounter.IsAvailable()) InputSystem.AddDevice(); diff --git a/Packages/com.unity.inputsystem/InputSystem/State/InputState.cs b/Packages/com.unity.inputsystem/InputSystem/State/InputState.cs index 787b5df377..69bae00f6b 100644 --- a/Packages/com.unity.inputsystem/InputSystem/State/InputState.cs +++ b/Packages/com.unity.inputsystem/InputSystem/State/InputState.cs @@ -44,8 +44,8 @@ public static class InputState /// public static event Action onChange { - add => InputSystem.s_Manager.onDeviceStateChange += value; - remove => InputSystem.s_Manager.onDeviceStateChange -= value; + add => InputSystem.manager.onDeviceStateChange += value; + remove => InputSystem.manager.onDeviceStateChange -= value; } public static unsafe void Change(InputDevice device, InputEventPtr eventPtr, InputUpdateType updateType = default) @@ -65,7 +65,7 @@ public static unsafe void Change(InputDevice device, InputEventPtr eventPtr, Inp else { #if UNITY_EDITOR - InputSystem.s_Manager.m_Diagnostics?.OnEventFormatMismatch(eventPtr, device); + InputSystem.manager.m_Diagnostics?.OnEventFormatMismatch(eventPtr, device); #endif return; } @@ -75,8 +75,8 @@ public static unsafe void Change(InputDevice device, InputEventPtr eventPtr, Inp $"State format {stateFormat} from event does not match state format {device.stateBlock.format} of device {device}", nameof(eventPtr)); - InputSystem.s_Manager.UpdateState(device, eventPtr, - updateType != default ? updateType : InputSystem.s_Manager.defaultUpdateType); + InputSystem.manager.UpdateState(device, eventPtr, + updateType != default ? updateType : InputSystem.manager.defaultUpdateType); } /// @@ -122,8 +122,8 @@ public static unsafe void Change(InputControl control, ref TState state, var statePtr = UnsafeUtility.AddressOf(ref state); var stateOffset = control.stateBlock.byteOffset - device.stateBlock.byteOffset; - InputSystem.s_Manager.UpdateState(device, - updateType != default ? updateType : InputSystem.s_Manager.defaultUpdateType, statePtr, stateOffset, + InputSystem.manager.UpdateState(device, + updateType != default ? updateType : InputSystem.manager.defaultUpdateType, statePtr, stateOffset, (uint)stateSize, eventPtr.valid ? eventPtr.internalTime @@ -207,7 +207,7 @@ public static void AddChangeMonitor(InputControl control, IInputStateChangeMonit if (!control.device.added) throw new ArgumentException($"Device for control '{control}' has not been added to system"); - InputSystem.s_Manager.AddStateChangeMonitor(control, monitor, monitorIndex, groupIndex); + InputSystem.manager.AddStateChangeMonitor(control, monitor, monitorIndex, groupIndex); } public static IInputStateChangeMonitor AddChangeMonitor(InputControl control, @@ -232,7 +232,7 @@ public static void RemoveChangeMonitor(InputControl control, IInputStateChangeMo if (monitor == null) throw new ArgumentNullException(nameof(monitor)); - InputSystem.s_Manager.RemoveStateChangeMonitor(control, monitor, monitorIndex); + InputSystem.manager.RemoveStateChangeMonitor(control, monitor, monitorIndex); } /// @@ -253,7 +253,7 @@ public static void AddChangeMonitorTimeout(InputControl control, IInputStateChan if (monitor == null) throw new ArgumentNullException(nameof(monitor)); - InputSystem.s_Manager.AddStateChangeMonitorTimeout(control, monitor, time, monitorIndex, timerIndex); + InputSystem.manager.AddStateChangeMonitorTimeout(control, monitor, time, monitorIndex, timerIndex); } public static void RemoveChangeMonitorTimeout(IInputStateChangeMonitor monitor, long monitorIndex = -1, int timerIndex = -1) @@ -261,7 +261,7 @@ public static void RemoveChangeMonitorTimeout(IInputStateChangeMonitor monitor, if (monitor == null) throw new ArgumentNullException(nameof(monitor)); - InputSystem.s_Manager.RemoveStateChangeMonitorTimeout(monitor, monitorIndex, timerIndex); + InputSystem.manager.RemoveStateChangeMonitorTimeout(monitor, monitorIndex, timerIndex); } private class StateChangeMonitorDelegate : IInputStateChangeMonitor diff --git a/Packages/com.unity.inputsystem/InputSystem/State/InputStateHistory.cs b/Packages/com.unity.inputsystem/InputSystem/State/InputStateHistory.cs index e98443475f..49c02bc30f 100644 --- a/Packages/com.unity.inputsystem/InputSystem/State/InputStateHistory.cs +++ b/Packages/com.unity.inputsystem/InputSystem/State/InputStateHistory.cs @@ -115,7 +115,7 @@ public int extraMemoryPerRecord public InputUpdateType updateMask { - get => m_UpdateMask ?? InputSystem.s_Manager.updateMask & ~InputUpdateType.Editor; + get => m_UpdateMask ?? InputSystem.manager.updateMask & ~InputUpdateType.Editor; set { if (value == InputUpdateType.None) diff --git a/Packages/com.unity.inputsystem/InputSystem/Utilities/DirtyAssetTracker.cs b/Packages/com.unity.inputsystem/InputSystem/Utilities/DirtyAssetTracker.cs new file mode 100644 index 0000000000..5586d4741e --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Utilities/DirtyAssetTracker.cs @@ -0,0 +1,44 @@ +#if UNITY_EDITOR +using System.Collections.Generic; +using UnityEditor; + +namespace UnityEngine.InputSystem.Utilities +{ + internal static class DirtyAssetTracker + { + /// + /// Keep track of InputActionAsset assets that you want to re-load. This is useful because some user actions, + /// such as adding a new input binding at runtime, change the in-memory representation of the input action asset and + /// those changes survive when exiting Play mode. If you re-open an Input Action Asset in the Editor that has been changed + /// this way, you see the new bindings that have been added during Play mode which you might not typically want to happen. + /// + /// You can avoid this by force re-loading from disk any asset that has been marked as dirty. + /// + /// + public static void TrackDirtyInputActionAsset(InputActionAsset asset) + { + if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(asset, out string assetGuid, out long _) == false) + return; + + s_TrackedDirtyAssets.Add(assetGuid); + } + + public static void ReloadDirtyAssets() + { + foreach (var assetGuid in s_TrackedDirtyAssets) + { + var assetPath = AssetDatabase.GUIDToAssetPath(assetGuid); + + if (string.IsNullOrEmpty(assetPath)) + continue; + + AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate); + } + + s_TrackedDirtyAssets.Clear(); + } + + private static HashSet s_TrackedDirtyAssets = new HashSet(); + } +} +#endif diff --git a/Packages/com.unity.inputsystem/InputSystem/Utilities/DirtyAssetTracker.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Utilities/DirtyAssetTracker.cs.meta new file mode 100644 index 0000000000..38c81f9be8 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Utilities/DirtyAssetTracker.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fa1cedc2b36c5fc49a203b0e02a22d64 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestFixture.cs b/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestFixture.cs index 2727be91be..0197b804b6 100644 --- a/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestFixture.cs +++ b/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestFixture.cs @@ -11,6 +11,8 @@ using UnityEngine.InputSystem.Utilities; using UnityEngine.TestTools; using UnityEngine.TestTools.Utils; +using UnityEngine.InputSystem.Users; + #if UNITY_EDITOR using UnityEditor; using UnityEngine.InputSystem.Editor; @@ -77,6 +79,8 @@ public virtual void Setup() { try { + m_StateManager = new InputTestStateManager(); + // Apparently, NUnit is reusing instances :( m_KeyInfos = default; m_IsUnityTest = default; @@ -92,7 +96,7 @@ public virtual void Setup() // Push current input system state on stack. #if DEVELOPMENT_BUILD || UNITY_EDITOR - InputSystem.SaveAndReset(enableRemoting: false, runtime: runtime); + m_StateManager.SaveAndReset(false, runtime); #endif // Override the editor messing with logic like canRunInBackground and focus and // make it behave like in the player. @@ -104,7 +108,7 @@ public virtual void Setup() // so turn them off. #if UNITY_EDITOR if (Application.isPlaying && IsUnityTest()) - InputSystem.s_Manager.m_UpdateMask &= ~InputUpdateType.Editor; + InputSystem.manager.m_UpdateMask &= ~InputUpdateType.Editor; #endif // We use native collections in a couple places. We when leak them, we want to know where exactly @@ -120,7 +124,7 @@ public virtual void Setup() NativeInputRuntime.instance.onUpdate = (InputUpdateType updateType, ref InputEventBuffer buffer) => { - if (InputSystem.s_Manager.ShouldRunUpdate(updateType)) + if (InputSystem.manager.ShouldRunUpdate(updateType)) InputSystem.Update(updateType); // We ignore any input coming from native. buffer.Reset(); @@ -175,7 +179,7 @@ public virtual void TearDown() try { #if DEVELOPMENT_BUILD || UNITY_EDITOR - InputSystem.Restore(); + m_StateManager.Restore(); #endif runtime.Dispose(); @@ -297,6 +301,7 @@ public static void AssertStickValues(StickControl stick, Vector2 stickValue, flo Assert.That(stick.right.ReadUnprocessedValue(), Is.EqualTo(right).Within(0.0001), "Incorrect 'right' value"); } + internal InputTestStateManager m_StateManager; private Dictionary> m_KeyInfos; private bool m_Initialized; @@ -899,20 +904,6 @@ public ActionConstraint AndThen(ActionConstraint constraint) } } - #if UNITY_EDITOR - internal void SimulateDomainReload() - { - // This quite invasively goes into InputSystem internals. Unfortunately, we - // have no proper way of simulating domain reloads ATM. So we directly call various - // internal methods here in a sequence similar to what we'd get during a domain reload. - - InputSystem.s_SystemObject.OnBeforeSerialize(); - InputSystem.s_SystemObject = null; - InputSystem.InitializeInEditor(runtime); - } - - #endif - #if UNITY_EDITOR /// /// Represents an analytics registration event captured by test harness. diff --git a/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestStateManager.cs b/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestStateManager.cs new file mode 100644 index 0000000000..6c357c5038 --- /dev/null +++ b/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestStateManager.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using Unity.Collections; +using Unity.Profiling; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.LowLevel; +using UnityEngine.InputSystem.Users; +using UnityEngine.Profiling; +using UnityEngine.InputSystem.EnhancedTouch; + +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine.InputSystem.Editor; +#endif + +namespace UnityEngine.InputSystem +{ + /// + /// Provides functions for saving and restore the InputSystem state across tests and domain reloads. + /// + internal class InputTestStateManager + { + static readonly ProfilerMarker k_InputResetMarker = new ProfilerMarker("InputSystem.Reset"); + + public InputSystemState GetSavedState() + { + return m_SavedStateStack.Peek(); + } + + /// + /// Push the current state of the input system onto a stack and + /// reset the system to its default state. + /// + /// + /// The save stack is not able to survive domain reloads. It is intended solely + /// for use in tests. + /// + public void SaveAndReset(bool enableRemoting, IInputRuntime runtime) + { + ////FIXME: does not preserve global state in InputActionState + ////TODO: preserve InputUser state + ////TODO: preserve EnhancedTouchSupport state + + m_SavedStateStack.Push(new InputSystemState + { + manager = InputSystem.manager, + remote = InputSystem.remoting, + remoteConnection = InputSystem.remoteConnection, + managerState = InputSystem.manager.SaveState(), + remotingState = InputSystem.remoting?.SaveState() ?? new InputRemoting.SerializedState(), +#if UNITY_EDITOR + userSettings = InputEditorUserSettings.s_Settings, + systemObject = JsonUtility.ToJson(InputSystem.domainStateManager), +#endif + inputActionState = InputActionState.SaveAndResetState(), + touchState = EnhancedTouch.Touch.SaveAndResetState(), + inputUserState = InputUser.SaveAndResetState() + }); + + Reset(enableRemoting, runtime ?? InputRuntime.s_Instance); // Keep current runtime. + } + + /// + /// Return the input system to its default state. + /// + public void Reset(bool enableRemoting, IInputRuntime runtime) + { + k_InputResetMarker.Begin(); + + InputSystem.TestHook_DisableActions(); + + // Some devices keep globals. Get rid of them by pretending the devices + // are removed. + if (InputSystem.manager != null) + { + foreach (var device in InputSystem.manager.devices) + device.NotifyRemoved(); + + InputSystem.manager.UninstallGlobals(); + } + +#if UNITY_EDITOR + // Perform special initialization for running Editor tests + InputSystem.TestHook_InitializeForPlayModeTests(enableRemoting, runtime); +#else + // For Player tests we can use the normal initialization + InputSystem.InitializeInPlayer(runtime, false); +#endif // UNITY_EDITOR + + Mouse.s_PlatformMouseDevice = null; + + InputEventListener.s_ObserverState = default; + InputUser.ResetGlobals(); + EnhancedTouchSupport.Reset(); + + InputSystem.TestHook_EnableActions(); + + k_InputResetMarker.End(); + } + + ////FIXME: this method doesn't restore things like InputDeviceDebuggerWindow.onToolbarGUI + /// + /// Restore the state of the system from the last state pushed with . + /// + public void Restore() + { + Debug.Assert(m_SavedStateStack.Count > 0); + + // Load back previous state. + var state = m_SavedStateStack.Pop(); + + state.inputUserState.StaticDisposeCurrentState(); + state.touchState.StaticDisposeCurrentState(); + state.inputActionState.StaticDisposeCurrentState(); + + InputSystem.TestHook_DestroyAndReset(); + + state.inputUserState.RestoreSavedState(); + state.touchState.RestoreSavedState(); + state.inputActionState.RestoreSavedState(); + + InputSystem.TestHook_RestoreFromSavedState(state); + InputUpdate.Restore(state.managerState.updateState); + + InputSystem.manager.InstallRuntime(InputSystem.manager.runtime); + InputSystem.manager.InstallGlobals(); + + // IMPORTANT + // If InputManager was using the "temporary" settings object, then it'll have been deleted during Reset() + // and the saved Manager settings state will also be null, since it's a ScriptableObject. + // In this case we manually create and set new temp settings object. + if (InputSystem.manager.settings == null) + { + var tmpSettings = ScriptableObject.CreateInstance(); + tmpSettings.hideFlags = HideFlags.HideAndDontSave; + InputSystem.manager.settings = tmpSettings; + } + else InputSystem.manager.ApplySettings(); + +#if UNITY_EDITOR + InputEditorUserSettings.s_Settings = state.userSettings; + JsonUtility.FromJsonOverwrite(state.systemObject, InputSystem.domainStateManager); +#endif + + // Get devices that keep global lists (like Gamepad) to re-initialize them + // by pretending the devices have been added. + foreach (var device in InputSystem.devices) + { + device.NotifyAdded(); + device.MakeCurrent(); + } + } + + private Stack m_SavedStateStack = new Stack(); + } +} diff --git a/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestStateManager.cs.meta b/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestStateManager.cs.meta new file mode 100644 index 0000000000..3242066427 --- /dev/null +++ b/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestStateManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 90487f30114ceb14093b1cc699c8b303 \ No newline at end of file