Skip to content

Instantly share code, notes, and snippets.

@Invertex
Last active February 1, 2024 16:00
Show Gist options
  • Star 39 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save Invertex/db99b1b16ca53805ae02697b1a51ea77 to your computer and use it in GitHub Desktop.
Save Invertex/db99b1b16ca53805ae02697b1a51ea77 to your computer and use it in GitHub Desktop.
Unity New Input System custom Hold "Interaction" where the .performed callback is constantly triggered while input is held.
using UnityEngine;
using UnityEngine.InputSystem;
//!!>> This script should NOT be placed in an "Editor" folder. Ideally placed in a "Plugins" folder.
namespace Invertex.UnityInputExtensions.Interactions
{
//https://gist.github.com/Invertex
/// <summary>
/// Custom Hold interaction for New Input System.
/// With this, the .performed callback will be called everytime the Input System updates.
/// Allowing a purely callback based approach to a button hold instead of polling it in an Update() loop or creating specific logic for it
/// .started will be called when the 'pressPoint' threshold has been met and held for the 'duration' (unless 'Trigger .started on Press Point' is checked).
/// .performed will continue to be called each frame after `.started` has triggered (or every amount of time set for "Performed Interval")
/// .cancelled will be called when no-longer actuated (but only if the input has actually 'started' triggering
/// </summary>
#if UNITY_EDITOR
using UnityEditor;
//Allow for the interaction to be utilized outside of Play Mode and so that it will actually show up as an option in the Input Manager
[UnityEditor.InitializeOnLoad]
#endif
[UnityEngine.Scripting.Preserve, System.ComponentModel.DisplayName("Holding"), System.Serializable]
public class CustomHoldingInteraction : IInputInteraction
{
public float delayBetweenPerformed = 0f;
public bool triggerStartedOnPressPoint = false;
public bool useDefaultSettingsPressPoint = true;
public float pressPoint = InputSystem.settings.defaultButtonPressPoint;
public bool useDefaultSettingsDuration = true;
public float duration = InputSystem.settings.defaultHoldTime;
private float _heldTime = 0f;
private float pressPointOrDefault => useDefaultSettingsPressPoint || pressPoint <= 0 ? InputSystem.settings.defaultButtonPressPoint : pressPoint;
private float durationOrDefault => useDefaultSettingsDuration || duration < 0 ? InputSystem.settings.defaultHoldTime : duration;
private InputInteractionContext ctx;
private void OnUpdate()
{
var isActuated = ctx.ControlIsActuated(pressPointOrDefault);
var phase = ctx.phase;
//Cancel and cleanup our action if it's no-longer actuated or been externally changed to a stopped state.
if (phase == InputActionPhase.Canceled || phase == InputActionPhase.Disabled || !ctx.action.actionMap.enabled || !isActuated)
{
Cancel(ref ctx);
return;
}
_heldTime += Time.deltaTime;
bool holdDurationElapsed = _heldTime >= durationOrDefault;
if (!holdDurationElapsed && !triggerStartedOnPressPoint) { return; }
if (phase == InputActionPhase.Waiting){ ctx.Started(); return; }
if (!holdDurationElapsed) { return; }
if (phase == InputActionPhase.Started) { ctx.PerformedAndStayPerformed(); return; }
float heldMinusDelay = _heldTime - delayBetweenPerformed;
//Held time has exceed our minimum hold time, plus any delay we've set,
//so perform it and assign back the hold time without the delay time to let increment back up again
if (heldMinusDelay >= durationOrDefault)
{
_heldTime = heldMinusDelay;
ctx.PerformedAndStayPerformed();
}
}
public void Process(ref InputInteractionContext context)
{
ctx = context; //Ensure our Update always has access to the most recently updated context
if (!ctx.ControlIsActuated(pressPointOrDefault)) { Cancel(ref context); return; } //Actuation changed and thus no longer performed, cancel it all.
if (ctx.phase != InputActionPhase.Performed && ctx.phase != InputActionPhase.Started)
{
EnableInputHooks();
}
}
private void Cleanup()
{
DisableInputHooks();
_heldTime = 0f;
}
private void Cancel(ref InputInteractionContext context)
{
Cleanup();
if (context.phase == InputActionPhase.Performed || context.phase == InputActionPhase.Started)
{ //Input was being held when this call was made. Trigger the .cancelled event.
context.Canceled();
}
}
public void Reset() => Cleanup();
private void OnLayoutChange(string layoutName, InputControlLayoutChange change) => Reset();
private void OnDeviceChange(InputDevice device, InputDeviceChange change) => Reset();
#if UNITY_EDITOR
private void PlayModeStateChange(UnityEditor.PlayModeStateChange state) => Reset();
#endif
private void EnableInputHooks()
{
InputSystem.onAfterUpdate -= OnUpdate; //Safeguard for duplicate registrations
InputSystem.onAfterUpdate += OnUpdate;
//In case layout or device changes, we'll want to trigger a cancelling of the current input action subscription to avoid errors.
InputSystem.onLayoutChange -= OnLayoutChange;
InputSystem.onLayoutChange += OnLayoutChange;
InputSystem.onDeviceChange -= OnDeviceChange;
InputSystem.onDeviceChange += OnDeviceChange;
//Prevent the update hook from persisting across a play mode change to avoid errors.
#if UNITY_EDITOR
UnityEditor.EditorApplication.playModeStateChanged -= PlayModeStateChange;
UnityEditor.EditorApplication.playModeStateChanged += PlayModeStateChange;
#endif
}
private void DisableInputHooks()
{
InputSystem.onAfterUpdate -= OnUpdate;
InputSystem.onLayoutChange -= OnLayoutChange;
InputSystem.onDeviceChange -= OnDeviceChange;
#if UNITY_EDITOR
UnityEditor.EditorApplication.playModeStateChanged -= PlayModeStateChange;
#endif
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
static void RegisterInteraction()
{
if (InputSystem.TryGetInteraction("CustomHolding") == null)
{ //For some reason if this is called again when it already exists, it permanently removees it from the drop-down options... So have to check first
InputSystem.RegisterInteraction<CustomHoldingInteraction>("CustomHolding");
}
}
//Constructor will be called by our Editor [InitializeOnLoad] attribute when outside Play Mode
static CustomHoldingInteraction() => RegisterInteraction();
}
#if UNITY_EDITOR
internal class CustomHoldInteractionEditor : UnityEngine.InputSystem.Editor.InputParameterEditor<CustomHoldingInteraction>
{
private static GUIContent pressPointWarning, holdTimeWarning, pressPointLabel, holdTimeLabel, startedTriggerOnPPLabel, startedTriggerOnPPToggleLabel, delayPerformedLabel;
protected override void OnEnable()
{
delayPerformedLabel = new GUIContent("'Performed' interval (s)", $"Delay in seconds between each <b>.performed</b> call.{System.Environment.NewLine}" +
"At the default value of 0, it will be every frame.");
startedTriggerOnPPLabel = new GUIContent("Trigger 'Started' on Press Point", $"Trigger the <b>.started</b> event as soon as input actuated beyond \"Press Point\",{System.Environment.NewLine}" +
$"instead of waiting for the \"Min Hold Time\" as well.");
startedTriggerOnPPToggleLabel = new GUIContent("", startedTriggerOnPPLabel.tooltip);
pressPointLabel = new GUIContent("Press Point", $"The minimum amount this input's actuation value must exceed to be considered \"held\".{System.Environment.NewLine}" +
"Value less-than or equal to 0 will result in the 'Default Button Press Point' value being used from your 'Project Settings > Input System'.");
holdTimeLabel = new GUIContent("Min Hold Time", $"The minimum amount of realtime seconds before the input is considered \"held\".{System.Environment.NewLine}" +
"Value less-than or equal to 0 will result in the 'Default Hold Time' value being used from your 'Project Settings > Input System'.");
pressPointWarning = EditorGUIUtility.TrTextContent("Using \"Default Button Press Point\" set in project-wide input settings.");
holdTimeWarning = EditorGUIUtility.TrTextContent("Using \"Default Hold Time\" set in project-wide input settings.");
}
public override void OnGUI()
{
target.delayBetweenPerformed = EditorGUILayout.FloatField(delayPerformedLabel,target.delayBetweenPerformed, GUILayout.ExpandWidth(false));
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(startedTriggerOnPPLabel, GUILayout.Width(205f));
target.triggerStartedOnPressPoint = GUILayout.Toggle( target.triggerStartedOnPressPoint, startedTriggerOnPPToggleLabel, GUILayout.ExpandWidth(false));
EditorGUILayout.EndHorizontal();
DrawDisableIfDefault(ref target.pressPoint, ref target.useDefaultSettingsPressPoint, pressPointLabel, pressPointWarning);
DrawDisableIfDefault(ref target.duration, ref target.useDefaultSettingsDuration, holdTimeLabel, holdTimeWarning, -Mathf.Epsilon);
}
private void DrawDisableIfDefault(ref float value, ref bool useDefault, GUIContent fieldName, GUIContent warningText, float compareOffset = 0f)
{
EditorGUILayout.BeginHorizontal();
EditorGUI.BeginDisabledGroup(useDefault);
value = EditorGUILayout.FloatField(fieldName, value, GUILayout.ExpandWidth(false));
value = Mathf.Clamp(value, 0f, float.MaxValue);
EditorGUI.EndDisabledGroup();
GUIContent content =
EditorGUIUtility.TrTextContent("Default",
$"If enabled, the default {fieldName.text.ToLower()} " +
$"configured globally in the input settings is used.{System.Environment.NewLine}" +
"See Edit >> Project Settings... >> Input System Package.");
useDefault = GUILayout.Toggle(useDefault, content, GUILayout.ExpandWidth(false));
EditorGUILayout.EndHorizontal();
if (useDefault || value <= 0 + compareOffset)
{
EditorGUILayout.HelpBox(warningText);
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
GUIContent settingsLabel = EditorGUIUtility.TrTextContent("Open Input Settings");
if (GUILayout.Button(settingsLabel, EditorStyles.miniButton))
SettingsService.OpenProjectSettings("Project/Input System Package");
EditorGUILayout.EndHorizontal();
}
}
}
#endif
}
@pzolla
Copy link

pzolla commented Nov 22, 2022

Thanks!

@Oirionman
Copy link

Sorry for the wait everyone, been really busy. It should be working now, and the pressPoint and hold time should be reliable as well. I've also added custom inspector code to make it match the inspector behaviour of the official interactions.

Let me know if you find any bugs, cheers!

Thanks so much Invertex! I still cant understand how unity could have possibly missed such a basic feature in their input system. Haven't been able to find any bugs so far! I'll make sure to direct anyone else I see with the same problem to this github page (it took a very deep google search to find it myself).

@ObjectOrientedAss
Copy link

Hello! I'm here to give my two cents, in case anyone is in the same situation i was.
I was using a similar script to this one for a callback based approach with continuous key down situations, but the last Unity update broke everything and, luckily, i bumped into this.
Works like a charm, but, there's a but.

In our games, we are used to switch the "application context" based on the situation, which also triggers enabling/disabling action maps to start using the proper input of the new context.
For example, if i bind W to move with this custom hold interaction (hold and press time both 0), disable its action map, and enable another action map WHILE it is being pressed, it generates a stack overflow in the input system, because apparently the Cancel method gets called continously.

I have workarounded this with just a bool control variable (comment arrows pointing on the modified parts). You could also perform the check before calling Cancel if you want to:

private bool canceled; //<------------------------

private void OnUpdate()
    {
        var isActuated = ctx.ControlIsActuated(pressPointOrDefault);
        var phase = ctx.phase;

        _heldTime += Time.deltaTime;

        //Cancel and cleanup our action if it's no-longer actuated or been externally changed to a stopped state.
        if (phase == InputActionPhase.Canceled || phase == InputActionPhase.Disabled || !ctx.action.actionMap.enabled || (!isActuated && (phase == InputActionPhase.Performed || phase == InputActionPhase.Started)))
        {
            Cancel(ref ctx);
            return;
        }

        if (_heldTime < durationOrDefault) { return; }  //Don't do anything yet, hold time not exceeded
        canceled = false; //<-----------------------------

        //We've held for long enough, start triggering the Performed state.
        if (phase == InputActionPhase.Performed || phase == InputActionPhase.Started)
            ctx.PerformedAndStayPerformed();
    }

    private void Cancel(ref InputInteractionContext context)
    {
        if (!canceled) //<-----------------------
        {
            canceled = true; //<-------------------------
            InputSystem.onAfterUpdate -= OnUpdate;
            _heldTime = 0f;

            if (context.phase != InputActionPhase.Canceled)
                context.Canceled();
        }
    }

Thanks again!

@robE127
Copy link

robE127 commented Feb 11, 2023

I was also trying to figure out a way to have something continuously trigger while a button was pressed using the new input system. This solution I've come up with is simple and also seems to work well.

From my script on the player object:

public class Player : MonoBehaviour
{
    public float SpeedLinear = 1f;
    public float SpeedAngular = 1f;
    public InputActionAsset actionAsset;

    private Rigidbody2D _rigidbody;

    private void Awake()
    {
        _rigidbody = GetComponent<Rigidbody2D>();
    }

    private void FixedUpdate()
    {
        var v = actionAsset.FindAction("Move").ReadValue<Vector2>();
        _rigidbody.AddForce(transform.up * v.y * SpeedLinear);
        _rigidbody.AddTorque(v.x * SpeedAngular);
    }

Then I just drag my InputActionAsset into the slot on my script in the UI to pass the reference and everything works!

@Invertex
Copy link
Author

Invertex commented Feb 11, 2023

@robE127
Yes this script isn't really for that use case, but for people using an event-based approach where you simply subscribe your methods to the .started .performed and .cancelled events of a given input action.

This has several advantages, the primary being that logic can be cleanly separated from inputs. And also that if Input is disabled/disconnected, Input-related actions are automatically no longer checked or performed without you having to put conditions everywhere.
You can also utilize Input messages to send to scripts automatically without the script needing to have reference to a specific input action.

And if you want to do input rebinds, it's simpler given that the input logic is better separated.

@jagru20
Copy link

jagru20 commented Feb 15, 2023

I like this script much, but recently I get apparently random errors in Unity Editor mode, mostly after quitting the play mode:

Map index out of range
UnityEngine.InputSystem.InputInteractionContext:get_action ()
Invertex.UnityInputExtensions.Interactions.CustomHoldingInteraction:OnUpdate () (at Assets/Plugins/Custom Hold.cs:49)
UnityEngine.InputSystem.LowLevel.NativeInputRuntime/<>c__DisplayClass7_0:<set_onUpdate>b__0 (UnityEngineInternal.Input.NativeInputUpdateType,UnityEngineInternal.Input.NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate (UnityEngineInternal.Input.NativeInputUpdateType,intptr)

Binding index out of range
UnityEngine.InputSystem.InputInteractionContext:get_action ()
Invertex.UnityInputExtensions.Interactions.CustomHoldingInteraction:OnUpdate () (at Assets/Plugins/Custom Hold.cs:49)
UnityEngine.InputSystem.LowLevel.NativeInputRuntime/<>c__DisplayClass7_0:<set_onUpdate>(UnityEngineInternal.Input.NativeInputUpdateType,UnityEngineInternal.Input.NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate (UnityEngineInternal.Input.NativeInputUpdateType,intptr)

NullReferenceException: Object reference not set to an instance of an object
UnityEngine.InputSystem.InputActionState+BindingState.get_actionIndex () (at Library/PackageCache/com.unity.inputsystem@1.4.4/InputSystem/Actions/InputActionState.cs:3392)
UnityEngine.InputSystem.InputActionState.GetActionOrNull (UnityEngine.InputSystem.InputActionState+TriggerState& trigger) (at Library/PackageCache/com.unity.inputsystem@1.4.4/InputSystem/Actions/InputActionState.cs:2536)
UnityEngine.InputSystem.InputInteractionContext.get_action () (at Library/PackageCache/com.unity.inputsystem@1.4.4/InputSystem/Actions/InputInteractionContext.cs:20)
Invertex.UnityInputExtensions.Interactions.CustomHoldingInteraction.OnUpdate () (at Assets/Plugins/Custom Hold.cs:49)
UnityEngine.InputSystem.Utilities.DelegateHelpers.InvokeCallbacksSafe (UnityEngine.InputSystem.Utilities.CallbackArray`1[System.Action]& callbacks, System.String callbackName, System.Object context) (at Library/PackageCache/com.unity.inputsystem@1.4.4/InputSystem/Utilities/DelegateHelpers.cs:21)
UnityEngine.InputSystem.LowLevel.<>c__DisplayClass7_0:<set_onUpdate>b__0(NativeInputUpdateType, NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate(NativeInputUpdateType, IntPtr)

NullReferenceException while executing 'InputSystem.onAfterUpdate' callbacks
UnityEngine.InputSystem.LowLevel.NativeInputRuntime/<>c__DisplayClass7_0:<set_onUpdate>b__0 (UnityEngineInternal.Input.NativeInputUpdateType,UnityEngineInternal.Input.NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate (UnityEngineInternal.Input.NativeInputUpdateType,intptr)

I do not really understand the problem, do you see it?

Edit: The above errors get thrown continuously, until I either restart the play mode or change a script, which triggers recompilation

@ssnd292
Copy link

ssnd292 commented Apr 2, 2023

Hey - I also have an issue:

NullReferenceException: Object reference not set to an instance of an object
Invertex.UnityInputExtensions.Interactions.CustomHoldingInteraction.OnUpdate () (at Assets/Scripts/InputHandler/CustomHoldingInteraction.cs:49)
UnityEngine.InputSystem.Utilities.DelegateHelpers.InvokeCallbacksSafe (UnityEngine.InputSystem.Utilities.CallbackArray`1[System.Action]& callbacks, System.String callbackName, System.Object context) (at ./Library/PackageCache/com.unity.inputsystem@1.5.1/InputSystem/Utilities/DelegateHelpers.cs:21)
UnityEngine.InputSystem.LowLevel.<>c__DisplayClass7_0:<set_onUpdate>b__0(NativeInputUpdateType, NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate(NativeInputUpdateType, IntPtr)
NullReferenceException while executing 'InputSystem.onAfterUpdate' callbacks
UnityEngine.InputSystem.LowLevel.NativeInputRuntime/<>c__DisplayClass7_0:<set_onUpdate>b__0 (UnityEngineInternal.Input.NativeInputUpdateType,UnityEngineInternal.Input.NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate (UnityEngineInternal.Input.NativeInputUpdateType,intptr)

@Invertex
Copy link
Author

Invertex commented Apr 9, 2023

Hey - I also have an issue:

NullReferenceException: Object reference not set to an instance of an object
Invertex.UnityInputExtensions.Interactions.CustomHoldingInteraction.OnUpdate () (at Assets/Scripts/InputHandler/CustomHoldingInteraction.cs:49)
UnityEngine.InputSystem.Utilities.DelegateHelpers.InvokeCallbacksSafe (UnityEngine.InputSystem.Utilities.CallbackArray`1[System.Action]& callbacks, System.String callbackName, System.Object context) (at ./Library/PackageCache/com.unity.inputsystem@1.5.1/InputSystem/Utilities/DelegateHelpers.cs:21)
UnityEngine.InputSystem.LowLevel.<>c__DisplayClass7_0:<set_onUpdate>b__0(NativeInputUpdateType, NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate(NativeInputUpdateType, IntPtr)
NullReferenceException while executing 'InputSystem.onAfterUpdate' callbacks
UnityEngine.InputSystem.LowLevel.NativeInputRuntime/<>c__DisplayClass7_0:<set_onUpdate>b__0 (UnityEngineInternal.Input.NativeInputUpdateType,UnityEngineInternal.Input.NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate (UnityEngineInternal.Input.NativeInputUpdateType,intptr)

I like this script much, but recently I get apparently random errors in Unity Editor mode, mostly after quitting the play mode:

Map index out of range
UnityEngine.InputSystem.InputInteractionContext:get_action ()
Invertex.UnityInputExtensions.Interactions.CustomHoldingInteraction:OnUpdate () (at Assets/Plugins/Custom Hold.cs:49)
UnityEngine.InputSystem.LowLevel.NativeInputRuntime/<>c__DisplayClass7_0:<set_onUpdate>b__0 (UnityEngineInternal.Input.NativeInputUpdateType,UnityEngineInternal.Input.NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate (UnityEngineInternal.Input.NativeInputUpdateType,intptr)

Binding index out of range
UnityEngine.InputSystem.InputInteractionContext:get_action ()
Invertex.UnityInputExtensions.Interactions.CustomHoldingInteraction:OnUpdate () (at Assets/Plugins/Custom Hold.cs:49)
UnityEngine.InputSystem.LowLevel.NativeInputRuntime/<>c__DisplayClass7_0:<set_onUpdate>(UnityEngineInternal.Input.NativeInputUpdateType,UnityEngineInternal.Input.NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate (UnityEngineInternal.Input.NativeInputUpdateType,intptr)

NullReferenceException: Object reference not set to an instance of an object
UnityEngine.InputSystem.InputActionState+BindingState.get_actionIndex () (at Library/PackageCache/com.unity.inputsystem@1.4.4/InputSystem/Actions/InputActionState.cs:3392)
UnityEngine.InputSystem.InputActionState.GetActionOrNull (UnityEngine.InputSystem.InputActionState+TriggerState& trigger) (at Library/PackageCache/com.unity.inputsystem@1.4.4/InputSystem/Actions/InputActionState.cs:2536)
UnityEngine.InputSystem.InputInteractionContext.get_action () (at Library/PackageCache/com.unity.inputsystem@1.4.4/InputSystem/Actions/InputInteractionContext.cs:20)
Invertex.UnityInputExtensions.Interactions.CustomHoldingInteraction.OnUpdate () (at Assets/Plugins/Custom Hold.cs:49)
UnityEngine.InputSystem.Utilities.DelegateHelpers.InvokeCallbacksSafe (UnityEngine.InputSystem.Utilities.CallbackArray`1[System.Action]& callbacks, System.String callbackName, System.Object context) (at Library/PackageCache/com.unity.inputsystem@1.4.4/InputSystem/Utilities/DelegateHelpers.cs:21)
UnityEngine.InputSystem.LowLevel.<>c__DisplayClass7_0:<set_onUpdate>b__0(NativeInputUpdateType, NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate(NativeInputUpdateType, IntPtr)

NullReferenceException while executing 'InputSystem.onAfterUpdate' callbacks
UnityEngine.InputSystem.LowLevel.NativeInputRuntime/<>c__DisplayClass7_0:<set_onUpdate>b__0 (UnityEngineInternal.Input.NativeInputUpdateType,UnityEngineInternal.Input.NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate (UnityEngineInternal.Input.NativeInputUpdateType,intptr)

I do not really understand the problem, do you see it?
Edit: The above errors get thrown continuously, until I either restart the play mode or change a script, which triggers recompilation

try using that fork: https://gist.github.com/aaronseng/63cf5ea706eb56e39015ebd13cbde5fa

as ChatGPT said: image

This is completely wrong. Please do not rely on ChatGPT, it pretends to know even when it doesn't, it's not a conscious being, it's just following simple patterns, which lead it down paths that are incorrect more often than not when it comes to exact things like this.
This class does not derive from another class for there to be anything to "override" for Reset(), and the InputSystem does not expect a class constructor to feed information into. There also aren't those variables to assign values to, nor a State() class. Everything about that is wrong.

@Invertex
Copy link
Author

Invertex commented Apr 9, 2023

I like this script much, but recently I get apparently random errors in Unity Editor mode, mostly after quitting the play mode:

Map index out of range
UnityEngine.InputSystem.InputInteractionContext:get_action ()
Invertex.UnityInputExtensions.Interactions.CustomHoldingInteraction:OnUpdate () (at Assets/Plugins/Custom Hold.cs:49)
UnityEngine.InputSystem.LowLevel.NativeInputRuntime/<>c__DisplayClass7_0:<set_onUpdate>b__0 (UnityEngineInternal.Input.NativeInputUpdateType,UnityEngineInternal.Input.NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate (UnityEngineInternal.Input.NativeInputUpdateType,intptr)

Binding index out of range
UnityEngine.InputSystem.InputInteractionContext:get_action ()
Invertex.UnityInputExtensions.Interactions.CustomHoldingInteraction:OnUpdate () (at Assets/Plugins/Custom Hold.cs:49)
UnityEngine.InputSystem.LowLevel.NativeInputRuntime/<>c__DisplayClass7_0:<set_onUpdate>(UnityEngineInternal.Input.NativeInputUpdateType,UnityEngineInternal.Input.NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate (UnityEngineInternal.Input.NativeInputUpdateType,intptr)

NullReferenceException: Object reference not set to an instance of an object
UnityEngine.InputSystem.InputActionState+BindingState.get_actionIndex () (at Library/PackageCache/com.unity.inputsystem@1.4.4/InputSystem/Actions/InputActionState.cs:3392)
UnityEngine.InputSystem.InputActionState.GetActionOrNull (UnityEngine.InputSystem.InputActionState+TriggerState& trigger) (at Library/PackageCache/com.unity.inputsystem@1.4.4/InputSystem/Actions/InputActionState.cs:2536)
UnityEngine.InputSystem.InputInteractionContext.get_action () (at Library/PackageCache/com.unity.inputsystem@1.4.4/InputSystem/Actions/InputInteractionContext.cs:20)
Invertex.UnityInputExtensions.Interactions.CustomHoldingInteraction.OnUpdate () (at Assets/Plugins/Custom Hold.cs:49)
UnityEngine.InputSystem.Utilities.DelegateHelpers.InvokeCallbacksSafe (UnityEngine.InputSystem.Utilities.CallbackArray`1[System.Action]& callbacks, System.String callbackName, System.Object context) (at Library/PackageCache/com.unity.inputsystem@1.4.4/InputSystem/Utilities/DelegateHelpers.cs:21)
UnityEngine.InputSystem.LowLevel.<>c__DisplayClass7_0:<set_onUpdate>b__0(NativeInputUpdateType, NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate(NativeInputUpdateType, IntPtr)

NullReferenceException while executing 'InputSystem.onAfterUpdate' callbacks
UnityEngine.InputSystem.LowLevel.NativeInputRuntime/<>c__DisplayClass7_0:<set_onUpdate>b__0 (UnityEngineInternal.Input.NativeInputUpdateType,UnityEngineInternal.Input.NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate (UnityEngineInternal.Input.NativeInputUpdateType,intptr)

I do not really understand the problem, do you see it?

Edit: The above errors get thrown continuously, until I either restart the play mode or change a script, which triggers recompilation

Hey - I also have an issue:

NullReferenceException: Object reference not set to an instance of an object
Invertex.UnityInputExtensions.Interactions.CustomHoldingInteraction.OnUpdate () (at Assets/Scripts/InputHandler/CustomHoldingInteraction.cs:49)
UnityEngine.InputSystem.Utilities.DelegateHelpers.InvokeCallbacksSafe (UnityEngine.InputSystem.Utilities.CallbackArray`1[System.Action]& callbacks, System.String callbackName, System.Object context) (at ./Library/PackageCache/com.unity.inputsystem@1.5.1/InputSystem/Utilities/DelegateHelpers.cs:21)
UnityEngine.InputSystem.LowLevel.<>c__DisplayClass7_0:<set_onUpdate>b__0(NativeInputUpdateType, NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate(NativeInputUpdateType, IntPtr)
NullReferenceException while executing 'InputSystem.onAfterUpdate' callbacks
UnityEngine.InputSystem.LowLevel.NativeInputRuntime/<>c__DisplayClass7_0:<set_onUpdate>b__0 (UnityEngineInternal.Input.NativeInputUpdateType,UnityEngineInternal.Input.NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate (UnityEngineInternal.Input.NativeInputUpdateType,intptr)

I believe I've fixed these issues in the latest update and made the code design more logical.
The behaviour of this input interaction now also makes more sense. "started" will now only be called once you've actually held the input for the required duration, as it doesn't make sense to have that event get triggered before it's considered "held". Before it would simply always be triggered once the actuation value was exceeded.
The "cancelled" event also will avoid being called unless the Action was actually in the performing/started state, not simply actuated.

@Zarpyk
Copy link

Zarpyk commented Apr 15, 2023

I think you can change this to make the GUI the same as the Unity GUI (Tested on InputSystem 1.4.4/Unity 2021.3)

        public bool useDefaultSettingsPressPoint = true;
        public float pressPoint = InputSystem.settings.defaultButtonPressPoint;

        public bool useDefaultSettingsDuration = true;
        public float duration = InputSystem.settings.defaultHoldTime;
        private void DrawDisableIfDefault(ref float value, ref bool useDefault, GUIContent fieldName,
                                          GUIContent warningText) {
            EditorGUILayout.BeginHorizontal();

            EditorGUI.BeginDisabledGroup(useDefault);
            value = EditorGUILayout.FloatField(fieldName, value, GUILayout.ExpandWidth(false));
            EditorGUI.EndDisabledGroup();

            GUIContent content =
                EditorGUIUtility.TrTextContent("Default",
                                               $"If enabled, the default {fieldName.text.ToLower()} " +
                                               "configured globally in the input settings is used. " +
                                               "See Edit >> Project Settings... >> Input (NEW).");
            useDefault = GUILayout.Toggle(useDefault, content, GUILayout.ExpandWidth(false));
            EditorGUILayout.EndHorizontal();

            if (useDefault || value <= 0) {
                EditorGUILayout.HelpBox(warningText);
                EditorGUILayout.BeginHorizontal();
                GUILayout.FlexibleSpace();
                GUIContent settingsLabel = EditorGUIUtility.TrTextContent("Open Input Settings");
                if (GUILayout.Button(settingsLabel, EditorStyles.miniButton))
                    SettingsService.OpenProjectSettings("Project/Input System Package");
                EditorGUILayout.EndHorizontal();
            }
        }

image

@Invertex
Copy link
Author

Invertex commented Apr 15, 2023

@Zarpyk
Mine already does this if you look at the bottom of the code. Thanks though!

@Zarpyk
Copy link

Zarpyk commented Apr 15, 2023

@Zarpyk Mine already does this if you look at the bottom of the code. Thanks though!

Yes, I know, my code is modified from your code, but it's a bit different compared to the Unity GUI.
image

@ssnd292
Copy link

ssnd292 commented Apr 15, 2023

@Invertex Unfortunately it still doesnt work:

NullReferenceException: Object reference not set to an instance of an object
Invertex.UnityInputExtensions.Interactions.CustomHoldingInteraction.OnUpdate () (at Assets/Scripts/InputHandler/CustomHoldingInteraction.cs:42)
UnityEngine.InputSystem.Utilities.DelegateHelpers.InvokeCallbacksSafe (UnityEngine.InputSystem.Utilities.CallbackArray`1[System.Action]& callbacks, System.String callbackName, System.Object context) (at ./Library/PackageCache/com.unity.inputsystem@1.5.1/InputSystem/Utilities/DelegateHelpers.cs:21)
UnityEngine.InputSystem.LowLevel.<>c__DisplayClass7_0:<set_onUpdate>b__0(NativeInputUpdateType, NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate(NativeInputUpdateType, IntPtr)

NullReferenceException while executing 'InputSystem.onAfterUpdate' callbacks
UnityEngine.InputSystem.LowLevel.NativeInputRuntime/<>c__DisplayClass7_0:<set_onUpdate>b__0 (UnityEngineInternal.Input.NativeInputUpdateType,UnityEngineInternal.Input.NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate (UnityEngineInternal.Input.NativeInputUpdateType,intptr)

@Invertex
Copy link
Author

@Zarpyk Mine already does this if you look at the bottom of the code. Thanks though!

Yes, I know, my code is modified from your code, but it's a bit different compared to the Unity GUI. image

Ahh I see thanks, I'll look into adding that.

@Invertex Unfortunately it still doesnt work:

NullReferenceException: Object reference not set to an instance of an object
Invertex.UnityInputExtensions.Interactions.CustomHoldingInteraction.OnUpdate () (at Assets/Scripts/InputHandler/CustomHoldingInteraction.cs:42)
UnityEngine.InputSystem.Utilities.DelegateHelpers.InvokeCallbacksSafe (UnityEngine.InputSystem.Utilities.CallbackArray`1[System.Action]& callbacks, System.String callbackName, System.Object context) (at ./Library/PackageCache/com.unity.inputsystem@1.5.1/InputSystem/Utilities/DelegateHelpers.cs:21)
UnityEngine.InputSystem.LowLevel.<>c__DisplayClass7_0:<set_onUpdate>b__0(NativeInputUpdateType, NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate(NativeInputUpdateType, IntPtr)
NullReferenceException while executing 'InputSystem.onAfterUpdate' callbacks
UnityEngine.InputSystem.LowLevel.NativeInputRuntime/<>c__DisplayClass7_0:<set_onUpdate>b__0 (UnityEngineInternal.Input.NativeInputUpdateType,UnityEngineInternal.Input.NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate (UnityEngineInternal.Input.NativeInputUpdateType,intptr)

Could you describe the scenario where this exception happens? Also what Unity and Input system version?

Thanks

@SenshiSentou
Copy link

A quick restart of the editor got rid of any warnings for me, but even with the Min Hold Time set to 0, there is an initial delay (±0.1 seconds) before the InputActionPhase.Started triggers. Am I missing something?

@Invertex
Copy link
Author

A quick restart of the editor got rid of any warnings for me, but even with the Min Hold Time set to 0, there is an initial delay (±0.1 seconds) before the InputActionPhase.Started triggers. Am I missing something?

This is because at 0, it uses your Default hold time in your project settings. It shows a little message below the value when you type in 0. This is how Unity's other interactions work too... But it is kind of a weird approach. So I've edited it now to use 0 if you want to instead.

@Invertex
Copy link
Author

Invertex commented Jun 7, 2023

@SenshiSentou was the issue resolved?

@klootas
Copy link

klootas commented Jun 8, 2023

Thanks for this. I'm confused though....

How does one configure the input binding to activate both when the button is pressed normally AND after it has been held for a while?

Basically I'm trying to make it behave just like pressing any key in a text editor would: Pressing the button once should trigger one performed (or started), and holding it should triggers several more performed.

Or do I need to register two separate inputs - one for the first press and another for the hold?
I tried placing two bindings under the same input without any luck.

Thanks!

@Invertex
Copy link
Author

Invertex commented Jun 9, 2023

@klootas That wasn't an intended use case for this really, but since you've brought it up, I've added it to this as a toggle.
When checked, .started will be triggered when the Press Point is met, regardless of duration. And then the .performed will still only be performed once it has been held for the duration.

@klootas
Copy link

klootas commented Jun 9, 2023

Wow, thanks a bunch for that - works great!

I can't leave without dropping one last suggestion: To fit my use-case perfectly, it would be awesome if it also had a throttle value (some way to specify how "often" the input would be called during hold). A throttle value of 0.5 could cause perform to be called twice every held second. Or just an int specifying how many calls to ignore between each call to perform...

@Invertex
Copy link
Author

Invertex commented Jun 9, 2023

@klootas Updated, give it a try :)

@klootas
Copy link

klootas commented Jun 10, 2023

@Invertex Perfect!

It's folks like you that make Unity's Swiss cheese taste full👌

@SenshiSentou
Copy link

@SenshiSentou was the issue resolved?

Sorry, I hadn't gotten around to this till now. This seems to work perfectly, yes! Thank you so much! :)

@SenshiSentou
Copy link

Hi, me again. Found another bug, and I haven't been able to solve it yet.

If the PlayerInput component sending the custom holding event is either disabled or deactivated while the interaction is taking place (the button is held down), Unity exits. No warnings, no errors, no nothing.

Looking through the code it seems you're trying to safeguard against this already, but something's still awry for sure. If I manage to solve it I'll follow-up here

@SenshiSentou
Copy link

Update: After disabling stack traces I found it was a stack overflow going on. I've uploaded a fix here: https://gist.github.com/SenshiSentou/384c5ca22282544cb630d24719301f4e

The gist of it is there is an infinite recursion going on between calling Reset(), which calls Cancel(), which calls context.Canceled(), which triggers another Reset(), which repeats the cycle. The quick and dirty fix I implemented adds an _isCanceled flag to prevent this infinite recursion.

There is a nicer way to do this however. I noticed Cancel()'s ref context parameter to be unnecessary, as it can just access ctx already. I merged it into Reset() in my gist, but there should probably be a proper separation between cancelling the event, and resetting internal state (_heldTime and the delegates). If I end up refactoring it later I'll post here again :)

@Invertex
Copy link
Author

Thanks for the heads up and figuring out the issue!

I've taken a bit simpler approach to fixing it by separating the cleanup logic from the Cancel and Reset and not making Reset() call Cancel(), since it should already be implied that it is entering the Canceled state if Reset() is being called.

Hopefully it's fully solved :)

@fralvarezz
Copy link

I have found that sometimes, when I was messing with multiple input devices at once, and switching back and forth from one to another, the _heldTime variable was not reset. I have dug a bit into it, and it looks like _isCanceled flag was not being correctly reset. I am not an expert, but the only place I can find that _isCanceled is being reset to false is on the Reset() function, but that one has an initial exit clause that breaks out of the function if _isCanceled == true. I have added a reset such as:

if(_isCanceled) { _isCanceled = false; return; }

and this seems to do the trick. Am I missing out on something, any reason this shouldn't be added?

@Invertex
Copy link
Author

Invertex commented Dec 4, 2023

@fralvarezz Thanks for the comment! You seem to be using an outdated version of the script though. The version in this post (July 11th) doesn't use an _isCanceled bool to track that, but relies more on Unity's internal systems instead. Try out the current version and let me know if you still have the issue!

@fralvarezz
Copy link

Oh, I'm very sorry, I am not sure how I missed that! I have pulled the latest version, and it seems to be a bit more consistent on my end, although I have managed to trigger a very similar scenario. Actuating and releasing two input devices at a time linked to the same action can lead to a weird state, where not matter what you do or how long you press a given input, the state of the InputActionContext.phase will be "Waiting". Using another input device once will cancel out this behavior and everything will start working great again.

I am aware that this is an edge case scenario and most games don't even allow having multiple devices for the same input action at once, so I'm not expecting anybody to fix it, although it's probably good to know. I'll be looking a bit more into this myself, too, and will try to reproduce it (and hopefully find a solution!) in an empty project that's less cluttered than my game :)
For reference too, I'm on Unity 2020.3.48, and Input System 1.7.0

@AdmiralEgg
Copy link

Great plug-in, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment