diff --git a/Assets/Samples/InGameHints/InGameHintsActions.cs b/Assets/Samples/InGameHints/InGameHintsActions.cs index b3a30f8c39..ad5df17f7a 100644 --- a/Assets/Samples/InGameHints/InGameHintsActions.cs +++ b/Assets/Samples/InGameHints/InGameHintsActions.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was auto-generated by com.unity.inputsystem:InputActionCodeGenerator -// version 1.8.1 +// version 1.8.2 // from Assets/Samples/InGameHints/InGameHintsActions.inputactions // // Changes to this file may cause incorrect behavior and will be lost if diff --git a/Assets/Samples/SimpleDemo/SimpleControls.cs b/Assets/Samples/SimpleDemo/SimpleControls.cs index e33d50f171..aae692d2c9 100644 --- a/Assets/Samples/SimpleDemo/SimpleControls.cs +++ b/Assets/Samples/SimpleDemo/SimpleControls.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was auto-generated by com.unity.inputsystem:InputActionCodeGenerator -// version 1.8.1 +// version 1.8.2 // from Assets/Samples/SimpleDemo/SimpleControls.inputactions // // Changes to this file may cause incorrect behavior and will be lost if diff --git a/Assets/Tests/InputSystem.Editor/ControlSchemeEditorTests.cs b/Assets/Tests/InputSystem.Editor/ControlSchemeEditorTests.cs index 2170c7ce1d..a90fedf61f 100644 --- a/Assets/Tests/InputSystem.Editor/ControlSchemeEditorTests.cs +++ b/Assets/Tests/InputSystem.Editor/ControlSchemeEditorTests.cs @@ -197,7 +197,6 @@ public void WhenControlSchemeIsSelected_SelectedControlSchemeIndexIsSet() [Test] [Category("AssetEditor")] - [Ignore("Instability ISX-1905")] public void WhenControlSchemeIsSelected_SelectedControlSchemeIsPopulatedWithSelection() { var asset = TestData.inputActionAsset @@ -260,9 +259,29 @@ public void DuplicateControlSchemeCommand_CreatesCopyOfControlSchemeWithUniqueNa Assert.That(newState.selectedControlScheme.deviceRequirements, Is.EqualTo(state.selectedControlScheme.deviceRequirements)); } + [Test(Description = "Verifies that when duplicating Control Scheme ending on an Int it increments that Int and jumps already existing Int names")] + [Category("AssetEditor")] + public void DuplicateControlSchemeCommand_CreatesCopyOfControlSchemeWithUniqueNameEndingOnIntJumpsExistingNumbers() + { + var asset = TestData.inputActionAsset.Generate(); + + asset.AddControlScheme(new InputControlScheme(("Test"))); + asset.AddControlScheme(new InputControlScheme(("Test1"))); + + //select "Test" Control Scheme + var state = TestData.EditorStateWithAsset(asset).Generate().With(selectedControlScheme: asset.controlSchemes[0]); + + state.serializedObject.Update(); + + //duplicate "Test" + var newState = ControlSchemeCommands.DuplicateSelectedControlScheme()(in state); + + //duplicated Control Scheme should be names "Test2", skipping "Test1" + Assert.That(newState.selectedControlScheme.name, Is.EqualTo("Test2")); + } + [Test] [Category("AssetEditor")] - [Ignore("Disabled: This should not be called in batch mode.")] public void DeleteControlSchemeCommand_DeletesSelectedControlScheme() { var asset = TestData.inputActionAsset.WithControlScheme(TestData.controlScheme.WithOptionalDevice()).Generate(); @@ -284,7 +303,6 @@ public void DeleteControlSchemeCommand_DeletesSelectedControlScheme() [TestCase(3, 2, 1, "Test1")] [TestCase(1, 0, -1, null)] [Category("AssetEditor")] - [Ignore("Disabled: This should not be called in batch mode.")] public void DeleteControlSchemeCommand_SelectsAnotherControlSchemeAfterDelete( int controlSchemeCount, int selectedControlSchemeIndex, diff --git a/Assets/Tests/InputSystem.Editor/ProjectWideInputActionsEditorTests.cs b/Assets/Tests/InputSystem.Editor/ProjectWideInputActionsEditorTests.cs index 37f3b02f82..9b1e31dd75 100644 --- a/Assets/Tests/InputSystem.Editor/ProjectWideInputActionsEditorTests.cs +++ b/Assets/Tests/InputSystem.Editor/ProjectWideInputActionsEditorTests.cs @@ -1,11 +1,13 @@ #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS using System; +using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using NUnit.Framework; using UnityEngine; using UnityEditor; +using UnityEditor.SearchService; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Editor; using UnityEngine.InputSystem.Utilities; @@ -214,40 +216,143 @@ public void ProjectWideActionsAsset_DefaultAssetFileHasDefaultContent() Assert.That(parsedAssetName, Is.EqualTo(expectedName)); } - // This test is only relevant for the InputForUI module which native part was introduced in 2023.2 -#if UNITY_2023_2_OR_NEWER - [Test(Description = "Verifies that modifying the default project-wide action UI map generates console warnings")] + private class TestReporter : ProjectWideActionsAsset.IReportInputActionAssetVerificationErrors + { + public const string kExceptionMessage = "Intentional Exception"; + public readonly List messages; + public bool throwsException; + + public TestReporter(List messages = null, bool throwsException = false) + { + this.messages = messages; + this.throwsException = throwsException; + } + + public void Report(string message) + { + if (throwsException) + throw new Exception(kExceptionMessage); + messages?.Add(message); + } + } + + [Test(Description = "Verifies that the default asset do not generate any verification errors (Regardless of existing requirements)")] [Category(kTestCategory)] - public void ProjectWideActions_ShowsErrorWhenUIActionMapHasNameChanges() + public void ProjectWideActions_ShouldSupportAssetVerification_AndHaveNoVerificationErrorsForDefaultAsset() { - // Create a default template asset that we then modify to generate various warnings + var messages = new List(); var asset = ProjectWideActionsAsset.CreateDefaultAssetAtPath(); + ProjectWideActionsAsset.Verify(asset, new TestReporter(messages)); + Assert.That(messages.Count, Is.EqualTo(0)); + } - var indexOf = asset.m_ActionMaps.IndexOf(x => x.name == "UI"); - var uiMap = asset.m_ActionMaps[indexOf]; + class TestVerifier : ProjectWideActionsAsset.IInputActionAssetVerifier + { + public const string kFailureMessage = "Intentional failure"; + public InputActionAsset forwardedAsset; + public bool throwsException; - // Change the name of the UI action map - uiMap.m_Name = "UI2"; + public TestVerifier(bool throwsException = false) + { + this.throwsException = throwsException; + } - ProjectWideActionsAsset.CheckForDefaultUIActionMapChanges(asset); + public void Verify(InputActionAsset asset, ProjectWideActionsAsset.IReportInputActionAssetVerificationErrors reporter) + { + if (throwsException) + throw new Exception(TestReporter.kExceptionMessage); + forwardedAsset = asset; + reporter.Report(kFailureMessage); + } + } - LogAssert.Expect(LogType.Warning, new Regex("The action map named 'UI' does not exist")); + [Test(Description = "Verifies that the default asset verification registers errors for a registered verifier)")] + [Category(kTestCategory)] + public void ProjectWideActions_ShouldSupportAssetVerification_IfVerifierHasBeenRegistered() + { + var messages = new List(); + var asset = ProjectWideActionsAsset.CreateDefaultAssetAtPath(); + var verifier = new TestVerifier(); + Func factory = () => verifier; + try + { + Assert.That(ProjectWideActionsAsset.RegisterInputActionAssetVerifier(factory), Is.True); + ProjectWideActionsAsset.Verify(asset, new TestReporter(messages)); + Assert.That(messages.Count, Is.EqualTo(1)); + Assert.That(messages[0], Is.EqualTo(TestVerifier.kFailureMessage)); + Assert.That(verifier.forwardedAsset, Is.EqualTo(asset)); + } + finally + { + Assert.That(ProjectWideActionsAsset.UnregisterInputActionAssetVerifier(factory), Is.True); + } + } - // Change the name of some UI map back to default and change the name of the actions - uiMap.m_Name = "UI"; - var defaultActionName0 = uiMap.m_Actions[0].m_Name; - var defaultActionName1 = uiMap.m_Actions[1].m_Name; + [Test(Description = "Verifies that a verification factory cannot be registered twice")] + [Category(kTestCategory)] + public void ProjectWideActions_ShouldReturnError_IfFactoryHasAlreadyBeenRegisteredAndAttemptingToRegisterAgain() + { + Func factory = () => null; + try + { + Assert.That(ProjectWideActionsAsset.RegisterInputActionAssetVerifier(factory), Is.True); + Assert.That(ProjectWideActionsAsset.RegisterInputActionAssetVerifier(factory), Is.False); + } + finally + { + Assert.That(ProjectWideActionsAsset.UnregisterInputActionAssetVerifier(factory), Is.True); + } + } - uiMap.m_Actions[0].Rename("Navigation"); - uiMap.m_Actions[1].Rename("Show"); + [Test(Description = "Verifies that a verification factory cannot be registered twice")] + [Category(kTestCategory)] + public void ProjectWideActions_ShouldReturnError_IfAttemptingToUnregisterAFactoryThatHasNotBeenRegistered() + { + ProjectWideActionsAsset.IInputActionAssetVerifier Factory() => null; + Assert.That(ProjectWideActionsAsset.UnregisterInputActionAssetVerifier(Factory), Is.False); + } - ProjectWideActionsAsset.CheckForDefaultUIActionMapChanges(asset); + [Test(Description = "Verifies that a throwing reporter is handled gracefully")] + [Category(kTestCategory)] + public void ProjectWideActions_ShouldCatchAndReportException_IfReporterThrows() + { + var asset = ProjectWideActionsAsset.CreateDefaultAssetAtPath(); + var verifier = new TestVerifier(); + Func factory = () => verifier; + try + { + // Note that reporter failures shouldn't affect verification result + Assert.That(ProjectWideActionsAsset.RegisterInputActionAssetVerifier(factory), Is.True); + Assert.That(ProjectWideActionsAsset.Verify(asset, new TestReporter(throwsException: true)), Is.True); + } + finally + { + Assert.That(ProjectWideActionsAsset.UnregisterInputActionAssetVerifier(factory), Is.True); + } - LogAssert.Expect(LogType.Warning, new Regex($"The UI action '{defaultActionName0}' name has been modified")); - LogAssert.Expect(LogType.Warning, new Regex($"The UI action '{defaultActionName1}' name has been modified")); + LogAssert.Expect(LogType.Exception, new Regex($"{TestReporter.kExceptionMessage}")); } -#endif // UNITY_2023_2_OR_NEWER + [Test(Description = "Verifies that a throwing verifier is handled gracefully and reported as a failure")] + [Category(kTestCategory)] + public void ProjectWideActions_ShouldCatchAndReportException_IfVerifierThrows() + { + var asset = ProjectWideActionsAsset.CreateDefaultAssetAtPath(); + var verifier = new TestVerifier(throwsException: true); + Func factory = () => verifier; + try + { + // Note that verifier failures should affect verification result + Assert.That(ProjectWideActionsAsset.RegisterInputActionAssetVerifier(factory), Is.True); + Assert.That(ProjectWideActionsAsset.Verify(asset, new TestReporter()), Is.False); + } + finally + { + Assert.That(ProjectWideActionsAsset.UnregisterInputActionAssetVerifier(factory), Is.True); + } + + LogAssert.Expect(LogType.Exception, new Regex($"{TestReporter.kExceptionMessage}")); + } [Test(Description = "Verifies that when assigning InputSystem.actions a callback is fired if value is different but not when value is not different")] [Category(kTestCategory)] diff --git a/Assets/Tests/InputSystem.Editor/UGUITests.cs b/Assets/Tests/InputSystem.Editor/UGUITests.cs new file mode 100644 index 0000000000..6dd636e33f --- /dev/null +++ b/Assets/Tests/InputSystem.Editor/UGUITests.cs @@ -0,0 +1,50 @@ +#if UNITY_EDITOR && UNITY_6000_0_OR_NEWER +using System; +using NUnit.Framework; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine.SceneManagement; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.InputSystem.UI; + +internal class UGUITests +{ + Scene m_Scene; + [SetUp] + public void SetUp() + { + // Ensure that the scene is clean before starting the test + m_Scene = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects); + } + + [TearDown] + public void TearDown() + { + EditorSceneManager.CloseScene(m_Scene, true); + } + + [Test] + [Category("UGUITests")] + // This test checks that when the Input System is enabled the EventSystem GameObject is created with the + // InputSystemUIInputModule component. + public void UGUITests_Editor_EventSystemGameObjectUsesUIInputModule() + { + m_Scene = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects); + + // Creates the EventSystem GameObject using the Editor menu + string menuItem = "GameObject/UI/Event System"; + Assert.AreEqual(2, m_Scene.rootCount); + EditorApplication.ExecuteMenuItem(menuItem); + Assert.AreEqual(3, m_Scene.rootCount); + + // Get the EventSystem GameObject from the scene to check that it has the correct input module + var rootGameObjects = m_Scene.GetRootGameObjects(); + GameObject eventSystem = rootGameObjects[2].GetComponent().gameObject; + + Assert.IsNotNull(eventSystem); + Assert.IsNotNull(eventSystem.GetComponent()); + } +} + +#endif diff --git a/Assets/Tests/InputSystem.Editor/UGUITests.cs.meta b/Assets/Tests/InputSystem.Editor/UGUITests.cs.meta new file mode 100644 index 0000000000..24db423527 --- /dev/null +++ b/Assets/Tests/InputSystem.Editor/UGUITests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a04e24d1a99a4fafb8fbe27635ccabe9 +timeCreated: 1712647437 \ No newline at end of file diff --git a/Assets/Tests/InputSystem.Editor/XRIPackageTest.cs b/Assets/Tests/InputSystem.Editor/XRIPackageTest.cs index cada7ef6f8..684a872d38 100644 --- a/Assets/Tests/InputSystem.Editor/XRIPackageTest.cs +++ b/Assets/Tests/InputSystem.Editor/XRIPackageTest.cs @@ -28,6 +28,12 @@ public IEnumerator TearDown() { yield return null; } + + //Delete the Assets/XRI folder (and its content) that the XRI package creates + if (AssetDatabase.IsValidFolder("Assets/XRI")) + { + AssetDatabase.DeleteAsset("Assets/XRI"); + } } [UnityTest] diff --git a/Assets/Tests/InputSystem.Editor/XRIPackageTest.cs.meta b/Assets/Tests/InputSystem.Editor/XRIPackageTest.cs.meta index f6fbfaf43e..9b966c9038 100644 --- a/Assets/Tests/InputSystem.Editor/XRIPackageTest.cs.meta +++ b/Assets/Tests/InputSystem.Editor/XRIPackageTest.cs.meta @@ -1,2 +1,11 @@ fileFormatVersion: 2 -guid: b69fa2235c5cf9140936a639fd7f0005 \ No newline at end of file +guid: b69fa2235c5cf9140936a639fd7f0005 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/InputSystem/CorePerformanceTests.cs b/Assets/Tests/InputSystem/CorePerformanceTests.cs index d6940f8afd..bc99278b86 100644 --- a/Assets/Tests/InputSystem/CorePerformanceTests.cs +++ b/Assets/Tests/InputSystem/CorePerformanceTests.cs @@ -546,24 +546,41 @@ public void TODO_CanSaveAndRestoreSystemInLessThan10Milliseconds() // Currently #endif - internal enum OptimizedControlsTest + internal enum OptimizationTestType { + NoOptimization, OptimizedControls, - NormalControls + ReadValueCaching, + OptimizedControlsAndReadValueCaching } - [Test, Performance] - [Category("Performance")] - [TestCase(OptimizedControlsTest.OptimizedControls)] - [TestCase(OptimizedControlsTest.NormalControls)] - public void Performance_OptimizedControls_ReadingMousePosition100kTimes(OptimizedControlsTest testSetup) + public void SetInternalFeatureFlagsFromTestType(OptimizationTestType testType) { - var useOptimizedControls = testSetup == OptimizedControlsTest.OptimizedControls; + var useOptimizedControls = testType == OptimizationTestType.OptimizedControls + || testType == OptimizationTestType.OptimizedControlsAndReadValueCaching; + var useReadValueCaching = testType == OptimizationTestType.ReadValueCaching + || testType == OptimizationTestType.OptimizedControlsAndReadValueCaching; + InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseOptimizedControls, useOptimizedControls); - InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseReadValueCaching, useOptimizedControls); + InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseReadValueCaching, useReadValueCaching); InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kParanoidReadValueCachingChecks, false); + } + + [Test, Performance] + [Category("Performance")] + [TestCase(OptimizationTestType.NoOptimization)] + [TestCase(OptimizationTestType.OptimizedControls)] + [TestCase(OptimizationTestType.ReadValueCaching)] + [TestCase(OptimizationTestType.OptimizedControlsAndReadValueCaching)] + // Isolated tests for reading from Mouse device to evaluate the performance of the optimizations. + // Does not take into account the performance of the InputSystem.Update() call. + public void Performance_OptimizedControls_ReadingMousePosition100kTimes(OptimizationTestType testType) + { + SetInternalFeatureFlagsFromTestType(testType); var mouse = InputSystem.AddDevice(); + var useOptimizedControls = testType == OptimizationTestType.OptimizedControls + || testType == OptimizationTestType.OptimizedControlsAndReadValueCaching; Assert.That(mouse.position.x.optimizedControlDataType, Is.EqualTo(useOptimizedControls ? InputStateBlock.FormatFloat : InputStateBlock.FormatInvalid)); Assert.That(mouse.position.y.optimizedControlDataType, Is.EqualTo(useOptimizedControls ? InputStateBlock.FormatFloat : InputStateBlock.FormatInvalid)); Assert.That(mouse.position.optimizedControlDataType, Is.EqualTo(useOptimizedControls ? InputStateBlock.FormatVector2 : InputStateBlock.FormatInvalid)); @@ -579,17 +596,188 @@ public void Performance_OptimizedControls_ReadingMousePosition100kTimes(Optimize .Run(); } + [Test, Performance] + [Category("Performance")] + [TestCase(OptimizationTestType.NoOptimization)] + [TestCase(OptimizationTestType.OptimizedControls)] + [TestCase(OptimizationTestType.ReadValueCaching)] + [TestCase(OptimizationTestType.OptimizedControlsAndReadValueCaching)] + // Currently these tests shows that all the optimizations have a performance cost when reading from a Mouse device. + // OptimizedControls option is slower because of an extra check that is only done in Editor and Development Builds. + // ReadValueCaching option is slower because Mouse state (FastMouse) is changed every update, which means cached + // values are always stale. And currently there is a cost when caching the value. + public void Performance_OptimizedControls_ReadAndUpdateMousePosition1kTimes(OptimizationTestType testType) + { + SetInternalFeatureFlagsFromTestType(testType); + + var mouse = InputSystem.AddDevice(); + InputSystem.Update(); + + var useOptimizedControls = testType == OptimizationTestType.OptimizedControls + || testType == OptimizationTestType.OptimizedControlsAndReadValueCaching; + Assert.That(mouse.position.x.optimizedControlDataType, Is.EqualTo(useOptimizedControls ? InputStateBlock.FormatFloat : InputStateBlock.FormatInvalid)); + Assert.That(mouse.position.y.optimizedControlDataType, Is.EqualTo(useOptimizedControls ? InputStateBlock.FormatFloat : InputStateBlock.FormatInvalid)); + Assert.That(mouse.position.optimizedControlDataType, Is.EqualTo(useOptimizedControls ? InputStateBlock.FormatVector2 : InputStateBlock.FormatInvalid)); + + Measure.Method(() => + { + var pos = new Vector2(); + for (var i = 0; i < 1000; ++i) + { + pos += mouse.position.ReadValue(); + InputSystem.Update(); + + if (i % 100 == 0) + { + // Make sure there's a new different value every 100 frames. + InputSystem.QueueStateEvent(mouse, new MouseState { position = new Vector2(i + 1, i + 2) }); + } + } + }) + .MeasurementCount(100) + .WarmupCount(5) + .Run(); + } + + [Test, Performance] + [Category("Performance")] + [TestCase(OptimizationTestType.NoOptimization)] + [TestCase(OptimizationTestType.ReadValueCaching)] + // These tests shows a use case where ReadValueCaching optimization will perform better than without any + // optimization. + // It shows that there's a performance improvement when the control values being read are not changing every frame. + public void Performance_OptimizedControls_ReadAndUpdateGamepad1kTimes(OptimizationTestType testType) + { + SetInternalFeatureFlagsFromTestType(testType); + + var gamepad = InputSystem.AddDevice(); + + InputSystem.Update(); + + Measure.Method(() => + { + var pos = new Vector2(); + InputSystem.QueueStateEvent(gamepad, new GamepadState { leftStick = new Vector2(0.3f, 0.1f) }); + InputSystem.Update(); + + pos = gamepad.leftStick.value; + Assert.That(gamepad.leftStick.m_CachedValueIsStale, Is.False); + + for (var i = 0; i < 1000; ++i) + { + InputSystem.Update(); + pos = gamepad.leftStick.value; + Assert.That(gamepad.leftStick.m_CachedValueIsStale, Is.False); + + if (i % 100 == 0) + { + // Make sure there's a new different value every 100 frames to mark the cached value as stale. + InputSystem.QueueStateEvent(gamepad, new GamepadState { leftStick = new Vector2(i / 1000f, i / 1000f) }); + InputSystem.Update(); + } + } + }) + .MeasurementCount(100) + .WarmupCount(10) + .Run(); + } + + [Test, Performance] + [Category("Performance")] + [TestCase(OptimizationTestType.NoOptimization)] + [TestCase(OptimizationTestType.ReadValueCaching)] + // This shows a use case where ReadValueCaching optimization will perform worse when controls have stale cached + // values every frame. Meaning, when control values change in every frame. + public void Performance_OptimizedControls_ReadAndUpdateGamepadNewValuesEveryFrame1kTimes(OptimizationTestType testType) + { + SetInternalFeatureFlagsFromTestType(testType); + + var gamepad = InputSystem.AddDevice(); + + InputSystem.Update(); + + Measure.Method(() => + { + var pos = new Vector2(); + InputSystem.QueueStateEvent(gamepad, new GamepadState { leftStick = new Vector2(0.1f, 0.1f) }); + InputSystem.Update(); + + gamepad.leftStick.ReadValue(); + Assert.That(gamepad.leftStick.m_CachedValueIsStale, Is.False); + + for (var i = 0; i < 1000; ++i) + { + InputSystem.Update(); + pos = gamepad.leftStick.value; + Assert.That(gamepad.leftStick.m_CachedValueIsStale, Is.False); + // Make sure there's a new different value every frames to mark the cached value as stale. + InputSystem.QueueStateEvent(gamepad, new GamepadState { leftStick = new Vector2(i / 1000f, i / 1000f) }); + } + }) + .MeasurementCount(100) + .WarmupCount(10) + .Run(); + } + + [Test, Performance] + [Category("Performance")] + [TestCase(OptimizationTestType.NoOptimization)] + [TestCase(OptimizationTestType.OptimizedControls)] + [TestCase(OptimizationTestType.ReadValueCaching)] + [TestCase(OptimizationTestType.OptimizedControlsAndReadValueCaching)] + // These tests evaluate the performance when there's no read value performed and only InputSystem.Update() is called. + // Emulates a scenario where the controls are not being changed to evaluate the impact of the optimizations. + public void Performance_OptimizedControls_UpdateOnly1kTimes(OptimizationTestType testType) + { + SetInternalFeatureFlagsFromTestType(testType); + + // This adds FastMouse, which updates state every frame and can lead to a performance cost + // when using ReadValueCaching. + var mouse = InputSystem.AddDevice(); + InputSystem.Update(); + + Measure.Method(() => + { + CallUpdate(); + }) + .MeasurementCount(100) + .SampleGroup("Mouse Only") + .WarmupCount(10) + .Run(); + + InputSystem.RemoveDevice(mouse); + InputSystem.AddDevice(); + InputSystem.Update(); + + Measure.Method(() => + { + CallUpdate(); + }) + .MeasurementCount(100) + .SampleGroup("Gamepad Only") + .WarmupCount(10) + .Run(); + + return; + + void CallUpdate() + { + for (var i = 0; i < 1000; ++i) InputSystem.Update(); + } + } + #if ENABLE_VR [Test, Performance] [Category("Performance")] - [TestCase(OptimizedControlsTest.OptimizedControls)] - [TestCase(OptimizedControlsTest.NormalControls)] - public void Performance_OptimizedControls_ReadingPose4kTimes(OptimizedControlsTest testSetup) + [TestCase(OptimizationTestType.NoOptimization)] + [TestCase(OptimizationTestType.OptimizedControls)] + [TestCase(OptimizationTestType.ReadValueCaching)] + [TestCase(OptimizationTestType.OptimizedControlsAndReadValueCaching)] + // Isolated tests for reading from XR Pose device to evaluate the performance of the optimizations. + // Does not take into account the performance of the InputSystem.Update() call. + public void Performance_OptimizedControls_ReadingPose4kTimes(OptimizationTestType testType) { - var useOptimizedControls = testSetup == OptimizedControlsTest.OptimizedControls; - InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseOptimizedControls, useOptimizedControls); - InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseReadValueCaching, useOptimizedControls); - InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kParanoidReadValueCachingChecks, false); + SetInternalFeatureFlagsFromTestType(testType); runtime.ReportNewInputDevice(XRTests.PoseDeviceState.CreateDeviceDescription().ToJson()); @@ -598,6 +786,8 @@ public void Performance_OptimizedControls_ReadingPose4kTimes(OptimizedControlsTe var device = InputSystem.devices[0]; var poseControl = device["posecontrol"] as UnityEngine.InputSystem.XR.PoseControl; + var useOptimizedControls = testType == OptimizationTestType.OptimizedControls + || testType == OptimizationTestType.OptimizedControlsAndReadValueCaching; Assert.That(poseControl.optimizedControlDataType, Is.EqualTo(useOptimizedControls ? InputStateBlock.FormatPose : InputStateBlock.FormatInvalid)); Measure.Method(() => diff --git a/Assets/Tests/InputSystem/InputActionCodeGeneratorActions.cs b/Assets/Tests/InputSystem/InputActionCodeGeneratorActions.cs index bff9d34f65..c9424a3cef 100644 --- a/Assets/Tests/InputSystem/InputActionCodeGeneratorActions.cs +++ b/Assets/Tests/InputSystem/InputActionCodeGeneratorActions.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was auto-generated by com.unity.inputsystem:InputActionCodeGenerator -// version 1.8.1 +// version 1.8.2 // from Assets/Tests/InputSystem/InputActionCodeGeneratorActions.inputactions // // Changes to this file may cause incorrect behavior and will be lost if diff --git a/Assets/Tests/InputSystem/Plugins/InputForUITests.cs b/Assets/Tests/InputSystem/Plugins/InputForUITests.cs index 5b849e77f5..e1cb63bf55 100644 --- a/Assets/Tests/InputSystem/Plugins/InputForUITests.cs +++ b/Assets/Tests/InputSystem/Plugins/InputForUITests.cs @@ -1,9 +1,16 @@ #if UNITY_2023_2_OR_NEWER // UnityEngine.InputForUI Module unavailable in earlier releases using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; using NUnit.Framework; using UnityEngine; using UnityEngine.InputForUI; using UnityEngine.InputSystem; +using UnityEngine.InputSystem.Controls; +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine.InputSystem.Editor; +#endif using UnityEngine.InputSystem.Plugins.InputForUI; using UnityEngine.TestTools; using Event = UnityEngine.InputForUI.Event; @@ -23,21 +30,25 @@ [PostBuildCleanup(typeof(ProjectWideActionsBuildSetup))] public class InputForUITests : InputTestFixture { + private const string kTestCategory = "InputForUI"; + readonly List m_InputForUIEvents = new List(); + private int m_CurrentInputEventToCheck; InputSystemProvider m_InputSystemProvider; + private InputActionAsset storedActions; + [SetUp] public override void Setup() { base.Setup(); + m_CurrentInputEventToCheck = 0; - // Test assumes a compatible project-wide action configuration exist for UI - // See CoreTests_ProjectWideActions for how its setup via build plugins. - Assert.That(InputSystem.actions, Is.Not.Null, - "Test is invalid since Project-wide Input System actions have not been setup for play-mode test environment"); + storedActions = InputSystem.actions; m_InputSystemProvider = new InputSystemProvider(); EventProvider.SetMockProvider(m_InputSystemProvider); + // Register at least one consumer so the mock update gets invoked EventProvider.Subscribe(InputForUIOnEvent); } @@ -49,9 +60,21 @@ public override void TearDown() EventProvider.ClearMockProvider(); m_InputForUIEvents.Clear(); + InputSystem.s_Manager.actions = storedActions; + +#if UNITY_EDITOR + if (File.Exists(kAssetPath)) + UnityEditor.AssetDatabase.DeleteAsset(kAssetPath); +#endif + base.TearDown(); } + internal Event GetNextRecordedUIEvent() + { + return m_InputForUIEvents[m_CurrentInputEventToCheck++]; + } + private bool InputForUIOnEvent(in Event ev) { m_InputForUIEvents.Add(ev); @@ -59,25 +82,16 @@ private bool InputForUIOnEvent(in Event ev) } [Test] - [Category("InputForUI")] - public void PointerEventsAreDispatchedFromMouse() + [Category(kTestCategory)] + public void InputSystemActionAssetIsNotNull() { - var mouse = InputSystem.AddDevice(); - Update(); - - PressAndRelease(mouse.leftButton); - - Update(); - - Assert.IsTrue(m_InputForUIEvents.Count == 2); - Assert.That(m_InputForUIEvents[0].type, Is.EqualTo(Event.Type.PointerEvent)); - Assert.That(m_InputForUIEvents[0].asPointerEvent.type, Is.EqualTo(PointerEvent.Type.ButtonPressed)); - Assert.That(m_InputForUIEvents[1].type, Is.EqualTo(Event.Type.PointerEvent)); - Assert.That(m_InputForUIEvents[1].asPointerEvent.type, Is.EqualTo(PointerEvent.Type.ButtonReleased)); + // Test assumes a compatible action asset configuration exists for UI + Assert.IsTrue(m_InputSystemProvider.ActionAssetIsNotNull(), + "Test is invalid since InputSystemProvider actions are not available"); } [Test] - [Category("InputForUI")] + [Category(kTestCategory)] // Checks that mouse events are ignored when a touch is active. // This is to workaround the issue ISXB-269 on Windows. public void TouchIsPressedAndMouseEventsAreIgnored() @@ -95,8 +109,8 @@ public void TouchIsPressedAndMouseEventsAreIgnored() Move(mouse.position, new Vector2(100f, 0.5f)); Update(); - Assert.IsTrue(m_InputForUIEvents.Count == 1); - Assert.That(m_InputForUIEvents[0] is Event + Assert.AreEqual(1, m_InputForUIEvents.Count); + Assert.That(GetNextRecordedUIEvent() is { type: Event.Type.PointerEvent, asPointerEvent: { type: PointerEvent.Type.ButtonPressed, @@ -104,42 +118,543 @@ public void TouchIsPressedAndMouseEventsAreIgnored() }); } + #region UI_Input_Actions + // Test all default UI actions, and sure that InputSystemProvider works with and without project-wide actions asset + // so that there is no impact in receiving the necessary input events for UI. + // When there are no project-wide actions asset, the InputSystemProvider should still work as it currently gets + // the actions from DefaultActionsAsset().asset. + + // Utility functions + void PressUpdateReleaseUpdate(ButtonControl button) + { + Press(button); + Update(); + Release(button); + Update(); + } + + void TestGamepadNavigationCardinalDirections() + { + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.NavigationEvent, + asNavigationEvent: { type: NavigationEvent.Type.Move, direction: NavigationEvent.Direction.Left, eventSource: EventSource.Gamepad } + }); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.NavigationEvent, + asNavigationEvent: { type: NavigationEvent.Type.Move, direction: NavigationEvent.Direction.Up, eventSource: EventSource.Gamepad } + }); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.NavigationEvent, + asNavigationEvent: { type: NavigationEvent.Type.Move, direction: NavigationEvent.Direction.Right, eventSource: EventSource.Gamepad } + }); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.NavigationEvent, + asNavigationEvent: { type: NavigationEvent.Type.Move, direction: NavigationEvent.Direction.Down, eventSource: EventSource.Gamepad } + }); + } + + void TestKeyboardNavigationCardinalDirections() + { + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.NavigationEvent, + asNavigationEvent: { type: NavigationEvent.Type.Move, direction: NavigationEvent.Direction.Left, eventSource: EventSource.Keyboard } + }); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.NavigationEvent, + asNavigationEvent: { type: NavigationEvent.Type.Move, direction: NavigationEvent.Direction.Up, eventSource: EventSource.Keyboard } + }); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.NavigationEvent, + asNavigationEvent: { type: NavigationEvent.Type.Move, direction: NavigationEvent.Direction.Right, eventSource: EventSource.Keyboard } + }); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.NavigationEvent, + asNavigationEvent: { type: NavigationEvent.Type.Move, direction: NavigationEvent.Direction.Down, eventSource: EventSource.Keyboard } + }); + } + [Test] - [Category("InputForUI")] - // Presses a gamepad left stick left and verifies that a navigation move event is dispatched - public void NavigationMoveWorks() + [Category(kTestCategory)] + [TestCase(true)] + [TestCase(false)] + public void UIActionNavigation_FiresUINavigationEvents_FromInputsGamepadJoystickAndKeyboard(bool useProjectWideActionsAsset) { + Update(); + if (!useProjectWideActionsAsset) + { + // 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; + } + Update(); + var gamepad = InputSystem.AddDevice(); + PressUpdateReleaseUpdate(gamepad.leftStick.left); + PressUpdateReleaseUpdate(gamepad.leftStick.up); + PressUpdateReleaseUpdate(gamepad.leftStick.right); + PressUpdateReleaseUpdate(gamepad.leftStick.down); + TestGamepadNavigationCardinalDirections(); + + PressUpdateReleaseUpdate(gamepad.rightStick.left); + PressUpdateReleaseUpdate(gamepad.rightStick.up); + PressUpdateReleaseUpdate(gamepad.rightStick.right); + PressUpdateReleaseUpdate(gamepad.rightStick.down); + TestGamepadNavigationCardinalDirections(); + + PressUpdateReleaseUpdate(gamepad.dpad.left); + PressUpdateReleaseUpdate(gamepad.dpad.up); + PressUpdateReleaseUpdate(gamepad.dpad.right); + PressUpdateReleaseUpdate(gamepad.dpad.down); + TestGamepadNavigationCardinalDirections(); + + + var joystick = InputSystem.AddDevice(); + PressUpdateReleaseUpdate(joystick.stick.left); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.NavigationEvent, + asNavigationEvent: { type: NavigationEvent.Type.Move, direction: NavigationEvent.Direction.Left, eventSource: EventSource.Unspecified} + }); + PressUpdateReleaseUpdate(joystick.stick.up); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.NavigationEvent, + asNavigationEvent: { type: NavigationEvent.Type.Move, direction: NavigationEvent.Direction.Up, eventSource: EventSource.Unspecified} + }); + PressUpdateReleaseUpdate(joystick.stick.right); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.NavigationEvent, + asNavigationEvent: { type: NavigationEvent.Type.Move, direction: NavigationEvent.Direction.Right, eventSource: EventSource.Unspecified} + }); + PressUpdateReleaseUpdate(joystick.stick.down); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.NavigationEvent, + asNavigationEvent: { type: NavigationEvent.Type.Move, direction: NavigationEvent.Direction.Down, eventSource: EventSource.Unspecified} + }); + + + var keyboard = InputSystem.AddDevice(); + PressUpdateReleaseUpdate(keyboard.aKey); + PressUpdateReleaseUpdate(keyboard.wKey); + PressUpdateReleaseUpdate(keyboard.dKey); + PressUpdateReleaseUpdate(keyboard.sKey); + TestKeyboardNavigationCardinalDirections(); + + PressUpdateReleaseUpdate(keyboard.leftArrowKey); + PressUpdateReleaseUpdate(keyboard.upArrowKey); + PressUpdateReleaseUpdate(keyboard.rightArrowKey); + PressUpdateReleaseUpdate(keyboard.downArrowKey); + TestKeyboardNavigationCardinalDirections(); + } + + [Test] + [Category(kTestCategory)] + [TestCase(true)] + [TestCase(false)] + public void UIActionSubmit_FiresUISubmitEvents_FromInputsGamepadJoystickAndKeyboard(bool useProjectWideActionsAsset) + { Update(); - Press(gamepad.leftStick.left); + if (!useProjectWideActionsAsset) + { + InputSystem.s_Manager.actions = null; + } Update(); - Release(gamepad.leftStick.left); + + PressUpdateReleaseUpdate(InputSystem.AddDevice().buttonSouth); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.NavigationEvent, + asNavigationEvent: { type: NavigationEvent.Type.Submit, eventSource: EventSource.Gamepad } + }); + + PressUpdateReleaseUpdate(InputSystem.AddDevice().trigger); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.NavigationEvent, + asNavigationEvent: { type: NavigationEvent.Type.Submit, eventSource: EventSource.Unspecified } + }); + + PressUpdateReleaseUpdate(InputSystem.AddDevice().enterKey); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.NavigationEvent, + asNavigationEvent: { type: NavigationEvent.Type.Submit, eventSource: EventSource.Keyboard } + }); + + Assert.AreEqual(3, m_InputForUIEvents.Count); + } + + [Test] + [Category(kTestCategory)] + [TestCase(true)] + [TestCase(false)] + public void UIActionCancel_FiresUICancelEvents_FromInputsGamepadAndKeyboard(bool useProjectWideActionsAsset) + { Update(); + if (!useProjectWideActionsAsset) + { + InputSystem.s_Manager.actions = null; + } + Update(); + + PressUpdateReleaseUpdate(InputSystem.AddDevice().buttonEast); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.NavigationEvent, + asNavigationEvent: { type: NavigationEvent.Type.Cancel, eventSource: EventSource.Gamepad } + }); - Assert.IsTrue(m_InputForUIEvents.Count == 1); - Assert.That(m_InputForUIEvents[0] is Event + PressUpdateReleaseUpdate(InputSystem.AddDevice().escapeKey); + Assert.That(GetNextRecordedUIEvent() is { type: Event.Type.NavigationEvent, - asNavigationEvent: { type: NavigationEvent.Type.Move, - direction: NavigationEvent.Direction.Left, - eventSource: EventSource.Gamepad} + asNavigationEvent: { type: NavigationEvent.Type.Cancel, eventSource: EventSource.Keyboard } + }); + + Assert.AreEqual(2, m_InputForUIEvents.Count); + } + + [Test] + [Category(kTestCategory)] + [TestCase(true)] + [TestCase(false)] + public void UIActionPoint_FiresUIPointEvents_FromInputsMousePenAndTouch(bool useProjectWideActionsAsset) + { + Update(); + if (!useProjectWideActionsAsset) + { + InputSystem.s_Manager.actions = null; + } + Update(); + + var mouse = InputSystem.AddDevice(); + Set(mouse.position, new Vector2(0.5f, 0.5f)); + Update(); + Move(mouse.position, new Vector2(100f, 0.5f)); + Update(); + + var pen = InputSystem.AddDevice(); + Set(pen.position, new Vector2(0.5f, 0.5f)); + Update(); + Move(pen.position, new Vector2(100f, 0.5f)); + Update(); + + InputSystem.AddDevice(); + BeginTouch(1, new Vector2(0.5f, 0.5f)); + Update(); + EndTouch(1, new Vector2(100f, 100f)); + Update(); + + // Touch screen move is three actions: press, move, release + Assert.AreEqual(5, m_InputForUIEvents.Count); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.PointerEvent, + asPointerEvent: { type: PointerEvent.Type.PointerMoved, eventSource: EventSource.Mouse } + }); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.PointerEvent, + asPointerEvent: { type: PointerEvent.Type.PointerMoved, eventSource: EventSource.Pen } + }); + // Skip button down event that touch generates + ++m_CurrentInputEventToCheck; + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.PointerEvent, + asPointerEvent: { type: PointerEvent.Type.PointerMoved, eventSource: EventSource.Touch } }); } [Test] - [Category("InputForUI")] - public void SendWheelEvent() + [Category(kTestCategory)] + [TestCase(true)] + [TestCase(false)] + public void UIActionClick_FiresUIClickEvents_FromInputsMousePenAndTouch(bool useProjectWideActionsAsset) { + Update(); + if (!useProjectWideActionsAsset) + { + InputSystem.s_Manager.actions = null; + } + Update(); + + var mouse = InputSystem.AddDevice(); + Click(mouse.leftButton); + Update(); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.PointerEvent, + asPointerEvent: { type: PointerEvent.Type.ButtonPressed, eventSource: EventSource.Mouse, button: PointerEvent.Button.MouseLeft, clickCount: 1 } + }); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.PointerEvent, + asPointerEvent: { type: PointerEvent.Type.ButtonReleased, eventSource: EventSource.Mouse, button: PointerEvent.Button.MouseLeft, clickCount: 1 } + }); + + Click(mouse.rightButton); + Update(); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.PointerEvent, + asPointerEvent: { type: PointerEvent.Type.ButtonPressed, eventSource: EventSource.Mouse, button: PointerEvent.Button.MouseRight, clickCount: 1 } + }); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.PointerEvent, + asPointerEvent: { type: PointerEvent.Type.ButtonReleased, eventSource: EventSource.Mouse, button: PointerEvent.Button.MouseRight, clickCount: 1 } + }); + + Click(mouse.middleButton); + Update(); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.PointerEvent, + asPointerEvent: { type: PointerEvent.Type.ButtonPressed, eventSource: EventSource.Mouse, button: PointerEvent.Button.MouseMiddle, clickCount: 1 } + }); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.PointerEvent, + asPointerEvent: { type: PointerEvent.Type.ButtonReleased, eventSource: EventSource.Mouse, button: PointerEvent.Button.MouseMiddle, clickCount: 1 } + }); + + var pen = InputSystem.AddDevice(); + Click(pen.tip); + Update(); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.PointerEvent, + asPointerEvent: { type: PointerEvent.Type.ButtonPressed, eventSource: EventSource.Pen, button: PointerEvent.Button.MouseLeft, clickCount: 1 } + }); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.PointerEvent, + asPointerEvent: { type: PointerEvent.Type.ButtonReleased, eventSource: EventSource.Pen, button: PointerEvent.Button.MouseLeft, clickCount: 1 } + }); + + InputSystem.AddDevice(); + BeginTouch(1, new Vector2(0.0f, 0.5f)); + EndTouch(1, new Vector2(0.0f, 0.5f)); + Update(); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.PointerEvent, + asPointerEvent: { type: PointerEvent.Type.ButtonPressed, eventSource: EventSource.Touch, button: PointerEvent.Button.MouseLeft, clickCount: 1 } + }); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.PointerEvent, + asPointerEvent: { type: PointerEvent.Type.ButtonReleased, eventSource: EventSource.Touch, button: PointerEvent.Button.MouseLeft, clickCount: 1 } + }); + + Assert.AreEqual(10, m_InputForUIEvents.Count); + } + + [Test] + [Category(kTestCategory)] + [TestCase(true)] + [TestCase(false)] + public void UIActionScroll_FiresUIScrollEvents_FromInputMouse(bool useProjectWideActionsAsset) + { + Update(); + if (!useProjectWideActionsAsset) + { + InputSystem.s_Manager.actions = null; + } + Update(); + var kScrollUGUIScaleFactor = 3.0f; // See InputSystemProvider OnScrollWheelPerformed() callback var mouse = InputSystem.AddDevice(); Update(); // Make the minimum step of scroll delta to be ±1.0f Set(mouse.scroll.y, -1f / kScrollUGUIScaleFactor); Update(); - Assert.IsTrue(m_InputForUIEvents.Count == 1); - Assert.That(m_InputForUIEvents[0].asPointerEvent.scroll, Is.EqualTo(new Vector2(0, 1))); + Assert.AreEqual(1, m_InputForUIEvents.Count); + Assert.That(GetNextRecordedUIEvent() is + { + type: Event.Type.PointerEvent, + asPointerEvent: { type: PointerEvent.Type.Scroll, eventSource: EventSource.Mouse, scroll: {x: 0, y: 1} } + }); + } + + #endregion + +#if UNITY_EDITOR + // These tests shouldn't really be in a non editor-only assembly but for now we guard them until moved. + private const string kAssetPath = "Assets/InputSystem_InputForUI_TestActions.inputactions"; + + [Test(Description = "Verifies that default actions (Project-wide) OR default actions have no verification errors.")] + [Category(kTestCategory)] + [TestCase(true)] + [TestCase(true)] + public void DefaultActions_ShouldNotGenerateAnyVerificationWarnings(bool useProjectWideActions) + { + if (!useProjectWideActions) + InputSystem.s_Manager.actions = null; + Update(); + LogAssert.NoUnexpectedReceived(); + } + + [Ignore("We currently allow a PWA asset without an UI action map and rely on defaults instead. This allows users that do not want it or use something else to avoid using it.")] + [Test(Description = "Verifies that user-supplied project-wide input actions generates warnings if action map is missing.")] + [Category(kTestCategory)] + public void ActionsWithoutUIMap_ShouldGenerateWarnings() + { + var asset = ProjectWideActionsAsset.CreateDefaultAssetAtPath(kAssetPath); + asset.RemoveActionMap(asset.FindActionMap("UI", throwIfNotFound: true)); + + InputSystem.s_Manager.actions = asset; + Update(); + + var link = EditorHelpers.GetHyperlink(kAssetPath); + LogAssert.Expect(LogType.Warning, new Regex($"^InputActionMap with path 'UI' in asset '{link}' could not be found.")); + if (InputActionAssetVerifier.DefaultReportPolicy == InputActionAssetVerifier.ReportPolicy.ReportAll) + { + LogAssert.Expect(LogType.Warning, new Regex($"^InputAction with path 'UI/Point' in asset '{link}' could not be found.")); + LogAssert.Expect(LogType.Warning, new Regex($"^InputAction with path 'UI/Navigate' in asset '{link}' could not be found.")); + LogAssert.Expect(LogType.Warning, new Regex($"^InputAction with path 'UI/Submit' in asset '{link}' could not be found.")); + LogAssert.Expect(LogType.Warning, new Regex($"^InputAction with path 'UI/Cancel' in asset '{link}' could not be found.")); + LogAssert.Expect(LogType.Warning, new Regex($"^InputAction with path 'UI/Click' in asset '{link}' could not be found.")); + LogAssert.Expect(LogType.Warning, new Regex($"^InputAction with path 'UI/MiddleClick' in asset '{link}' could not be found.")); + LogAssert.Expect(LogType.Warning, new Regex($"^InputAction with path 'UI/RightClick' in asset '{link}' could not be found.")); + LogAssert.Expect(LogType.Warning, new Regex($"^InputAction with path 'UI/ScrollWheel' in asset '{link}' could not be found.")); + } + // else: expect suppression of child errors + LogAssert.NoUnexpectedReceived(); } + [Test(Description = "Verifies that user-supplied project-wide input actions generates warnings if any required action is missing.")] + [Category(kTestCategory)] + [TestCase("UI/Point")] + [TestCase("UI/Navigate")] + [TestCase("UI/Submit")] + [TestCase("UI/Cancel")] + [TestCase("UI/Click")] + [TestCase("UI/MiddleClick")] + [TestCase("UI/RightClick")] + [TestCase("UI/ScrollWheel")] + public void ActionMapWithNonExistentRequiredAction_ShouldGenerateWarning(string actionPath) + { + var asset = ProjectWideActionsAsset.CreateDefaultAssetAtPath(kAssetPath); + var action = asset.FindAction(actionPath); + action.Rename("Other"); + + InputSystem.s_Manager.actions = asset; + Update(); + + //var link = AssetDatabase.GetAssetPath()//EditorHelpers.GetHyperlink(kAssetPath); + LogAssert.Expect(LogType.Warning, new Regex($"^InputAction with path '{actionPath}' in asset \"{kAssetPath}\" could not be found.")); + LogAssert.NoUnexpectedReceived(); + } + + [Test(Description = "Verifies that user-supplied project-wide input actions generates warnings if they lack bindings.")] + [Category(kTestCategory)] + [TestCase("UI/Point")] + [TestCase("UI/Navigate")] + [TestCase("UI/Submit")] + [TestCase("UI/Cancel")] + [TestCase("UI/Click")] + [TestCase("UI/MiddleClick")] + [TestCase("UI/RightClick")] + [TestCase("UI/ScrollWheel")] + public void ActionMapWithUnboundRequiredAction_ShouldGenerateWarning(string actionPath) + { + var asset = ProjectWideActionsAsset.CreateDefaultAssetAtPath(kAssetPath); + var map = asset.FindActionMap("UI"); + + // Recreate a map with selected action bindings removed (unfortunately there is no remove so this is what we got) + asset.RemoveActionMap(map); + var newMap = new InputActionMap(map.name); + var n = map.actions.Count; + for (var i = 0; i < n; ++i) + { + // Create a cloned action with only binding changed + var source = map.actions[i]; + var action = newMap.AddAction(name: source.name, + type: source.type, + binding: actionPath == map.name + '/' + source.name ? null : "SomeIrrelevantBindingThatWillNeverResolve", + processors: source.processors, + interactions: source.interactions, + expectedControlLayout: null, + groups: null); + action.expectedControlType = source.expectedControlType; + } + + asset.AddActionMap(newMap); + + InputSystem.s_Manager.actions = asset; + Update(); + + LogAssert.Expect(LogType.Warning, new Regex($"^InputAction with path '{actionPath}' in asset \"{kAssetPath}\" do not have any configured bindings.")); + LogAssert.NoUnexpectedReceived(); + } + + [Test(Description = + "Verifies that user-supplied project-wide input actions generates warnings if they have a different action type")] + [TestCase("UI/Point", InputActionType.Button)] + [TestCase("UI/Navigate", InputActionType.Button)] + [TestCase("UI/Submit", InputActionType.Value)] + [TestCase("UI/Cancel", InputActionType.Value)] + [TestCase("UI/Click", InputActionType.Value)] + [TestCase("UI/MiddleClick", InputActionType.Value)] + [TestCase("UI/RightClick", InputActionType.Value)] + [TestCase("UI/ScrollWheel", InputActionType.Button)] + public void ActionWithUnexpectedActionType_ShouldGenerateWarning(string actionPath, InputActionType unexpectedType) + { + var asset = ProjectWideActionsAsset.CreateDefaultAssetAtPath(kAssetPath); + var action = asset.FindAction(actionPath); + Debug.Assert(action.type != unexpectedType); // not really an assert, sanity check test assumption for correctness + var expectedType = action.type; + action.m_Type = unexpectedType; // change directly via internals for now + + InputSystem.s_Manager.actions = asset; + Update(); + + LogAssert.Expect(LogType.Warning, + new Regex($"^InputAction with path '{actionPath}' in asset \"{kAssetPath}\" has 'type' set to 'InputActionType.{unexpectedType}'")); + LogAssert.NoUnexpectedReceived(); + } + + [Test(Description = + "Verifies that user-supplied project-wide input actions generates warnings if they have a specified expected control type")] + [TestCase("UI/Point", "Quaternion")] + [TestCase("UI/Navigate", "Touch")] + [TestCase("UI/Submit", "Vector2")] + [TestCase("UI/Cancel", "Vector3")] + [TestCase("UI/Click", "Axis")] + [TestCase("UI/MiddleClick", "Delta")] + [TestCase("UI/RightClick", "Bone")] + [TestCase("UI/ScrollWheel", "Eyes")] + public void ActionWithDifferentExpectedControlType_ShouldGenerateWarning(string actionPath, string unexpectedControlType) + { + var asset = ProjectWideActionsAsset.CreateDefaultAssetAtPath(kAssetPath); + var action = asset.FindAction(actionPath); + Debug.Assert(action.expectedControlType != unexpectedControlType); // not really an assert, sanity check test assumption for correctness + var expectedControlType = action.expectedControlType; + action.expectedControlType = unexpectedControlType; + + InputSystem.s_Manager.actions = asset; + Update(); + + LogAssert.Expect(LogType.Warning, + new Regex($"^InputAction with path '{actionPath}' in asset \"{kAssetPath}\" has 'expectedControlType' set to '{unexpectedControlType}'")); + LogAssert.NoUnexpectedReceived(); + } + +#endif // UNITY_EDITOR + static void Update() { EventProvider.NotifyUpdate(); diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 095edc1266..802122f619 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -8,6 +8,26 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. Due to package verification, the latest version below is the unpublished version and the date is meaningless. however, it has to be formatted properly to pass verification tests. +## [1.8.2] - 2024-04-29 + +### Added +- Additional tests for UI Input default actions (Navigate, Submit, Scroll etc.) + +### Fixed +- Fixed an issue where UI interactions would not function without setting up a project-wide actions asset in Project Settings. Default UI actions are now created on the fly, if no asset for project-wide actions has been set. [ISXB-811](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-811). +- Physical keyboards used on Android/ChromeOS could have keys "stuck" reporting as pressed after a long press and release [ISXB-475](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-475). +- NullReferenceException thrown when right-clicking an empty Action Map list in Input Actions Editor windows [ISXB-833](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-833). +- Fixed an issue where `System.ObjectDisposedException` would be thrown when deleting the last ActionMap item in the Input Actions Asset editor. +- Fixed DualSense Edge's vibration and light bar not working on Windows +- Fixed Project-wide Actions asset failing to reload properly after deleting project's Library folder. +- Fixed an issue where `System.InvalidOperationException` is thrown when entering PlayMode after deleting an ActionMap from Project-wide actions and later resetting it. +- Fixed Input Actions Editor window resource leak that could result in unexpected exceptions [ISXB-865](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-865). +- Fixed an issue where UI integration would throw exceptions when Project-wide Input Actions asset did not contain the implicitly required `UI` action map or was missing any of the required actions. Additionally this fix now also generates warnings in the console for any divergence from expected action configuration or lack of bindings in edit-mode. + +### Changed +- For Unity 6.0 and above, when an `EventSystem` GameObject is created in the Editor it will have the +`InputSystemUIInputModule` by default if the Input System package is installed and enabled. + ## [1.8.1] - 2024-03-14 ### Fixed diff --git a/Packages/com.unity.inputsystem/Documentation~/Images/ProjectSettingsInputActionsUIActionMap.png b/Packages/com.unity.inputsystem/Documentation~/Images/ProjectSettingsInputActionsUIActionMap.png index 2d9f7bda4d..4ed4aaa4a5 100644 Binary files a/Packages/com.unity.inputsystem/Documentation~/Images/ProjectSettingsInputActionsUIActionMap.png and b/Packages/com.unity.inputsystem/Documentation~/Images/ProjectSettingsInputActionsUIActionMap.png differ diff --git a/Packages/com.unity.inputsystem/Documentation~/PlayerInput.md b/Packages/com.unity.inputsystem/Documentation~/PlayerInput.md index 731b62c091..f877b89d99 100644 --- a/Packages/com.unity.inputsystem/Documentation~/PlayerInput.md +++ b/Packages/com.unity.inputsystem/Documentation~/PlayerInput.md @@ -26,9 +26,9 @@ There are a few options for doing exactly how the Player Input component does th ### Handling local multiplayer scenarios -You can also use multiple **Player Input** components (each on a separate instance of a prefab) along with the [**Player Input Manager**](PlayerInputManager.md) component to implement local multiplayer features, such as player lobbies, device filtering, and screen-splitting. +You can also have multiple **Player Input** components active at the same time (each on a separate instance of a prefab) along with the [**Player Input Manager**](PlayerInputManager.md) component to implement local multiplayer features, such as device filtering, and screen-splitting. -In these local multiplayer scenarios, the Player Input component should be on a prefab which the [**Player Input Manager**](PlayerInputManager.md) has a reference to. The **Player Input Manager** then instantiates players as they join the game and pairs each player instance to a unique device that the player uses exclusively (for example, one gamepad for each player). You can also manually pair devices in a way that enables two or more players to share a Device (for example, left/right keyboard splits or hot seat use). +In these local multiplayer scenarios, the Player Input component should be on a prefab that represents a player in your game, which the [**Player Input Manager**](PlayerInputManager.md) has a reference to. The **Player Input Manager** then instantiates players as they join the game and pairs each player instance to a unique device that the player uses exclusively (for example, one gamepad for each player). You can also manually pair devices in a way that enables two or more players to share a Device (for example, left/right keyboard splits or hot seat use). Each `PlayerInput` corresponds to one [`InputUser`](UserManagement.md). You can use [`PlayerInput.user`](../api/UnityEngine.InputSystem.PlayerInput.html#UnityEngine_InputSystem_PlayerInput_user) to query the `InputUser` from the component. @@ -62,7 +62,7 @@ The simplest workflow is to use the project-wide actions defined in the [Input A #### Enabling and disabling Actions -The Player Input component automatically handles enabling and disabling Actions, and also handles installing [callbacks](RespondingToActions.md#responding-to-actions-using-callbacks) on the Actions. When multiple Player Input components use the same Actions, the components automatically create [private copies of the Actions](RespondingToActions.md#using-actions-with-multiple-players). +The Player Input component automatically handles enabling and disabling Actions, and also handles installing [callbacks](RespondingToActions.md#responding-to-actions-using-callbacks) on the Actions. When multiple Player Input components use the same Actions, the components automatically create [private copies of the Actions](RespondingToActions.md#using-actions-with-multiple-players). This is why, when writing input code that works with the PlayerInput component, you should not use `InputSystem.actions` because this references the "singleton" copy of the actions rather than the specific private copy associated with the PlayerInput instance you are coding for. When first enabled, the Player Input component enables all Actions from the the [`Default Action Map`](../api/UnityEngine.InputSystem.PlayerInput.html#UnityEngine_InputSystem_PlayerInput_defaultActionMap). If no default Action Map exists, the Player Input component does not enable any Actions. To manually enable Actions, you can call [`Enable`](../api/UnityEngine.InputSystem.InputActionMap.html#UnityEngine_InputSystem_InputActionMap_Enable) and [`Disable`](../api/UnityEngine.InputSystem.InputActionMap.html#UnityEngine_InputSystem_InputActionMap_Disable) on the Action Maps or Actions, like you would do [without `PlayerInput`](Actions.md). To check which Action Map is currently enabled, or to switch to a different one, use the [`PlayerInput.currentActionMap`](../api/UnityEngine.InputSystem.PlayerInput.html#UnityEngine_InputSystem_PlayerInput_currentActionMap) property. To switch Action Maps with an Action Map name, you can also call [`PlayerInput.SwitchCurrentActionMap`](../api/UnityEngine.InputSystem.PlayerInput.html#UnityEngine_InputSystem_PlayerInput_SwitchCurrentActionMap_System_String_). diff --git a/Packages/com.unity.inputsystem/Documentation~/TableOfContents.md b/Packages/com.unity.inputsystem/Documentation~/TableOfContents.md index ecf99a8d8f..b292f2c1b0 100644 --- a/Packages/com.unity.inputsystem/Documentation~/TableOfContents.md +++ b/Packages/com.unity.inputsystem/Documentation~/TableOfContents.md @@ -5,8 +5,8 @@ * [Concepts](Concepts.md) * [Workflows](Workflows.md) * [Workflow - Actions](Workflow-Actions.md) - * [Workflow - PlayerInput Component](Workflow-PlayerInput.md) - * [Workflow - Directly Read Devices](Workflow-Direct.md) + * [Workflow - Actions & PlayerInput](Workflow-PlayerInput.md) + * [Workflow - Direct](Workflow-Direct.md) * [Using the Input System]() * [Project-Wide Actions](ProjectWideActions.md) * [Configuring Input](ActionsEditor.md) diff --git a/Packages/com.unity.inputsystem/Documentation~/Touch.md b/Packages/com.unity.inputsystem/Documentation~/Touch.md index 717dd2d9ce..d50809eea8 100644 --- a/Packages/com.unity.inputsystem/Documentation~/Touch.md +++ b/Packages/com.unity.inputsystem/Documentation~/Touch.md @@ -120,6 +120,6 @@ To get all current touches from the touchscreen, use [`EnhancedTouch.Touch.activ } ``` ->__Note__: You must first enable enhanced touch support by calling [`InputSystem.EnhancedTouch.Enable()`](../api/UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport.html#UnityEngine_InputSystem_EnhancedTouch_EnhancedTouchSupport_Enable). +>__Note__: You must first enable enhanced touch support by calling [`InputSystem.EnhancedTouchSupport.Enable()`](../api/UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport.html#UnityEngine_InputSystem_EnhancedTouch_EnhancedTouchSupport_Enable). You can also use the lower-level [`Touchscreen.current.touches`](../api/UnityEngine.InputSystem.Touchscreen.html#UnityEngine_InputSystem_Touchscreen_touches) API. diff --git a/Packages/com.unity.inputsystem/Documentation~/UISupport.md b/Packages/com.unity.inputsystem/Documentation~/UISupport.md index 3f60fbb749..6f5de9732e 100644 --- a/Packages/com.unity.inputsystem/Documentation~/UISupport.md +++ b/Packages/com.unity.inputsystem/Documentation~/UISupport.md @@ -3,15 +3,19 @@ uid: input-system-ui-support --- # UI support -* [Setting up UI Input](#setting-up-ui-input) - * [How the bindings work](#how-the-bindings-work) - * [Pointer-type input](#pointer-type-input) - * [Navigation-type input](#navigation-type-input) - * [Tracked-type input](#tracked-type-input) -* [Multiplayer UIs](#multiplayer-uis) -* [Virtual mouse cursor control](#virtual-mouse-cursor-control) -* [UI and game input](#ui-and-game-input) -* [UI Toolkit support](#ui-toolkit-support) +- [UI support](#ui-support) + - [Setting up UI input](#setting-up-ui-input) + - [Required Actions for UI](#required-actions-for-ui) + - [Input System Module](#input-system-module) + - [How the bindings work](#how-the-bindings-work) + - [Multiplayer UIs](#multiplayer-uis) + - [Virtual mouse cursor control](#virtual-mouse-cursor-control) + - [UI and game input](#ui-and-game-input) + - [Handling ambiguities for pointer-type input](#handling-ambiguities-for-pointer-type-input) + - [Handling ambiguities for navigation-type input](#handling-ambiguities-for-navigation-type-input) + - [UI Toolkit support](#ui-toolkit-support) + - [Unity 2023.2 and onwards](#unity-20232-and-onwards) + - [Unity 2023.1 and earlier](#unity-20231-and-earlier) You can use the Input System package to control any in-game UI bindings created with the [Unity UI package](https://docs.unity3d.com/Manual/UISystem.html). @@ -29,10 +33,29 @@ When using [project-wide actions](Workflow-Actions.html) in Unity 2023.2 and new ![ProjectSettingsInputActionsUIActionMap](Images/ProjectSettingsInputActionsUIActionMap.png) -You can modify, add, or remove bindings to the named actions in the UI action map to suit your project, however in order to remain compatible with UI Toolkit, the name of the action map ("UI") and the names of the actions it contains ("Navigate", "Submit", "Cancel", etc) and their respective *Action Types* must remain the same to be compatible with expectations indirectly defined by the [UI Input Module](../api/UnityEngine.InputSystem.UI.InputSystemUIInputModule.html) class. +## Required Actions for UI + +The default project-wide actions comes with all the required actions to be compatible with UI Toolkit. + +You can modify, add, or remove bindings to the named actions in the UI action map to suit your project, however in order to remain compatible with UI Toolkit, the name of the action map ("**UI**"), the names of the actions it contains, and their respective **Action Types** must remain the same. + +These specific actions and types, which are expected by the [UI Input Module](../api/UnityEngine.InputSystem.UI.InputSystemUIInputModule.html) class, are as follows: + +**Action**|**Action Type**|**Control Type** +-|-|- +Navigate|PassThrough|Vector2 +Submit|Button|Button +Cancel|Button|Button +Point|PassThrough|Vector2 +Click|PassThrough|Button +RightClick|PassThrough|Button +MiddleClick|PassThrough|Button +ScrollWheel|PassThrough|Vector2 You can also reset the UI action map to its default bindings by selecting **Reset** from the **More (⋮)** menu, at the top right of the actions editor window. However, this will reset both the 'Player' and 'UI' action maps to their default bindings. +### Input System Module + > **Note:** > If you have an instance of the [Input System UI Input Module](../api/UnityEngine.InputSystem.UI.InputSystemUIInputModule.html) component in your scene, the settings on that component takes priority and are used instead of the UI settings in your project-wide actions. diff --git a/Packages/com.unity.inputsystem/Documentation~/Workflow-Actions.md b/Packages/com.unity.inputsystem/Documentation~/Workflow-Actions.md index 9d930b1c2d..83045c7ea3 100644 --- a/Packages/com.unity.inputsystem/Documentation~/Workflow-Actions.md +++ b/Packages/com.unity.inputsystem/Documentation~/Workflow-Actions.md @@ -98,3 +98,11 @@ public class Example : MonoBehaviour > **Note:** You should avoid using `FindAction` in your Update() loop, because it performs a string-based lookup which could impact performance. This is why the Action refeferences in the example above are found during the Start() functionm, and stored in variables after finding them. > **Note:** The [InputSystem.actions](../api/UnityEngine.InputSystem.InputSystem.html) API refers specifically to the Action Asset assigned as the [project-wide actions](ProjectWideActions.md). Most projects only require one Action Asset, but if you are using more than one Action Asset, you must create a reference using the type InputActionAsset to the asset you wish to access. + +## Pros and Cons + +This is the recommended workflow with the Input System Package, providing a flexible but simple solution suitable for most projects. + +You benefit from the Action-based features such as Action Maps, Bindings, and the ability to configure them in the Actions Editor. You can also implement [user rebinding at run time](ActionBindings.html#interactive-rebinding). + +This workflow alone doesn't provide built-in support for local multiplayer scenarios with multiple devices, so if you are producing a local multiplayer game you might want to consider using the [Actions & PlayerInput](./Workflow-PlayerInput.md) workflow. diff --git a/Packages/com.unity.inputsystem/Documentation~/Workflow-Direct.md b/Packages/com.unity.inputsystem/Documentation~/Workflow-Direct.md index 3b32f555f6..b1c338cfc4 100644 --- a/Packages/com.unity.inputsystem/Documentation~/Workflow-Direct.md +++ b/Packages/com.unity.inputsystem/Documentation~/Workflow-Direct.md @@ -40,7 +40,9 @@ public class MyPlayerScript : MonoBehaviour The example above reads values directly from the right trigger, and the left stick, of the currently connected [gamepad](Gamepad.html). It does not use the input system’s "Action" class, and instead the conceptual actions in your game or app, such as "move" and "use", are implicitly defined by what your code does in response to the input. You can use the same approach for other Device types such as the [keyboard](../api/UnityEngine.InputSystem.Keyboard.html) or [mouse](../api/UnityEngine.InputSystem.Mouse.html). -This is often the fastest way to set up some code which responds to input, but it is the least flexible because there is no abstraction between your code and the values generated by a specific device. +## Pros and Cons + +This can be the fastest way to set up some code which responds to input, but it is the least flexible because there is no abstraction between your code and the values generated by a specific device. If you choose to use this technique: diff --git a/Packages/com.unity.inputsystem/Documentation~/Workflow-PlayerInput.md b/Packages/com.unity.inputsystem/Documentation~/Workflow-PlayerInput.md index fb053d44f8..61c78bfc3e 100644 --- a/Packages/com.unity.inputsystem/Documentation~/Workflow-PlayerInput.md +++ b/Packages/com.unity.inputsystem/Documentation~/Workflow-PlayerInput.md @@ -11,7 +11,7 @@ The highest level of abstraction provided by the Input System is when you use [A It allows you to set up these connections using a UI in the inspector using an event-driven model, instead writing code to poll the values of your Actions as described in the [previous workflow example](Workflow-Actions.html)). -The PlayerInput component also helps with multi-player scenarios. You can use the PlayerInput component along with the PlayerInputManager component to handle automatic instantiation of new players when input occurs on new devices. For example, if you were making a four-player local cooperative game, PlayerInput with PlayerInputManager can handle allowing new players to join when they press start on their respective controller. +The PlayerInput component also helps with local multi-player scenarios. You can use the PlayerInput component along with the PlayerInputManager component to handle automatic instantiation of new players when input occurs on new devices. For example, if you were making a four-player local cooperative game, PlayerInput with PlayerInputManager can handle allowing new players to join when they press start on their respective controller. ![image alt text](./Images/PlayerInputWithGameplayEvents.png) @@ -55,9 +55,13 @@ public class ExampleScript : MonoBehaviour > > This is because the PlayerInput component performs device filtering to automatically assign devices to multiple players, so each instance has its own copy of the actions filtered for each player. If you bypass this by reading `InputSystem.actions` directly, the automatic device assignment won't work. -This workflow has pros and cons when compared to the previous workflow which uses an [Actions without a PlayerInput component](Workflow-Actions.html). +## Pros and Cons -You can see compared with the previous workflow, this workflow requires less code, because you do not have to reference the Actions Asset or set up the event handler methods in your own script. However it does require more set-up in the Editor, and could make debugging more difficult because the connections between your actions and code are not hard-coded. +This workflow has pros and cons when compared to using [Actions without a PlayerInput component](Workflow-Actions.html). Because it builds on the use of Actions, it comes with all the benefits provided by them, such as Action Maps, Bindings, and the ability to configure them in the Actions Editor. You can also implement [user rebinding at run time](ActionBindings.html#interactive-rebinding). + +This workflow also allows you to set up callbacks in the Editor using an interface in the Inspector, which can sometimes reduce code complexity but can also make debugging more difficult, because the connections between your actions and code are not themselves defined in your code. + +It also provides ready-made handling of the [assignment of devices](PlayerInput.html#device-assignments) and [screen-splitting](PlayerInputManager.html#split-screen) in local multiplayer scenarios. While these are things you can implement yourself, having a simple solution ready to go can be beneficial. However if you choose this option, the implementation is somewhat of a "black box", meaning you are less able to customise how it works. As with the other workflows described in this section, there is a trade-off between flexibility, simplicity, and speed of implementation. diff --git a/Packages/com.unity.inputsystem/Documentation~/Workflows.md b/Packages/com.unity.inputsystem/Documentation~/Workflows.md index 4b97018db0..2e28018c34 100644 --- a/Packages/com.unity.inputsystem/Documentation~/Workflows.md +++ b/Packages/com.unity.inputsystem/Documentation~/Workflows.md @@ -15,9 +15,9 @@ The descriptions below describe these main workflows and link to more detailed d | | | |---|---| -|[**Configure Actions in the Editor and read their values from Action references in your script**](Workflow-Actions.md)

This is the **recommended** workflow for most situations. In this workflow, you use the Input Settings window to configure your project-wide actions and bindings, then set up references and read the values for those actions in your code [(read more)](Workflow-Actions.md).

|![image alt text](Images/Workflow-Actions.png)| -|[**Configure input in the Editor, use PlayerInput component to handle events**](Workflow-PlayerInput.html)

This workflow adds more complex functionality on top of the first workflow. It provides features that allow you to connect up **callbacks** directly from Actions to your own callback handler methods, removing the need to deal with Action references in your code. It also provides features that are useful in **local multiplayer** scenarios such as device assignment and split-screen functionality. [(read more)](Workflow-PlayerInput.html).

|![image alt text](Images/Workflow-PlayerInput.png)| -|[**Read user input directly from devices**](Workflow-Direct.html)

This workflow is a simplified, script-only approach. It bypasses the Actions and Bindings features entirely, and instead your script explicitly references specific device controls (such as "left gamepad stick") and reads the values directly. This is suitable for **fast prototyping**, or single fixed platform scenarios. It is a **less flexible** workflow because it bypasses some of the main input system features [(read more)](Workflow-Direct.html).

|![image alt text](Images/Workflow-Direct.png)| +|[**Using Actions**](Workflow-Actions.md)

This is the **recommended** workflow for most situations. In this workflow, you use the [Actions Editor window](./ActionsEditor.md) to configure sets of actions and bindings, then set up references and read the values for those actions in your code [(read more)](Workflow-Actions.md).

|![image alt text](Images/Workflow-Actions.png)| +|[**Using Actions and the PlayerInput Component**](Workflow-PlayerInput.html)

This workflow provides extra features that allow you to connect up **callbacks** directly from Actions to your own callback handler methods, removing the need to deal with Action references in your code. It also provides features that are useful in **local multiplayer** scenarios such as device assignment and split-screen functionality. [(read more)](Workflow-PlayerInput.html).

|![image alt text](Images/Workflow-PlayerInput.png)| +|[**Directly read device states**](Workflow-Direct.html)

This workflow is a simplified, script-only approach which bypasses the Actions and Bindings features entirely. Instead your script explicitly references specific device controls (such as "left gamepad stick") and reads the values directly. This is suitable for **fast prototyping**, or single fixed platform scenarios. It is a **less flexible** workflow because it bypasses some of the main input system features [(read more)](Workflow-Direct.html).

|![image alt text](Images/Workflow-Direct.png)| diff --git a/Packages/com.unity.inputsystem/InputSystem/AssemblyInfo.cs b/Packages/com.unity.inputsystem/InputSystem/AssemblyInfo.cs index 4a184a7f64..4e0681f922 100644 --- a/Packages/com.unity.inputsystem/InputSystem/AssemblyInfo.cs +++ b/Packages/com.unity.inputsystem/InputSystem/AssemblyInfo.cs @@ -7,6 +7,7 @@ [assembly: InternalsVisibleTo("Unity.InputSystem.Tests.Editor")] [assembly: InternalsVisibleTo("Unity.InputSystem.Tests")] [assembly: InternalsVisibleTo("Unity.InputSystem.IntegrationTests")] +[assembly: InternalsVisibleTo("Unity.InputSystem.ForUI")] // To avoid minor bump namespace UnityEngine.InputSystem { @@ -15,7 +16,7 @@ public static partial class InputSystem // Keep this in sync with "Packages/com.unity.inputsystem/package.json". // NOTE: Unfortunately, System.Version doesn't use semantic versioning so we can't include // "-preview" suffixes here. - internal const string kAssemblyVersion = "1.8.1"; + internal const string kAssemblyVersion = "1.8.2"; internal const string kDocUrl = "https://docs.unity3d.com/Packages/com.unity.inputsystem@1.8"; } } diff --git a/Packages/com.unity.inputsystem/InputSystem/Devices/Precompiled/FastKeyboard.cs b/Packages/com.unity.inputsystem/InputSystem/Devices/Precompiled/FastKeyboard.cs index 9b5dec6bf3..c47ea3f931 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Devices/Precompiled/FastKeyboard.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Devices/Precompiled/FastKeyboard.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was auto-generated by com.unity.inputsystem:InputLayoutCodeGenerator -// version 1.8.1 +// version 1.8.2 // from "Keyboard" layout // // Changes to this file may cause incorrect behavior and will be lost if diff --git a/Packages/com.unity.inputsystem/InputSystem/Devices/Precompiled/FastMouse.cs b/Packages/com.unity.inputsystem/InputSystem/Devices/Precompiled/FastMouse.cs index faae0ec995..8bd47775f5 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Devices/Precompiled/FastMouse.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Devices/Precompiled/FastMouse.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was auto-generated by com.unity.inputsystem:InputLayoutCodeGenerator -// version 1.8.1 +// version 1.8.2 // from "Mouse" layout // // Changes to this file may cause incorrect behavior and will be lost if diff --git a/Packages/com.unity.inputsystem/InputSystem/Devices/Precompiled/FastTouchscreen.cs b/Packages/com.unity.inputsystem/InputSystem/Devices/Precompiled/FastTouchscreen.cs index 93dc76e419..933d5f3eea 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Devices/Precompiled/FastTouchscreen.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Devices/Precompiled/FastTouchscreen.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was auto-generated by com.unity.inputsystem:InputLayoutCodeGenerator -// version 1.8.1 +// version 1.8.2 // from "Touchscreen" layout // // Changes to this file may cause incorrect behavior and will be lost if diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/EditorHelpers.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/EditorHelpers.cs index 1b2ee708b6..88d570c751 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/EditorHelpers.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/EditorHelpers.cs @@ -41,6 +41,16 @@ public static string GetTooltip(this SerializedProperty property) return string.Empty; } + public static string GetHyperlink(string text, string path) + { + return "{text}"; + } + + public static string GetHyperlink(string path) + { + return GetHyperlink(path, path); + } + public static void RestartEditorAndRecompileScripts(bool dryRun = false) { // The API here are not public. Use reflection to get to them. diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs index c654a08a40..784b52506d 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs @@ -35,6 +35,15 @@ private static void OnPostprocessAllAssets(string[] importedAssets, string[] del MoveInputManagerAssetActionsToProjectWideInputActionAsset(); migratedInputActionAssets = true; } + + if (!Application.isPlaying) + { + // If the Library folder is deleted, InputSystem will fail to retrieve the assigned Project-wide Asset because this look-up occurs + // during initialization while the Library is being rebuilt. So, afterwards perform another check and assign PWA asset if needed. + var pwaAsset = ProjectWideActionsBuildProvider.actionsToIncludeInPlayerBuild; + if (InputSystem.actions == null && pwaAsset != null) + InputSystem.actions = pwaAsset; + } } } @@ -119,80 +128,178 @@ internal static InputActionAsset CreateDefaultAssetAtPath(string assetPath = kDe // These may be moved out to internal types if decided to extend validation at a later point. - internal interface IReportInputActionAssetValidationErrors - { - bool OnValidationError(InputAction action, string message); - } - - private class DefaultInputActionAssetValidationReporter : IReportInputActionAssetValidationErrors - { - public bool OnValidationError(InputAction action, string message) - { - Debug.LogWarning(message); - return true; - } - } - - internal static bool Validate(InputActionAsset asset, IReportInputActionAssetValidationErrors reporter = null) + /// + /// Interface for reporting asset verification errors. + /// + internal interface IReportInputActionAssetVerificationErrors { -#if UNITY_2023_2_OR_NEWER - reporter ??= new DefaultInputActionAssetValidationReporter(); - CheckForDefaultUIActionMapChanges(asset, reporter); -#endif // UNITY_2023_2_OR_NEWER - return true; + /// + /// Reports a failure to comply to requirements with a message meaningful to the user. + /// + /// User-friendly error message. + void Report(string message); } - private static bool ReportError(IReportInputActionAssetValidationErrors reporter, InputAction action, string message) + /// + /// Interface for asset verification. + /// + internal interface IInputActionAssetVerifier { - return reporter.OnValidationError(action, message); + /// + /// Verifies the given asset. + /// + /// The asset to be verified + /// The reporter to be used to report failure to meet requirements. + public void Verify(InputActionAsset asset, IReportInputActionAssetVerificationErrors reporter); } -#if UNITY_2023_2_OR_NEWER /// - /// Checks if the default InputForUI UI action map has been modified or removed, to let the user know if their changes will - /// break the UI input at runtime, when using the UI Toolkit. + /// Verifier managing verification and reporting of asset compliance with external requirements. /// - internal static bool CheckForDefaultUIActionMapChanges(InputActionAsset asset, IReportInputActionAssetValidationErrors reporter = null) + class Verifier : IReportInputActionAssetVerificationErrors { - reporter ??= new DefaultInputActionAssetValidationReporter(); + private readonly IReportInputActionAssetVerificationErrors m_Reporter; + + // Default verification error reporter which generates feedback as debug warnings. + private class DefaultInputActionAssetVerificationReporter : IReportInputActionAssetVerificationErrors + { + public void Report(string message) + { + Debug.LogWarning(message); + } + } + + /// + /// Constructs a an instance associated with the given reporter. + /// + /// The associated reporter instance. If null, a default reporter will be constructed. + public Verifier(IReportInputActionAssetVerificationErrors reporter = null) + { + m_Reporter = reporter ?? new DefaultInputActionAssetVerificationReporter(); + errors = 0; + } + + #region IReportInputActionAssetVerificationErrors interface + + /// + public void Report(string message) + { + ++errors; - var defaultUIActionMap = GetDefaultUIActionMap(); - var uiMapIndex = asset.actionMaps.IndexOf(x => x.name == "UI"); + try + { + m_Reporter.Report(message); + } + catch (Exception e) + { + // Only log unexpected but non-fatal exception + Debug.LogException(e); + } + } + + #endregion + + /// + /// Returns the total number of errors seen in verification (accumulative). + /// + public int errors { get; private set; } - // "UI" action map has been removed or renamed. - if (uiMapIndex == -1) + /// + /// Returns true if the number of reported errors in verification is zero, else false. + /// + public bool isValid => errors == 0; + + private static List> s_VerifierFactories; + + /// + /// Registers a factory instance. + /// + /// The factory instance. + /// true if successfully added, false if the factory have already been registered. + public static bool RegisterFactory(Func factory) { - ReportError(reporter, null, - "The action map named 'UI' does not exist.\r\n " + - "This will break the UI input at runtime. Please revert the changes to have an action map named 'UI'."); - return false; + if (s_VerifierFactories == null) + s_VerifierFactories = new List>(1); + if (s_VerifierFactories.Contains(factory)) + return false; + s_VerifierFactories.Add(factory); + return true; } - var uiMap = asset.m_ActionMaps[uiMapIndex]; - foreach (var action in defaultUIActionMap.actions) + + /// + /// Unregisters a factory instance that has previously been registered. + /// + /// The factory instance to be removed. + /// true if successfully unregistered, false if the given factory instance could not be found. + public static bool UnregisterFactory(Func factory) + { + return s_VerifierFactories.Remove(factory); + } + + /// + /// Verifies the given project-wide input action asset using all registered verifiers. + /// + /// The asset to be verified. + /// true if no verification errors occurred, else false. + /// + /// Throws System.ArgumentNullException if asset is null. + /// + /// If any registered factory and/or verifier instance throws an exception this will be evaluated + /// as a verification error since the execution of the verifier could not continue. However, any + /// exceptions thrown will be caught and logged but not stop execution of the calling thread. + /// + bool Verify(InputActionAsset asset) { - // "UI" actions have been modified. - if (uiMap.FindAction(action.name) == null) + if (asset == null) + throw new ArgumentNullException(nameof(asset)); + + if (s_VerifierFactories == null || s_VerifierFactories.Count == 0) + return true; + + var instance = new Verifier(m_Reporter); + foreach (var factory in s_VerifierFactories) { - var abort = !ReportError(reporter, action, - $"The UI action '{action.name}' name has been modified.\r\n" + - $"This will break the UI input at runtime. Please make sure the action name with '{action.name}' exists."); - if (abort) - return false; + try + { + factory.Invoke().Verify(asset, instance); + } + catch (Exception e) + { + // Only log unexpected but non-fatal exception and count to fail verification + ++errors; + Debug.LogException(e); + } } - // TODO Add additional validation here, e.g. check expected action type etc. this is currently missing. + return errors == 0; } - return true; + /// + /// Verifies the given project-wide input action asset using all registered verifiers. + /// + /// The asset to be verified. + /// The reporter to be used. If this argument is null the default reporter will be used. + /// true if no verification errors occurred, else false. + /// Throws System.ArgumentNullException if asset is null. + public static bool Verify(InputActionAsset asset, IReportInputActionAssetVerificationErrors reporter = null) + { + return (s_VerifierFactories == null || s_VerifierFactories.Count == 0) || new Verifier(reporter).Verify(asset); + } + } + + internal static bool Verify(InputActionAsset asset, IReportInputActionAssetVerificationErrors reporter = null) + { + return Verifier.Verify(asset, reporter); } -#endif // UNITY_2023_2_OR_NEWER + internal static bool RegisterInputActionAssetVerifier(Func factory) + { + return Verifier.RegisterFactory(factory); + } - // Returns the default UI action map as represented by the default template JSON. - private static InputActionMap GetDefaultUIActionMap() + internal static bool UnregisterInputActionAssetVerifier(Func factory) { - var actionMaps = InputActionMap.FromJson(GetDefaultAssetJson()); - return actionMaps[actionMaps.IndexOf(x => x.name == "UI")]; + return Verifier.UnregisterFactory(factory); } // Creates an asset at the given path containing the given JSON content. diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs index 2126df011d..1d28ca1bc5 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs @@ -10,6 +10,8 @@ namespace UnityEngine.InputSystem.Editor { internal class InputActionsEditorSettingsProvider : SettingsProvider { + private static InputActionsEditorSettingsProvider s_Provider; + public static string SettingsPath => InputSettingsPath.kSettingsRootPath; [SerializeField] InputActionsEditorState m_State; @@ -76,6 +78,9 @@ public override void OnDeactivate() m_RootVisualElement.UnregisterCallback(OnEditFocus); } + // Make sure any remaining changes are actually saved + SaveAssetOnFocusLost(); + // Note that OnDeactivate will also trigger when opening the Project Settings (existing instance). // Hence we guard against duplicate OnDeactivate() calls. if (m_HasEditFocus) @@ -103,11 +108,11 @@ private void OnEditFocus(FocusInEvent @event) void SaveAssetOnFocusLost() { - #if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST +#if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST var asset = GetAsset(); if (asset != null) ValidateAndSaveAsset(asset); - #endif +#endif } public static void SetIMGUIDropdownVisible(bool visible, bool optionWasSelected) @@ -159,19 +164,19 @@ private void OnEditFocusLost(FocusOutEvent @event) private void OnStateChanged(InputActionsEditorState newState) { - #if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST +#if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST // No action, auto-saved on edit-focus lost - #else +#else // Project wide input actions always auto save - don't check the asset auto save status var asset = GetAsset(); if (asset != null) ValidateAndSaveAsset(asset); - #endif +#endif } private void ValidateAndSaveAsset(InputActionAsset asset) { - ProjectWideActionsAsset.Validate(asset); // Ignore validation result for save + ProjectWideActionsAsset.Verify(asset); // Ignore verification result for save EditorHelpers.SaveAsset(AssetDatabase.GetAssetPath(asset), asset.ToJson()); } @@ -232,23 +237,18 @@ private void BuildUI() // Remove input action editor if already present { - VisualElement element; - do - { - element = m_RootVisualElement.Q("action-editor"); - if (element != null) - m_RootVisualElement.Remove(element); - } - while (element != null); + VisualElement element = m_RootVisualElement.Q("action-editor"); + if (element != null) + m_RootVisualElement.Remove(element); } // If the editor is associated with an asset we show input action editor if (hasAsset) { - m_StateContainer = new StateContainer(m_RootVisualElement, m_State); + m_StateContainer = new StateContainer(m_State); m_StateContainer.StateChanged += OnStateChanged; m_View = new InputActionsEditorView(m_RootVisualElement, m_StateContainer, true, null); - m_StateContainer.Initialize(); + m_StateContainer.Initialize(m_RootVisualElement.Q("action-editor")); } } @@ -272,6 +272,8 @@ private void ModeChanged(PlayModeStateChange change) SetObjectFieldEnabled(true); break; case PlayModeStateChange.ExitingEditMode: + // Ensure any changes are saved to the asset; FocusLost isn't always triggered when entering PlayMode. + SaveAssetOnFocusLost(); SetObjectFieldEnabled(false); break; case PlayModeStateChange.EnteredPlayMode: @@ -284,7 +286,10 @@ private void ModeChanged(PlayModeStateChange change) [SettingsProvider] public static SettingsProvider CreateGlobalInputActionsEditorProvider() { - return new InputActionsEditorSettingsProvider(SettingsPath, SettingsScope.Project); + if (s_Provider == null) + s_Provider = new InputActionsEditorSettingsProvider(SettingsPath, SettingsScope.Project); + + return s_Provider; } #region Shortcuts diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindow.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindow.cs index a4e1b42fb1..2ecf5c9bdd 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindow.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindow.cs @@ -7,6 +7,8 @@ using UnityEditor.Callbacks; using UnityEditor.PackageManager.UI; using UnityEditor.ShortcutManagement; +using UnityEngine.UIElements; +using UnityEditor.UIElements; namespace UnityEngine.InputSystem.Editor { @@ -212,7 +214,7 @@ private void BuildUI() { CleanupStateContainer(); - m_StateContainer = new StateContainer(rootVisualElement, m_State); + m_StateContainer = new StateContainer(m_State); m_StateContainer.StateChanged += OnStateChanged; rootVisualElement.Clear(); @@ -220,7 +222,7 @@ private void BuildUI() rootVisualElement.styleSheets.Add(InputActionsEditorWindowUtils.theme); m_View = new InputActionsEditorView(rootVisualElement, m_StateContainer, false, Save); - m_StateContainer.Initialize(); + m_StateContainer.Initialize(rootVisualElement.Q("action-editor")); } private void OnStateChanged(InputActionsEditorState newState) @@ -253,7 +255,7 @@ private void Save() #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS var projectWideActions = InputSystem.actions; if (projectWideActions != null && path == AssetDatabase.GetAssetPath(projectWideActions)) - ProjectWideActionsAsset.Validate(GetEditedAsset()); + ProjectWideActionsAsset.Verify(GetEditedAsset()); #endif if (InputActionAssetManager.SaveAsset(path, GetEditedAsset().ToJson())) TryUpdateFromAsset(); diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsProjectSettings.uxml b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsProjectSettings.uxml index b6fb62ec4d..d8bde95b30 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsProjectSettings.uxml +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsProjectSettings.uxml @@ -8,7 +8,7 @@ - + diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/SerializedInputAction.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/SerializedInputAction.cs index e8494086a9..80d53fa687 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/SerializedInputAction.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/SerializedInputAction.cs @@ -18,6 +18,7 @@ public SerializedInputAction(SerializedProperty serializedProperty) type = (InputActionType)serializedProperty.FindPropertyRelative(nameof(InputAction.m_Type)).intValue; interactions = serializedProperty.FindPropertyRelative(nameof(InputAction.m_Interactions)).stringValue; processors = serializedProperty.FindPropertyRelative(nameof(InputAction.m_Processors)).stringValue; + propertyPath = wrappedProperty.propertyPath; initialStateCheck = ReadInitialStateCheck(serializedProperty); actionTypeTooltip = serializedProperty.FindPropertyRelative(nameof(InputAction.m_Type)).GetTooltip(); expectedControlTypeTooltip = serializedProperty.FindPropertyRelative(nameof(InputAction.m_ExpectedControlType)).GetTooltip(); @@ -29,6 +30,7 @@ public SerializedInputAction(SerializedProperty serializedProperty) public InputActionType type { get; } public string interactions { get; } public string processors { get; } + public string propertyPath { get; } public bool initialStateCheck { get; } public string actionTypeTooltip { get; } public string expectedControlTypeTooltip { get; } @@ -60,7 +62,7 @@ public bool Equals(SerializedInputAction other) && initialStateCheck == other.initialStateCheck && actionTypeTooltip == other.actionTypeTooltip && expectedControlTypeTooltip == other.expectedControlTypeTooltip - && wrappedProperty.propertyPath == other.wrappedProperty.propertyPath; + && propertyPath == other.propertyPath; } public override bool Equals(object obj) @@ -79,7 +81,7 @@ public override int GetHashCode() hashCode.Add(initialStateCheck); hashCode.Add(actionTypeTooltip); hashCode.Add(expectedControlTypeTooltip); - hashCode.Add(wrappedProperty.propertyPath); + hashCode.Add(propertyPath); return hashCode.ToHashCode(); } } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/SerializedInputBinding.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/SerializedInputBinding.cs index e55fe9590c..a2e7e942b1 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/SerializedInputBinding.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/SerializedInputBinding.cs @@ -24,6 +24,7 @@ public SerializedInputBinding(SerializedProperty serializedProperty) interactions = serializedProperty.FindPropertyRelative("m_Interactions").stringValue; processors = serializedProperty.FindPropertyRelative("m_Processors").stringValue; action = serializedProperty.FindPropertyRelative("m_Action").stringValue; + propertyPath = wrappedProperty.propertyPath; var bindingGroups = serializedProperty.FindPropertyRelative(nameof(InputBinding.m_Groups)).stringValue; controlSchemes = bindingGroups != null ? bindingGroups.Split(InputBinding.kSeparatorString, StringSplitOptions.RemoveEmptyEntries) @@ -44,6 +45,7 @@ public SerializedInputBinding(SerializedProperty serializedProperty) public string interactions { get; } public string processors { get; } public string action { get; } + public string propertyPath { get; } public string[] controlSchemes { get; } public InputBinding.Flags flags { get; } @@ -88,7 +90,7 @@ public bool Equals(SerializedInputBinding other) && isPartOfComposite == other.isPartOfComposite && compositePath == other.compositePath && controlSchemes.SequenceEqual(other.controlSchemes) - && wrappedProperty.propertyPath == other.wrappedProperty.propertyPath; + && propertyPath == other.propertyPath; } public override bool Equals(object obj) @@ -110,7 +112,7 @@ public override int GetHashCode() hashCode.Add(isPartOfComposite); hashCode.Add(compositePath); hashCode.Add(controlSchemes); - hashCode.Add(wrappedProperty.propertyPath); + hashCode.Add(propertyPath); return hashCode.ToHashCode(); } } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/StateContainer.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/StateContainer.cs index a70b283563..772db5edae 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/StateContainer.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/StateContainer.cs @@ -11,20 +11,12 @@ internal class StateContainer { public event Action StateChanged; - private readonly VisualElement m_RootVisualElement; + private VisualElement m_RootVisualElement; private InputActionsEditorState m_State; - public StateContainer(VisualElement rootVisualElement, InputActionsEditorState initialState) + public StateContainer(InputActionsEditorState initialState) { - m_RootVisualElement = rootVisualElement; m_State = initialState; - - rootVisualElement.Unbind(); - m_RootVisualElement.TrackSerializedObjectValue(initialState.serializedObject, so => - { - StateChanged?.Invoke(m_State); - }); - rootVisualElement.Bind(initialState.serializedObject); } public void Dispatch(Command command) @@ -51,9 +43,20 @@ public void Dispatch(Command command) }); } - public void Initialize() + public void Initialize(VisualElement rootVisualElement) { + // We need to use a root element for the TrackSerializedObjectValue that is destroyed with the view. + // Using a root element from the settings window would not enable the tracking callback to be destroyed or garbage collected. + + m_RootVisualElement = rootVisualElement; + + m_RootVisualElement.Unbind(); + m_RootVisualElement.TrackSerializedObjectValue(m_State.serializedObject, so => + { + StateChanged?.Invoke(m_State); + }); StateChanged?.Invoke(m_State); + rootVisualElement.Bind(m_State.serializedObject); } /// diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Views/ActionMapsView.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Views/ActionMapsView.cs index 43d2c6e2e8..678b14d80d 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Views/ActionMapsView.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Views/ActionMapsView.cs @@ -67,6 +67,8 @@ public ActionMapsView(VisualElement root, StateContainer stateContainer) m_AddActionMapButton.clicked += AddActionMap; ContextMenu.GetContextMenuForActionMapsEmptySpace(this, root.Q("rclick-area-to-add-new-action-map")); + // Only bring up this context menu for the List when it's empty, so we can treat it like right-clicking the empty space: + ContextMenu.GetContextMenuForActionMapsEmptySpace(this, m_ListView, onlyShowIfListIsEmpty: true); } void OnDroppedHandler(int mapIndex) @@ -154,6 +156,11 @@ internal void AddActionMap() m_EnterRenamingMode = true; } + internal int GetMapCount() + { + return m_ListView.itemsSource.Count; + } + private void OnExecuteCommand(ExecuteCommandEvent evt) { var selectedItem = m_ListView.GetRootElementForIndex(m_ListView.selectedIndex); @@ -218,7 +225,8 @@ private void OnPointerDown(PointerDownEvent evt) if (evt.button == (int)MouseButton.RightMouse && evt.clickCount == 1) { var actionMap = (evt.target as VisualElement).GetFirstAncestorOfType(); - m_ListView.SetSelection(actionMap.parent.IndexOf(actionMap)); + if (actionMap != null) + m_ListView.SetSelection(actionMap.parent.IndexOf(actionMap)); } } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Views/ActionsTreeView.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Views/ActionsTreeView.cs index f9f9d24233..e301f6e6b5 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Views/ActionsTreeView.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Views/ActionsTreeView.cs @@ -18,6 +18,7 @@ namespace UnityEngine.InputSystem.Editor /// internal class ActionsTreeView : ViewBase { + private readonly ListView m_ActionMapsListView; private readonly TreeView m_ActionsTreeView; private readonly Button m_AddActionButton; private readonly ScrollView m_PropertiesScrollview; @@ -31,6 +32,7 @@ internal class ActionsTreeView : ViewBase public ActionsTreeView(VisualElement root, StateContainer stateContainer) : base(root, stateContainer) { + m_ActionMapsListView = root.Q("action-maps-list-view"); m_AddActionButton = root.Q