238 lines
10 KiB
C#
238 lines
10 KiB
C#
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<float>? It goes by actuation just like Hold etc.
|
|
/// <summary>
|
|
/// Interaction that requires multiple taps (press and release within <see cref="tapTime"/>) spaced no more
|
|
/// than <see cref="tapDelay"/> seconds apart. This equates to a chain of <see cref="TapInteraction"/> with
|
|
/// a maximum delay between each tap.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The interaction goes into <see cref="InputActionPhase.Started"/> on the first press and then will not
|
|
/// trigger again until either the full tap sequence is performed (in which case the interaction triggers
|
|
/// <see cref="InputActionPhase.Performed"/>) or the multi-tap is aborted by a timeout being hit (in which
|
|
/// case the interaction will trigger <see cref="InputActionPhase.Canceled"/>).
|
|
/// </remarks>
|
|
public class MultiTapInteraction : IInputInteraction<float>
|
|
{
|
|
/// <summary>
|
|
/// The time in seconds within which the control needs to be pressed and released to perform the interaction.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If this value is equal to or smaller than zero, the input system will use (<see cref="InputSettings.defaultTapTime"/>) instead.
|
|
/// </remarks>
|
|
[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;
|
|
|
|
/// <summary>
|
|
/// The time in seconds which is allowed to pass between taps.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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 <see cref="tapTime"/> instead.
|
|
/// </remarks>
|
|
[Tooltip("The maximum delay (in seconds) allowed between each tap. If this time is exceeded, the multi-tap is canceled.")]
|
|
public float tapDelay;
|
|
|
|
/// <summary>
|
|
/// The number of taps required to perform the interaction.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// How many taps need to be performed in succession. Two means double-tap, three means triple-tap, and so on.
|
|
/// </remarks>
|
|
[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;
|
|
|
|
/// <summary>
|
|
/// Magnitude threshold that must be crossed by an actuated control for the control to
|
|
/// be considered pressed.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If this is less than or equal to 0 (the default), <see cref="InputSettings.defaultButtonPressPoint"/> is used instead.
|
|
/// </remarks>
|
|
/// <seealso cref="InputControl.EvaluateMagnitude()"/>
|
|
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;
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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
|
|
/// <summary>
|
|
/// UI that is displayed when editing <see cref="HoldInteraction"/> in the editor.
|
|
/// </summary>
|
|
internal class MultiTapInteractionEditor : InputParameterEditor<MultiTapInteraction>
|
|
{
|
|
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
|
|
}
|