-
-
Save Invertex/db99b1b16ca53805ae02697b1a51ea77 to your computer and use it in GitHub Desktop.
| 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; | |
| using UnityEngine.UIElements; | |
| //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 | |
| [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(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 | |
| EditorApplication.playModeStateChanged -= PlayModeStateChange; | |
| EditorApplication.playModeStateChanged += PlayModeStateChange; | |
| #endif | |
| } | |
| private void DisableInputHooks() | |
| { | |
| InputSystem.onAfterUpdate -= OnUpdate; | |
| InputSystem.onLayoutChange -= OnLayoutChange; | |
| InputSystem.onDeviceChange -= OnDeviceChange; | |
| #if UNITY_EDITOR | |
| 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; | |
| #if UNITY_2021_3_OR_NEWER | |
| public override void OnDrawVisualElements(VisualElement root, System.Action onChangedCallback) | |
| { | |
| var container = new VisualElement(); | |
| var settingsContainer = new VisualElement { style = { flexDirection = FlexDirection.Column, marginRight = 6 } }; | |
| var delayBetweenPerformedFloat = new FloatField(delayPerformedLabel.text) | |
| { | |
| tooltip = delayPerformedLabel.tooltip, | |
| value = target.delayBetweenPerformed | |
| }; | |
| var startTriggerOnPressPointToggle = new Toggle(startedTriggerOnPPLabel.text) | |
| { | |
| tooltip = startedTriggerOnPPLabel.tooltip, | |
| value = target.triggerStartedOnPressPoint | |
| }; | |
| //// PRESS POINT SETTING | |
| var pressPointContainer = new VisualElement { style = { flexDirection = FlexDirection.Row } }; | |
| var pressPointFloat = new FloatField(pressPointLabel.text) | |
| { | |
| tooltip = pressPointLabel.tooltip, | |
| value = target.pressPoint | |
| }; | |
| var pressPointUseDefaultToggle = new Toggle("Default") { value = target.useDefaultSettingsPressPoint }; | |
| var usingDefaultPressPointTip = new HelpBox(pressPointWarning.text, HelpBoxMessageType.None); | |
| var openSettingsBtnPressPoint = new UnityEngine.UIElements.Button(() => { SettingsService.OpenProjectSettings("Project/Input System Package"); }) { text = "Open Input Settings" }; | |
| pressPointFloat.style.flexShrink = 0.2f; | |
| pressPointFloat.style.flexBasis = new StyleLength(200); | |
| pressPointFloat.SetEnabled(!target.useDefaultSettingsPressPoint); | |
| pressPointUseDefaultToggle.style.flexDirection = FlexDirection.RowReverse; | |
| usingDefaultPressPointTip.style.display = target.useDefaultSettingsPressPoint ? DisplayStyle.Flex : DisplayStyle.None; | |
| openSettingsBtnPressPoint.style.display = target.useDefaultSettingsPressPoint ? DisplayStyle.Flex : DisplayStyle.None; | |
| openSettingsBtnPressPoint.style.alignSelf = Align.FlexEnd; | |
| ///// Hold Duration Setting | |
| var holdDurationContainer = new VisualElement { style = { flexDirection = FlexDirection.Row } }; | |
| var holdDurationMinFloat = new FloatField(holdTimeLabel.text) | |
| { | |
| tooltip = holdTimeLabel.tooltip, | |
| value = target.duration | |
| }; | |
| var holdDurationDefaultToggle = new Toggle("Default") { value = target.useDefaultSettingsDuration }; | |
| var usingDefaultHoldDurationTip = new HelpBox(holdTimeWarning.text, HelpBoxMessageType.None); | |
| var openSettingsBtnHoldDuration = new UnityEngine.UIElements.Button(() => { SettingsService.OpenProjectSettings("Project/Input System Package"); }) { text = "Open Input Settings" }; | |
| holdDurationMinFloat.style.flexShrink = 0.2f; | |
| holdDurationMinFloat.style.flexBasis = new StyleLength(200); | |
| holdDurationMinFloat?.SetEnabled(!target.useDefaultSettingsDuration); | |
| holdDurationDefaultToggle.style.flexDirection = FlexDirection.RowReverse; | |
| usingDefaultHoldDurationTip.style.display = target.useDefaultSettingsDuration ? DisplayStyle.Flex : DisplayStyle.None; | |
| openSettingsBtnHoldDuration.style.display = target.useDefaultSettingsDuration ? DisplayStyle.Flex : DisplayStyle.None; | |
| openSettingsBtnHoldDuration.style.alignSelf = Align.FlexEnd; | |
| // Setup Events | |
| delayBetweenPerformedFloat.RegisterValueChangedCallback(evt => { target.delayBetweenPerformed = evt.newValue; }); | |
| delayBetweenPerformedFloat.RegisterCallback<BlurEvent>(_ => { onChangedCallback?.Invoke(); }); | |
| startTriggerOnPressPointToggle.RegisterValueChangedCallback(evt => { | |
| target.triggerStartedOnPressPoint = evt.newValue; | |
| onChangedCallback(); | |
| }); | |
| pressPointFloat.RegisterValueChangedCallback(evt => { | |
| target.pressPoint = evt.newValue; | |
| bool isDefault = evt.newValue < Mathf.Epsilon; | |
| usingDefaultPressPointTip.style.display = isDefault || target.useDefaultSettingsPressPoint ? DisplayStyle.Flex : DisplayStyle.None; | |
| }); | |
| // Trigger onChangedCallback here to avoid it happening with each typed digit and causing stalls | |
| pressPointFloat.RegisterCallback<BlurEvent>(_ => { onChangedCallback?.Invoke(); }); | |
| holdDurationMinFloat.RegisterValueChangedCallback(evt => { | |
| target.duration = evt.newValue; | |
| bool isDefault = evt.newValue <= Mathf.Epsilon; | |
| usingDefaultHoldDurationTip.style.display = isDefault || target.useDefaultSettingsDuration ? DisplayStyle.Flex : DisplayStyle.None; | |
| }); | |
| holdDurationMinFloat.RegisterCallback<BlurEvent>(_ => { onChangedCallback?.Invoke(); }); | |
| pressPointUseDefaultToggle.RegisterValueChangedCallback((evt) => { | |
| target.useDefaultSettingsPressPoint = evt.newValue; | |
| pressPointFloat?.SetEnabled(!evt.newValue); | |
| usingDefaultPressPointTip.style.display = evt.newValue ? DisplayStyle.Flex : DisplayStyle.None; | |
| openSettingsBtnPressPoint.style.display = evt.newValue ? DisplayStyle.Flex : DisplayStyle.None; | |
| onChangedCallback(); | |
| }); | |
| holdDurationDefaultToggle.RegisterValueChangedCallback((evt) => { | |
| target.useDefaultSettingsDuration = evt.newValue; | |
| holdDurationMinFloat?.SetEnabled(!evt.newValue); | |
| usingDefaultHoldDurationTip.style.display = evt.newValue ? DisplayStyle.Flex : DisplayStyle.None; | |
| openSettingsBtnHoldDuration.style.display = evt.newValue ? DisplayStyle.Flex : DisplayStyle.None; | |
| onChangedCallback(); | |
| }); | |
| usingDefaultPressPointTip.style.display = target.pressPoint <= Mathf.Epsilon || target.useDefaultSettingsPressPoint ? DisplayStyle.Flex : DisplayStyle.None; | |
| usingDefaultHoldDurationTip.style.display = target.duration <= Mathf.Epsilon || target.useDefaultSettingsDuration ? DisplayStyle.Flex : DisplayStyle.None; | |
| // Begin adding elems | |
| settingsContainer.Add(delayBetweenPerformedFloat); | |
| settingsContainer.Add(startTriggerOnPressPointToggle); | |
| // Press Point Elems | |
| pressPointContainer.Add(pressPointFloat); | |
| pressPointContainer.Add(pressPointUseDefaultToggle); | |
| settingsContainer.Add(pressPointContainer); | |
| settingsContainer.Add(usingDefaultPressPointTip); | |
| settingsContainer.Add(openSettingsBtnPressPoint); | |
| // Hold Duration Elems | |
| holdDurationContainer.Add(holdDurationMinFloat); | |
| holdDurationContainer.Add(holdDurationDefaultToggle); | |
| settingsContainer.Add(holdDurationContainer); | |
| settingsContainer.Add(usingDefaultHoldDurationTip); | |
| settingsContainer.Add(openSettingsBtnHoldDuration); | |
| container.Add(settingsContainer); | |
| root.Add(container); | |
| } | |
| public override void OnGUI() { } | |
| #else | |
| 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 | |
| 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."); | |
| } | |
| } | |
| } | |
| #endif |
Ok, it is what I thought (the deprecation) but regarding the doc I was not sure. Perhaps I didn't check the right doc/version.
Thanks I will try it latter today (I try to debug it/fixed/proposed something if I found issue).
it is working. As you said I have to restart Unity first after adding the custom interaction.
Thank you again.
I should have fixed it my self but I was not sure of the problem and report the issue before spending more time on it.
Glad it's working!
And nah I wouldn't have expected you to fix it, the code needed to be updated for newer versions, I had to completely recreate the Editor UI code using the UIElements API, so it wasn't just a quick few-line change fix.
Good enough excuse for me to learn some about the UIElements system, since I'd never really needed to before!
Yeah for sure it was not a one line fix. It meant that it is something I am able to do and I just ask for help before trying to understand the issue. But glad I make you learn UIToolkit (I have some experience with it).
Anyway thanks again.
@rxra Updated it to use the UIElements system. Should hopefully work well!