Confusion with "Hold" Interaction "context.canceled" on Unity Input System 6.1

I’m having issues with the “Hold” interaction as I understand it from the documentation. I’m using the Unity Input System v6.1 and Editor version 2022.3.4f1.

My understanding with a “Hold” interaction on a “Button” mapped to a keyboard key will:

  • send context.started when the input is first held down;
  • send context.performed when/if the input is held down for the specified duration;
  • send context.canceled only when the input is released before reaching the duration.

However, that’s not the behavior I’m seeing, and I’m not sure why. Basically, once I hold the input past the threshold, it begins the registered action. But then if I release the input, it still triggers the cancel, even though it’s already triggered performed.

I did a bit of reading and can think of a few workarounds, but I don’t understand what I’m doing wrong, and I’d like not to create a hacky solution if it’s an easy fix. I’ll attach some relevant photos.

Code (truncated):

# InputReader.cs

    private Controls _controls;
    public Action onReleaseKnifeCanceled;
    public Action onReleaseKnifePerformed;

    // setup code...

    public void OnReleaseKnife(InputAction.CallbackContext context)
    {
        if (context.performed) onReleaseKnifePerformed?.Invoke();
        if (context.canceled) onReleaseKnifeCanceled?.Invoke();
    }
# Controller.cs

    private void OnEnable()
    {
        inputReader.onReleaseKnifeCanceled += ProcessDiscardKnife;
        inputReader.onReleaseKnifePerformed += ProcessReleaseKnife;
    }

Any wisdom would be greatly appreciated! Thanks in advance.

Yeah, this was changed during the preview phase of the Input System and it seems some of the documentation still hasn’t been updated.

e.g. the page on Interactions says this about hold’s canceled (emphasis mine):

Control magnitude goes back below pressPoint before duration (that is, the button was not held long enough).

but the documentation on HoldInteraction says this (emphasis mine):

The action is started when the control is pressed. If the control is released before the set duration, the action is canceled. As soon as the hold time is reached, the action performs. The action then stays performed until the control is released, at which point the action cancels.

For all the built-in interactions, I think you can assume they follow the default interaction, i.e. canceled is always triggered when the control is released, wether the action was previously performed or not.

This was also described in the changelog for 1.0.0-preview.5:

Likewise, the system will now always trigger InputAction.canceled before going back to waiting state. Like with InputAction.started, if this isn’t done explicitly, it will happen implicitly. This implies that InputAction.canceled no longer signifies an action getting aborted because it stopped after it started but before it performed. It now simply means “the action has ended” whether it actually got performed or not.

1 Like

Thanks for the response, that clears it up for me! Should’ve gone a little deeper into the docs.

Seems like a strange decision to me. I would think it would be useful to know on a context object whether the action being canceled has been performed or not. Is there any other data in context that contains that information? Or is the easiest solution just to check the context.duration value before I invoke the OnCanceled event?

Both can be useful, knowing whether an action was started but never got performed (what canceled was at the beginning) as well as getting a cleanup callback regardless of whether the action was performed or not (what canceled is now). I suspect they wanted to avoid adding another event and opted for the cleanup callback, since the former can be implemented by users but the latter cannot.

The most robust way would be to track the action’s phase in your code. You can e.g. reset a flag in started, set it in performed, and then check it in canceled, to find out whether performed was called or not.

Checking the duration can work as well (you can also get the HoldInteraction and its duration through CallbackContext.interaction) but this will couple your code to the specific setup of the action, which is a trade-off to consider.