Come differenziare correttamente i singoli clic e fare doppio clic in Unity3D utilizzando C #?


15

Quello che sto cercando di capire e imparare qui è una cosa molto semplice: qual è il modo più generale e più raffinato per farlo correttamente differenziare i clic singoli e doppi per i 3 pulsanti principali del mouse in Unity, usando C #?

La mia enfasi sulla parola corretta non è per niente. Anche se sono rimasto sorpreso dal fatto che questo non sia mai stato chiesto in questo sito Web prima, ovviamente ho cercato e trovato numerose domande simili sui forum di Unity e sulle pagine di domande e risposte di Unity. Tuttavia, come spesso accade con le risposte pubblicate in queste risorse, sembravano tutte o semplicemente sbagliate o almeno un po 'troppo amatoriali.

Lasciatemi spiegare. Le soluzioni proposte hanno sempre avuto uno dei due approcci e i relativi difetti:

  1. ogni volta che si verifica un clic, calcolare il delta temporale dall'ultimo clic e se fosse inferiore a una determinata soglia, verrà rilevato un doppio clic. Tuttavia, questa soluzione comporta il rilevamento di un singolo clic rapido prima che venga rilevato il doppio clic, il che è indesiderabile nella maggior parte dei casi perché attiva quindi le azioni sia a singolo clic che a doppio clic.

Esempio approssimativo:

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. impostare un ritardo di attesa per l'attivazione del singolo clic, il che significa che, ad ogni clic, attivare le azioni del singolo clic solo se il ritardo dall'ultimo clic è superiore a una determinata soglia. Tuttavia, ciò fornisce risultati lenti, poiché è necessario attendere prima che sia possibile notare l'attesa prima che vengano rilevati i clic singoli. Peggio ancora, questo tipo di soluzione soffre di discrepanze tra le macchine, dal momento che non tiene conto di quanto sia lenta o veloce l'impostazione della velocità del doppio clic dell'utente a livello di sistema operativo.

Esempio approssimativo:

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

   }

Pertanto, le mie domande diventano le seguenti. Esiste un modo più accurato e affidabile per rilevare i doppi clic in Unity utilizzando C #, oltre a queste soluzioni, e uno che gestisce i problemi di cui sopra?


Hmm. Rilevare correttamente i doppi clic ... Posso solo pensare a come farei a farlo, che probabilmente non è meno amatoriale.
Draco18s non si fida più di SE

2
Non credo che tu possa rilevare meglio i clic - non puoi * prevedere i risultati dell'azione fisica dell'utente. Preferirei concentrarmi sulla progettazione della mia GUI e della meccanica in modo che le azioni attivate nascondano il fatto il più bene possibile. * beh, in teoria potresti implementare un po 'di AI analizzando il comportamento d'uso, ma i risultati non sarebbero coerenti
Wondra,

1
Facendo riferimento alla risposta 1, "Tuttavia, questa soluzione comporta che viene rilevato un singolo clic rapido prima che venga rilevato il doppio clic, il che è indesiderabile." Non vedo davvero quello che stai cercando. Se c'è un problema con questa soluzione, penso che siano le meccaniche di gioco che devono essere modificate. Prendi un RTS per esempio. Fai clic su un'unità per selezionarla (azione A), fai doppio clic per selezionare tutte le altre unità (azione B). Se fai doppio clic, non vuoi solo l'azione B, vuoi sia A che B. Se vuoi che accada qualcosa di completamente diverso, dovrebbe essere un clic destro, o ctrl + clic, ecc.
Peter

Risposte:


13

Le coroutine sono divertenti:

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

}

Questo non sembra rilevare quando viene premuto un singolo pulsante; proprio quando si fa clic sullo schermo o si fa doppio clic in generale (anche se OP non lo ha richiesto, quindi non è giusto chiedere così tanto). Qualche suggerimento su come farlo?
zavtra,

Non vedo come questo sia diverso dal secondo metodo proposto nella domanda.
John Hamilton,

5

Non so leggere l'inglese. Ma UniRx potrebbe essere in grado di aiutarti.

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

Funziona, ma non esattamente come i doppi clic sono implementati in Windows. Quando si fa clic, ad esempio, 6 volte di seguito, viene prodotto solo un doppio clic. In Windows, sarebbero 3 doppi clic. Puoi provare Control Panel → Mouse.
Maxim Kamalov,

2

Il ritardo di doppio clic predefinito di Windows è 500 ms = 0,5 secondi.

Generalmente in un gioco è molto più semplice gestire il doppio clic sul controllo su cui si fa clic, non a livello globale. Se decidi di gestire i doppi clic a livello globale, devi implementare soluzioni alternative per evitare 2 effetti collaterali molto indesiderati:

  1. Ogni singolo clic potrebbe essere ritardato di 0,5 secondi.
  2. Un singolo clic su un controllo seguito rapidamente da un singolo clic su un altro controllo potrebbe essere interpretato erroneamente come un doppio clic sul secondo controllo.

Se devi usare un'implementazione globale, dovrai guardare anche la posizione del clic: se il mouse si è spostato troppo, non è un doppio clic. Normalmente si implementerebbe un tracker di messa a fuoco che reimposta il contatore di doppio clic ogni volta che la messa a fuoco cambia.

Alla domanda se è necessario attendere fino allo scadere del timeout doppio clic, questo deve essere gestito in modo diverso per ciascun controllo. Hai 4 eventi diversi:

  1. Hover.
  2. Clic singolo, che può diventare o meno un doppio clic.
  3. Doppio click.
  4. Timeout doppio clic, singolo clic confermato per non essere un doppio clic.

Quando possibile, si tenta di progettare le interazioni della GUI in modo tale che non venga utilizzato l'ultimo evento: Esplora file di Windows selezionerà immediatamente un elemento con un singolo clic, lo aprirà con un doppio clic e non farà nulla su un singolo confermato clic.

In altre parole: si utilizzano entrambe le opzioni suggerite, a seconda del comportamento desiderato del controllo.


2

Le soluzioni di cui sopra funzionano per i clic effettuati sull'intero schermo. Se vuoi rilevare i doppi clic sui singoli pulsanti, ho scoperto che questa modifica a una risposta sopra sta funzionando bene:

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

Collega questo script a tutti i pulsanti che desideri. Quindi nella sezione Al clic dell'editor (per ogni pulsante a cui lo allego), fai clic su "+", aggiungi il pulsante a se stesso come oggetto di gioco, quindi seleziona "DoubleClickTest -> startClick ()" come funzione da chiamare quando si preme il pulsante.


1

Stavo cercando un vero gestore di clic doppio. Ho trovato questo argomento e raccolto alcune idee davvero valide. Quindi finalmente ho trovato la seguente soluzione e voglio condividerla con te. Spero che questo metodo ti sia stato utile.

Penso che sia un ottimo approccio per usare l'ispettore di Unity, perché in questo modo il tuo codice è più flessibile in quanto puoi fare riferimento a tutti i tuoi oggetti di gioco visivamente e in sicurezza.

Per prima cosa aggiungi il componente Sistema eventi a uno qualsiasi dei tuoi oggetti di gioco. Ciò aggiungerà anche il componente Modulo di input autonomo . Questo si occuperà degli eventi di input come clic e tocchi del mouse. Consiglio di farlo su un singolo oggetto di gioco.

Quindi aggiungi il componente Trigger eventi a qualsiasi oggetto di gioco abilitato per raycast . Come un elemento dell'interfaccia utente o uno sprite, un oggetto 3D con un collider . Questo invierà l'evento click al nostro script. Aggiungi il tipo di evento Click puntatore e imposta l'oggetto di gioco del ricevitore che ha il componente dello script Click Controller su di esso e infine seleziona il suo onClick () metodo . (Inoltre puoi modificare lo script per ricevere tutti i clic in un aggiornamento () e saltare questo passaggio.)

Quindi l'oggetto di gioco del ricevitore, che ovviamente può essere quello con il trigger di evento, farà qualsiasi cosa con gli eventi di clic singolo o doppio.

È possibile impostare il limite di tempo per il doppio clic, il pulsante desiderato e gli eventi personalizzati per il singolo e doppio clic. L'unica limitazione è che puoi scegliere un solo pulsante alla volta per un oggetto di gioco .

inserisci qui la descrizione dell'immagine

Lo script stesso avvia una routine ad ogni primo clic con due timer. FirstClickTime e currentTime sincronizzati con il primo.

Questo coroutine scorre una volta alla fine di ogni fotogramma fino alla scadenza del limite di tempo. Nel frattempo il metodo onClick () continua a contare i clic.

Allo scadere del termine, chiama l'evento singolo o doppio clic in base al clickCounter. Quindi imposta questo contatore su zero in modo che la coroutine possa terminare e l'intero processo inizi dall'inizio.

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

Bene, giusto, il mio primo post dovrebbe essere un po 'più informativo. Quindi ho aggiunto una descrizione dettagliata e perfezionato un po 'lo script. Rimuovi il tuo -1 se ti piace.
Norb,

0

Penso che non ci sia modo di leggere la mente dell'utente né farà clic su singolo o doppio, dopotutto dobbiamo aspettare l'input. Sebbene sia possibile impostare l'attesa di 0,01 secondi (minimo) per il secondo clic. A questo proposito sceglierei l'opzione di attesa.

E SÌ Coroutinessono divertenti ...

Un'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;
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.