15
\$\begingroup\$

What I am trying to understand and learn here is a very basic thing: what is the most general and most polished way to properly differentiate single-clicks and double-clicks for the 3 main mouse buttons in Unity, using C#?

My emphasis on the word properly is not for nothing. Although I was surprised that this seems to have never been asked in this website before, of course I have searched and found numerous similar questions on Unity's forums and Unity's own question-answer pages. However, as it is often the case with answers posted in these resources, they all seemed to be either plain wrong or at least a bit too amateurish.

Let me explain. Proposed solutions always had either of the two approaches and their corresponding flaws:

  1. every time a click happens, calculate time delta since last click and if it was smaller than a given threshold, a double click would be detected. However, this solution results in that a quick single click is detected before the double-click is detected, which is undesirable in most situations because then it activates both single and double click actions.

Rough example:

float dclick_threshold = 0.25f;
double timerdclick = 0;

if (Input.GetMouseButtonDown(0) 
{
   if (Time.time - timerdclick) > dclick_threshold)
   {
       Debug.Log("single click");
       //call the SingleClick() function, not shown
   } else {
       Debug.Log("double click");
       //call the DoubleClick() function, not shown
   }
   timerdclick = Time.time;
}
  1. set a wait-for delay for the single-click to be activated, which means, at every click, only activate single click actions if the time delay since last click is above a given threshold. However, this gives sluggish results, since a waiting before it is possible to notice the waiting before single-clicks are detected. Worse than that, this kind of solution suffers from discrepancies across machines, since it does not take into account how slow or how fast is the user's double-click speed setting at the OS level.

Rough example:

   float one_click = false;
   float dclick_threshold = 0.25f;
   double timerdclick = 0;

   if (one_click && ((Time.time - timerdclick) > dclick_threshold))
   {
       Debug.Log("single click");
       //call the SingleClick() function, not shown
       one_click = false;
   }

   if (Input.GetMouseButtonDown(0))
   {

       if (!one_click)
       {
           dclick = -1;
           timerdclick = Time.time;
           one_click = true;
       }

       else if (one_click && ((Time.time - timerdclick) < dclick_threshold))
       {
           Debug.Log("double click");
           //call the DoubleClick() function, not shown
           one_click = false;
       }

   }

Therefore, my questions becomes the following. Is there a more polished and reliable way of detecting double clicks in Unity using C#, other than these solutions, and one that handles the problems aforementioned?

\$\endgroup\$
  • \$\begingroup\$ Hmm. Properly detecting double-clicks...I can only think about how I'd go about doing it, which probably isn't any less amateurish. \$\endgroup\$ – Draco18s Feb 10 '16 at 22:28
  • 2
    \$\begingroup\$ I dont think you can detect clicks any better - you cannot* predict results of user physical action. I would rather focus on designing my GUI and mechanics so that triggered actions hide the fact as good as they can. * well, in theory you could, implementing some AI analyzing use behaviour, but the results would not be consistent \$\endgroup\$ – wondra Feb 10 '16 at 23:07
  • 1
    \$\begingroup\$ Referring to answer 1, "However, this solution results in that a quick single click is detected before the double-click is detected, which is undesirable." I don't really see what you're looking for. If there's a problem with this solution, I think it's the game mechanics that need tweaking. Take an RTS for example. You click a unit to select it (action A), double click to select all other units (action B). If you double click, you don't just want action B, you want both A and B. If you want something entirely different to happen, it should be a right click, or ctrl+click, etc. \$\endgroup\$ – Peter Feb 11 '16 at 0:01
13
\$\begingroup\$

Coroutines are fun:

using UnityEngine;
using System.Collections;

public class DoubleClickTest : MonoBehaviour 
{
private float doubleClickTimeLimit = 0.25f;

protected void Start()
{
    StartCoroutine(InputListener());
}

// Update is called once per frame
private IEnumerator InputListener() 
{
    while(enabled)
    { //Run as long as this is activ

        if(Input.GetMouseButtonDown(0))
            yield return ClickEvent();

        yield return null;
    }
}

private IEnumerator ClickEvent()
{
    //pause a frame so you don't pick up the same mouse down event.
    yield return new WaitForEndOfFrame();

    float count = 0f;
    while(count < doubleClickTimeLimit)
    {
        if(Input.GetMouseButtonDown(0))
        {
            DoubleClick();
            yield break;
        }
        count += Time.deltaTime;// increment counter by change in time between frames
        yield return null; // wait for the next frame
    }
    SingleClick();
}


private void SingleClick()
{
    Debug.Log("Single Click");
}

private void DoubleClick()
{
    Debug.Log("Double Click");
}

}
\$\endgroup\$
  • \$\begingroup\$ This doesn't seem to detect when an individual button is pressed; just when the screen is clicked or double clicked in general (although OP didn't request that, so it's not fair to ask so much). Any suggestions on how one could do that? \$\endgroup\$ – zavtra Jul 26 '16 at 14:45
  • \$\begingroup\$ I don't see how this is any different than the second method proposed in the question. \$\endgroup\$ – John Hamilton Dec 18 '17 at 13:13
5
\$\begingroup\$

I can't read English. But UniRx may be able to help you.

https://github.com/neuecc/UniRx#introduction

void Start () {

    var clickStream = Observable.EveryUpdate().Where(_ => Input.GetMouseButtonDown(0));
    var bufferStream = clickStream.Buffer (clickStream.Throttle (TimeSpan.FromMilliseconds (250))).Publish ().RefCount ();
    bufferStream.Where (xs => xs.Count == 1).Subscribe (_ => Debug.Log ("single click"));
    bufferStream.Where (xs => xs.Count >= 2).Subscribe (_ => Debug.Log ("double click"));
}
\$\endgroup\$
  • \$\begingroup\$ It works, but not exactly like double clicks are implemented in Windows. When you click, for example, 6 times in a row, only one double click is produced. In Windows, it would be 3 double clicks. You can try in Control Panel → Mouse. \$\endgroup\$ – Maxim Kamalov Feb 16 '16 at 19:20
2
\$\begingroup\$

Windows default double click delay is 500ms = 0.5 seconds.

Generally in a game it's much easier to handle double click on the control that's being clicked, not globally. If you decide to handle double clicks globally, you have to implement workarounds to avoid 2 very undesirable side effects:

  1. Every single click could be delayed by 0.5 seconds.
  2. A single click on one control followed quickly by a single click on another control could be misinterpreted as a double-click on the second control.

If you have to use a global implementation, you'll need to look at the location of the click as well - if the mouse moved too far it's not a double click. You would normally implement a focus tracker that resets the double click counter whenever the focus changes.

To the question if you should wait until the double-click timeout passes, this needs to be handled differently for each control. You have 4 different events:

  1. Hover.
  2. Single click, which may or may not become a double click.
  3. Double click.
  4. Double click timeout, single click confirmed to not be a double click.

Whenever possible, you try to design the GUI interactions such that the last event will not be used: Windows File Explorer will select an item on a single click immediately, and will open it on a double click, and will do nothing on a confirmed single click.

In other words: You use both options you suggested, depending on what the desired behavior of the control is.

\$\endgroup\$
2
\$\begingroup\$

The above solutions work for clicks done on the whole screen. If you want to detect double-clicks on individual buttons, I've found this modification to an above answer is working well:

using UnityEngine;
using System.Collections;

public class DoubleClickTest : MonoBehaviour
{
    private float doubleClickTimeLimit = 0.35f;
    bool clickedOnce = false;
    public string singleTapMove;
    public string doubleTapMove;
    float count = 0f;

    public void startClick(){
        StartCoroutine (ClickEvent ());
    }

    public IEnumerator ClickEvent()
    {
        if (!clickedOnce && count < doubleClickTimeLimit) {
            clickedOnce = true;
        } else {
            clickedOnce = false;
            yield break;  //If the button is pressed twice, don't allow the second function call to fully execute.
        }
        yield return new WaitForEndOfFrame();

        while(count < doubleClickTimeLimit)
        {
            if(!clickedOnce)
            {
                DoubleClick();
                count = 0f;
                clickedOnce = false;
                yield break;
            }
            count += Time.deltaTime;// increment counter by change in time between frames
            yield return null; // wait for the next frame
        }
        SingleClick();
        count = 0f;
        clickedOnce = false;
    }
    private void SingleClick()
    {
        Debug.Log("Single Click");
    }

    private void DoubleClick()
    {
        Debug.Log("Double Click");
    }
}

Attach this script to as many buttons as you want. Then in the On Click section of the editor (for each button you attach this to), click '+', add the button to itself as the game object, and then select "DoubleClickTest -> startClick()' as the function to call when the button is pressed.

\$\endgroup\$
1
\$\begingroup\$

I was looking for a proper single-double click handler. I've found this topic and gathered some really good ideas. So finally I've came up with the following solution and I want to share it with you. I hope you find this method helpful.

I think it's a great approach to use Unity's inspector, because this way your code is more flexible as you can reference any of your game objects visually and safely.

First add the Event System component to any of your game objects. This will also add the Standalone Input Module component. This will take care of the input events like mouse clicks and touches. I recommend to do this on an individual game object.

Next add the Event Trigger component to any of your raycast enabled game object. Like a UI element or a sprite, a 3D object with a collider. This will deliver the click event to our script. Add the Pointer Click event type and set the receiver game object which has the Click Controller script component on it and finally select its onClick() method. (Also you can alter the script to receive all clicks in an Update() and skip this step.)

So the receiver game object, which of course can be the one with the event trigger, will do anything with the single or doudle click events.

You can set the time limit for double click, the desired button and the custom events for single and double click. The only limitation is that you can choose only one button at a time for one game object.

enter image description here

The script itself starts a coroutine on every first clicks with two timers. The firstClickTime and the currentTime in sync with the first.

This coroutine loops once at the end of every frame till the time limit expires. Meanwhile the onClick() method continues counting the clicks.

When the time limit expires it calls the single- or doubleclick event according to the clickCounter. Then it sets this counter to zero so that the coroutine can finish and the whole process starts from the beginning.

using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using System.Collections;

public class ClickController : MonoBehaviour
{
    public float timeLimit = 0.25f;
    public PointerEventData.InputButton button;

    [System.Serializable]
    public class OnSingleClick : UnityEvent {};
    public OnSingleClick onSingleClick;

    [System.Serializable]
    public class OnDoubleClick : UnityEvent {};
    public OnDoubleClick onDoubleClick;

    private int clickCount;
    private float firstClickTime;
    private float currentTime;

    private ClickController () {
        clickCount = 0;
    }

    public void onClick (BaseEventData data) {

        PointerEventData pointerData = data as PointerEventData;

        if (this.button != pointerData.button) {
            return;
        }

        this.clickCount++;

        if (this.clickCount == 1) {
            firstClickTime = pointerData.clickTime;
            currentTime = firstClickTime;
            StartCoroutine (ClickRoutine ());
        }
    }

    private IEnumerator ClickRoutine () {

        while (clickCount != 0)
        {
            yield return new WaitForEndOfFrame();

            currentTime += Time.deltaTime;

            if (currentTime >= firstClickTime + timeLimit) {
                if (clickCount == 1) {
                    onSingleClick.Invoke ();
                } else {
                    onDoubleClick.Invoke ();
                }
                clickCount = 0;
            }
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ Well, right, my first post should be a bit more informative. So I added detailed description and refined the script a bit. Please remove your -1 if you like it. \$\endgroup\$ – Norb Nov 3 '16 at 23:06
0
\$\begingroup\$

I think there is no way to read user's mind either he is gonna click single or double, after all we have to wait for the input. Although you can set wait of 0.01 seconds (low as much) for the second click. In this regard I'd go for the waiting option.

And YES Coroutines are fun...

An alternative

float _threshold = 0.25f;

void Start ()
{
    StartCoroutine ("DetectTouchInput");
}

IEnumerator DetectTouchInput ()
{
    while (true) {
        float duration = 0;
        bool doubleClicked = false;
        if (Input.GetMouseButtonDown (0)) {
            while (duration < _threshold) {
                duration += Time.deltaTime;
                yield return new WaitForSeconds (0.005f);
                if (Input.GetMouseButtonDown (0)) {
                    doubleClicked = true;
                    duration = _threshold;
                    // Double click/tap
                    print ("Double Click detected");
                }
            }
            if (!doubleClicked) {
                // Single click/tap
                print ("Single Click detected");
            }
        }
        yield return null;
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.