如何使用C#在Unity3D中正确区分单击和双击?


15

我要在这里理解和学习的东西是非常基本的:使用C#正确区分Unity中3个主要鼠标按钮的单击和双击的最通用,最精致的方法是什么?

我对“ 正确 ”一词的强调并非一无是处。尽管令我感到惊讶的是,以前似乎从未在此网站上问过这个问题,但是我当然已经在Unity的论坛和Unity自己的问答页面上搜索并找到了许多类似的问题。但是,正如在这些资源中张贴答案的情况通常一样,它们似乎都是错误的,或者至少有点过于业余。

让我解释。提出的解决方案始终具有两种方法中的任何一种及其相应的缺陷:

  1. 每次发生单击时,请计算自上次单击以来的时间增量,如果该时间增量小于给定的阈值,则将检测到双击。但是,此解决方案导致在检测到双击之前检测到快速的单击,这在大多数情况下是不希望的,因为这样会同时激活单击和双击操作。

粗略的例子:

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. 设置激活单击的等待延迟,这意味着,每次单击后,如果自上次单击以来的时间延迟超过给定阈值,则仅激活单击操作。但是,这会产生缓慢的结果,因为有可能在检测到单击之前注意到等待。更糟糕的是,这种解决方案在计算机之间存在差异,因为它没有考虑用户在操作系统级别上双击速度设置的速度或速度有多慢。

粗略的例子:

   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;
       }

   }

因此,我的问题如下。除了这些解决方案之外,还有使用C#在Unity中使用双击检测双击的更完善,更可靠的方法,并且可以解决上述问题吗?


嗯 正确检测双击......我只能想想怎么我会去这样做,这可能不是任何较为专业。
Draco18s不再信任SE SE 2016年

2
我认为您无法更好地检测到点击-您无法*预测用户身体行为的结果。我宁愿专注于设计GUI和机制,以便触发的动作尽可能地掩盖事实。* 好吧,从理论上讲,您可以实施一些AI分析使用行为,但结果将不一致
wondra

1
参考答案1,“但是,该解决方案导致在检测到双击之前检测到快速的单击,这是不希望的。” 我真的看不到你在找什么。如果此解决方案有问题,我认为是需要调整的游戏机制。以RTS为例。单击一个单位以将其选中(动作A),双击以选择所有其他单位(动作B)。如果双击,您不仅需要动作B,还希望动作A和B。如果您想要完全不同的动作,则应单击鼠标右键,或按ctrl + click等
Peter

Answers:


13

协程很有趣:

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");
}

}

这似乎无法检测到何时按下了单个按钮。通常是在单击屏幕或双击屏幕时(尽管OP并没有要求,所以要求这么多是不公平的)。关于如何做到这一点的任何建议?
zavtra '16

我看不出这与问题中提出的第二种方法有何不同。
约翰·汉密尔顿,

5

我不会看英文。但是UniRx也许可以为您提供帮助。

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"));
}

它可以工作,但不完全像Windows中实现双击一样。例如,当您连续单击6次时,只会产生一次双击。在Windows中,将双击3次。你可以试试看Control Panel → Mouse
Maxim Kamalov '16

2

Windows默认双击延迟为500ms = 0.5秒。

通常,在游戏中,要双击正在被单击的控件而不是全局控件要容易得多。如果您决定全局处理双击,则必须实施变通办法以避免2个非常不希望的副作用:

  1. 每次单击可能会延迟0.5秒。
  2. 一次单击一个控件,然后快速单击另一个控件,可能会被误解为双击第二个控件。

如果必须使用全局实现,则还需要查看单击的位置-如果鼠标移得太远,则不是双击。通常,您将实现一个焦点跟踪器,该焦点跟踪器会在焦点发生变化时重置双击计数器。

对于您是否应该等待双击超时过去的问题,需要对每个控件进行不同的处理。您有4个不同的事件:

  1. 徘徊。
  2. 单击,这可能会或可能不会变为双击。
  3. 双击。
  4. 双击超时,确认单击不是双击。

只要有可能,就尝试设计GUI交互,以使不使用上一个事件:Windows File Explorer将立即单击一次选择一个项目,然后双击将其打开,而对已确认的单击将不执行任何操作点击。

换句话说:根据控件的期望行为,同时使用建议的两个选项。


2

上述解决方案适用于整个屏幕上的点击。如果您想检测单个按钮上的双击,我发现对上述答案的修改效果很好:

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");
    }
}

将此脚本附加到任意数量的按钮上。然后在编辑器的“单击时”部分中(对于您将按钮附加到的每个按钮),单击“ +”,将按钮添加为游戏对象本身,然后选择“ DoubleClickTest-> startClick()”作为要调用的函数当按下按钮时。


1

我一直在寻找合适的双击处理程序。我找到了这个主题,并收集了一些非常好的想法。所以最终我提出了以下解决方案,并希望与您分享。希望这种方法对您有所帮助。

我认为这是使用Unity的检查器的好方法,因为这样您的代码更加灵活,因为您可以直观,安全地引用任何游戏对象。

首先将事件系统组件添加到您的任何游戏对象中。这还将添加独立输入模块组件。这将处理输入事件,例如鼠标单击和触摸。我建议对单个游戏对象执行此操作。

接下来,将事件触发器组件添加到任何启用了射线广播的游戏对象中。像UI元素精灵,带有collider的3D对象。这会将click事件传递到我们的脚本。添加Pointer Click事件类型,并设置带有Click Controller脚本组件的接收方游戏对象,最后选择其onClick()方法。(此外,您可以更改脚本以在Update()中接收所有点击,并跳过此步骤。)

因此,接收者游戏对象(当然可以是带有事件触发器的对象)将对单击或双重单击事件执行任何操作。

您可以设置双击的时间限制,所需的按钮以及单击和双击的自定义事件。唯一的限制是,你可以选择只有一个按钮在同一时间对一个游戏对象

在此处输入图片说明

该脚本本身会使用两个计时器在每次首次点击时启动协程。firstClickTime和currentTime与第一个同步。

该协程在每个帧的末尾循环一次,直到时间限制到期为止。同时,onClick()方法继续计算点击次数。

时限到期后,它将根据clickCounter调用单击事件或双击事件。然后,将该计数器设置为零,以便协程可以完成,并且整个过程从头开始。

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;
            }
        }
    }
}

好吧,对,我的第一篇文章应该提供更多信息。因此,我添加了详细说明并对该脚本进行了一些改进。如果您喜欢,请删除-1。
Norb '16

0

我认为无法读懂用户的想法,要么单击一次,要么单击两次,毕竟我们必须等待输入。尽管您可以将第二次点击的等待时间设置为0.01秒(尽可能低)。在这方面,我会选择等待选项。

是的Coroutines很有趣...

替代

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;
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.