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/** diff --git a/Assets/Tests/InputSystem/CoreTests_Actions.cs b/Assets/Tests/InputSystem/CoreTests_Actions.cs index aa84d64f0b..99c6560754 100644 --- a/Assets/Tests/InputSystem/CoreTests_Actions.cs +++ b/Assets/Tests/InputSystem/CoreTests_Actions.cs @@ -1616,6 +1616,346 @@ public void Actions_CanQueryIfPerformedInCurrentFrame() Assert.That(holdAction.WasPerformedThisFrame(), Is.False); } + [Test] + [Category("Actions")] + public void Actions_CanQueryIfHoldInteractionCompletedInCurrentFrame() + { + var gamepad = InputSystem.AddDevice(); + + var holdAction = new InputAction(binding: "/buttonSouth", interactions: "hold(duration=0.5)"); + + holdAction.Enable(); + + Assert.That(holdAction.WasPressedThisFrame(), Is.False); + Assert.That(holdAction.WasPerformedThisFrame(), Is.False); + + Press(gamepad.buttonSouth); + + Assert.That(holdAction.WasPressedThisFrame(), Is.True); + Assert.That(holdAction.WasPerformedThisFrame(), Is.False); + + InputSystem.Update(); + + Assert.That(holdAction.WasPressedThisFrame(), Is.False); + Assert.That(holdAction.WasPerformedThisFrame(), Is.False); + + // Release before the hold duration threshold was met. + Release(gamepad.buttonSouth); + + Assert.That(holdAction.WasReleasedThisFrame(), Is.True); + Assert.That(holdAction.WasCompletedThisFrame(), Is.False); + + Press(gamepad.buttonSouth); + + Assert.That(holdAction.WasPressedThisFrame(), Is.True); + Assert.That(holdAction.WasPerformedThisFrame(), Is.False); + + currentTime += 1; + InputSystem.Update(); + + Assert.That(holdAction.WasPressedThisFrame(), Is.False); + Assert.That(holdAction.WasPerformedThisFrame(), Is.True); + + InputSystem.Update(); + + Assert.That(holdAction.WasPressedThisFrame(), Is.False); + Assert.That(holdAction.WasPerformedThisFrame(), Is.False); + + // Release after the hold duration threshold was met. + Release(gamepad.buttonSouth); + + Assert.That(holdAction.WasReleasedThisFrame(), Is.True); + Assert.That(holdAction.WasCompletedThisFrame(), Is.True); + + InputSystem.Update(); + + Assert.That(holdAction.WasPressedThisFrame(), Is.False); + Assert.That(holdAction.WasPerformedThisFrame(), Is.False); + } + + [Test] + [Category("Actions")] + public void Actions_WhenDisabled_DoesNotBecomeCompleted() + { + 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.WasCompletedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.False); + Assert.That(holdAction.WasCompletedThisFrame(), Is.False); + + Press(gamepad.buttonSouth); + + Assert.That(simpleAction.WasReleasedThisFrame(), Is.False); + Assert.That(simpleAction.WasCompletedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.False); + Assert.That(holdAction.WasCompletedThisFrame(), Is.False); + + currentTime += 1; + InputSystem.Update(); + + Assert.That(simpleAction.WasReleasedThisFrame(), Is.False); + Assert.That(simpleAction.WasCompletedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.False); + Assert.That(holdAction.WasCompletedThisFrame(), Is.False); + + holdAction.Disable(); + + Assert.That(holdAction.WasReleasedThisFrame(), Is.False); + Assert.That(holdAction.WasCompletedThisFrame(), Is.False); + + holdAction.Enable(); + + Assert.That(holdAction.WasReleasedThisFrame(), Is.False); + Assert.That(holdAction.WasCompletedThisFrame(), Is.False); + + InputSystem.Update(); + + Assert.That(simpleAction.WasReleasedThisFrame(), Is.False); + Assert.That(simpleAction.WasCompletedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.False); + Assert.That(holdAction.WasCompletedThisFrame(), Is.False); + + Release(gamepad.buttonSouth); + + Assert.That(simpleAction.WasReleasedThisFrame(), Is.True); + Assert.That(simpleAction.WasCompletedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.True); + Assert.That(holdAction.WasCompletedThisFrame(), Is.False); + + simpleAction.Disable(); + holdAction.Disable(); + + Assert.That(simpleAction.WasReleasedThisFrame(), Is.True); + Assert.That(simpleAction.WasCompletedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.True); + Assert.That(holdAction.WasCompletedThisFrame(), Is.False); + + simpleAction.Enable(); + holdAction.Enable(); + + Assert.That(simpleAction.WasReleasedThisFrame(), Is.True); + Assert.That(simpleAction.WasCompletedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.True); + Assert.That(holdAction.WasCompletedThisFrame(), Is.False); + } + + [Test] + [Category("Actions")] + [TestCase(InputActionType.Value)] + [TestCase(InputActionType.Button)] + [TestCase(InputActionType.PassThrough)] + public void Actions_CanDistinguishCanceledAndCompletedInCurrentFrame(InputActionType actionType) + { + var gamepad = InputSystem.AddDevice(); + + var defaultAction = new InputAction(type: actionType, binding: "/buttonSouth"); + var pressAction = new InputAction(type: actionType, binding: "/buttonSouth", interactions: "press"); + var holdAction = new InputAction(type: actionType, binding: "/buttonSouth", interactions: "hold(duration=0.5)"); + + defaultAction.Enable(); + pressAction.Enable(); + holdAction.Enable(); + + Assert.That(defaultAction.phase, Is.EqualTo(InputActionPhase.Waiting)); + Assert.That(pressAction.phase, Is.EqualTo(InputActionPhase.Waiting)); + Assert.That(holdAction.phase, Is.EqualTo(InputActionPhase.Waiting)); + + using (var defaultTrace = new InputActionTrace(defaultAction)) + using (var pressTrace = new InputActionTrace(pressAction)) + using (var holdTrace = new InputActionTrace(holdAction)) + { + // Press button. Actions should be considered pressed, but the + // hold action should not be considered performed yet. + Press(gamepad.buttonSouth); + + Assert.That(defaultTrace, actionType != InputActionType.PassThrough + ? Started(defaultAction).AndThen(Performed(defaultAction)) + : Performed(defaultAction)); + Assert.That(defaultAction.WasPressedThisFrame(), Is.True); + Assert.That(defaultAction.WasReleasedThisFrame(), Is.False); + Assert.That(defaultAction.WasPerformedThisFrame(), Is.True); + Assert.That(defaultAction.WasCompletedThisFrame(), Is.False); + + Assert.That(pressTrace, Started(pressAction).AndThen(Performed(pressAction))); + Assert.That(pressAction.WasPressedThisFrame(), Is.True); + Assert.That(pressAction.WasReleasedThisFrame(), Is.False); + Assert.That(pressAction.WasPerformedThisFrame(), Is.True); + Assert.That(pressAction.WasCompletedThisFrame(), Is.False); + + Assert.That(holdTrace, Started(holdAction)); + Assert.That(holdAction.WasPressedThisFrame(), Is.True); + Assert.That(holdAction.WasReleasedThisFrame(), Is.False); + Assert.That(holdAction.WasPerformedThisFrame(), Is.False); + Assert.That(holdAction.WasCompletedThisFrame(), Is.False); + + defaultTrace.Clear(); + pressTrace.Clear(); + holdTrace.Clear(); + + // Keep holding button but for less than the hold duration needed. + InputSystem.Update(); + + Assert.That(defaultTrace, Is.Empty); + Assert.That(defaultAction.WasPressedThisFrame(), Is.False); + Assert.That(defaultAction.WasReleasedThisFrame(), Is.False); + Assert.That(defaultAction.WasPerformedThisFrame(), Is.False); + Assert.That(defaultAction.WasCompletedThisFrame(), Is.False); + + Assert.That(pressTrace, Is.Empty); + Assert.That(pressAction.WasPressedThisFrame(), Is.False); + Assert.That(pressAction.WasReleasedThisFrame(), Is.False); + Assert.That(pressAction.WasPerformedThisFrame(), Is.False); + Assert.That(pressAction.WasCompletedThisFrame(), Is.False); + + Assert.That(holdTrace, Is.Empty); + Assert.That(holdAction.WasPressedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.False); + Assert.That(holdAction.WasPerformedThisFrame(), Is.False); + Assert.That(holdAction.WasCompletedThisFrame(), Is.False); + + defaultTrace.Clear(); + pressTrace.Clear(); + holdTrace.Clear(); + + // Release button, the actions should cancel. The hold action should not be considered completed + // since it was not held long enough to be performed. + Release(gamepad.buttonSouth); + + Assert.That(defaultTrace, actionType != InputActionType.PassThrough + ? Canceled(defaultAction) + : Performed(defaultAction)); + Assert.That(defaultAction.WasPressedThisFrame(), Is.False); + Assert.That(defaultAction.WasReleasedThisFrame(), Is.True); + Assert.That(defaultAction.WasPerformedThisFrame(), Is.EqualTo(actionType == InputActionType.PassThrough)); + Assert.That(defaultAction.WasCompletedThisFrame(), Is.EqualTo(actionType == InputActionType.Button)); + + Assert.That(pressTrace, Canceled(pressAction)); + Assert.That(pressAction.WasPressedThisFrame(), Is.False); + Assert.That(pressAction.WasReleasedThisFrame(), Is.True); + Assert.That(pressAction.WasPerformedThisFrame(), Is.False); + Assert.That(pressAction.WasCompletedThisFrame(), Is.True); + + Assert.That(holdTrace, Canceled(holdAction)); + Assert.That(holdAction.WasPressedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.True); + Assert.That(holdAction.WasPerformedThisFrame(), Is.False); + Assert.That(holdAction.WasCompletedThisFrame(), Is.False); + + defaultTrace.Clear(); + pressTrace.Clear(); + holdTrace.Clear(); + + // Press button again. Same assertions as before. + Press(gamepad.buttonSouth); + + Assert.That(defaultTrace, actionType != InputActionType.PassThrough + ? Started(defaultAction).AndThen(Performed(defaultAction)) + : Performed(defaultAction)); + Assert.That(defaultAction.WasPressedThisFrame(), Is.True); + Assert.That(defaultAction.WasReleasedThisFrame(), Is.False); + Assert.That(defaultAction.WasPerformedThisFrame(), Is.True); + Assert.That(defaultAction.WasCompletedThisFrame(), Is.False); + + Assert.That(pressTrace, Started(pressAction).AndThen(Performed(pressAction))); + Assert.That(pressAction.WasPressedThisFrame(), Is.True); + Assert.That(pressAction.WasReleasedThisFrame(), Is.False); + Assert.That(pressAction.WasPerformedThisFrame(), Is.True); + Assert.That(pressAction.WasCompletedThisFrame(), Is.False); + + Assert.That(holdTrace, Started(holdAction)); + Assert.That(holdAction.WasPressedThisFrame(), Is.True); + Assert.That(holdAction.WasReleasedThisFrame(), Is.False); + Assert.That(holdAction.WasPerformedThisFrame(), Is.False); + Assert.That(holdAction.WasCompletedThisFrame(), Is.False); + + defaultTrace.Clear(); + pressTrace.Clear(); + holdTrace.Clear(); + + // Hold button for long enough. + currentTime += 1; + InputSystem.Update(); + + Assert.That(defaultTrace, Is.Empty); + Assert.That(defaultAction.WasPressedThisFrame(), Is.False); + Assert.That(defaultAction.WasReleasedThisFrame(), Is.False); + Assert.That(defaultAction.WasPerformedThisFrame(), Is.False); + Assert.That(defaultAction.WasCompletedThisFrame(), Is.False); + + Assert.That(pressTrace, Is.Empty); + Assert.That(pressAction.WasPressedThisFrame(), Is.False); + Assert.That(pressAction.WasReleasedThisFrame(), Is.False); + Assert.That(pressAction.WasPerformedThisFrame(), Is.False); + Assert.That(pressAction.WasCompletedThisFrame(), Is.False); + + Assert.That(holdTrace, Performed(holdAction)); + Assert.That(holdAction.WasPressedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.False); + Assert.That(holdAction.WasPerformedThisFrame(), Is.True); + Assert.That(holdAction.WasCompletedThisFrame(), Is.False); + + defaultTrace.Clear(); + pressTrace.Clear(); + holdTrace.Clear(); + + // Keep holding button. + InputSystem.Update(); + + Assert.That(defaultTrace, Is.Empty); + Assert.That(defaultAction.WasPressedThisFrame(), Is.False); + Assert.That(defaultAction.WasReleasedThisFrame(), Is.False); + Assert.That(defaultAction.WasPerformedThisFrame(), Is.False); + Assert.That(defaultAction.WasCompletedThisFrame(), Is.False); + + Assert.That(pressTrace, Is.Empty); + Assert.That(pressAction.WasPressedThisFrame(), Is.False); + Assert.That(pressAction.WasReleasedThisFrame(), Is.False); + Assert.That(pressAction.WasPerformedThisFrame(), Is.False); + Assert.That(pressAction.WasCompletedThisFrame(), Is.False); + + Assert.That(holdTrace, Is.Empty); + Assert.That(holdAction.WasPressedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.False); + Assert.That(holdAction.WasPerformedThisFrame(), Is.False); + Assert.That(holdAction.WasCompletedThisFrame(), Is.False); + + defaultTrace.Clear(); + pressTrace.Clear(); + holdTrace.Clear(); + + // Release button, the actions should cancel. The hold action should now be considered completed + // since it was held long enough to be performed. + Release(gamepad.buttonSouth); + + Assert.That(defaultTrace, actionType != InputActionType.PassThrough + ? Canceled(defaultAction) + : Performed(defaultAction)); + Assert.That(defaultAction.WasPressedThisFrame(), Is.False); + Assert.That(defaultAction.WasReleasedThisFrame(), Is.True); + Assert.That(defaultAction.WasPerformedThisFrame(), Is.EqualTo(actionType == InputActionType.PassThrough)); + Assert.That(defaultAction.WasCompletedThisFrame(), Is.EqualTo(actionType == InputActionType.Button)); + + Assert.That(pressTrace, Canceled(pressAction)); + Assert.That(pressAction.WasPressedThisFrame(), Is.False); + Assert.That(pressAction.WasReleasedThisFrame(), Is.True); + Assert.That(pressAction.WasPerformedThisFrame(), Is.False); + Assert.That(pressAction.WasCompletedThisFrame(), Is.True); + + Assert.That(holdTrace, Canceled(holdAction)); + Assert.That(holdAction.WasPressedThisFrame(), Is.False); + Assert.That(holdAction.WasReleasedThisFrame(), Is.True); + Assert.That(holdAction.WasPerformedThisFrame(), Is.False); + Assert.That(holdAction.WasCompletedThisFrame(), Is.True); + } + } + [Test] [Category("Actions")] public void Actions_CanReadValueFromAction() @@ -1698,11 +2038,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. @@ -1829,9 +2171,661 @@ public void Actions_CanReadValueFromAction_AsButton(InputActionType actionType, Set(gamepad.leftTrigger, 0.25f, queueEventOnly: true); Set(gamepad.leftTrigger, 0.75f); - Assert.That(action.IsPressed(), Is.True); - Assert.That(action.WasPressedThisFrame(), Is.True); - Assert.That(action.WasReleasedThisFrame(), Is.True); + Assert.That(action.IsPressed(), Is.True); + Assert.That(action.WasPressedThisFrame(), Is.True); + Assert.That(action.WasReleasedThisFrame(), Is.True); + } + + [Test] + [Category("Actions")] + [TestCase(InputActionType.Value)] + [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_CanReadPerformedFromAction_AsButton(InputActionType actionType, string interactions = null) + { + // This test is structured the same as Actions_CanReadValueFromAction_AsButton above, + // but with additional testing that the phase changes are correct for the given action type and interaction, + // and additionally test functionality of WasPerformedThisFrame() and WasCompletedThisFrame(), which can + // be different than WasPressedThisFrame() and WasReleasedThisFrame(). + + // Set global press and release points to known values. + InputSystem.settings.defaultButtonPressPoint = 0.5f; + InputSystem.settings.buttonReleaseThreshold = 0.8f; // 80% puts the release point at 0.4. + + var gamepad = InputSystem.AddDevice(); + + var action = new InputAction(type: actionType, binding: "/leftTrigger", interactions: interactions); + + var isHold = interactions?.StartsWith("hold") ?? false; + var isPress = interactions?.StartsWith("press") ?? false; + var isButtonLike = (action.type == InputActionType.Value && isPress) || + (action.type == InputActionType.Button && !isHold); + + using (var trace = new InputActionTrace(action)) + { + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Disabled)); + + action.Enable(); + + Assert.That(action.IsPressed(), Is.False); + Assert.That(action.WasPressedThisFrame(), Is.False); + 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.WasCompletedThisFrame(), Is.False); + + // Press such that it stays below press threshold. + Set(gamepad.leftTrigger, 0.25f); + + // W = Waiting, S = Started, P = Performed, C = Canceled + // P/C = Performed/Completed, T = True, F = False + // (* means listeners not invoked) + // | Type | Interaction | Phase Change | P/C | isButtonLike | isHold | + // |--------|-------------|------------------|-----|--------------|--------| + // | Value | Default | W -> S, P, S* | T/F | | | + // | Value | Press | W -> S | F/F | true | | + // | Value | Hold | W (No Change) | F/F | | true | + // | Button | Default | W -> S | F/F | true | | + // | Button | Press | W -> S | F/F | true | | + // | Button | Hold | W (No Change) | F/F | | true | + // | Pass | Default | W -> P | T/F | | | + + 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), Is.False); + } + + 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/C = Performed/Completed, T = True, F = False + // (* means listeners not invoked) + // | Type | Interaction | Phase Change | P/C | isButtonLike | isHold | + // |--------|-------------|------------------|-----|--------------|--------| + // | Value | Default | S -> P, S* | T/F | | | + // | Value | Press | S -> P | T/F | true | | + // | Value | Hold | W -> S | F/F | | true | + // | Button | Default | S -> P | T/F | true | | + // | Button | Press | S -> P | T/F | true | | + // | Button | Hold | W -> S | F/F | | true | + // | Pass | Default | P -> P | T/F | | | + + Assert.That(action.IsPressed(), Is.True); + Assert.That(action.WasPressedThisFrame(), Is.True); + Assert.That(action.WasReleasedThisFrame(), Is.False); + + 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.True); + Assert.That(action.WasReleasedThisFrame(), Is.False); + + 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.WasCompletedThisFrame(), Is.False); + } + else if (isHold) + { + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasCompletedThisFrame(), Is.False); + } + else + { + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasCompletedThisFrame(), Is.False); + } + + 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); + + 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.WasCompletedThisFrame(), Is.False); + } + else if (isHold) + { + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasCompletedThisFrame(), Is.False); + } + else + { + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasCompletedThisFrame(), Is.False); + } + + trace.Clear(); + + // Advance one frame. + InputSystem.Update(); + + // W = Waiting, S = Started, P = Performed, C = Canceled + // P/C = Performed/Completed, T = True, F = False + // (* means listeners not invoked) + // | Type | Interaction | Phase Change | P/C | isButtonLike | isHold | + // |--------|-------------|------------------|-----|--------------|--------| + // | Value | Default | W -> S, P, S* | T/F | | | + // | Value | Press | W -> S, P | T/F | true | | + // | Value | Hold | W -> S | F/F | | true | + // | Button | Default | W (No Change) | F/F | true | | + // | Button | Press | W (No Change) | F/F | true | | + // | Button | Hold | W (No Change) | F/F | | true | + // | Pass | Default | W (No Change) | F/F | | | + + // 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); + + 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), Is.False); + } + } + else + { + Assert.That(action.IsPressed(), Is.False); + Assert.That(action.WasPressedThisFrame(), Is.False); + 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.WasCompletedThisFrame(), Is.False); + + trace.Clear(); + + Set(gamepad.leftTrigger, 0.6f); + + // W = Waiting, S = Started, P = Performed, C = Canceled + // P/C = Performed/Completed, T = True, F = False + // | Type | Interaction | Phase Change | P/C | isButtonLike | isHold | + // |--------|-------------|------------------|-----|--------------|--------| + // | Button | Default | W -> S, P | T/F | true | | + // | Button | Press | W -> S, P | T/F | true | | + // | Button | Hold | W -> S | F/F | | true | + // | Pass | Default | W -> P | T/F | | | + + Assert.That(action.IsPressed(), Is.True); + Assert.That(action.WasPressedThisFrame(), Is.True); + Assert.That(action.WasReleasedThisFrame(), Is.False); + + 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), Is.False); + } + } + + trace.Clear(); + + // Release a bit but remain above release threshold. + Set(gamepad.leftTrigger, 0.41f); + + // W = Waiting, S = Started, P = Performed, C = Canceled + // P/C = Performed/Completed, T = True, F = False + // (* means listeners not invoked) + // | Type | Interaction | Phase Change | P/C | isButtonLike | isHold | + // |--------|-------------|------------------|-----|--------------|--------| + // | Value | Default | S -> P, S* | T/F | | | + // | Value | Press | P (No Change) | F/F | true | | + // | Value | Hold | S (No Change) | F/F | | true | + // | Button | Default | P (No Change) | F/F | true | | + // | Button | Press | P (No Change) | F/F | true | | + // | Button | Hold | S (No Change) | F/F | | true | + // | Pass | Default | P -> P | T/F | | | + + Assert.That(action.IsPressed(), Is.True); + Assert.That(action.WasPressedThisFrame(), Is.False); + Assert.That(action.WasReleasedThisFrame(), Is.False); + + 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), Is.False); + } + + trace.Clear(); + + // Go below release threshold. + Set(gamepad.leftTrigger, 0.2f); + + // W = Waiting, S = Started, P = Performed, C = Canceled + // P/C = Performed/Completed, T = True, F = False + // (* means listeners not invoked) + // | Type | Interaction | Phase Change | P/C | isButtonLike | isHold | + // |--------|-------------|------------------|-----|--------------|--------| + // | Value | Default | S -> P, S* | T/F | | | + // | Value | Press | P -> S | F/T | true | | + // | Value | Hold | S (No Change) | F/F | | true | + // | Button | Default | P -> S | F/T | true | | + // | Button | Press | P -> S | F/T | true | | + // | Button | Hold | S (No Change) | F/F | | true | + // | Pass | Default | P -> P | T/F | | | + + 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), Is.False); + } + else if (isButtonLike) + { + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasCompletedThisFrame(), Is.True); + } + else if (isHold) + { + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasCompletedThisFrame(), Is.False); + } + else + { + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), Is.False); + } + else if (isButtonLike) + { + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasCompletedThisFrame(), Is.True); + } + else if (isHold) + { + Assert.That(action.WasPerformedThisFrame(), Is.False); + Assert.That(action.WasCompletedThisFrame(), Is.False); + } + else + { + Assert.That(action.WasPerformedThisFrame(), Is.True); + Assert.That(action.WasCompletedThisFrame(), Is.False); + } + + trace.Clear(); + + // Advance one frame. Should reset WasReleasedThisFrame(). + InputSystem.Update(); + + // W = Waiting, S = Started, P = Performed, C = Canceled + // P/C = Performed/Completed, T = True, F = False + // (* means listeners not invoked) + // | Type | Interaction | Phase Change | P/C | isButtonLike | isHold | + // |--------|-------------|------------------|-----|--------------|--------| + // | Value | Default | W -> S, P, S* | T/F | | | + // | Value | Press | W -> S | F/F | true | | + // | Value | Hold | W (No Change) | F/F | | true | + // | Button | Default | W (No Change) | F/F | true | | + // | Button | Press | W (No Change) | F/F | true | | + // | Button | Hold | W (No Change) | F/F | | true | + // | Pass | Default | W (No Change) | F/F | | | + + 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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/C = Performed/Completed, T = True, F = False + // (* means listeners not invoked) + // | Type | Interaction | Phase Change | P/C | isButtonLike | isHold | + // |--------|-------------|------------------|-----|--------------|--------| + // | Value | Default | S -> P, S*, P, S*| T/F | | | + // | Value | Press | S -> P, S | T/T | true | | + // | Value | Hold | W -> S | F/F | | true | + // | Button | Default | W -> S, P, S | T/T | true | | + // | Button | Press | W -> S, P, S | T/T | true | | + // | Button | Hold | W -> S | F/F | | true | + // | Pass | Default | W -> P, P | T/F | | | + + 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), Is.False); + } + + trace.Clear(); + + // Advance one frame. + InputSystem.Update(); + + // W = Waiting, S = Started, P = Performed, C = Canceled + // | Type | Interaction | Phase Change | P/C | isButtonLike | isHold | + // |--------|-------------|------------------|-----|--------------|--------| + // | Value | Default | S (No Change) | F/F | | | + // | Value | Press | S (No Change) | F/F | true | | + // | Value | Hold | S (No Change) | F/F | | true | + // | Button | Default | S (No Change) | F/F | true | | + // | Button | Press | S (No Change) | F/F | true | | + // | Button | Hold | S (No Change) | F/F | | true | + // | Pass | Default | P (No Change) | F/F | | | + + 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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/C | isButtonLike | isHold | + // |--------|-------------|--------------------------|-----|--------------|--------| + // | Value | Default | S -> P, S*, P, S*, P, S* | T/F | | | + // | Value | Press | S -> P, S, P | T/T | true | | + // | Value | Hold | S (No Change) | F/F | | true | + // | Button | Default | S -> P, S, P | T/T | true | | + // | Button | Press | S -> P, S, P | T/T | true | | + // | Button | Hold | S (No Change) | F/F | | true | + // | Pass | Default | P -> P, P, P | T/F | | | + + 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), Is.False); + } + } } [Test] @@ -1950,6 +2944,99 @@ public unsafe void Actions_CanReadValueFromAction_InCallback_WithoutKnowingValue Is.EqualTo((Vector2.up + Vector2.left).normalized.y).Within(0.00001)); } + [Test] + [Category("Actions")] + public 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_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() @@ -2032,6 +3119,274 @@ 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")] + [TestCase(InputActionType.Value)] + [TestCase(InputActionType.Value, "Scale(factor=2)")] + [TestCase(InputActionType.Button)] + [TestCase(InputActionType.Button, "Scale(factor=2)")] + public void Actions_CanQueryMagnitudeFromAction_WithAxisControl(InputActionType actionType, string processors = null) + { + var gamepad = InputSystem.AddDevice(); + + var action = new InputAction(type: actionType, binding: "/leftTrigger", processors: processors); + + Assert.That(action.activeControl, Is.Null); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Disabled)); + Assert.That(action.ReadValue(), Is.EqualTo(0f)); + Assert.That(action.GetMagnitude(), Is.EqualTo(0f)); + + action.Enable(); + + Assert.That(action.activeControl, Is.Null); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Waiting)); + Assert.That(action.ReadValue(), Is.EqualTo(0f)); + Assert.That(action.GetMagnitude(), Is.EqualTo(0f)); + + Set(gamepad.leftTrigger, 0.123f); + + const float factor = 2f; + var expectedValue = processors == null ? 0.123f : 0.123f * factor; + + Assert.That(action.activeControl, Is.SameAs(gamepad.leftTrigger)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.ReadValue(), Is.EqualTo(expectedValue).Within(0.00001)); + Assert.That(action.GetMagnitude(), Is.EqualTo(0.123f).Within(0.00001)); + + Set(gamepad.leftTrigger, 0f); + + Assert.That(action.activeControl, Is.Null); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Waiting)); + Assert.That(action.ReadValue(), Is.EqualTo(0f)); + Assert.That(action.GetMagnitude(), Is.EqualTo(0f)); + } + + [Test] + [Category("Actions")] + public void Actions_CanQueryMagnitudeFromAction_WithStickControl() + { + var gamepad = InputSystem.AddDevice(); + + var action = new InputAction(binding: "/leftStick"); + + Assert.That(action.activeControl, Is.Null); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Disabled)); + Assert.That(action.ReadValue(), Is.EqualTo(Vector2.zero)); + Assert.That(action.GetMagnitude(), Is.EqualTo(0f)); + + action.Enable(); + + Assert.That(action.activeControl, Is.Null); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Waiting)); + Assert.That(action.ReadValue(), Is.EqualTo(Vector2.zero)); + Assert.That(action.GetMagnitude(), Is.EqualTo(0f)); + + Set(gamepad.leftStick, new Vector2(0.123f, 0.234f)); + + var expectedValue = new StickDeadzoneProcessor().Process(new Vector2(0.123f, 0.234f)); + + Assert.That(action.activeControl, Is.SameAs(gamepad.leftStick)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.ReadValue(), Is.EqualTo(expectedValue).Using(Vector2EqualityComparer.Instance)); + Assert.That(action.GetMagnitude(), Is.EqualTo(expectedValue.magnitude)); + + Set(gamepad.leftStick, Vector2.zero); + + Assert.That(action.activeControl, Is.Null); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Waiting)); + Assert.That(action.ReadValue(), Is.EqualTo(Vector2.zero)); + Assert.That(action.GetMagnitude(), Is.EqualTo(0f)); + } + + [Test] + [Category("Actions")] + [TestCase(InputActionType.Value)] + [TestCase(InputActionType.Value, "Scale(factor=2)")] + public void Actions_CanQueryMagnitudeFromAction_WithCompositeAxisControl(InputActionType actionType, string processors = null) + { + var keyboard = InputSystem.AddDevice(); + + // Adding the Scale processor to the composite binding, not the action, to make sure + // the scaling is only applied once instead of scaling each part binding in addition to scaling the output + // of the Axis composite. + var action = new InputAction(type: actionType); + action.AddCompositeBinding($"Axis(minValue=-5,maxValue=5)", processors: processors) + .With("Negative", "/a") + .With("Positive", "/d"); + + Assert.That(action.activeControl, Is.Null); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Disabled)); + Assert.That(action.ReadValue(), Is.EqualTo(0f)); + Assert.That(action.GetMagnitude(), Is.EqualTo(0f)); + + action.Enable(); + + Assert.That(action.activeControl, Is.Null); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Waiting)); + Assert.That(action.ReadValue(), Is.EqualTo(0f)); + Assert.That(action.GetMagnitude(), Is.EqualTo(0f)); + + Press(keyboard.dKey); + + const float factor = 2f; + var expectedValue = processors == null ? 5f : 5f * factor; + + Assert.That(action.activeControl, Is.SameAs(keyboard.dKey)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.ReadValue(), Is.EqualTo(expectedValue).Within(0.00001)); + Assert.That(action.GetMagnitude(), Is.EqualTo(1f).Within(0.00001)); + + Release(keyboard.dKey); + Press(keyboard.aKey); + + Assert.That(action.activeControl, Is.SameAs(keyboard.aKey)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.ReadValue(), Is.EqualTo(-expectedValue).Within(0.00001)); + Assert.That(action.GetMagnitude(), Is.EqualTo(1f).Within(0.00001)); + + Release(keyboard.aKey); + + Assert.That(action.activeControl, Is.Null); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Waiting)); + Assert.That(action.ReadValue(), Is.EqualTo(0f)); + Assert.That(action.GetMagnitude(), Is.EqualTo(0f)); + } + + [Test] + [Category("Actions")] + [TestCase(InputActionType.Value)] + [TestCase(InputActionType.Value, "ScaleVector2(x=2,y=2)")] + public void Actions_CanQueryMagnitudeFromAction_WithComposite2DVectorControl(InputActionType actionType, string processors = null) + { + var keyboard = InputSystem.AddDevice(); + + // Adding the Scale processor to the composite binding, not the action, to make sure + // the scaling is only applied once instead of scaling each part binding in addition to scaling the output + // of the Axis composite. + var action = new InputAction(type: actionType); + action.AddCompositeBinding("2DVector(mode=1)", processors: processors) // Mode.Digital + .With("Up", "/w") + .With("Down", "/s") + .With("Left", "/a") + .With("Right", "/d"); + + Assert.That(action.activeControl, Is.Null); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Disabled)); + Assert.That(action.ReadValue(), Is.EqualTo(Vector2.zero)); + Assert.That(action.GetMagnitude(), Is.EqualTo(0f)); + + action.Enable(); + + Assert.That(action.activeControl, Is.Null); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Waiting)); + Assert.That(action.ReadValue(), Is.EqualTo(Vector2.zero)); + Assert.That(action.GetMagnitude(), Is.EqualTo(0f)); + + Press(keyboard.sKey); + + const float factor = 2f; + var expectedValue = processors == null ? Vector2.down : Vector2.down * factor; + + Assert.That(action.activeControl, Is.SameAs(keyboard.sKey)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.ReadValue(), Is.EqualTo(expectedValue).Using(Vector2EqualityComparer.Instance)); + Assert.That(action.GetMagnitude(), Is.EqualTo(1f).Within(0.00001)); + + Press(keyboard.dKey); + + expectedValue = processors == null ? new Vector2(1f, -1f) : new Vector2(1f, -1f) * factor; + + Assert.That(action.activeControl, Is.SameAs(keyboard.dKey)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.ReadValue(), Is.EqualTo(expectedValue).Using(Vector2EqualityComparer.Instance)); + Assert.That(action.GetMagnitude(), Is.EqualTo(new Vector2(1f, -1f).magnitude).Within(0.00001)); + + Release(keyboard.dKey); + Release(keyboard.sKey); + + Assert.That(action.activeControl, Is.Null); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Waiting)); + Assert.That(action.ReadValue(), Is.EqualTo(Vector2.zero)); + Assert.That(action.GetMagnitude(), Is.EqualTo(0f)); + } + + [Test] + [Category("Actions")] + public void Actions_CanQueryMagnitudeFromAction_WithQuaternionControl_ReturnsInvalidMagnitude() + { + var sensor = InputSystem.AddDevice(); + + var action = new InputAction(binding: "/attitude"); + + // Verify that the default value is not Quaternion.identity but instead a zero quaternion. + // When the control changes to the default value, the phase changes back to Waiting, + // and the magnitude is explicitly cleared. + Assert.That(sensor.attitude.ReadDefaultValue(), Is.EqualTo(default(Quaternion))); + + Assert.That(action.activeControl, Is.Null); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Disabled)); + Assert.That(action.ReadValue(), Is.EqualTo(default(Quaternion))); + Assert.That(action.GetMagnitude(), Is.EqualTo(0f)); + + action.Enable(); + + Assert.That(action.activeControl, Is.Null); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Waiting)); + Assert.That(action.ReadValue(), Is.EqualTo(default(Quaternion))); + Assert.That(action.GetMagnitude(), Is.EqualTo(0f)); + + Set(sensor.attitude, Quaternion.Euler(30f, 60f, 45f)); + + Assert.That(action.activeControl, Is.SameAs(sensor.attitude)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.ReadValue(), Is.EqualTo(Quaternion.Euler(30f, 60f, 45f)).Using(QuaternionEqualityComparer.Instance)); + Assert.That(action.GetMagnitude(), Is.EqualTo(-1f)); + + Set(sensor.attitude, Quaternion.identity); + + Assert.That(action.activeControl, Is.SameAs(sensor.attitude)); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Started)); + Assert.That(action.ReadValue(), Is.EqualTo(Quaternion.identity)); + Assert.That(action.GetMagnitude(), Is.EqualTo(-1f)); + + Set(sensor.attitude, default(Quaternion)); + + Assert.That(action.activeControl, Is.Null); + Assert.That(action.phase, Is.EqualTo(InputActionPhase.Waiting)); + Assert.That(action.ReadValue(), Is.EqualTo(default(Quaternion))); + Assert.That(action.GetMagnitude(), Is.EqualTo(0f)); + } + [Test] [Category("Actions")] public void Actions_ResettingDevice_CancelsOngoingActionsThatAreDrivenByIt() @@ -2738,35 +4093,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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), Is.False); Assert.That(action.activeControl, Is.Null); } @@ -9166,38 +10531,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.WasCompletedThisFrame(), 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.WasCompletedThisFrame(), 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)); } diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 44a3766ff2..272116b5bb 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -13,7 +13,14 @@ however, it has to be formatted properly to pass verification tests. ### Changed - From 2023.2 forward: UI toolkit now uses the "UI" action map of project-wide actions as their default input actions. Previously, the actions were hardcoded and were based on `DefaultInputActions` asset which didn't allow user changes. Also, removing bindings or renaming the 'UI' action map of project wide actions will break UI input for UI toolkit. +### Added +- Added new methods and properties to [`InputAction`](xref:UnityEngine.InputSystem.InputAction): + - [`InputAction.activeValueType`](xref:UnityEngine.InputSystem.InputAction.activeValueType) returns the `Type` expected by `ReadValue` based on the currently active control that is driving the action. + - [`InputAction.GetMagnitude`](xref:UnityEngine.InputSystem.InputAction.GetMagnitude) returns the current amount of actuation of the control that is driving the action. + - [`InputAction.WasCompletedThisFrame`](xref:UnityEngine.InputSystem.InputAction.WasCompletedThisFrame) returns `true` on the frame that the action stopped being in the performed phase. This allows for similar functionality to [`WasPressedThisFrame`](xref:UnityEngine.InputSystem.InputAction.WasPressedThisFrame)/[`WasReleasedThisFrame`](xref:UnityEngine.InputSystem.InputAction.WasReleasedThisFrame) when paired with [`WasPerformedThisFrame`](xref:UnityEngine.InputSystem.InputAction.WasPerformedThisFrame) except it is directly based on the interactions driving the action. For example, you can use it to distinguish between the button being released or whether it was released after being held for long enough to perform when using the Hold interaction. + ### Fixed +- Fixed syntax of code examples in API documentation for [`AxisComposite`](xref:UnityEngine.InputSystem.Composites.AxisComposite). - Fixed missing confirmation popup when deleting a control scheme. - Fixed support for menu bar/customisable keyboard shortcuts used when interacting with Actions and Action Maps. - Fixed add bindings button to support left button click. diff --git a/Packages/com.unity.inputsystem/Documentation~/Interactions.md b/Packages/com.unity.inputsystem/Documentation~/Interactions.md index 4698dd985c..ae21bf1bf6 100644 --- a/Packages/com.unity.inputsystem/Documentation~/Interactions.md +++ b/Packages/com.unity.inputsystem/Documentation~/Interactions.md @@ -35,7 +35,7 @@ An Interaction has a set of distinct phases it can go through in response to rec |`Waiting`|The Interaction is waiting for input.| |`Started`|The Interaction has been started (that is, it received some of its expected input), but is not complete yet.| |`Performed`|The Interaction is complete.| -|`Canceled`|The Interaction was interrupted and aborted. For example, the user pressed and then released a button before the minimum time required for a [hold Interaction](#hold) to complete.| +|`Canceled`|The Interaction was interrupted and aborted. For example, the user pressed and then released a button before the minimum time required for a [hold Interaction](#hold) to complete.| Not every Interaction triggers every phase, and the pattern in which specific Interactions trigger phases depends on the Interaction type. @@ -163,7 +163,7 @@ If you haven't specifically added an Interaction to a Binding or its Action, the |__Callback__|[`InputActionType.Value`](RespondingToActions.md#value)|[`InputActionType.Button`](RespondingToActions.md#button)|[`InputActionType.PassThrough`](RespondingToActions.md#pass-through)| |-----------|-------------|------------|-----------------| -|[`started`](../api/UnityEngine.InputSystem.InputAction.html#UnityEngine_InputSystem_InputAction_started)|Control(s) changed value away from the default value.|Button started being pressed but has not necessarily crossed the press threshold yet.|First Control actuation after Action was enabled.| +|[`started`](../api/UnityEngine.InputSystem.InputAction.html#UnityEngine_InputSystem_InputAction_started)|Control(s) changed value away from the default value.|Button started being pressed but has not necessarily crossed the press threshold yet.|not used| |[`performed`](../api/UnityEngine.InputSystem.InputAction.html#UnityEngine_InputSystem_InputAction_performed)|Control(s) changed value.|Button was pressed to at least the button [press threshold](../api/UnityEngine.InputSystem.InputSettings.html#UnityEngine_InputSystem_InputSettings_defaultButtonPressPoint).|Control changed value.| |[`canceled`](../api/UnityEngine.InputSystem.InputAction.html#UnityEngine_InputSystem_InputAction_canceled)|Control(s) are no longer actuated.|Button was released. If the button was pressed above the press threshold, the button has now fallen to or below the [release threshold](../api/UnityEngine.InputSystem.InputSettings.html#UnityEngine_InputSystem_InputSettings_buttonReleaseThreshold). If the button was never fully pressed, the button is now back to completely unpressed.|Action is disabled.| diff --git a/Packages/com.unity.inputsystem/Documentation~/RespondingToActions.md b/Packages/com.unity.inputsystem/Documentation~/RespondingToActions.md index 31a6e1bcb9..0c9b6f2a35 100644 --- a/Packages/com.unity.inputsystem/Documentation~/RespondingToActions.md +++ b/Packages/com.unity.inputsystem/Documentation~/RespondingToActions.md @@ -3,7 +3,7 @@ There are two main techniques you can use to respond to Actions in your project. These are to either use **polling** or an **event-driven** approach. -- The **Polling** approach refers to the technique of repeatedly checking the current state of the Actions you are interested in. Typically you would do this in the Update() method of a Monobehaviour script. +- The **Polling** approach refers to the technique of repeatedly checking the current state of the Actions you are interested in. Typically you would do this in the `Update()` method of a `MonoBehaviour` script. - The **Event-driven** approach involves creating your own methods in code that are automatically called when an action is performed. For most common scenarios, especially action games where the user's input should have a continuous effect on an in-game character, **Polling** is usually simpler and easier to implement. @@ -39,8 +39,16 @@ public class Example : MonoBehaviour Note that the value type has to correspond to the value type of the control that the value is being read from. -To determine whether an action was performed in the current frame, you can use [`InputAction.WasPerformedThisFrame()`](../api/UnityEngine.InputSystem.InputAction.html#UnityEngine_InputSystem_InputAction_WasPerformedThisFrame): +There are two methods you can use to poll for `performed` [action callbacks](#action-callbacks) to determine whether an action was performed or stopped performing in the current frame. +These methods differ from [`InputAction.WasPressedThisFrame()`](../api/UnityEngine.InputSystem.InputAction.html#UnityEngine_InputSystem_InputAction_WasPressedThisFrame) and [`InputAction.WasReleasedThisFrame()`](../api/UnityEngine.InputSystem.InputAction.html#UnityEngine_InputSystem_InputAction_WasReleasedThisFrame) in that these depend directly on the [Interactions](Interactions.md) driving the action (including the [default Interaction](Interactions.md#default-interaction) if no specific interaction has been added to the action or binding). + +|Method|Description| +|------|-----------| +|[`InputAction.WasPerformedThisFrame()`](../api/UnityEngine.InputSystem.InputAction.html#UnityEngine_InputSystem_InputAction_WasPerformedThisFrame)|True if the [`InputAction.phase`](../api/UnityEngine.InputSystem.InputAction.html#UnityEngine_InputSystem_InputAction_phase) of the action has, at any point during the current frame, changed to [`Performed`](../api/UnityEngine.InputSystem.InputActionPhase.html#UnityEngine_InputSystem_InputActionPhase_Performed).| +|[`InputAction.WasCompletedThisFrame()`](../api/UnityEngine.InputSystem.InputAction.html#UnityEngine_InputSystem_InputAction_WasCompletedThisFrame)|True if the [`InputAction.phase`](../api/UnityEngine.InputSystem.InputAction.html#UnityEngine_InputSystem_InputAction_phase) of the action has, at any point during the current frame, changed away from [`Performed`](../api/UnityEngine.InputSystem.InputActionPhase.html#UnityEngine_InputSystem_InputActionPhase_Performed) to any other phase. This can be useful for [Button](#button) actions or [Value](#value) actions with interactions like [Press](Interactions.md#press) or [Hold](Interactions.md#hold) when you want to know the frame the interaction stops being performed. For actions with the [default Interaction](Interactions.md#default-interaction), this method will always return false for [Value](#value) and [Pass-Through](#pass-through) actions (since the phase stays in [`Started`](../api/UnityEngine.InputSystem.InputActionPhase.html#UnityEngine_InputSystem_InputActionPhase_Started) for Value actions and stays in [`Performed`](../api/UnityEngine.InputSystem.InputActionPhase.html#UnityEngine_InputSystem_InputActionPhase_Performed) for Pass-Through).| + +This example uses the Interact action from the [default actions](ActionsEditor.md#the-default-actions), which has a [Hold](Interactions.md#hold) interaction to make it perform only after the bound control is held for a period of time (for example, 0.4 seconds): ```CSharp using UnityEngine; @@ -48,18 +56,23 @@ using UnityEngine.InputSystem; public class Example : MonoBehaviour { - InputAction jumpAction; + InputAction interactAction; private void Start() { - jumpAction = InputSystem.actions.FindAction("Jump"); + interactAction = InputSystem.actions.FindAction("Interact"); } void Update() { - if (jumpAction.WasPerformedThisFrame()) + if (interactAction.WasPerformedThisFrame()) { - // your code to respond to the Jump action here + // your code to respond to the first frame that the Interact action is held for enough time + } + + if (interactAction.WasCompletedThisFrame()) + { + // your code to respond to the frame that the Interact action is released after being held for enough time } } } @@ -73,7 +86,7 @@ Finally, there are three methods you can use to poll for button presses and rele |[`InputAction.WasPressedThisFrame()`](../api/UnityEngine.InputSystem.InputAction.html#UnityEngine_InputSystem_InputAction_WasPressedThisFrame)|True if the level of [actuation](../api/UnityEngine.InputSystem.InputControl.html#UnityEngine_InputSystem_InputControl_EvaluateMagnitude) on the action has, at any point during the current frame, reached or gone above the [press point](../api/UnityEngine.InputSystem.InputSettings.html#UnityEngine_InputSystem_InputSettings_defaultButtonPressPoint).| |[`InputAction.WasReleasedThisFrame()`](../api/UnityEngine.InputSystem.InputAction.html#UnityEngine_InputSystem_InputAction_WasReleasedThisFrame)|True if the level of [actuation](../api/UnityEngine.InputSystem.InputControl.html#UnityEngine_InputSystem_InputControl_EvaluateMagnitude) on the action has, at any point during the current frame, gone from being at or above the [press point](../api/UnityEngine.InputSystem.InputSettings.html#UnityEngine_InputSystem_InputSettings_defaultButtonPressPoint) to at or below the [release threshold](../api/UnityEngine.InputSystem.InputSettings.html#UnityEngine_InputSystem_InputSettings_buttonReleaseThreshold).| -This example uses three actions called Shield, Teleport and Submit (which are not included in the [default actions]()): +This example uses three actions called Shield, Teleport and Submit (which are not included in the [default actions](ActionsEditor.md#the-default-actions)): ```CSharp using UnityEngine; @@ -101,15 +114,13 @@ public class Example : MonoBehaviour if (teleportAction.WasPressedThisFrame()) { - // teleport occurs on the first frame that the action was performed, and not again until the button is released + // teleport occurs on the first frame that the action is pressed, and not again until the button is released } if (submit.WasReleasedThisFrame()) { // submit occurs on the frame that the action is released, a common technique for buttons relating to UI controls. } - - } } ``` diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/Composites/AxisComposite.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/Composites/AxisComposite.cs index 8375e4c6f2..298e771f5d 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/Composites/AxisComposite.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/Composites/AxisComposite.cs @@ -16,7 +16,7 @@ namespace UnityEngine.InputSystem.Composites /// /// /// var action = new InputAction(); - /// action.AddCompositeBinding("Axis(minValue=0,maxValue=2") + /// action.AddCompositeBinding("Axis(minValue=0,maxValue=2)") /// .With("Negative", "<Keyboard>/a") /// .With("Positive", "<Keyboard>/d"); /// @@ -73,7 +73,7 @@ public class AxisComposite : InputBindingComposite /// /// /// var action = new InputAction(); - /// action.AddCompositeBinding("Axis(minValue=0,maxValue=2") + /// action.AddCompositeBinding("Axis(minValue=0,maxValue=2)") /// .With("Negative", "<Keyboard>/a") /// .With("Positive", "<Keyboard>/d"); /// @@ -95,7 +95,7 @@ public class AxisComposite : InputBindingComposite /// /// /// var action = new InputAction(); - /// action.AddCompositeBinding("Axis(minValue=0,maxValue=2") + /// action.AddCompositeBinding("Axis(minValue=0,maxValue=2)") /// .With("Negative", "<Keyboard>/a") /// .With("Positive", "<Keyboard>/d"); /// diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs index 51e7e4afd5..4ba97865ac 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputAction.cs @@ -501,7 +501,6 @@ public ReadOnlyArray controls /// only ever use and not go to or (as /// pass-through actions do not follow the start-performed-canceled model in general). - /// Also, interactions can choose their /// /// While an action is disabled, its phase is . /// @@ -583,7 +582,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 +607,48 @@ public unsafe InputControl activeControl } } + /// + /// Type of value returned by and currently expected + /// 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. + /// + /// 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). + /// + /// The active value type may change depending on which controls are actuated if there are multiple + /// bindings with different control types. This property can be used to ensure you are calling the + /// method with the expected type parameter if your action is + /// configured to allow multiple control types as otherwise that method will throw an + /// if the type of the control that triggered the action does not match the type parameter. + /// + /// + /// + /// + public unsafe Type activeValueType + { + 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 +1031,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() @@ -1027,6 +1069,38 @@ public unsafe object ReadValueAsObject() return null; } + /// + /// Read the current amount of actuation of the control that is driving this action. + /// + /// Returns the current level of control actuation (usually [0..1]) or -1 if + /// the control is actuated but does not support computing magnitudes. + /// + /// Magnitudes do not make sense for all types of controls. Controls that have no meaningful magnitude + /// will return -1 when calling this method. Any negative magnitude value should be considered an invalid value. + ///
+ /// The magnitude 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 the magnitude and not the individual control that triggered. + /// Instead, the value of the control that triggered the action will be fed into the composite magnitude calculation. + ///
+ /// + /// + public unsafe float GetMagnitude() + { + var state = GetOrCreateActionMap().m_State; + if (state != null) + { + var actionStatePtr = &state.actionStates[m_ActionIndexInState]; + if (actionStatePtr->haveMagnitude) + return actionStatePtr->magnitude; + } + + return 0f; + } + /// /// Reset the action state to default. /// @@ -1203,7 +1277,7 @@ public unsafe bool WasPressedThisFrame() /// /// /// - /// + /// public unsafe bool WasReleasedThisFrame() { var state = GetOrCreateActionMap().m_State; @@ -1259,6 +1333,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() @@ -1275,6 +1350,79 @@ public unsafe bool WasPerformedThisFrame() return false; } + /// + /// Check whether transitioned from to any other phase + /// value at least once in the current frame. + /// + /// True if the action completed this frame. + /// + /// Although is technically a phase, this method does not consider disabling + /// the action while the action is in to be "completed". + /// + /// 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 space bar and that the binding has a + /// assigned to it. In the frame where the space bar + /// is pressed, will be true (because the button/key is now pressed) + /// but will still be false (because the hold has not been performed yet). + /// If at that time the space bar is released, will be true (because the + /// button/key is now released) but WasCompletedThisFrame will still be false (because the hold + /// had not been performed yet). If instead the space bar is held down for long enough for the hold interaction, + /// the phase will change to and stay and + /// will be true for one frame as it meets the duration threshold. Once released, WasCompletedThisFrame will be true + /// (because the action is no longer performed) and only in the frame where the hold transitioned away from Performed. + /// + /// For another example where the action could be considered pressed but also completed, let's say the action + /// is bound to the thumbstick and that the binding has a Sector interaction from the XR Interaction Toolkit 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, WasCompletedThisFrame 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. For more details about the Sector interaction, see + /// SectorInteraction + /// in the XR Interaction Toolkit Scripting API documentation. + ///
+ /// 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 completed at least once. + /// + /// + /// + /// var teleport = playerInput.actions["Teleport"]; + /// if (teleport.WasPerformedThisFrame()) + /// InitiateTeleport(); + /// else if (teleport.WasCompletedThisFrame()) + /// 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 WasCompletedThisFrame() + { + var state = GetOrCreateActionMap().m_State; + + if (state != null) + { + var actionStatePtr = &state.actionStates[m_ActionIndexInState]; + var currentUpdateStep = InputUpdate.s_UpdateStepCount; + return actionStatePtr->lastCompletedInUpdate == currentUpdateStep && currentUpdateStep != default; + } + + return false; + } + /// /// Return the completion percentage of the timeout (if any) running on the current interaction. /// @@ -1298,8 +1446,8 @@ public unsafe bool WasPerformedThisFrame() /// completion percentage and . /// /// The meaning of the timeout is dependent on the interaction in play. For a , - /// the timeout represents "completion" (that is, the time until a "hold" is considered to be performed), whereas - /// for a it represents "time to failure" (that is, the remaining time window + /// "completion" represents the duration timeout (that is, the time until a "hold" is considered to be performed), whereas + /// for a "completion" represents "time to failure" (that is, the remaining time window /// that the interaction can be completed within). /// /// Note that an interaction might run multiple timeouts in succession. One such example is . @@ -1847,6 +1995,7 @@ public unsafe double startTime /// /// /// + /// public Type valueType => m_State?.GetValueType(bindingIndex, controlIndex); /// @@ -1876,8 +2025,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 , diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs index 3177e2cd7f..f1a1b11509 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.lastCompletedInUpdate = oldActionState.lastCompletedInUpdate; newActionState.pressedInUpdate = oldActionState.pressedInUpdate; newActionState.releasedInUpdate = oldActionState.releasedInUpdate; newActionState.startTime = oldActionState.startTime; @@ -839,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); } } } @@ -852,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); } } @@ -872,6 +874,7 @@ public void ResetActionState(int actionIndex, InputActionPhase toPhase = InputAc { actionState->lastCanceledInUpdate = default; actionState->lastPerformedInUpdate = default; + actionState->lastCompletedInUpdate = default; actionState->pressedInUpdate = default; actionState->releasedInUpdate = default; } @@ -1599,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. @@ -2133,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. @@ -2151,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; @@ -2165,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]; @@ -2185,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) @@ -2195,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) @@ -2221,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. @@ -2237,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; @@ -2293,7 +2301,9 @@ internal void ChangePhaseOfInteraction(InputActionPhase newPhase, ref TriggerSta /// /// New phase to transition to. /// Trigger that caused the change in phase. - /// + /// The phase to immediately transition to after + /// when that is or ( + /// by default). /// /// The change in phase is visible to observers, i.e. on the various callbacks and notifications. /// @@ -2330,7 +2340,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; } @@ -2356,7 +2367,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; @@ -2380,7 +2392,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"); @@ -2394,7 +2406,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 +2434,15 @@ private void ChangePhaseOfActionInternal(int actionIndex, TriggerState* actionSt newState.lastPerformedInUpdate = actionState->lastPerformedInUpdate; 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) + newState.lastCompletedInUpdate = InputUpdate.s_UpdateStepCount; + else + newState.lastCompletedInUpdate = actionState->lastCompletedInUpdate; + newState.pressedInUpdate = actionState->pressedInUpdate; newState.releasedInUpdate = actionState->releasedInUpdate; if (newPhase == InputActionPhase.Started) @@ -2597,7 +2618,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"); @@ -2616,7 +2637,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; } @@ -3568,7 +3591,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; @@ -3591,6 +3614,7 @@ public struct TriggerState [FieldOffset(36)] private uint m_LastCanceledInUpdate; [FieldOffset(40)] private uint m_PressedInUpdate; [FieldOffset(44)] private uint m_ReleasedInUpdate; + [FieldOffset(48)] private uint m_LastCompletedInUpdate; /// /// Phase being triggered by the control value change. @@ -3738,7 +3762,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 +3770,16 @@ public uint lastPerformedInUpdate set => m_LastPerformedInUpdate = 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. + /// + public uint lastCompletedInUpdate + { + get => m_LastCompletedInUpdate; + set => m_LastCompletedInUpdate = value; + } + public uint lastCanceledInUpdate { get => m_LastCanceledInUpdate;