Jak właściwie rozróżnić pojedyncze kliknięcia i podwójne kliknięcie w Unity3D za pomocą C #?

15

To, co próbuję zrozumieć i nauczyć się tutaj, jest bardzo podstawową rzeczą: jaki jest najbardziej ogólny i najbardziej dopracowany sposób prawidłowego rozróżnienia pojedynczych kliknięć i podwójnych kliknięć dla 3 głównych przycisków myszy w Unity przy użyciu C #?

Mój nacisk na to słowo właściwie nie jest na darmo. Chociaż byłem zaskoczony, że wydaje się, że nigdy wcześniej nie zadawano tego na tej stronie, oczywiście przeszukałem i znalazłem wiele podobnych pytań na forach Unity i na własnych stronach z odpowiedziami. Jednak, jak to często bywa z odpowiedziami zamieszczonymi w tych zasobach, wszystkie wydawały się albo błędne, albo przynajmniej nieco amatorskie.

Pozwól mi wyjaśnić. Proponowane rozwiązania zawsze miały jedno z dwóch podejść i odpowiadające im wady:

  1. za każdym razem, gdy nastąpi kliknięcie, obliczyć różnicę czasu od ostatniego kliknięcia, a jeśli byłby on mniejszy niż określony próg, wykryte zostanie podwójne kliknięcie. Jednak to rozwiązanie powoduje wykrycie szybkiego pojedynczego kliknięcia przed wykryciem podwójnego kliknięcia, co jest niepożądane w większości sytuacji, ponieważ wtedy aktywuje zarówno pojedyncze, jak i podwójne kliknięcie.

Szorstki przykład:

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. ustaw opóźnienie oczekiwania na aktywację pojedynczego kliknięcia, co oznacza, że ​​przy każdym kliknięciu aktywuj akcje pojedynczego kliknięcia tylko wtedy, gdy opóźnienie od ostatniego kliknięcia przekracza określony próg. Daje to jednak powolne wyniki, ponieważ oczekiwanie przed zauważeniem oczekiwania przed wykryciem pojedynczych kliknięć. Co gorsza, w tego rodzaju rozwiązaniu występują rozbieżności między komputerami, ponieważ nie bierze ono pod uwagę szybkości i szybkości ustawienia podwójnego kliknięcia przez użytkownika na poziomie systemu operacyjnego.

Szorstki przykład:

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

   }

Dlatego moje pytania stają się następujące. Czy istnieje bardziej dopracowany i niezawodny sposób wykrywania podwójnych kliknięć w Unity przy użyciu C #, inny niż te rozwiązania, i taki, który radzi sobie z wyżej wymienionymi problemami?

Bennton
źródło
Hmm Prawidłowe wykrywanie podwójnych kliknięć ... Mogę tylko myśleć o tym , jak bym to zrobił, co prawdopodobnie nie jest mniej amatorskie.
Draco18s nie ufa już
2
Nie sądzę, aby można było lepiej wykrywać kliknięcia - nie można * przewidzieć wyników działań fizycznych użytkownika. Wolę skupić się na zaprojektowaniu GUI i mechaniki, aby uruchamiane działania ukrywały ten fakt tak dobrze, jak to tylko możliwe. * cóż, teoretycznie można, wprowadzając pewne zachowania analizujące sztuczną inteligencję, ale wyniki nie byłyby spójne
wondra
1
Nawiązując do odpowiedzi 1, „Rozwiązanie to powoduje jednak, że wykrywane jest szybkie pojedyncze kliknięcie przed wykryciem podwójnego kliknięcia, co jest niepożądane”. Naprawdę nie widzę tego, czego szukasz. Jeśli jest problem z tym rozwiązaniem, myślę, że to mechanika gry wymaga modyfikacji. Weźmy na przykład RTS. Kliknij jednostkę, aby ją wybrać (akcja A), kliknij dwukrotnie, aby wybrać wszystkie inne jednostki (akcja B). Jeśli klikniesz dwukrotnie, nie chcesz tylko akcji B, chcesz zarówno A, jak i B. Jeśli chcesz, aby wydarzyło się coś zupełnie innego, powinno to być kliknięcie prawym przyciskiem myszy lub kliknięcie z wciśniętym klawiszem Ctrl itp.
Peter

Odpowiedzi:

13

Coroutines są fajne:

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
źródło
Wydaje się, że nie wykrywa to naciśnięcia pojedynczego przycisku; tylko wtedy, gdy ekran zostanie kliknięty lub kliknięty dwukrotnie (chociaż OP tego nie zażądało, więc niesprawiedliwe jest tyle pytań). Wszelkie sugestie, jak to zrobić?
zavtra
Nie rozumiem, jak to się różni od drugiej metody zaproponowanej w pytaniu.
John Hamilton,
5

Nie umiem czytać po angielsku. Ale UniRx może ci pomóc.

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"));
}
użytkownik2971260
źródło
Działa, ale nie dokładnie tak, jak podwójne kliknięcia są zaimplementowane w systemie Windows. Po kliknięciu, na przykład 6 razy z rzędu, powstaje tylko jedno podwójne kliknięcie. W systemie Windows byłyby to 3 podwójne kliknięcia. Możesz spróbować Control Panel → Mouse.
Maxim Kamalov
2

Domyślne opóźnienie podwójnego kliknięcia w systemie Windows wynosi 500 ms = 0,5 sekundy.

Zasadniczo w grze znacznie łatwiej jest obsługiwać podwójne kliknięcie kontrolki, która jest klikana, nie globalnie. Jeśli zdecydujesz się obsługiwać podwójne kliknięcia na całym świecie, musisz wdrożyć obejścia, aby uniknąć 2 bardzo niepożądanych efektów ubocznych:

  1. Każde pojedyncze kliknięcie może być opóźnione o 0,5 sekundy.
  2. Pojedyncze kliknięcie jednej kontrolki, a następnie szybkie kliknięcie innej kontrolki może zostać źle zinterpretowane jako dwukrotne kliknięcie drugiej kontrolki.

Jeśli musisz użyć globalnej implementacji, musisz także spojrzeć na lokalizację kliknięcia - jeśli mysz przesunie się za daleko, nie będzie to podwójne kliknięcie. Zwykle zaimplementowałbyś moduł śledzenia fokusu, który resetuje licznik podwójnych kliknięć za każdym razem, gdy zmienia się fokus.

Na pytanie, czy należy poczekać, aż upłynie limit czasu podwójnego kliknięcia, należy to rozpatrzyć inaczej dla każdej kontroli. Masz 4 różne wydarzenia:

  1. Unosić się.
  2. Pojedyncze kliknięcie, które może, ale nie musi, stać się podwójnym kliknięciem.
  3. Podwójne kliknięcie.
  4. Limit czasu podwójnego kliknięcia, jedno kliknięcie potwierdziło, że nie jest podwójnym kliknięciem.

O ile to możliwe, próbujesz zaprojektować interakcje interfejsu GUI w taki sposób, aby ostatnie zdarzenie nie było używane: Eksplorator plików systemu Windows natychmiast wybierze element za pomocą jednego kliknięcia i otworzy go za pomocą podwójnego kliknięcia i nie zrobi nic na potwierdzonym pojedynczym Kliknij.

Innymi słowy: korzystasz z obu zasugerowanych opcji, w zależności od pożądanego zachowania kontrolki.

Piotr
źródło
2

Powyższe rozwiązania działają w przypadku kliknięć wykonanych na całym ekranie. Jeśli chcesz wykryć podwójne kliknięcie poszczególnych przycisków, zauważyłem, że modyfikacja powyższej odpowiedzi działa dobrze:

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

Dołącz ten skrypt do dowolnej liczby przycisków. Następnie w sekcji Edytora po kliknięciu (dla każdego przycisku, do którego go dołączasz), kliknij „+”, dodaj przycisk do siebie jako obiekt gry, a następnie wybierz „DoubleClickTest -> startClick ()” jako funkcję do wywołania po naciśnięciu przycisku.

zavtra
źródło
1

Szukałem odpowiedniego modułu obsługi pojedynczego kliknięcia. Znalazłem ten temat i zebrałem kilka naprawdę dobrych pomysłów. W końcu wymyśliłem następujące rozwiązanie i chcę się z tobą podzielić. Mam nadzieję, że uznasz tę metodę za pomocną.

Myślę, że to świetne podejście do korzystania z inspektora Unity, ponieważ w ten sposób twój kod jest bardziej elastyczny, ponieważ możesz wizualnie i bezpiecznie odwoływać się do dowolnego obiektu gry.

Najpierw dodaj komponent System zdarzeń do dowolnego obiektu gry. Spowoduje to również dodanie modułu autonomicznego modułu wejściowego . To zajmie się wejściowymi zdarzeniami, takimi jak kliknięcia i dotknięcia myszy. Polecam to zrobić na pojedynczym obiekcie gry.

Następnie dodaj składnik Event Trigger do dowolnego obiektu gry obsługującego funkcję raycast . Podobnie jak element interfejsu użytkownika lub duszek, obiekt 3D ze zderzaczem . Spowoduje to dostarczenie zdarzenia kliknięcia do naszego skryptu. Dodaj typ zdarzenia Pointer Click i ustaw obiekt gry odbiornika, który ma składnik skryptu Click Controller , a następnie wybierz jego onClick () metodę . (Możesz także zmienić skrypt, aby otrzymywać wszystkie kliknięcia w Aktualizacji () i pomijać ten krok.)

Tak więc obiekt gry odbiornika, którym oczywiście może być wyzwalacz zdarzenia, zrobi wszystko ze zdarzeniami pojedynczego lub podwójnego kliknięcia.

Możesz ustawić limit czasu podwójnego kliknięcia, żądany przycisk oraz zdarzenia niestandardowe dla pojedynczego i podwójnego kliknięcia. Jedynym ograniczeniem jest to, że możesz wybrać tylko jeden przycisk na raz dla jednego obiektu gry .

wprowadź opis zdjęcia tutaj

Sam skrypt uruchamia coroutine przy każdym pierwszym kliknięciu za pomocą dwóch timerów. FirstClickTime i currentTime w synchronizacji z pierwszym.

Ta coroutine zapętla się na końcu każdej klatki, aż do upływu terminu. Tymczasem metoda onClick () kontynuuje liczenie kliknięć.

Po upływie terminu wywołuje zdarzenie jedno- lub dwukrotne kliknięcie zgodnie z parametrem clickCounter. Następnie ustawia ten licznik na zero, dzięki czemu coroutine może się skończyć, a cały proces rozpoczyna się od początku.

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
źródło
Cóż, prawda, mój pierwszy post powinien być nieco bardziej pouczający. Dodałem więc szczegółowy opis i nieco udoskonaliłem skrypt. Usuń swoje -1, jeśli Ci się podoba.
Norb
0

Myślę, że nie ma sposobu, aby odczytać zdanie użytkownika, albo kliknie jeden lub dwa razy, w końcu musimy czekać na dane wejściowe. Chociaż możesz ustawić oczekiwanie na 0,01 sekundy (maksymalnie o tyle) na drugie kliknięcie. W związku z tym wybrałbym opcję oczekiwania.

I tak Coroutines są fajne ...

Alternatywa

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
źródło