diff --git a/Assets/Samples/UIvsGameInput/UIvsGameInputHandler.cs b/Assets/Samples/UIvsGameInput/UIvsGameInputHandler.cs index 386218b923..151a2ec809 100644 --- a/Assets/Samples/UIvsGameInput/UIvsGameInputHandler.cs +++ b/Assets/Samples/UIvsGameInput/UIvsGameInputHandler.cs @@ -137,7 +137,7 @@ public void Update() transform.rotation = default; // When using a pointer-based control scheme, we engage camera look explicitly. - if (m_ControlStyle != ControlStyle.GamepadJoystick && m_LookEngageAction.WasPressedThisFrame() && IsPointerInsideScreen()) + if (m_ControlStyle != ControlStyle.GamepadJoystick && m_LookEngageAction.WasPressedThisDynamicUpdate() && IsPointerInsideScreen()) EngageCameraControl(); // With gamepad/joystick, we can freely rotate the camera at any time. @@ -166,14 +166,14 @@ public void Update() if (m_Mouse != null) m_MousePositionToWarpToAfterCursorUnlock = m_MousePositionToWarpToAfterCursorUnlock.Value + m_Mouse.delta.ReadValue(); - if (m_CancelAction.WasPressedThisFrame() || !m_LookEngageAction.IsPressed()) + if (m_CancelAction.WasPressedThisDynamicUpdate() || !m_LookEngageAction.IsPressed()) DisengageCameraControl(); break; case State.InMenu: - if (m_CancelAction.WasPressedThisFrame()) + if (m_CancelAction.WasPressedThisDynamicUpdate()) OnContinueClicked(); break; diff --git a/Assets/Tests/InputSystem/CoreTests_Actions.cs b/Assets/Tests/InputSystem/CoreTests_Actions.cs index 02c7cc8fb6..65a33219aa 100644 --- a/Assets/Tests/InputSystem/CoreTests_Actions.cs +++ b/Assets/Tests/InputSystem/CoreTests_Actions.cs @@ -58,6 +58,120 @@ public void Settings_ShouldStoreSettingsAndFeatureFlags(string featureName) } } + [UnityTest] + [Category("Actions")] + public IEnumerator Actions_WasStateReachedThisRenderingFrameOperatesIndependentlyFromInputUpdateStep() + { + var updateMode = InputSystem.settings.updateMode; + InputSystem.settings.updateMode = InputSettings.UpdateMode.ProcessEventsInDynamicUpdate; + + var gamepad = InputSystem.AddDevice(); + var simpleAction = new InputAction(binding: "/buttonSouth"); + simpleAction.Enable(); + + Assert.That(simpleAction.WasPerformedThisDynamicUpdate(), Is.False); + Assert.That(simpleAction.WasPressedThisDynamicUpdate(), Is.False); + Assert.That(simpleAction.WasReleasedThisDynamicUpdate(), Is.False); + Assert.That(simpleAction.WasCompletedThisDynamicUpdate(), Is.False); + + PressAndRelease(gamepad.buttonSouth); + + yield return null; // InputSystem.Update is called and the action state chenges + + Assert.That(simpleAction.WasPerformedThisDynamicUpdate(), Is.True); + Assert.That(simpleAction.WasPressedThisDynamicUpdate(), Is.True); + Assert.That(simpleAction.WasReleasedThisDynamicUpdate(), Is.True); + + InputSystem.Update(); // a manual update happens between two frames, that does not affect the output of the WasPerformedThisDynamicUpdate + + Assert.That(simpleAction.WasPerformedThisDynamicUpdate(), Is.True); + Assert.That(simpleAction.WasPressedThisDynamicUpdate(), Is.True); + Assert.That(simpleAction.WasReleasedThisDynamicUpdate(), Is.True); + + //Reset State + InputSystem.settings.updateMode = updateMode; + } + + [UnityTest] + [Category("Actions")] + public IEnumerator Actions_WasStateReachedThisFrameOperatesIndependentlyFromRenderingFrame() + { + var updateMode = InputSystem.settings.updateMode; + InputSystem.settings.updateMode = InputSettings.UpdateMode.ProcessEventsManually; + + var gamepad = InputSystem.AddDevice(); + var simpleAction = new InputAction(binding: "/buttonSouth"); + simpleAction.Enable(); + + Assert.That(simpleAction.WasPerformedThisFrame(), Is.False); + Assert.That(simpleAction.WasPressedThisFrame(), Is.False); + Assert.That(simpleAction.WasReleasedThisFrame(), Is.False); + Assert.That(simpleAction.WasCompletedThisFrame(), Is.False); + + PressAndRelease(gamepad.buttonSouth); + + yield return null; // InputSystem.Update is not called and the action state does not change + yield return null; + yield return null; + + Assert.That(simpleAction.WasPerformedThisFrame(), Is.False); + Assert.That(simpleAction.WasPressedThisFrame(), Is.False); + Assert.That(simpleAction.WasReleasedThisFrame(), Is.False); + + InputSystem.Update(); // a manual update happens between two frames, that does not affect the output of the WasXYZThisRenderingFrame but does affect the WasXYZThisFrame + + Assert.That(simpleAction.WasPerformedThisFrame(), Is.True); + Assert.That(simpleAction.WasPressedThisFrame(), Is.True); + Assert.That(simpleAction.WasReleasedThisFrame(), Is.True); + + //Reset State + InputSystem.settings.updateMode = updateMode; + } + + [UnityTest] + [Category("Actions")] + public IEnumerator Actions_WasStateReachedThisFrameAndWasStateReachedThisRenderingFrameCanOperateSimultanously() + { + var updateMode = InputSystem.settings.updateMode; + InputSystem.settings.updateMode = InputSettings.UpdateMode.ProcessEventsManually; + + var gamepad = InputSystem.AddDevice(); + var simpleAction = new InputAction(binding: "/buttonSouth"); + simpleAction.Enable(); + + Assert.That(simpleAction.WasPerformedThisFrame(), Is.False); + Assert.That(simpleAction.WasPressedThisFrame(), Is.False); + Assert.That(simpleAction.WasReleasedThisFrame(), Is.False); + Assert.That(simpleAction.WasCompletedThisFrame(), Is.False); + + PressAndRelease(gamepad.buttonSouth); + + yield return null; // InputSystem.Update is not called and the action state does not change + + Assert.That(simpleAction.WasPerformedThisFrame(), Is.False); + Assert.That(simpleAction.WasPressedThisFrame(), Is.False); + Assert.That(simpleAction.WasReleasedThisFrame(), Is.False); + + InputSystem.Update(); // a manual update happens between two frames, that does not affect the output of the WasXYZThisRenderingFrame but does affect the WasXYZThisFrame + + Assert.That(simpleAction.WasPerformedThisFrame(), Is.True); + Assert.That(simpleAction.WasPressedThisFrame(), Is.True); + Assert.That(simpleAction.WasReleasedThisFrame(), Is.True); + + yield return null; + + Assert.That(simpleAction.WasPerformedThisDynamicUpdate(), Is.True); + Assert.That(simpleAction.WasPressedThisDynamicUpdate(), Is.True); + Assert.That(simpleAction.WasReleasedThisDynamicUpdate(), Is.True); + + yield return null; + + Assert.That(simpleAction.WasCompletedThisDynamicUpdate(), Is.False); + + //Reset State + InputSystem.settings.updateMode = updateMode; + } + [Test] [Category("Actions")] public void Actions_WhenShortcutsDisabled_AllConflictingActionsTrigger() diff --git a/Assets/Tests/Samples/RebindingUITests.cs b/Assets/Tests/Samples/RebindingUITests.cs index 577310a149..1a76bd55a6 100644 --- a/Assets/Tests/Samples/RebindingUITests.cs +++ b/Assets/Tests/Samples/RebindingUITests.cs @@ -1,3 +1,4 @@ +using System.Collections; using System.IO; using NUnit.Framework; using UnityEngine; @@ -5,6 +6,7 @@ using UnityEngine.InputSystem; using UnityEngine.InputSystem.Samples.RebindUI; using UnityEngine.InputSystem.UI; +using UnityEngine.TestTools; using UnityEngine.UI; public class RebindingUITests : CoreTestsFixture @@ -84,9 +86,9 @@ public void Samples_RebindingUI_UpdatesWhenKeyboardLayoutChanges() } // https://fogbugz.unity3d.com/f/cases/1271591/ - [Test] + [UnityTest] [Category("Samples")] - public void Samples_RebindingUI_SuppressingEventsDoesNotInterfereWithUIInput() + public IEnumerator Samples_RebindingUI_SuppressingEventsDoesNotInterfereWithUIInput() { var keyboard = InputSystem.AddDevice(); @@ -135,6 +137,8 @@ public void Samples_RebindingUI_SuppressingEventsDoesNotInterfereWithUIInput() // UI should be fine with that. PressAndRelease(keyboard.enterKey); eventSystem.InvokeUpdate(); + yield return null; + Assert.That(rebind.ongoingRebind, Is.Not.Null); Assert.That(rebind.ongoingRebind.started, Is.True); @@ -144,6 +148,7 @@ public void Samples_RebindingUI_SuppressingEventsDoesNotInterfereWithUIInput() Press(keyboard.bKey); eventSystem.InvokeUpdate(); + yield return null; Assert.That(rebind.ongoingRebind, Is.Not.Null); Assert.That(rebind.ongoingRebind.started, Is.True); @@ -162,6 +167,7 @@ public void Samples_RebindingUI_SuppressingEventsDoesNotInterfereWithUIInput() // Start another rebind via "Submit". PressAndRelease(keyboard.enterKey); eventSystem.InvokeUpdate(); + yield return null; Assert.That(rebind.ongoingRebind, Is.Not.Null); Assert.That(rebind.ongoingRebind.started, Is.True); diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 6773efbc0a..c3ac3ee766 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -28,12 +28,16 @@ however, it has to be formatted properly to pass verification tests. - Fixed arrow key navigation of Input Actions after Action rename. [ISXB-1024](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1024) - Fixed gamepad navigation in UI Toolkit TextField when using InputSystemUIInputModule. [UUM-77364](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-77364) - Fixed issue where asset editor window splitter positions were not persisted [ISXB-1316] +- Fixed an issue where updating the InputSystem outside of the dynamic Update would lead to UI input and navigation events get lost. [ISXB-1313](https://issuetracker.unity3d.com/issues/ui-onclick-events-sometimes-do-not-trigger-when-manual-update-is-utilized-with-input-system) - Fixed a bug that would cause `TrackedPoseDriver` to update position and rotation when no HMD device is connected [ISXB-699](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-699) instead of keeping it unchanged. ### Changed - Changed default input action asset name from New Controls to New Actions. - Added disabling of action maps in rebinding UI sample. +### Added +- An alternative way to access if an action state reached a certain phase during this rendering frame (Update()). This can be utilized even if the InputSystem update mode is set to manual or FixedUpdate. It can be used to access the action phase during rendering, eg for perform updates to the UI. + ## [1.13.0] - 2025-02-05 ### Fixed diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs index d4c1dd41d5..5447f0f4c2 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs @@ -1248,10 +1248,11 @@ private int ExpectedFrame() /// This method will disregard whether the action is currently enabled or disabled. It will keep returning /// true for the duration of the frame even if the action was subsequently disabled in the frame. /// - /// The meaning of "frame" is either the current "dynamic" update (MonoBehaviour.Update) or the current - /// fixed update (MonoBehaviour.FixedUpdate) depending on the value of the setting. + /// NOTE: If the is set to or and InputSystem.Update() is not called in + /// the dynamic Update, use during dynamic Update instead. /// /// + /// /// /// /// @@ -1262,7 +1263,46 @@ public unsafe bool WasPressedThisFrame() { var actionStatePtr = &state.actionStates[m_ActionIndexInState]; var currentUpdateStep = InputUpdate.s_UpdateStepCount; - return actionStatePtr->pressedInUpdate == currentUpdateStep && currentUpdateStep != default && actionStatePtr->frame == ExpectedFrame(); + return actionStatePtr->pressedInUpdate == currentUpdateStep && currentUpdateStep != default; + } + + return false; + } + + /// + /// Returns true if the action's value crossed the press threshold (see ) + /// in the MonoBehaviour Update cycle (rendering frame). + /// + /// True if the action was pressed in the MonoBehaviour Update cycle (rendering frame). + /// + /// Unlike , this method will return true only if the InputSystem was updated and the action was pressed in the current dynamic Update cycle (in between the previous and the current frame). + /// This can be used in dynamic update if the is set to or . + /// If the update mode is set to , this method will behave exactly like . + /// + /// When processing input events manually, updating the InputSystem in the dynamic Update cycle will lead to a delay of one frame for WasPressedThisDynamicUpdate, + /// you may want to use WasPressedThisFrame to avoid this, or set the input update mode to InputSettings.UpdateMode.ProcessEventsInDynamicUpdate. + /// + /// + /// + /// var fire = playerInput.actions["fire"]; + /// if (fire.WasPressedThisDynamicUpdate() && fire.IsPressed()) + /// StartFiring(); + /// else if (fire.WasReleasedThisDynamicUpdate()) + /// StopFiring(); + /// + /// + /// + /// + /// + /// + /// + public unsafe bool WasPressedThisDynamicUpdate() + { + var state = GetOrCreateActionMap().m_State; + if (state != null) + { + var actionStatePtr = &state.actionStates[m_ActionIndexInState]; + return actionStatePtr->framePressed == ExpectedFrame(); } return false; @@ -1297,10 +1337,11 @@ public unsafe bool WasPressedThisFrame() /// This method will disregard whether the action is currently enabled or disabled. It will keep returning /// true for the duration of the frame even if the action was subsequently disabled in the frame. /// - /// The meaning of "frame" is either the current "dynamic" update (MonoBehaviour.Update) or the current - /// fixed update (MonoBehaviour.FixedUpdate) depending on the value of the setting. + /// NOTE: If the is set to or and InputSystem.Update() is not called in + /// the dynamic Update, use during dynamic Update instead. /// /// + /// /// /// /// @@ -1311,7 +1352,47 @@ public unsafe bool WasReleasedThisFrame() { var actionStatePtr = &state.actionStates[m_ActionIndexInState]; var currentUpdateStep = InputUpdate.s_UpdateStepCount; - return actionStatePtr->releasedInUpdate == currentUpdateStep && currentUpdateStep != default && actionStatePtr->frame == ExpectedFrame(); + return actionStatePtr->releasedInUpdate == currentUpdateStep && currentUpdateStep != default; + } + + return false; + } + + /// + /// Returns true if the action's value crossed the release threshold (see ) + /// at any point in the MonoBehaviour Update cycle (rendering frame). + /// + /// True if the action was released in the MonoBehaviour Update cycle (rendering frame). + /// + /// Unlike , this method will return true only if the InputSystem was updated and the action was released in the current dynamic Update cycle (in between the previous and the current frame). + /// This can be used in dynamic update if the is set to or . + /// If the update mode is set to , this method will behave exactly like . + /// + /// When processing input events manually, updating the InputSystem in the dynamic Update cycle will lead to a delay of one frame for WasReleasedThisDynamicUpdate, + /// you may want to use WasReleasedThisFrame to avoid this, or set the input update mode to InputSettings.UpdateMode.ProcessEventsInDynamicUpdate. + /// + /// + /// + /// var fire = playerInput.actions["fire"]; + /// if (fire.WasPressedThisDynamicUpdate() && fire.IsPressed()) + /// StartFiring(); + /// else if (fire.WasReleasedThisDynamicUpdate()) + /// StopFiring(); + /// + /// + /// + /// + /// + /// + /// + /// + public unsafe bool WasReleasedThisDynamicUpdate() + { + var state = GetOrCreateActionMap().m_State; + if (state != null) + { + var actionStatePtr = &state.actionStates[m_ActionIndexInState]; + return actionStatePtr->frameReleased == ExpectedFrame(); } return false; @@ -1356,9 +1437,10 @@ public unsafe bool WasReleasedThisFrame() /// This method will disregard whether the action is currently enabled or disabled. It will keep returning /// true for the duration of the frame even if the action was subsequently disabled in the frame. /// - /// The meaning of "frame" is either the current "dynamic" update (MonoBehaviour.Update) or the current - /// fixed update (MonoBehaviour.FixedUpdate) depending on the value of the setting. + /// NOTE: If the is set to or and InputSystem.Update() is not called in + /// the dynamic Update, use when trying to access in dynamic Update instead. /// + /// /// /// /// @@ -1370,7 +1452,45 @@ public unsafe bool WasPerformedThisFrame() { var actionStatePtr = &state.actionStates[m_ActionIndexInState]; var currentUpdateStep = InputUpdate.s_UpdateStepCount; - return actionStatePtr->lastPerformedInUpdate == currentUpdateStep && currentUpdateStep != default && actionStatePtr->frame == ExpectedFrame(); + return actionStatePtr->lastPerformedInUpdate == currentUpdateStep && currentUpdateStep != default; + } + + return false; + } + + /// + /// Check whether was at any point + /// in the MonoBehaviour Update cycle (rendering frame). + /// + /// True if the action performed in the MonoBehaviour Update cycle (rendering frame). + /// + /// Unlike , this method will return true only if the InputSystem was updated and the action was performed in the current dynamic Update cycle (in between the previous and the current frame). + /// This can be used in dynamic update if the is set to or . + /// If the update mode is set to , this method will behave exactly like . + /// + /// When processing input events manually, updating the InputSystem in the dynamic Update cycle will lead to a delay of one frame for WasPerformedThisDynamicUpdate, + /// you may want to use WasPerformedThisFrame to avoid this, or set the input update mode to InputSettings.UpdateMode.ProcessEventsInDynamicUpdate. + /// + /// + /// + /// var warp = playerInput.actions["Warp"]; + /// if (warp.WasPerformedThisDynamicUpdate()) + /// InitiateWarp(); + /// + /// + /// + /// + /// + /// + /// + public unsafe bool WasPerformedThisDynamicUpdate() + { + var state = GetOrCreateActionMap().m_State; + + if (state != null) + { + var actionStatePtr = &state.actionStates[m_ActionIndexInState]; + return actionStatePtr->framePerformed == ExpectedFrame(); } return false; @@ -1426,8 +1546,8 @@ public unsafe bool WasPerformedThisFrame() /// true for the duration of the frame even if the action was subsequently disabled in the frame. /// /// - /// The meaning of "frame" is either the current "dynamic" update (MonoBehaviour.Update) or the current - /// fixed update (MonoBehaviour.FixedUpdate) depending on the value of the setting. + /// NOTE: If the is set to or and InputSystem.Update() is not called in + /// the dynamic Update, use to access this during dynamic Update instead. /// /// /// @@ -1439,6 +1559,7 @@ public unsafe bool WasPerformedThisFrame() /// StopTeleport(); /// /// + /// /// /// /// @@ -1450,7 +1571,47 @@ public unsafe bool WasCompletedThisFrame() { var actionStatePtr = &state.actionStates[m_ActionIndexInState]; var currentUpdateStep = InputUpdate.s_UpdateStepCount; - return actionStatePtr->lastCompletedInUpdate == currentUpdateStep && currentUpdateStep != default && actionStatePtr->frame == ExpectedFrame(); + return actionStatePtr->lastCompletedInUpdate == currentUpdateStep && currentUpdateStep != default; + } + + return false; + } + + /// + /// Check whether transitioned from to any other phase + /// value at least once in the MonoBehaviour Update cycle (rendering frame). + /// + /// True if the action completed in this MonoBehaviour Update cycle (rendering frame). + /// + /// Unlike , this method will return true only if the InputSystem was updated and the action was completed in the current dynamic Update cycle (in between the previous and the current frame). + /// This can be used in dynamic update if the is set to or . + /// If the update mode is set to , this method will behave exactly like . + /// + /// When processing input events manually, updating the InputSystem in the dynamic Update cycle will lead to a delay of one frame for WasCompletedThisDynamicUpdate, + /// you may want to use WasCompletedThisFrame to avoid this, or set the input update mode to InputSettings.UpdateMode.ProcessEventsInDynamicUpdate. + /// + /// + /// + /// var teleport = playerInput.actions["Teleport"]; + /// if (teleport.WasPerformedThisDynamicUpdate()) + /// InitiateTeleport(); + /// else if (teleport.WasCompletedThisDynamicUpdate()) + /// StopTeleport(); + /// + /// + /// + /// + /// + /// + /// + public unsafe bool WasCompletedThisDynamicUpdate() + { + var state = GetOrCreateActionMap().m_State; + + if (state != null) + { + var actionStatePtr = &state.actionStates[m_ActionIndexInState]; + return actionStatePtr->frameCompleted == ExpectedFrame(); } return false; diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs index 79cfa1b8cb..fe8df0911c 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs @@ -559,8 +559,11 @@ private void RestoreActionStatesAfterReResolvingBindings(UnmanagedMemory oldStat newActionState.pressedInUpdate = oldActionState.pressedInUpdate; newActionState.releasedInUpdate = oldActionState.releasedInUpdate; newActionState.startTime = oldActionState.startTime; + newActionState.framePerformed = oldActionState.framePerformed; + newActionState.frameCompleted = oldActionState.frameCompleted; + newActionState.framePressed = oldActionState.framePressed; + newActionState.frameReleased = oldActionState.frameReleased; newActionState.bindingIndex = oldActionState.bindingIndex; - newActionState.frame = oldActionState.frame; if (oldActionState.phase != InputActionPhase.Disabled) { @@ -885,7 +888,10 @@ public void ResetActionState(int actionIndex, InputActionPhase toPhase = InputAc actionState->lastCompletedInUpdate = default; actionState->pressedInUpdate = default; actionState->releasedInUpdate = default; - actionState->frame = default; + actionState->framePerformed = default; + actionState->frameCompleted = default; + actionState->framePressed = default; + actionState->frameReleased = default; } Debug.Assert(!actionState->isStarted, "Cannot reset an action to started phase"); @@ -1563,18 +1569,18 @@ private void ProcessButtonState(ref TriggerState trigger, int actionIndex, Bindi var actionState = &actionStates[actionIndex]; if (!actionState->isPressed && actuation >= pressPoint) { + actionState->framePressed = Time.frameCount; actionState->pressedInUpdate = InputUpdate.s_UpdateStepCount; actionState->isPressed = true; - actionState->frame = Time.frameCount; } else if (actionState->isPressed) { var releasePoint = pressPoint * ButtonControl.s_GlobalDefaultButtonReleaseThreshold; if (actuation <= releasePoint) { + actionState->frameReleased = Time.frameCount; actionState->releasedInUpdate = InputUpdate.s_UpdateStepCount; actionState->isPressed = false; - actionState->frame = Time.frameCount; } } } @@ -2433,9 +2439,9 @@ private void ChangePhaseOfActionInternal(int actionIndex, TriggerState* actionSt newState.magnitude = 0f; newState.phase = newPhase; - newState.frame = Time.frameCount; if (newPhase == InputActionPhase.Performed) { + newState.framePerformed = Time.frameCount; newState.lastPerformedInUpdate = InputUpdate.s_UpdateStepCount; newState.lastCanceledInUpdate = actionState->lastCanceledInUpdate; @@ -2453,23 +2459,34 @@ private void ChangePhaseOfActionInternal(int actionIndex, TriggerState* actionSt { newState.lastCanceledInUpdate = InputUpdate.s_UpdateStepCount; newState.lastPerformedInUpdate = actionState->lastPerformedInUpdate; + newState.framePerformed = actionState->framePerformed; } else { newState.lastPerformedInUpdate = actionState->lastPerformedInUpdate; + newState.framePerformed = actionState->framePerformed; newState.lastCanceledInUpdate = actionState->lastCanceledInUpdate; } // When we go from Performed to Disabling, we take a detour through Canceled. // To replicate the behavior of releasedInUpdate where it doesn't get updated when the action is disabled // from being performed, we skip updating lastCompletedInUpdate if Disabled is the phase after Canceled. - if (actionState->phase == InputActionPhase.Performed && newPhase != InputActionPhase.Performed && !isDisablingAction) + if (actionState->phase == InputActionPhase.Performed && newPhase != InputActionPhase.Performed && + !isDisablingAction) + { + newState.frameCompleted = Time.frameCount; newState.lastCompletedInUpdate = InputUpdate.s_UpdateStepCount; + } else + { newState.lastCompletedInUpdate = actionState->lastCompletedInUpdate; + newState.frameCompleted = actionState->frameCompleted; + } newState.pressedInUpdate = actionState->pressedInUpdate; + newState.framePressed = actionState->framePressed; newState.releasedInUpdate = actionState->releasedInUpdate; + newState.frameReleased = actionState->frameReleased; if (newPhase == InputActionPhase.Started) newState.startTime = newState.time; *actionState = newState; @@ -3640,7 +3657,10 @@ public struct TriggerState [FieldOffset(40)] private uint m_PressedInUpdate; [FieldOffset(44)] private uint m_ReleasedInUpdate; [FieldOffset(48)] private uint m_LastCompletedInUpdate; - [FieldOffset(52)] private int m_Frame; + [FieldOffset(52)] internal int framePerformed; + [FieldOffset(56)] internal int framePressed; + [FieldOffset(60)] internal int frameReleased; + [FieldOffset(64)] internal int frameCompleted; /// /// Phase being triggered by the control value change. @@ -3796,12 +3816,6 @@ public uint lastPerformedInUpdate set => m_LastPerformedInUpdate = value; } - internal int frame - { - get => m_Frame; - set => m_Frame = value; - } - /// /// Update step count () in which action completed last. /// Zero if the action did not become completed yet. Also reset to zero when the action is hard reset. diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs index cdf25d9b1c..1b333b0ed4 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs @@ -116,6 +116,9 @@ public override void OnGUI(string searchContext) EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(m_UpdateMode, m_UpdateModeContent); + if (InputSystem.settings?.updateMode == InputSettings.UpdateMode.ProcessEventsManually) + CustomUpdateModeHelpBox(); + var runInBackground = Application.runInBackground; using (new EditorGUI.DisabledScope(!runInBackground)) EditorGUILayout.PropertyField(m_BackgroundBehavior, m_BackgroundBehaviorContent); @@ -186,6 +189,22 @@ public override void OnGUI(string searchContext) } } + private void CustomUpdateModeHelpBox() + { + var message = + "This is not recommended, the default update mode is dynamic update and should only be changed for compelling reasons.\nPlease refer to the documentation."; + Uri link = new Uri(InputSystem.kDocUrl + "/manual/Settings.html#update-mode"); + GUILayout.BeginHorizontal(EditorStyles.helpBox); + GUILayout.Label(EditorGUIUtility.IconContent("console.warnicon"), GUILayout.ExpandWidth(false)); + GUILayout.BeginVertical(); + GUILayout.Label(message, EditorStyles.label); + if (GUILayout.Button("Read more", EditorStyles.linkLabel)) + System.Diagnostics.Process.Start(link.AbsoluteUri); + EditorGUIUtility.AddCursorRect(GUILayoutUtility.GetLastRect(), MouseCursor.Link); + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + } + private static void ShowPlatformSettings() { // Would be nice to get BuildTargetDiscovery.GetBuildTargetInfoList since that contains information about icons etc diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs index f2443f4fa7..756e192e88 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs @@ -895,7 +895,7 @@ internal void ProcessNavigation(ref NavigationModel navigationState) // Process submit and cancel events. if (!usedSelectionChange && eventSystem.currentSelectedGameObject != null) { - // NOTE: Whereas we use callbacks for the other actions, we rely on WasPressedThisFrame() for + // NOTE: Whereas we use callbacks for the other actions, we rely on WasPerformedThisDynamicUpdate() for // submit and cancel. This makes their behavior inconsistent with pointer click behavior where // a click will register on button *up*, but consistent with how other UI systems work where // click occurs on key press. This nuance in behavior becomes important in combination with @@ -914,9 +914,9 @@ internal void ProcessNavigation(ref NavigationModel navigationState) data.device = m_SubmitCancelState.device; - if (cancelAction != null && cancelAction.WasPerformedThisFrame()) + if (cancelAction != null && cancelAction.WasPerformedThisDynamicUpdate()) ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.cancelHandler); - if (!data.used && submitAction != null && submitAction.WasPerformedThisFrame()) + if (!data.used && submitAction != null && submitAction.WasPerformedThisDynamicUpdate()) ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.submitHandler); } }