using System; using UnityEngine.InputSystem.Controls; using UnityEngine.Scripting; #if UNITY_EDITOR using UnityEditor; using UnityEngine.InputSystem.Editor; using UnityEngine.UIElements; using UnityEditor.UIElements; #endif ////TODO: add ability to respond to any of the taps in the sequence (e.g. one response for single tap, another for double tap) ////TODO: add ability to perform on final press rather than on release ////TODO: change this so that the interaction stays performed when the tap count is reached until the button is released namespace UnityEngine.InputSystem.Interactions { ////REVIEW: Why is this deriving from IInputInteraction? It goes by actuation just like Hold etc. /// /// Interaction that requires multiple taps (press and release within ) spaced no more /// than seconds apart. This equates to a chain of with /// a maximum delay between each tap. /// /// /// The interaction goes into on the first press and then will not /// trigger again until either the full tap sequence is performed (in which case the interaction triggers /// ) or the multi-tap is aborted by a timeout being hit (in which /// case the interaction will trigger ). /// public class MultiTapInteraction : IInputInteraction { /// /// The time in seconds within which the control needs to be pressed and released to perform the interaction. /// /// /// If this value is equal to or smaller than zero, the input system will use () instead. /// [Tooltip("The maximum time (in seconds) allowed to elapse between pressing and releasing a control for it to register as a tap.")] public float tapTime; /// /// The time in seconds which is allowed to pass between taps. /// /// /// If this time is exceeded, the multi-tap interaction is canceled. /// If this value is equal to or smaller than zero, the input system will use the duplicate value of instead. /// [Tooltip("The maximum delay (in seconds) allowed between each tap. If this time is exceeded, the multi-tap is canceled.")] public float tapDelay; /// /// The number of taps required to perform the interaction. /// /// /// How many taps need to be performed in succession. Two means double-tap, three means triple-tap, and so on. /// [Tooltip("How many taps need to be performed in succession. Two means double-tap, three means triple-tap, and so on.")] public int tapCount = 2; /// /// Magnitude threshold that must be crossed by an actuated control for the control to /// be considered pressed. /// /// /// If this is less than or equal to 0 (the default), is used instead. /// /// public float pressPoint; private float tapTimeOrDefault => tapTime > 0.0 ? tapTime : InputSystem.settings.defaultTapTime; internal float tapDelayOrDefault => tapDelay > 0.0 ? tapDelay : InputSystem.settings.multiTapDelayTime; private float pressPointOrDefault => pressPoint > 0 ? pressPoint : ButtonControl.s_GlobalDefaultButtonPressPoint; private float releasePointOrDefault => pressPointOrDefault * ButtonControl.s_GlobalDefaultButtonReleaseThreshold; /// public void Process(ref InputInteractionContext context) { if (context.timerHasExpired) { // We use timers multiple times but no matter what, if they expire it means // that we didn't get input in time. context.Canceled(); return; } switch (m_CurrentTapPhase) { case TapPhase.None: if (context.ControlIsActuated(pressPointOrDefault)) { m_CurrentTapPhase = TapPhase.WaitingForNextRelease; m_CurrentTapStartTime = context.time; context.Started(); var maxTapTime = tapTimeOrDefault; var maxDelayInBetween = tapDelayOrDefault; context.SetTimeout(maxTapTime); // We'll be using multiple timeouts so set a total completion time that // effects the result of InputAction.GetTimeoutCompletionPercentage() // such that it accounts for the total time we allocate for the interaction // rather than only the time of one single timeout. context.SetTotalTimeoutCompletionTime(maxTapTime * tapCount + (tapCount - 1) * maxDelayInBetween); } break; case TapPhase.WaitingForNextRelease: if (!context.ControlIsActuated(releasePointOrDefault)) { if (context.time - m_CurrentTapStartTime <= tapTimeOrDefault) { ++m_CurrentTapCount; if (m_CurrentTapCount >= tapCount) { context.Performed(); } else { m_CurrentTapPhase = TapPhase.WaitingForNextPress; m_LastTapReleaseTime = context.time; context.SetTimeout(tapDelayOrDefault); } } else { context.Canceled(); } } break; case TapPhase.WaitingForNextPress: if (context.ControlIsActuated(pressPointOrDefault)) { if (context.time - m_LastTapReleaseTime <= tapDelayOrDefault) { m_CurrentTapPhase = TapPhase.WaitingForNextRelease; m_CurrentTapStartTime = context.time; context.SetTimeout(tapTimeOrDefault); } else { context.Canceled(); } } break; } } /// public void Reset() { m_CurrentTapPhase = TapPhase.None; m_CurrentTapCount = 0; m_CurrentTapStartTime = 0; m_LastTapReleaseTime = 0; } private TapPhase m_CurrentTapPhase; private int m_CurrentTapCount; private double m_CurrentTapStartTime; private double m_LastTapReleaseTime; private enum TapPhase { None, WaitingForNextRelease, WaitingForNextPress, } } #if UNITY_EDITOR /// /// UI that is displayed when editing in the editor. /// internal class MultiTapInteractionEditor : InputParameterEditor { protected override void OnEnable() { m_TapTimeSetting.Initialize("Max Tap Duration", "Time (in seconds) within with a control has to be released again for it to register as a tap. If the control is held " + "for longer than this time, the tap is canceled.", "Default Tap Time", () => target.tapTime, x => target.tapTime = x, () => InputSystem.settings.defaultTapTime); m_TapDelaySetting.Initialize("Max Tap Spacing", "The maximum delay (in seconds) allowed between each tap. If this time is exceeded, the multi-tap is canceled.", "Default Tap Spacing", () => target.tapDelay, x => target.tapDelay = x, () => InputSystem.settings.multiTapDelayTime); m_PressPointSetting.Initialize("Press Point", "The amount of actuation a control requires before being considered pressed. If not set, default to " + "'Default Button Press Point' in the global input settings.", "Default Button Press Point", () => target.pressPoint, v => target.pressPoint = v, () => InputSystem.settings.defaultButtonPressPoint); } public override void OnGUI() { #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS if (!InputSystem.settings.IsFeatureEnabled(InputFeatureNames.kUseIMGUIEditorForAssets)) return; #endif target.tapCount = EditorGUILayout.IntField(m_TapCountLabel, target.tapCount); m_TapDelaySetting.OnGUI(); m_TapTimeSetting.OnGUI(); m_PressPointSetting.OnGUI(); } #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS public override void OnDrawVisualElements(VisualElement root, Action onChangedCallback) { var tapCountField = new IntegerField(m_TapCountLabel.text) { value = target.tapCount, tooltip = m_TapCountLabel.tooltip }; tapCountField.RegisterValueChangedCallback(evt => { target.tapCount = evt.newValue; onChangedCallback?.Invoke(); }); root.Add(tapCountField); m_TapDelaySetting.OnDrawVisualElements(root, onChangedCallback); m_TapTimeSetting.OnDrawVisualElements(root, onChangedCallback); m_PressPointSetting.OnDrawVisualElements(root, onChangedCallback); } #endif private readonly GUIContent m_TapCountLabel = new GUIContent("Tap Count", "How many taps need to be performed in succession. Two means double-tap, three means triple-tap, and so on."); private CustomOrDefaultSetting m_PressPointSetting; private CustomOrDefaultSetting m_TapTimeSetting; private CustomOrDefaultSetting m_TapDelaySetting; } #endif }