Hi. I’m using the new Input System (Unity 2021.3.33) and have a problem with mouse double click.My action looks like:
And the problem is that the double click applies even if I click two different elements, despite the different mouse position between the clicks. Press point param dosn’t affect at all.
Is it a bug or correct behaviour? How can I limit my mouse double click to a radius of a few pixels?
You should change the Max Tap Spacing value. That should limit the double tap distance between two taps if they considered double tap or two separate taps.
Also always should go for .performed event if you are using events, because those fire once the parameters satisfy the interaction. The .started and .canceled may be called independently in order to keep tab on clicks and potential interactions.
If you share code we can help with that too.
Ouch, checked the source code, you’re right, someone on the InputSystem team felt funny and renamed the tapDelay to tapSpacing. I’m afraid in this case you will need to code the distance restriction yourself. Probably a good idea if you create your own, working multitap interaction. And maybe submit a bug report about this craziness.
According to Unity source code, there is no any distance check at all…
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;
Yep, that’s why you were told to create your own that is based on that file but add a distance check (for our stuff, I believe that instead of distance we check if both taps / raycasts we do hit the same collider, and if they do then we consider that a double click / tap, worked well for us, but depends on how your game is set up).
Why Unity’s doesn’t have a distance check by default is anyone’s guess…
Sure, I already did it based on Unity sources. But I had to use Mouse.current.position.value to store a click position.
The next code just doesn’t work, throws an exception:
The final code is tested in the editor and the Win build. I hope this will be useful to someone. And I will be grateful if anyone can tell me how to get rid of Input.mousePosition.
namespace UnityEngine.InputSystem.Interactions
{
#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
[InitializeOnLoad]
#endif
[Scripting.Preserve, System.ComponentModel.DisplayName("StableMultiTap"), System.Serializable]
public class StableMultiTapInteraction : 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>
/// <seealso cref="InputControl.EvaluateMagnitude()"/>
public float pressPoint;
public float maxTapDistance = 5;
private float tapTimeOrDefault => tapTime > 0.0 ? tapTime : InputSystem.settings.defaultTapTime;
internal float tapDelayOrDefault => tapDelay > 0.0 ? tapDelay : InputSystem.settings.multiTapDelayTime;
private float pressPointOrDefault => pressPoint;
private float releasePointOrDefault => pressPointOrDefault;
/// <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 && (Input.mousePosition - m_LastTapPosition).sqrMagnitude < maxTapDistance * maxTapDistance)
{
context.Performed();
}
else
{
m_CurrentTapPhase = TapPhase.WaitingForNextPress;
m_LastTapReleaseTime = context.time;
context.SetTimeout(tapDelayOrDefault);
m_LastTapPosition = Input.mousePosition;
}
}
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;
m_LastTapPosition = Vector3.zero;
}
private TapPhase m_CurrentTapPhase;
private int m_CurrentTapCount;
private double m_CurrentTapStartTime;
private double m_LastTapReleaseTime;
private Vector3 m_LastTapPosition = Vector3.zero;
private enum TapPhase
{
None,
WaitingForNextRelease,
WaitingForNextPress,
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
private static void RegisterInteraction()
{
if (InputSystem.TryGetInteraction("StableMultiTap") == 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<StableMultiTapInteraction>("StableMultiTap");
}
}
//Constructor will be called by our Editor [InitializeOnLoad] attribute when outside Play Mode
static StableMultiTapInteraction() => RegisterInteraction();
}
}
Wow, thank you for this! I just tried it out now and it works perfectly. I ran into the same issue of how to do a proper double-click in the new input system. This will definitely help me!