From 9f411e74634b11a3a9f99b5b6263a7bf4872ddff Mon Sep 17 00:00:00 2001 From: Chris Massie Date: Mon, 30 Oct 2023 14:41:18 -0700 Subject: [PATCH 01/42] Reverted explicit value initialization to avoid false positive failures of API_MinorVersionsHaveNoBreakingChanges --- .../InputSystem/Actions/Composites/Vector2Composite.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/Composites/Vector2Composite.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/Composites/Vector2Composite.cs index ae09cb1057..c5f001587b 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/Composites/Vector2Composite.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/Composites/Vector2Composite.cs @@ -124,7 +124,7 @@ public class Vector2Composite : InputBindingComposite /// /// /// - public Mode mode = Mode.DigitalNormalized; + public Mode mode; /// public override Vector2 ReadValue(ref InputBindingCompositeContext context) From 9d4f808c55b3d0148692da353012a132c74ab5da Mon Sep 17 00:00:00 2001 From: Chris Massie Date: Mon, 30 Oct 2023 14:42:15 -0700 Subject: [PATCH 02/42] Added valueType property to InputAction, allowing access to it outside of InputAction.CallbackContext --- .../InputSystem/Actions/InputAction.cs | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs index 51e7e4afd5..9e630f5682 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs @@ -583,7 +583,7 @@ public event Action performed public bool triggered => WasPerformedThisFrame(); /// - /// The currently active control that is driving the action. Null while the action + /// The currently active control that is driving the action. while the action /// is in waiting () or canceled () /// state. Otherwise the control that last had activity on it which wasn't ignored. /// @@ -608,6 +608,39 @@ public unsafe InputControl activeControl } } + /// + /// Type of value returned by and expected + /// by . + /// + /// Type of object returned when reading a value. + /// + /// The type of value returned by an action is usually determined by the + /// that triggered the action, i.e. by the + /// control referenced from . + /// + /// However, if the binding that triggered is a composite, then the composite + /// will determine values and not the individual control that triggered (that + /// one just feeds values into the composite). + /// + /// + /// + public unsafe Type valueType + { + get + { + var state = GetOrCreateActionMap().m_State; + if (state != null) + { + var actionStatePtr = &state.actionStates[m_ActionIndexInState]; + var controlIndex = actionStatePtr->controlIndex; + if (controlIndex != InputActionState.kInvalidIndex) + return state.GetValueType(actionStatePtr->bindingIndex, controlIndex); + } + + return null; + } + } + /// /// Whether the action wants a state check on its bound controls as soon as it is enabled. This is always /// true for actions but can optionally be enabled for @@ -990,7 +1023,8 @@ public unsafe TValue ReadValue() where TValue : struct { var state = GetOrCreateActionMap().m_State; - if (state == null) return default(TValue); + if (state == null) + return default(TValue); var actionStatePtr = &state.actionStates[m_ActionIndexInState]; return actionStatePtr->phase.IsInProgress() @@ -1847,6 +1881,7 @@ public unsafe double startTime /// /// /// + /// public Type valueType => m_State?.GetValueType(bindingIndex, controlIndex); /// @@ -1876,8 +1911,6 @@ public int valueSizeInBytes } } - ////TODO: need ability to read as button - /// /// Read the value of the action as a raw byte buffer. This allows reading /// values without having to know value types but also, unlike , From b8042e332e4239b35aa7e33c7e79e036f58ff427 Mon Sep 17 00:00:00 2001 From: Chris Massie Date: Mon, 30 Oct 2023 16:11:13 -0700 Subject: [PATCH 03/42] Renamed to make it clear it functions like activeControl where it can go null when not in progress - Added tests for activeValueType --- Assets/Tests/InputSystem/CoreTests_Actions.cs | 79 +++++++++++++++++++ .../InputSystem/Actions/InputAction.cs | 9 ++- 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/Assets/Tests/InputSystem/CoreTests_Actions.cs b/Assets/Tests/InputSystem/CoreTests_Actions.cs index aa84d64f0b..51f28668bc 100644 --- a/Assets/Tests/InputSystem/CoreTests_Actions.cs +++ b/Assets/Tests/InputSystem/CoreTests_Actions.cs @@ -1950,6 +1950,55 @@ public unsafe void Actions_CanReadValueFromAction_InCallback_WithoutKnowingValue Is.EqualTo((Vector2.up + Vector2.left).normalized.y).Within(0.00001)); } + [Test] + [Category("Actions")] + public unsafe void Actions_CanReadValueTypeFromAction() + { + var action = new InputAction(); + action.AddBinding("/leftStick"); + action.AddCompositeBinding("Dpad") + .With("Up", "/w") + .With("Down", "/s") + .With("Left", "/a") + .With("Right", "/d"); + + var gamepad = InputSystem.AddDevice(); + var keyboard = InputSystem.AddDevice(); + + action.Enable(); + + action.performed += + ctx => + { + Assert.That(ctx.valueType, Is.EqualTo(typeof(Vector2))); + Assert.That(ctx.action.activeValueType, Is.EqualTo(typeof(Vector2))); + }; + + InputSystem.QueueStateEvent(gamepad, new GamepadState { leftStick = new Vector2(0.123f, 0.234f) }); + InputSystem.Update(); + + Assert.That(action.activeControl, Is.SameAs(gamepad.leftStick)); + Assert.That(action.activeControl.valueType, Is.EqualTo(typeof(Vector2))); + Assert.That(action.activeValueType, Is.EqualTo(typeof(Vector2))); + + Assert.That(action.ReadValue(), + Is.EqualTo(new StickDeadzoneProcessor().Process(new Vector2(0.123f, 0.234f))) + .Using(Vector2EqualityComparer.Instance)); + + InputSystem.QueueStateEvent(keyboard, new KeyboardState(Key.W, Key.A)); + InputSystem.Update(); + + // The active control is one of the two keyboard keys held, which has a value type of float. + // But since the composite has type Vector2, the action's value type is Vector2. + Assert.That(new[] { keyboard.wKey, keyboard.aKey }, Contains.Item(action.activeControl)); + Assert.That(action.activeControl.valueType, Is.EqualTo(typeof(float))); + Assert.That(action.activeValueType, Is.EqualTo(typeof(Vector2))); + + Assert.That(action.ReadValue(), + Is.EqualTo(new StickDeadzoneProcessor().Process((Vector2.up + Vector2.left).normalized)) + .Using(Vector2EqualityComparer.Instance)); + } + [Test] [Category("Actions")] public void Actions_ReadingValueOfIncorrectType_ThrowsHelpfulException() @@ -2032,6 +2081,36 @@ public void Actions_CanQueryActiveControl() Assert.That(action.activeControl, Is.SameAs(gamepad.buttonNorth)); } + [Test] + [Category("Actions")] + public void Actions_CanQueryActiveValueType() + { + var gamepad = InputSystem.AddDevice(); + + var action = new InputAction(type: InputActionType.Button); + action.AddBinding(gamepad.buttonSouth); + action.AddBinding(gamepad.buttonNorth); + action.Enable(); + + Assert.That(action.activeControl, Is.Null); + Assert.That(action.activeValueType, Is.Null); + + Press(gamepad.buttonSouth); + + Assert.That(action.activeControl, Is.SameAs(gamepad.buttonSouth)); + Assert.That(action.activeValueType, Is.EqualTo(gamepad.buttonSouth.valueType)); + + Release(gamepad.buttonSouth); + + Assert.That(action.activeControl, Is.Null); + Assert.That(action.activeValueType, Is.Null); + + Press(gamepad.buttonNorth); + + Assert.That(action.activeControl, Is.SameAs(gamepad.buttonNorth)); + Assert.That(action.activeValueType, Is.EqualTo(gamepad.buttonNorth.valueType)); + } + [Test] [Category("Actions")] public void Actions_ResettingDevice_CancelsOngoingActionsThatAreDrivenByIt() diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs index 9e630f5682..d504bf1511 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs @@ -610,7 +610,9 @@ public unsafe InputControl activeControl /// /// Type of value returned by and expected - /// by . + /// by . while the action + /// is in waiting () or canceled () + /// state as this is based on the currently active control that is driving the action. /// /// Type of object returned when reading a value. /// @@ -624,7 +626,8 @@ public unsafe InputControl activeControl /// /// /// - public unsafe Type valueType + /// + public unsafe Type activeValueType { get { @@ -1881,7 +1884,7 @@ public unsafe double startTime /// /// /// - /// + /// public Type valueType => m_State?.GetValueType(bindingIndex, controlIndex); /// From 13cd9ad38d3a85653cf4e8bc921245f710b34751 Mon Sep 17 00:00:00 2001 From: Chris Massie Date: Mon, 30 Oct 2023 16:37:52 -0700 Subject: [PATCH 04/42] Added test using dynamic composite type to verify activeValueType is as expected --- Assets/Tests/InputSystem/CoreTests_Actions.cs | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/Assets/Tests/InputSystem/CoreTests_Actions.cs b/Assets/Tests/InputSystem/CoreTests_Actions.cs index 51f28668bc..9708388cc9 100644 --- a/Assets/Tests/InputSystem/CoreTests_Actions.cs +++ b/Assets/Tests/InputSystem/CoreTests_Actions.cs @@ -1952,7 +1952,7 @@ public unsafe void Actions_CanReadValueFromAction_InCallback_WithoutKnowingValue [Test] [Category("Actions")] - public unsafe void Actions_CanReadValueTypeFromAction() + public void Actions_CanReadValueTypeFromAction() { var action = new InputAction(); action.AddBinding("/leftStick"); @@ -1999,6 +1999,50 @@ public unsafe void Actions_CanReadValueTypeFromAction() .Using(Vector2EqualityComparer.Instance)); } + [Test] + [Category("Actions")] + public void Actions_CanReadValueTypeFromAction_WithDynamicCompositeType() + { + var action = new InputAction(); + action.AddCompositeBinding("OneModifier") + .With("Modifier", "/leftTrigger") + .With("Binding", "/leftStick"); + + var gamepad = InputSystem.AddDevice(); + + action.Enable(); + + action.performed += + ctx => + { + Assert.That(ctx.valueType, Is.EqualTo(typeof(Vector2))); + Assert.That(ctx.action.activeValueType, Is.EqualTo(typeof(Vector2))); + }; + + InputSystem.QueueStateEvent(gamepad, new GamepadState { leftStick = new Vector2(0.123f, 0.234f) }); + InputSystem.Update(); + + Assert.That(action.activeControl, Is.Null); + Assert.That(action.activeValueType, Is.Null); + + Assert.That(action.ReadValue(), + Is.EqualTo(new StickDeadzoneProcessor().Process(Vector2.zero)) + .Using(Vector2EqualityComparer.Instance)); + + InputSystem.QueueStateEvent(gamepad, new GamepadState { leftStick = new Vector2(0.123f, 0.234f), leftTrigger = 1f }); + InputSystem.Update(); + + // The active control is the most recent change (left trigger), which has a value type of float. + // But since the composite has evaluated type Vector2, the action's value type is Vector2. + Assert.That(action.activeControl, Is.SameAs(gamepad.leftTrigger)); + Assert.That(action.activeControl.valueType, Is.EqualTo(typeof(float))); + Assert.That(action.activeValueType, Is.EqualTo(typeof(Vector2))); + + Assert.That(action.ReadValue(), + Is.EqualTo(new StickDeadzoneProcessor().Process(new Vector2(0.123f, 0.234f))) + .Using(Vector2EqualityComparer.Instance)); + } + [Test] [Category("Actions")] public void Actions_ReadingValueOfIncorrectType_ThrowsHelpfulException() From acbaf8b1e02738654336da6f624228d967bdf9fc Mon Sep 17 00:00:00 2001 From: Chris Massie Date: Mon, 30 Oct 2023 16:41:50 -0700 Subject: [PATCH 05/42] Added ignore rule of file generated in Editor_LeavingPlayMode_DiscardsInputActionAssetChanges --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index bcc1b3137d..7f12203655 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,8 @@ docerrors.log Assets/**/*.api Assets/**/*.api.meta +Assets/__TestInputAsset.inputactions +Assets/__TestInputAsset.inputactions.meta Packages/packages-lock.json Packages/com.unity.inputsystem/artifacts/** From 0aa780c109d9881c56bab1dbd878c95e42f6d0c3 Mon Sep 17 00:00:00 2001 From: Chris Massie Date: Tue, 31 Oct 2023 08:03:57 -0700 Subject: [PATCH 06/42] Added WasUnperformedThisFrame for the counterpart to WasPerformedThisFrame and WasPressed/ReleasedThisFrame --- .../InputSystem/Actions/InputAction.cs | 59 ++++++++++++++++++- .../InputSystem/Actions/InputActionState.cs | 31 ++++++++-- 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs index d504bf1511..5e4a0bf125 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs @@ -1240,7 +1240,7 @@ public unsafe bool WasPressedThisFrame() /// /// /// - /// + /// public unsafe bool WasReleasedThisFrame() { var state = GetOrCreateActionMap().m_State; @@ -1296,6 +1296,7 @@ public unsafe bool WasReleasedThisFrame() /// 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. /// + /// /// /// public unsafe bool WasPerformedThisFrame() @@ -1312,6 +1313,62 @@ public unsafe bool WasPerformedThisFrame() return false; } + /// + /// Check whether changed away from at any point + /// in the current frame after being in performed state. + /// + /// True if the action unperformed this frame. + /// + /// This method is different from in that it depends directly on the + /// interaction(s) driving the action (including the default interaction if no specific interaction + /// has been added to the action or binding). + /// + /// For example, let's say the action is bound to the thumbstick and that the binding has a + /// custom Sector interaction assigned to it such that it only performs in the forward sector area past a button press threshold. + /// In the frame where the thumbstick is pushed forward, both will be true + /// (because the thumbstick actuation is now considered pressed) and will be true + /// (because the thumbstick is in the forward sector). If the thumbstick is then moved to the left in a sweeping motion, + /// will still be true. However, WasUnperformedThisFrame will also be true (because the thumbstick + /// is no longer in the forward sector while still crossed the button press threshold) and only in the frame where the thumbstick + /// was no longer within the forward sector. + /// + /// Unlike , which will reset when the action goes back to waiting + /// state, this property will stay true for the duration of the current frame (that is, until the next + /// runs) as long as the action was unperformed at least once. + /// + /// + /// + /// var teleport = playerInput.actions["Teleport"]; + /// if (teleport.WasPerformedThisFrame()) + /// InitiateTeleport(); + /// else if (teleport.WasUnperformedThisFrame()) + /// StopTeleport(); + /// + /// + /// + /// 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. + /// + /// + /// + /// + public unsafe bool WasUnperformedThisFrame() + { + var state = GetOrCreateActionMap().m_State; + + if (state != null) + { + var actionStatePtr = &state.actionStates[m_ActionIndexInState]; + var currentUpdateStep = InputUpdate.s_UpdateStepCount; + return actionStatePtr->lastUnperformedInUpdate == currentUpdateStep && currentUpdateStep != default; + } + + return false; + } + /// /// Return the completion percentage of the timeout (if any) running on the current interaction. /// diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs index 2524c7a6df..b22f576a6b 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs @@ -548,6 +548,7 @@ private void RestoreActionStatesAfterReResolvingBindings(UnmanagedMemory oldStat newActionState.lastCanceledInUpdate = oldActionState.lastCanceledInUpdate; newActionState.lastPerformedInUpdate = oldActionState.lastPerformedInUpdate; + newActionState.lastUnperformedInUpdate = oldActionState.lastUnperformedInUpdate; newActionState.pressedInUpdate = oldActionState.pressedInUpdate; newActionState.releasedInUpdate = oldActionState.releasedInUpdate; newActionState.startTime = oldActionState.startTime; @@ -872,6 +873,7 @@ public void ResetActionState(int actionIndex, InputActionPhase toPhase = InputAc { actionState->lastCanceledInUpdate = default; actionState->lastPerformedInUpdate = default; + actionState->lastUnperformedInUpdate = default; actionState->pressedInUpdate = default; actionState->releasedInUpdate = default; } @@ -2394,7 +2396,7 @@ private void ChangePhaseOfActionInternal(int actionIndex, TriggerState* actionSt if (newPhase != InputActionPhase.Canceled) newState.magnitude = trigger.magnitude; else - newState.magnitude = 0; + newState.magnitude = 0f; newState.phase = newPhase; if (newPhase == InputActionPhase.Performed) @@ -2422,6 +2424,12 @@ private void ChangePhaseOfActionInternal(int actionIndex, TriggerState* actionSt newState.lastPerformedInUpdate = actionState->lastPerformedInUpdate; newState.lastCanceledInUpdate = actionState->lastCanceledInUpdate; } + + if (newPhase != InputActionPhase.Performed && actionState->phase == InputActionPhase.Performed) + newState.lastUnperformedInUpdate = InputUpdate.s_UpdateStepCount; + else + newState.lastUnperformedInUpdate = actionState->lastUnperformedInUpdate; + newState.pressedInUpdate = actionState->pressedInUpdate; newState.releasedInUpdate = actionState->releasedInUpdate; if (newPhase == InputActionPhase.Started) @@ -3568,7 +3576,7 @@ public int partIndex /// other is to represent the current actuation state of an action as a whole. The latter is stored in /// while the former is passed around as temporary instances on the stack. /// - [StructLayout(LayoutKind.Explicit, Size = 48)] + [StructLayout(LayoutKind.Explicit, Size = 52)] public struct TriggerState { public const int kMaxNumMaps = byte.MaxValue; @@ -3588,9 +3596,10 @@ public struct TriggerState [FieldOffset(26)] private ushort m_InteractionIndex; [FieldOffset(28)] private float m_Magnitude; [FieldOffset(32)] private uint m_LastPerformedInUpdate; - [FieldOffset(36)] private uint m_LastCanceledInUpdate; - [FieldOffset(40)] private uint m_PressedInUpdate; - [FieldOffset(44)] private uint m_ReleasedInUpdate; + [FieldOffset(36)] private uint m_LastUnperformedInUpdate; + [FieldOffset(40)] private uint m_LastCanceledInUpdate; + [FieldOffset(44)] private uint m_PressedInUpdate; + [FieldOffset(48)] private uint m_ReleasedInUpdate; /// /// Phase being triggered by the control value change. @@ -3738,7 +3747,7 @@ public int interactionIndex /// /// Update step count () in which action triggered/performed last. - /// Zero if the action did not trigger yet. Also reset to zero when the action is disabled. + /// Zero if the action did not trigger yet. Also reset to zero when the action is hard reset. /// public uint lastPerformedInUpdate { @@ -3746,6 +3755,16 @@ public uint lastPerformedInUpdate set => m_LastPerformedInUpdate = value; } + /// + /// Update step count () in which action unperformed last. + /// Zero if the action did not become unperformed yet. Also reset to zero when the action is hard reset. + /// + public uint lastUnperformedInUpdate + { + get => m_LastUnperformedInUpdate; + set => m_LastUnperformedInUpdate = value; + } + public uint lastCanceledInUpdate { get => m_LastCanceledInUpdate; From 11b6e30036babf81c632a15b2cc609a38628b3e5 Mon Sep 17 00:00:00 2001 From: Chris Massie Date: Wed, 1 Nov 2023 00:08:48 -0700 Subject: [PATCH 07/42] Updated methods to pass along the phase after canceled so it can know the action is being disabled to avoid setting unperformed --- .../InputSystem/Actions/InputActionState.cs | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs index b22f576a6b..0bc8c777a7 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs @@ -840,7 +840,7 @@ public void ResetActionState(int actionIndex, InputActionPhase toPhase = InputAc for (var i = 0; i < interactionCount; ++i) { var interactionIndex = interactionStartIndex + i; - ResetInteractionStateAndCancelIfNecessary(mapIndex, bindingIndex, interactionIndex); + ResetInteractionStateAndCancelIfNecessary(mapIndex, bindingIndex, interactionIndex, phaseAfterCanceled: toPhase); } } } @@ -853,7 +853,8 @@ public void ResetActionState(int actionIndex, InputActionPhase toPhase = InputAc "Action has been triggered but apparently not from an interaction yet there's interactions on the binding that got triggered?!?"); if (actionState->phase != InputActionPhase.Canceled) - ChangePhaseOfAction(InputActionPhase.Canceled, ref actionStates[actionIndex]); + ChangePhaseOfAction(InputActionPhase.Canceled, ref actionStates[actionIndex], + phaseAfterPerformedOrCanceled: toPhase); } } @@ -1601,7 +1602,7 @@ private static bool ShouldIgnoreInputOnCompositeBinding(BindingState* binding, I /// If an action has multiple controls bound to it, control state changes on the action may conflict with each other. /// If that happens, we resolve the conflict by always sticking to the most actuated control. /// - /// Pass-through actions () will always bypass conflict resolution and respond + /// Pass-through actions () will always bypass conflict resolution and respond /// to every value change. /// /// Actions that are resolved to only a single control will early out of conflict resolution. @@ -2135,6 +2136,8 @@ private void StopTimeout(int interactionIndex) /// (default), (if the action is supposed /// to be oscillate between started and performed), or (if the action is /// supposed to perform over and over again until canceled). + /// If is , + /// this determines which phase to transition to after the action has been canceled. /// Indicates if the system should try and change the phase of other /// interactions on the same action that are already started or performed after cancelling this interaction. This should be /// false when resetting interactions. @@ -2142,10 +2145,10 @@ private void StopTimeout(int interactionIndex) /// Multiple interactions on the same binding can be started concurrently but the /// first interaction that starts will get to drive an action until it either cancels /// or performs the action. - /// + /// /// If an interaction driving an action performs it, all interactions will reset and /// go back waiting. - /// + /// /// If an interaction driving an action cancels it, the next interaction in the list which /// has already started will get to drive the action (example: a TapInteraction and a /// SlowTapInteraction both start and the TapInteraction gets to drive the action because @@ -2153,7 +2156,9 @@ private void StopTimeout(int interactionIndex) /// long and the SlowTapInteraction will get to drive the action next). /// internal void ChangePhaseOfInteraction(InputActionPhase newPhase, ref TriggerState trigger, - InputActionPhase phaseAfterPerformed = InputActionPhase.Waiting, bool processNextInteractionOnCancel = true) + InputActionPhase phaseAfterPerformed = InputActionPhase.Waiting, + InputActionPhase phaseAfterCanceled = InputActionPhase.Waiting, + bool processNextInteractionOnCancel = true) { var interactionIndex = trigger.interactionIndex; var bindingIndex = trigger.bindingIndex; @@ -2167,6 +2172,8 @@ internal void ChangePhaseOfInteraction(InputActionPhase newPhase, ref TriggerSta var phaseAfterPerformedOrCanceled = InputActionPhase.Waiting; if (newPhase == InputActionPhase.Performed) phaseAfterPerformedOrCanceled = phaseAfterPerformed; + else if (newPhase == InputActionPhase.Canceled) + phaseAfterPerformedOrCanceled = phaseAfterCanceled; // Any time an interaction changes phase, we cancel all pending timeouts. ref var interactionState = ref interactionStates[interactionIndex]; @@ -2187,8 +2194,7 @@ internal void ChangePhaseOfInteraction(InputActionPhase newPhase, ref TriggerSta if (actionStates[actionIndex].phase == InputActionPhase.Waiting) { // We're the first interaction to go to the start phase. - if (!ChangePhaseOfAction(newPhase, ref trigger, - phaseAfterPerformedOrCanceled: phaseAfterPerformedOrCanceled)) + if (!ChangePhaseOfAction(newPhase, ref trigger, phaseAfterPerformedOrCanceled)) return; } else if (newPhase == InputActionPhase.Canceled && actionStates[actionIndex].interactionIndex == trigger.interactionIndex) @@ -2197,7 +2203,7 @@ internal void ChangePhaseOfInteraction(InputActionPhase newPhase, ref TriggerSta // to go into start phase. *Or* there's an interaction that has // already performed. - if (!ChangePhaseOfAction(newPhase, ref trigger)) + if (!ChangePhaseOfAction(newPhase, ref trigger, phaseAfterPerformedOrCanceled)) return; if (processNextInteractionOnCancel == false) @@ -2223,7 +2229,7 @@ internal void ChangePhaseOfInteraction(InputActionPhase newPhase, ref TriggerSta time = startTime, startTime = startTime, }; - if (!ChangePhaseOfAction(InputActionPhase.Started, ref triggerForInteraction)) + if (!ChangePhaseOfAction(InputActionPhase.Started, ref triggerForInteraction, phaseAfterPerformedOrCanceled)) return; // If the interaction has already performed, trigger it now. @@ -2239,7 +2245,7 @@ internal void ChangePhaseOfInteraction(InputActionPhase newPhase, ref TriggerSta time = interactionStates[index].performedTime, // Time when the interaction performed. startTime = startTime, }; - if (!ChangePhaseOfAction(InputActionPhase.Performed, ref triggerForInteraction)) + if (!ChangePhaseOfAction(InputActionPhase.Performed, ref triggerForInteraction, phaseAfterPerformedOrCanceled)) return; } break; @@ -2332,7 +2338,8 @@ private bool ChangePhaseOfAction(InputActionPhase newPhase, ref TriggerState tri if (actionState->isPassThrough && trigger.interactionIndex == kInvalidIndex) { // No constraints on pass-through actions except if there are interactions driving the action. - ChangePhaseOfActionInternal(actionIndex, actionState, newPhase, ref trigger); + ChangePhaseOfActionInternal(actionIndex, actionState, newPhase, ref trigger, + isDisablingAction: newPhase == InputActionPhase.Canceled && phaseAfterPerformedOrCanceled == InputActionPhase.Disabled); if (!actionState->inProcessing) return false; } @@ -2358,7 +2365,8 @@ private bool ChangePhaseOfAction(InputActionPhase newPhase, ref TriggerState tri } else if (actionState->phase != newPhase || newPhase == InputActionPhase.Performed) // We allow Performed to trigger repeatedly. { - ChangePhaseOfActionInternal(actionIndex, actionState, newPhase, ref trigger); + ChangePhaseOfActionInternal(actionIndex, actionState, newPhase, ref trigger, + isDisablingAction: newPhase == InputActionPhase.Canceled && phaseAfterPerformedOrCanceled == InputActionPhase.Disabled); if (!actionState->inProcessing) return false; @@ -2382,7 +2390,7 @@ private bool ChangePhaseOfAction(InputActionPhase newPhase, ref TriggerState tri return true; } - private void ChangePhaseOfActionInternal(int actionIndex, TriggerState* actionState, InputActionPhase newPhase, ref TriggerState trigger) + private void ChangePhaseOfActionInternal(int actionIndex, TriggerState* actionState, InputActionPhase newPhase, ref TriggerState trigger, bool isDisablingAction = false) { Debug.Assert(trigger.mapIndex == actionState->mapIndex, "Map index on trigger does not correspond to map index of trigger state"); @@ -2425,7 +2433,10 @@ private void ChangePhaseOfActionInternal(int actionIndex, TriggerState* actionSt newState.lastCanceledInUpdate = actionState->lastCanceledInUpdate; } - if (newPhase != InputActionPhase.Performed && actionState->phase == InputActionPhase.Performed) + // 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 lastUnperformedInUpdate if Disabled is the phase after Canceled. + if (actionState->phase == InputActionPhase.Performed && newPhase != InputActionPhase.Performed && !isDisablingAction) newState.lastUnperformedInUpdate = InputUpdate.s_UpdateStepCount; else newState.lastUnperformedInUpdate = actionState->lastUnperformedInUpdate; @@ -2605,7 +2616,7 @@ internal InputActionMap GetActionMap(int bindingIndex) } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "mapIndex", Justification = "Keep this for future implementation")] - private void ResetInteractionStateAndCancelIfNecessary(int mapIndex, int bindingIndex, int interactionIndex) + private void ResetInteractionStateAndCancelIfNecessary(int mapIndex, int bindingIndex, int interactionIndex, InputActionPhase phaseAfterCanceled) { Debug.Assert(interactionIndex >= 0 && interactionIndex < totalInteractionCount, "Interaction index out of range"); Debug.Assert(bindingIndex >= 0 && bindingIndex < totalBindingCount, "Binding index out of range"); @@ -2624,7 +2635,9 @@ private void ResetInteractionStateAndCancelIfNecessary(int mapIndex, int binding { case InputActionPhase.Started: case InputActionPhase.Performed: - ChangePhaseOfInteraction(InputActionPhase.Canceled, ref actionStates[actionIndex], processNextInteractionOnCancel: false); + ChangePhaseOfInteraction(InputActionPhase.Canceled, ref actionStates[actionIndex], + phaseAfterCanceled: phaseAfterCanceled, + processNextInteractionOnCancel: false); break; } From 10cc4b76cc22256489646ba3a3a577a2dc919905 Mon Sep 17 00:00:00 2001 From: Chris Massie Date: Wed, 1 Nov 2023 00:09:59 -0700 Subject: [PATCH 08/42] Added tests for WasPerformedThisFrame/WasUnperformedThisFrame along with additional verification of phase transitions in mega test --- Assets/Tests/InputSystem/CoreTests_Actions.cs | 790 +++++++++++++++--- 1 file changed, 692 insertions(+), 98 deletions(-) diff --git a/Assets/Tests/InputSystem/CoreTests_Actions.cs b/Assets/Tests/InputSystem/CoreTests_Actions.cs index 9708388cc9..a2a47e402c 100644 --- a/Assets/Tests/InputSystem/CoreTests_Actions.cs +++ b/Assets/Tests/InputSystem/CoreTests_Actions.cs @@ -1616,6 +1616,79 @@ public void Actions_CanQueryIfPerformedInCurrentFrame() Assert.That(holdAction.WasPerformedThisFrame(), Is.False); } + [Test] + [Category("Actions")] + public void Actions_CanQueryIfUnperformedInCurrentFrame() + { + var gamepad = InputSystem.AddDevice(); + + var simpleAction = new InputAction(binding: "/buttonSouth"); + var holdAction = new InputAction(binding: "/buttonSouth", interactions: "hold(duration=0.5)"); + + simpleAction.Enable(); + holdAction.Enable(); + + Assert.That(simpleAction.WasReleasedThisFrame(), Is.False); + Assert.That(simpleAction.WasUnperformedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.False); + Assert.That(holdAction.WasUnperformedThisFrame(), Is.False); + + Press(gamepad.buttonSouth); + + Assert.That(simpleAction.WasReleasedThisFrame(), Is.False); + Assert.That(simpleAction.WasUnperformedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.False); + Assert.That(holdAction.WasUnperformedThisFrame(), Is.False); + + currentTime += 1; + InputSystem.Update(); + + Assert.That(simpleAction.WasReleasedThisFrame(), Is.False); + Assert.That(simpleAction.WasUnperformedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.False); + Assert.That(holdAction.WasUnperformedThisFrame(), Is.False); + + holdAction.Disable(); + + Assert.That(holdAction.WasReleasedThisFrame(), Is.False); + Assert.That(holdAction.WasUnperformedThisFrame(), Is.False); + + holdAction.Enable(); + + Assert.That(holdAction.WasReleasedThisFrame(), Is.False); + Assert.That(holdAction.WasUnperformedThisFrame(), Is.False); + + InputSystem.Update(); + + Assert.That(simpleAction.WasReleasedThisFrame(), Is.False); + Assert.That(simpleAction.WasUnperformedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.False); + Assert.That(holdAction.WasUnperformedThisFrame(), Is.False); + + Release(gamepad.buttonSouth); + + Assert.That(simpleAction.WasReleasedThisFrame(), Is.True); + Assert.That(simpleAction.WasUnperformedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.True); + Assert.That(holdAction.WasUnperformedThisFrame(), Is.False); + + simpleAction.Disable(); + holdAction.Disable(); + + Assert.That(simpleAction.WasReleasedThisFrame(), Is.True); + Assert.That(simpleAction.WasUnperformedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.True); + Assert.That(holdAction.WasUnperformedThisFrame(), Is.False); + + simpleAction.Enable(); + holdAction.Enable(); + + Assert.That(simpleAction.WasReleasedThisFrame(), Is.True); + Assert.That(simpleAction.WasUnperformedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.True); + Assert.That(holdAction.WasUnperformedThisFrame(), Is.False); + } + [Test] [Category("Actions")] public void Actions_CanReadValueFromAction() @@ -1698,11 +1771,13 @@ public void Actions_CanReadValueFromAction() [Test] [Category("Actions")] - [TestCase(InputActionType.Button)] [TestCase(InputActionType.Value)] - [TestCase(InputActionType.PassThrough)] - [TestCase(InputActionType.Button, "hold(duration=0.5)")] + [TestCase(InputActionType.Value, "press")] + [TestCase(InputActionType.Value, "hold(duration=0.5)")] + [TestCase(InputActionType.Button)] [TestCase(InputActionType.Button, "press")] + [TestCase(InputActionType.Button, "hold(duration=0.5)")] + [TestCase(InputActionType.PassThrough)] public void Actions_CanReadValueFromAction_AsButton(InputActionType actionType, string interactions = null) { // Set global press and release points to known values. @@ -1712,126 +1787,633 @@ public void Actions_CanReadValueFromAction_AsButton(InputActionType actionType, var gamepad = InputSystem.AddDevice(); var action = new InputAction(type: actionType, binding: "/leftTrigger", interactions: interactions); - action.Enable(); - Assert.That(action.IsPressed(), Is.False); - Assert.That(action.WasPressedThisFrame(), Is.False); - Assert.That(action.WasReleasedThisFrame(), Is.False); + var isHold = interactions?.StartsWith("hold") ?? false; + var isPress = interactions?.StartsWith("press") ?? false; + var isButtonLike = (action.type == InputActionType.Value && isPress) || + (action.type == InputActionType.Button && !isHold); - // Press such that it stays below press threshold. - Set(gamepad.leftTrigger, 0.25f); + using (var trace = new InputActionTrace(action)) + { + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Disabled)); - Assert.That(action.IsPressed(), Is.False); - Assert.That(action.WasPressedThisFrame(), Is.False); - Assert.That(action.WasReleasedThisFrame(), Is.False); + action.Enable(); - // Press some more such that it crosses the press threshold. - Set(gamepad.leftTrigger, 0.75f); + Assert.That(action.IsPressed(), Is.False); + Assert.That(action.WasPressedThisFrame(), Is.False); + Assert.That(action.WasReleasedThisFrame(), Is.False); - Assert.That(action.IsPressed(), Is.True); - Assert.That(action.WasPressedThisFrame(), Is.True); - Assert.That(action.WasReleasedThisFrame(), Is.False); + Assert.That(trace, Is.Empty); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Waiting)); + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); - // Disabling an action at this point should affect IsPressed() but should - // not affect WasPressedThisFrame() and WasReleasedThisFrame(). - action.Disable(); + // Press such that it stays below press threshold. + Set(gamepad.leftTrigger, 0.25f); - Assert.That(action.IsPressed(), Is.False); - Assert.That(action.WasPressedThisFrame(), Is.True); - Assert.That(action.WasReleasedThisFrame(), Is.False); + // W = Waiting, S = Started, P = Performed, C = Canceled + // P/U = Performed/Unperformed + // (* means listeners not invoked) + // | Type | Interaction | Phase Change | P/U | isButtonLike | isHold | + // |--------|-------------|------------------|-----|--------------|--------| + // | Value | Default | W -> S, P, S* | T/ | | | + // | Value | Press | W -> S | / | true | | + // | Value | Hold | W (No Change) | / | | true | + // | Button | Default | W -> S | / | true | | + // | Button | Press | W -> S | / | true | | + // | Button | Hold | W (No Change) | / | | true | + // | Pass | Default | W -> P | T/ | | | - // Re-enabling it should have no effect on WasPressedThisFrame() and - // WasReleasedThisFrame() either. Also IsPressed() should remain false - // as the button may have been released and the action wouldn't see - // the update while disabled. - action.Enable(); + Assert.That(action.IsPressed(), Is.False); + Assert.That(action.WasPressedThisFrame(), Is.False); + Assert.That(action.WasReleasedThisFrame(), Is.False); - Assert.That(action.IsPressed(), Is.False); - Assert.That(action.WasPressedThisFrame(), Is.True); - Assert.That(action.WasReleasedThisFrame(), Is.False); + if (action.type == InputActionType.Value && interactions == null) + { + Assert.That(trace, Started(action).AndThen(Performed(action))); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (isButtonLike) + { + Assert.That(trace, Started(action)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (isHold) + { + Assert.That(trace, Is.Empty); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Waiting)); + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (action.type == InputActionType.PassThrough) + { + Assert.That(trace, Performed(action)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Performed)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } - // Advance one frame. - InputSystem.Update(); + trace.Clear(); + + // Press some more such that it crosses the press threshold. + Set(gamepad.leftTrigger, 0.75f); + + // W = Waiting, S = Started, P = Performed, C = Canceled + // P/U = Performed/Unperformed + // (* means listeners not invoked) + // | Type | Interaction | Phase Change | P/U | isButtonLike | isHold | + // |--------|-------------|------------------|-----|--------------|--------| + // | Value | Default | S -> P, S* | T/ | | | + // | Value | Press | S -> P | T/ | true | | + // | Value | Hold | W -> S | / | | true | + // | Button | Default | S -> P | T/ | true | | + // | Button | Press | S -> P | T/ | true | | + // | Button | Hold | W -> S | / | | true | + // | Pass | Default | P -> P | T/ | | | - // Value actions perform an initial state check which flips the press state - // back on. - if (action.type == InputActionType.Value) - { Assert.That(action.IsPressed(), Is.True); Assert.That(action.WasPressedThisFrame(), Is.True); Assert.That(action.WasReleasedThisFrame(), Is.False); - } - else - { + + if (action.type == InputActionType.Value && interactions == null) + { + Assert.That(trace, Performed(action)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (isHold) + { + Assert.That(trace, Started(action)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else + { + Assert.That(trace, Performed(action)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Performed)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + + trace.Clear(); + + // Disabling an action at this point should affect IsPressed() but should + // not affect WasPressedThisFrame() and WasReleasedThisFrame(). + action.Disable(); + Assert.That(action.IsPressed(), Is.False); - Assert.That(action.WasPressedThisFrame(), Is.False); + Assert.That(action.WasPressedThisFrame(), Is.True); Assert.That(action.WasReleasedThisFrame(), Is.False); - Set(gamepad.leftTrigger, 0.6f); + Assert.That(trace, Canceled(action)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Disabled)); + if (action.type == InputActionType.Value && interactions == null) + { + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (isHold) + { + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else + { + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } - Assert.That(action.IsPressed(), Is.True); + trace.Clear(); + + // Re-enabling it should have no effect on WasPressedThisFrame() and + // WasReleasedThisFrame() either. Also IsPressed() should remain false + // as the button may have been released and the action wouldn't see + // the update while disabled. + action.Enable(); + + Assert.That(action.IsPressed(), Is.False); Assert.That(action.WasPressedThisFrame(), Is.True); Assert.That(action.WasReleasedThisFrame(), Is.False); - } - // Release a bit but remain above release threshold. - Set(gamepad.leftTrigger, 0.41f); + Assert.That(trace, Is.Empty); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Waiting)); + if (action.type == InputActionType.Value && interactions == null) + { + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (isHold) + { + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else + { + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } - Assert.That(action.IsPressed(), Is.True); - Assert.That(action.WasPressedThisFrame(), Is.False); - Assert.That(action.WasReleasedThisFrame(), Is.False); + trace.Clear(); - // Go below release threshold. - Set(gamepad.leftTrigger, 0.2f); + // Advance one frame. + InputSystem.Update(); - Assert.That(action.IsPressed(), Is.False); - Assert.That(action.WasPressedThisFrame(), Is.False); - Assert.That(action.WasReleasedThisFrame(), Is.True); + // W = Waiting, S = Started, P = Performed, C = Canceled + // P/U = Performed/Unperformed + // (* means listeners not invoked) + // | Type | Interaction | Phase Change | P/U | isButtonLike | isHold | + // |--------|-------------|------------------|-----|--------------|--------| + // | Value | Default | W -> S, P, S* | T/ | | | + // | Value | Press | W -> S, P | T/ | true | | + // | Value | Hold | W -> S | / | | true | + // | Button | Default | W (No Change) | / | true | | + // | Button | Press | W (No Change) | / | true | | + // | Button | Hold | W (No Change) | / | | true | + // | Pass | Default | W (No Change) | / | | | + + // Value actions perform an initial state check which flips the press state + // back on. + if (action.type == InputActionType.Value) + { + Assert.That(action.IsPressed(), Is.True); + Assert.That(action.WasPressedThisFrame(), Is.True); + Assert.That(action.WasReleasedThisFrame(), Is.False); - // Disabling should not affect WasReleasedThisFrame(). - action.Disable(); + if (interactions == null) + { + Assert.That(trace, Started(action).AndThen(Performed(action))); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (isPress) + { + Assert.That(trace, Started(action).AndThen(Performed(action))); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Performed)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (isHold) + { + Assert.That(trace, Started(action)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + } + else + { + Assert.That(action.IsPressed(), Is.False); + Assert.That(action.WasPressedThisFrame(), Is.False); + Assert.That(action.WasReleasedThisFrame(), Is.False); - Assert.That(action.IsPressed(), Is.False); - Assert.That(action.WasPressedThisFrame(), Is.False); - Assert.That(action.WasReleasedThisFrame(), Is.True); + Assert.That(trace, Is.Empty); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Waiting)); + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); - // So should re-enabling. - action.Enable(); + trace.Clear(); - Assert.That(action.IsPressed(), Is.False); - Assert.That(action.WasPressedThisFrame(), Is.False); - Assert.That(action.WasReleasedThisFrame(), Is.True); + Set(gamepad.leftTrigger, 0.6f); - // Advance one frame. Should reset WasReleasedThisFrame(). - InputSystem.Update(); + // W = Waiting, S = Started, P = Performed, C = Canceled + // P/U = Performed/Unperformed + // | Type | Interaction | Phase Change | P/U | isButtonLike | isHold | + // |--------|-------------|------------------|-----|--------------|--------| + // | Button | Default | W -> S, P | T/ | true | | + // | Button | Press | W -> S, P | T/ | true | | + // | Button | Hold | W -> S | / | | true | + // | Pass | Default | W -> P | T/ | | | - Assert.That(action.IsPressed(), Is.False); - Assert.That(action.WasPressedThisFrame(), Is.False); - Assert.That(action.WasReleasedThisFrame(), Is.False); + Assert.That(action.IsPressed(), Is.True); + Assert.That(action.WasPressedThisFrame(), Is.True); + Assert.That(action.WasReleasedThisFrame(), Is.False); - // Press-and-release in same frame. - Set(gamepad.leftTrigger, 0.75f, queueEventOnly: true); - Set(gamepad.leftTrigger, 0.25f); + if (isButtonLike) + { + Assert.That(trace, Started(action).AndThen(Performed(action))); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Performed)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (isHold) + { + Assert.That(trace, Started(action)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else + { + Assert.That(trace, Performed(action)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Performed)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + } - Assert.That(action.IsPressed(), Is.False); - Assert.That(action.WasPressedThisFrame(), Is.True); - Assert.That(action.WasReleasedThisFrame(), Is.True); + trace.Clear(); - // Advance one frame. - InputSystem.Update(); + // Release a bit but remain above release threshold. + Set(gamepad.leftTrigger, 0.41f); + + // W = Waiting, S = Started, P = Performed, C = Canceled + // P/U = Performed/Unperformed + // (* means listeners not invoked) + // | Type | Interaction | Phase Change | P/U | isButtonLike | isHold | + // |--------|-------------|------------------|-----|--------------|--------| + // | Value | Default | S -> P, S* | T/ | | | + // | Value | Press | P (No Change) | / | true | | + // | Value | Hold | S (No Change) | / | | true | + // | Button | Default | P (No Change) | / | true | | + // | Button | Press | P (No Change) | / | true | | + // | Button | Hold | S (No Change) | / | | true | + // | Pass | Default | P -> P | T/ | | | - Assert.That(action.IsPressed(), Is.False); - Assert.That(action.WasPressedThisFrame(), Is.False); - Assert.That(action.WasReleasedThisFrame(), Is.False); + Assert.That(action.IsPressed(), Is.True); + Assert.That(action.WasPressedThisFrame(), Is.False); + Assert.That(action.WasReleasedThisFrame(), Is.False); - // Press-and-release-and-press-again in same frame. - Set(gamepad.leftTrigger, 0.75f, queueEventOnly: true); - Set(gamepad.leftTrigger, 0.25f, queueEventOnly: true); - Set(gamepad.leftTrigger, 0.75f); + if (action.type == InputActionType.Value && interactions == null) + { + Assert.That(trace, Performed(action)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (isButtonLike) + { + Assert.That(trace, Is.Empty); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Performed)); + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (isHold) + { + Assert.That(trace, Is.Empty); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else + { + Assert.That(trace, Performed(action)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Performed)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } - Assert.That(action.IsPressed(), Is.True); - Assert.That(action.WasPressedThisFrame(), Is.True); - Assert.That(action.WasReleasedThisFrame(), Is.True); + trace.Clear(); + + // Go below release threshold. + Set(gamepad.leftTrigger, 0.2f); + + // W = Waiting, S = Started, P = Performed, C = Canceled + // P/U = Performed/Unperformed + // (* means listeners not invoked) + // | Type | Interaction | Phase Change | P/U | isButtonLike | isHold | + // |--------|-------------|------------------|-----|--------------|--------| + // | Value | Default | S -> P, S* | T/ | | | + // | Value | Press | P -> S | /T | true | | + // | Value | Hold | S (No Change) | / | | true | + // | Button | Default | P -> S | /T | true | | + // | Button | Press | P -> S | /T | true | | + // | Button | Hold | S (No Change) | / | | true | + // | Pass | Default | P -> P | T/ | | | + + Assert.That(action.IsPressed(), Is.False); + Assert.That(action.WasPressedThisFrame(), Is.False); + Assert.That(action.WasReleasedThisFrame(), Is.True); + + if (action.type == InputActionType.Value && interactions == null) + { + Assert.That(trace, Performed(action)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (isButtonLike) + { + Assert.That(trace, Started(action)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.True); + } + else if (isHold) + { + Assert.That(trace, Is.Empty); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else + { + Assert.That(trace, Performed(action)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Performed)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + + trace.Clear(); + + // Disabling should not affect WasReleasedThisFrame(). + action.Disable(); + + Assert.That(action.IsPressed(), Is.False); + Assert.That(action.WasPressedThisFrame(), Is.False); + Assert.That(action.WasReleasedThisFrame(), Is.True); + + Assert.That(trace, Canceled(action)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Disabled)); + if (action.type == InputActionType.Value && interactions == null) + { + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (isButtonLike) + { + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.True); + } + else if (isHold) + { + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else + { + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + + trace.Clear(); + + // So should re-enabling. + action.Enable(); + + Assert.That(action.IsPressed(), Is.False); + Assert.That(action.WasPressedThisFrame(), Is.False); + Assert.That(action.WasReleasedThisFrame(), Is.True); + + Assert.That(trace, Is.Empty); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Waiting)); + if (action.type == InputActionType.Value && interactions == null) + { + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (isButtonLike) + { + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.True); + } + else if (isHold) + { + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else + { + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + + trace.Clear(); + + // Advance one frame. Should reset WasReleasedThisFrame(). + InputSystem.Update(); + + // W = Waiting, S = Started, P = Performed, C = Canceled + // P/U = Performed/Unperformed + // (* means listeners not invoked) + // | Type | Interaction | Phase Change | P/U | isButtonLike | isHold | + // |--------|-------------|------------------|-----|--------------|--------| + // | Value | Default | W -> S, P, S* | T/ | | | + // | Value | Press | W -> S | / | true | | + // | Value | Hold | W (No Change) | / | | true | + // | Button | Default | W (No Change) | / | true | | + // | Button | Press | W (No Change) | / | true | | + // | Button | Hold | W (No Change) | / | | true | + // | Pass | Default | W (No Change) | / | | | + + Assert.That(action.IsPressed(), Is.False); + Assert.That(action.WasPressedThisFrame(), Is.False); + Assert.That(action.WasReleasedThisFrame(), Is.False); + + if (action.type == InputActionType.Value && interactions == null) + { + Assert.That(trace, Started(action).AndThen(Performed(action))); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (action.type == InputActionType.Value && isPress) + { + Assert.That(trace, Started(action)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else + { + Assert.That(trace, Is.Empty); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Waiting)); + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + + trace.Clear(); + + // Press-and-release in same frame. + Set(gamepad.leftTrigger, 0.75f, queueEventOnly: true); + Set(gamepad.leftTrigger, 0.25f); + + // W = Waiting, S = Started, P = Performed, C = Canceled + // P/U = Performed/Unperformed + // (* means listeners not invoked) + // | Type | Interaction | Phase Change | P/U | isButtonLike | isHold | + // |--------|-------------|------------------|-----|--------------|--------| + // | Value | Default | S -> P, S*, P, S*| T/ | | | + // | Value | Press | S -> P, S | T/T | true | | + // | Value | Hold | W -> S | / | | true | + // | Button | Default | W -> S, P, S | T/T | true | | + // | Button | Press | W -> S, P, S | T/T | true | | + // | Button | Hold | W -> S | / | | true | + // | Pass | Default | W -> P, P | T/ | | | + + Assert.That(action.IsPressed(), Is.False); + Assert.That(action.WasPressedThisFrame(), Is.True); + Assert.That(action.WasReleasedThisFrame(), Is.True); + + if (action.type == InputActionType.Value && interactions == null) + { + Assert.That(trace, Performed(action).AndThen(Performed(action))); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (action.type == InputActionType.Value && isPress) + { + Assert.That(trace, Performed(action).AndThen(Started(action))); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.True); + } + else if (isHold) + { + Assert.That(trace, Started(action)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (action.type == InputActionType.Button && isButtonLike) + { + Assert.That(trace, Started(action).AndThen(Performed(action)).AndThen(Started(action))); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.True); + } + else + { + Assert.That(trace, Performed(action).AndThen(Performed(action))); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Performed)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + + trace.Clear(); + + // Advance one frame. + InputSystem.Update(); + + // W = Waiting, S = Started, P = Performed, C = Canceled + // | Type | Interaction | Phase Change | P/U | isButtonLike | isHold | + // |--------|-------------|------------------|-----|--------------|--------| + // | Value | Default | S (No Change) | / | | | + // | Value | Press | S (No Change) | / | true | | + // | Value | Hold | S (No Change) | / | | true | + // | Button | Default | S (No Change) | / | true | | + // | Button | Press | S (No Change) | / | true | | + // | Button | Hold | S (No Change) | / | | true | + // | Pass | Default | P (No Change) | / | | | + + Assert.That(action.IsPressed(), Is.False); + Assert.That(action.WasPressedThisFrame(), Is.False); + Assert.That(action.WasReleasedThisFrame(), Is.False); + + if (action.type != InputActionType.PassThrough) + { + Assert.That(trace, Is.Empty); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else + { + Assert.That(trace, Is.Empty); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Performed)); + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + + trace.Clear(); + + // Press-and-release-and-press-again in same frame. + Set(gamepad.leftTrigger, 0.75f, queueEventOnly: true); + Set(gamepad.leftTrigger, 0.25f, queueEventOnly: true); + Set(gamepad.leftTrigger, 0.75f); + + // W = Waiting, S = Started, P = Performed, C = Canceled + // (* means listeners not invoked) + // | Type | Interaction | Phase Change | P/U | isButtonLike | isHold | + // |--------|-------------|--------------------------|-----|--------------|--------| + // | Value | Default | S -> P, S*, P, S*, P, S* | T/ | | | + // | Value | Press | S -> P, S, P | T/T | true | | + // | Value | Hold | S (No Change) | / | | true | + // | Button | Default | S -> P, S, P | T/T | true | | + // | Button | Press | S -> P, S, P | T/T | true | | + // | Button | Hold | S (No Change) | / | | true | + // | Pass | Default | P -> P, P, P | T/ | | | + + Assert.That(action.IsPressed(), Is.True); + Assert.That(action.WasPressedThisFrame(), Is.True); + Assert.That(action.WasReleasedThisFrame(), Is.True); + + if (action.type == InputActionType.Value && interactions == null) + { + Assert.That(trace, Performed(action).AndThen(Performed(action)).AndThen(Performed(action))); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (isButtonLike) + { + Assert.That(trace, Performed(action).AndThen(Started(action)).AndThen(Performed(action))); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Performed)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.True); + } + else if (isHold) + { + Assert.That(trace, Is.Empty); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + else if (action.type == InputActionType.PassThrough) + { + Assert.That(trace, Performed(action).AndThen(Performed(action)).AndThen(Performed(action))); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Performed)); + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); + } + } } [Test] @@ -2861,35 +3443,45 @@ public void Actions_WithMultipleBoundControls_CanHandleButtonPressesAndReleases( Assert.That(action.IsPressed(), Is.False); Assert.That(action.WasPressedThisFrame(), Is.False); + Assert.That(action.WasPerformedThisFrame(), Is.False); Assert.That(action.WasReleasedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); Assert.That(action.activeControl, Is.Null); Set(gamepad.leftTrigger, 1f); Assert.That(action.IsPressed(), Is.True); Assert.That(action.WasPressedThisFrame(), Is.True); + Assert.That(action.WasPerformedThisFrame(), Is.True); Assert.That(action.WasReleasedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); Assert.That(action.activeControl, Is.SameAs(gamepad.leftTrigger)); Set(gamepad.rightTrigger, 0.6f); Assert.That(action.IsPressed(), Is.True); Assert.That(action.WasPressedThisFrame(), Is.False); + Assert.That(action.WasPerformedThisFrame(), Is.False); Assert.That(action.WasReleasedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); Assert.That(action.activeControl, Is.SameAs(gamepad.leftTrigger)); Set(gamepad.leftTrigger, 0f); Assert.That(action.IsPressed(), Is.True); Assert.That(action.WasPressedThisFrame(), Is.False); + Assert.That(action.WasPerformedThisFrame(), Is.True); Assert.That(action.WasReleasedThisFrame(), Is.False); + Assert.That(action.WasUnperformedThisFrame(), Is.False); Assert.That(action.activeControl, Is.SameAs(gamepad.rightTrigger)); Set(gamepad.rightTrigger, 0f); Assert.That(action.IsPressed(), Is.False); Assert.That(action.WasPressedThisFrame(), Is.False); + Assert.That(action.WasPerformedThisFrame(), Is.False); Assert.That(action.WasReleasedThisFrame(), Is.True); + Assert.That(action.WasUnperformedThisFrame(), Is.False); Assert.That(action.activeControl, Is.Null); } @@ -9289,38 +9881,40 @@ public void Actions_OnActionWithMultipleBindings_ControlWithHighestActuationIsTr Set(gamepad.leftTrigger, 1f); - Assert.That(buttonAction.WasPerformedThisFrame()); + Assert.That(buttonAction.WasPerformedThisFrame(), Is.True); Assert.That(buttonAction.activeControl, Is.SameAs(gamepad.leftTrigger)); - Assert.That(passThroughAction.WasPerformedThisFrame()); + Assert.That(passThroughAction.WasPerformedThisFrame(), Is.True); Assert.That(passThroughAction.activeControl, Is.SameAs(gamepad.leftTrigger)); Set(gamepad.rightTrigger, 0.5f); - Assert.That(!buttonAction.WasPerformedThisFrame()); + Assert.That(buttonAction.WasPerformedThisFrame(), Is.False); Assert.That(buttonAction.activeControl, Is.SameAs(gamepad.leftTrigger)); - Assert.That(passThroughAction.WasPerformedThisFrame()); + Assert.That(passThroughAction.WasPerformedThisFrame(), Is.True); Assert.That(passThroughAction.activeControl, Is.SameAs(gamepad.rightTrigger)); Set(gamepad.leftTrigger, 0f); - Assert.That(!buttonAction.WasPerformedThisFrame()); - Assert.That(!buttonAction.WasReleasedThisFrame()); + Assert.That(buttonAction.WasPerformedThisFrame(), Is.False); + Assert.That(buttonAction.WasReleasedThisFrame(), Is.False); + Assert.That(buttonAction.WasUnperformedThisFrame(), Is.False); Assert.That(buttonAction.activeControl, Is.SameAs(gamepad.rightTrigger)); - Assert.That(passThroughAction.WasPerformedThisFrame()); + Assert.That(passThroughAction.WasPerformedThisFrame(), Is.True); Assert.That(passThroughAction.activeControl, Is.SameAs(gamepad.leftTrigger)); Set(gamepad.rightTrigger, 0.6f); - Assert.That(!buttonAction.WasPerformedThisFrame()); + Assert.That(buttonAction.WasPerformedThisFrame(), Is.False); Assert.That(buttonAction.activeControl, Is.SameAs(gamepad.rightTrigger)); - Assert.That(passThroughAction.WasPerformedThisFrame()); + Assert.That(passThroughAction.WasPerformedThisFrame(), Is.True); Assert.That(passThroughAction.activeControl, Is.SameAs(gamepad.rightTrigger)); Set(gamepad.rightTrigger, 0f); - Assert.That(buttonAction.WasReleasedThisFrame()); + Assert.That(buttonAction.WasReleasedThisFrame(), Is.True); + Assert.That(buttonAction.WasUnperformedThisFrame(), Is.True); Assert.That(buttonAction.activeControl, Is.Null); - Assert.That(passThroughAction.WasPerformedThisFrame()); + Assert.That(passThroughAction.WasPerformedThisFrame(), Is.True); Assert.That(passThroughAction.activeControl, Is.SameAs(gamepad.rightTrigger)); } From 4c61a30e31b93aee2153bae407a4639da9c7109b Mon Sep 17 00:00:00 2001 From: Rita Merkl <127492464+ritamerkl@users.noreply.github.com> Date: Wed, 1 Nov 2023 09:21:48 -0700 Subject: [PATCH 09/42] fix for neutron CI warning (#1787) --- .../UITKAssetEditor/Resources/InputActionsEditorStyles.uss | 7 +++++++ .../Resources/InputActionsTreeViewItem.uxml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsEditorStyles.uss b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsEditorStyles.uss index a33b6d2b7d..b050e34c4c 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsEditorStyles.uss +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsEditorStyles.uss @@ -80,6 +80,13 @@ padding-bottom: 7px; } +.tree-view-item-icon{ + justify-content: center; + background-image: url("/Packages/com.unity.inputsystem/InputSystem/Editor/Icons/d_InputControl.png"); + width: 16px; + height: 16px; +} + .header { height: 22px; flex-direction: row; diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsTreeViewItem.uxml b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsTreeViewItem.uxml index 8948d8f9bc..bd6564b42e 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsTreeViewItem.uxml +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsTreeViewItem.uxml @@ -2,7 +2,7 @@