Como diferenciar adequadamente um clique e clique duas vezes no Unity3D usando C #?

15

O que estou tentando entender e aprender aqui é uma coisa muito básica: qual é a maneira mais geral e mais refinada de diferenciar adequadamente cliques únicos e cliques duplos dos três botões principais do mouse no Unity, usando C #?

Minha ênfase na palavra corretamente não é à toa. Embora eu tenha ficado surpreso que isso nunca tenha sido feito neste site, é claro que pesquisei e encontrei inúmeras perguntas semelhantes nos fóruns do Unity e nas próprias páginas de perguntas e respostas do Unity. No entanto, como geralmente ocorre com as respostas postadas nesses recursos, todas pareciam estar completamente erradas ou pelo menos um pouco amadoras.

Deixe-me explicar. As soluções propostas sempre tiveram uma das duas abordagens e suas falhas correspondentes:

  1. toda vez que um clique acontece, calcule o delta de tempo desde o último clique e, se fosse menor que um determinado limite, um clique duplo seria detectado. No entanto, essa solução resulta em que um clique rápido é detectado antes que o clique duplo seja detectado, o que é indesejável na maioria das situações, pois ativa ações de clique único e duplo.

Exemplo aproximado:

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. defina um atraso de espera para o clique único a ser ativado, o que significa que, a cada clique, somente ative as ações de clique único se o tempo decorrido desde o último clique estiver acima de um determinado limite. No entanto, isso fornece resultados lentos, pois uma espera antes é possível notar a espera antes que os cliques únicos sejam detectados. Pior que isso, esse tipo de solução sofre discrepâncias entre as máquinas, pois não leva em conta a velocidade ou a velocidade lenta da configuração de velocidade do clique duplo do usuário no nível do sistema operacional.

Exemplo aproximado:

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

   }

Portanto, minhas perguntas se tornam as seguintes. Existe uma maneira mais refinada e confiável de detectar cliques duplos no Unity usando C #, além dessas soluções, e uma que lide com os problemas mencionados anteriormente?

Bennton
fonte
Hmm. Corretamente detectar clica duas vezes ... eu só posso pensar em como eu iria fazer sobre isso, o que provavelmente não é menos amador.
Draco18s não confia mais no SE
2
Acho que você não consegue detectar melhor os cliques - você não pode * prever os resultados da ação física do usuário. Prefiro me concentrar no design da minha GUI e mecânica, para que as ações desencadeadas ocultem o fato da melhor maneira possível. * Bem, em teoria, você poderia, implementar algum comportamento analisando uso AI, mas os resultados não seria coerente
Wondra
11
Referindo-se à resposta 1, "No entanto, esta solução resulta em um único clique rápido antes da detecção do clique duplo, o que é indesejável". Eu realmente não vejo o que você está procurando. Se houver um problema com esta solução, acho que é a mecânica do jogo que precisa ser aprimorada. Veja um RTS, por exemplo. Você clica em uma unidade para selecioná-la (ação A), clique duas vezes para selecionar todas as outras unidades (ação B). Se você clicar duas vezes, não deseja apenas a ação B, deseja A e B. Se quiser que algo completamente diferente aconteça, deve ser um clique com o botão direito do mouse ou ctrl + clique, etc.
Peter

Respostas:

13

As corotinas são divertidas:

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

}
CostelloNicho
fonte
Isso não parece detectar quando um botão individual é pressionado; apenas quando a tela é clicada ou clicada duas vezes em geral (embora o OP não tenha solicitado isso, não é justo pedir muito). Alguma sugestão de como alguém poderia fazer isso?
zavtra
Não vejo como isso seja diferente do segundo método proposto na pergunta.
John Hamilton
5

Eu não sei ler ingles Mas o UniRx pode ajudá-lo.

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"));
}
user2971260
fonte
Funciona, mas não exatamente como cliques duplos são implementados no Windows. Quando você clica, por exemplo, 6 vezes seguidas, apenas um clique duplo é produzido. No Windows, seriam 3 cliques duplos. Você pode experimentar Control Panel → Mouse.
Maxim Kamalov
2

O atraso padrão do clique duplo do Windows é 500ms = 0,5 segundos.

Geralmente em um jogo, é muito mais fácil lidar com o clique duplo no controle que está sendo clicado, não globalmente. Se você decidir lidar com cliques duplos globalmente, precisará implementar soluções alternativas para evitar dois efeitos colaterais muito indesejáveis:

  1. Cada clique pode ter um atraso de 0,5 segundos.
  2. Um único clique em um controle seguido rapidamente por um único clique em outro controle pode ser mal interpretado como um clique duplo no segundo controle.

Se você precisar usar uma implementação global, precisará verificar também o local do clique - se o mouse se mover muito longe, não será um clique duplo. Você normalmente implementaria um rastreador de foco que redefine o contador de clique duplo sempre que o foco muda.

Para a pergunta se você deve esperar até que o tempo limite do clique duplo passe, isso precisa ser tratado de maneira diferente para cada controle. Você tem 4 eventos diferentes:

  1. Flutuar.
  2. Clique único, que pode ou não se tornar um clique duplo.
  3. Duplo click.
  4. Tempo limite de clique duplo, clique único confirmado para não ser um clique duplo.

Sempre que possível, você tenta projetar as interações da GUI para que o último evento não seja usado: O Windows File Explorer selecionará um item em um único clique imediatamente e o abrirá em um clique duplo e não fará nada em um único confirmado. clique.

Em outras palavras: Você usa as duas opções sugeridas, dependendo do comportamento desejado do controle.

Peter - Unban Robert Harvey
fonte
2

As soluções acima funcionam para cliques feitos em toda a tela. Se você deseja detectar cliques duplos em botões individuais, achei que esta modificação da resposta acima está funcionando bem:

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

Anexe esse script a quantos botões desejar. Em seguida, na seção On Click do editor (para cada botão ao qual você anexa), clique em '+', adicione o botão a ele mesmo como objeto de jogo e selecione "DoubleClickTest -> startClick ()" como a função a ser chamada quando o botão é pressionado.

zavtra
fonte
1

Eu estava procurando por um manipulador adequado de clique duplo. Eu encontrei este tópico e reuni algumas idéias realmente boas. Então, finalmente, eu vim com a seguinte solução e quero compartilhar com você. Espero que você ache este método útil.

Eu acho que é uma ótima abordagem usar o inspetor do Unity, porque dessa maneira o seu código é mais flexível, pois você pode fazer referência a qualquer um dos objetos do jogo visualmente e com segurança.

Primeiro adicione o componente Sistema de Eventos a qualquer um dos seus objetos de jogo. Isso também adicionará o componente Módulo de entrada independente . Isso cuidará dos eventos de entrada, como cliques e toques do mouse. Eu recomendo fazer isso em um objeto de jogo individual.

Em seguida, adicione o componente Trigger de eventos a qualquer objeto do jogo ativado por raycast . Como um elemento da interface do usuário ou um sprite, um objeto 3D com um colisor . Isso entregará o evento click ao nosso script. Adicione o tipo de evento Pointer Click e defina o objeto do jogo receptor que possui o componente de script Click Controller e, finalmente, selecione o método onClick () . (Além disso, você pode alterar o script para receber todos os cliques em uma atualização () e pular esta etapa.)

Portanto, o objeto do jogo receptor, que obviamente pode ser o acionador do evento, fará qualquer coisa com os eventos de clique único ou doudle.

Você pode definir o limite de tempo para o clique duplo, o botão desejado e os eventos personalizados para o clique único e duplo. A única limitação é que você pode escolher apenas um botão de cada vez para um objeto de jogo .

insira a descrição da imagem aqui

O script em si inicia uma rotina a cada primeiro clique com dois temporizadores. O firstClickTime e o currentTime em sincronia com o primeiro.

Essa corotina faz um loop uma vez no final de cada quadro até o prazo expirar. Enquanto isso, o método onClick () continua contando os cliques.

Quando o prazo expira, ele chama o evento de clique único ou duplo, de acordo com o clickCounter. Em seguida, ele define esse contador como zero, para que a corotina possa terminar e todo o processo comece do início.

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;
            }
        }
    }
}
Norb
fonte
Bem, certo, meu primeiro post deve ser um pouco mais informativo. Então, adicionei uma descrição detalhada e refinei um pouco o script. Por favor, remova o seu -1, se quiser.
Norb
0

Acho que não há como ler a mente do usuário, pois ele clicará simples ou duas vezes, afinal temos que aguardar a entrada. Embora você possa definir a espera de 0,01 segundos (o mínimo) para o segundo clique. A este respeito, eu iria para a opção de espera.

E SIM Coroutinessão divertidos ...

Uma alternativa

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;
    }
}
Hamza Hasan
fonte