Jak nie zamrozić głównego wątku w Unity?

33

Mam algorytm generowania poziomów, który jest ciężki obliczeniowo. Wywołanie go zawsze powoduje zawieszenie się ekranu gry. Jak mogę umieścić funkcję w drugim wątku, gdy gra nadal wyświetla ekran ładowania, aby wskazać, że gra się nie zawiesiła?

DarkDestry
źródło
1
Możesz używać systemu wątków do uruchamiania innych zadań w tle ... ale nie mogą one wywoływać niczego w interfejsie API Unity, ponieważ te rzeczy nie są bezpieczne dla wątków. Tylko komentuję, ponieważ tego nie zrobiłem i nie mogę szybko podać przykładowego kodu.
Almo
Użycie ListAkcji do przechowywania funkcji, które chcesz wywołać w głównym wątku. zablokuj i skopiuj Actionlistę w Updatefunkcji do listy tymczasowej, wyczyść oryginalną listę, a następnie wykonaj Actionkod w tym Listw głównym wątku. Zobacz UnityThread z mojego innego postu, jak to zrobić. Na przykład, aby wywołać funkcję w głównym wątku, UnityThread.executeInUpdate(() => { transform.Rotate(new Vector3(0f, 90f, 0f)); });
Programator

Odpowiedzi:

48

Aktualizacja: W 2018 roku Unity wprowadza system zadań C # jako sposób na odciążenie pracy i wykorzystanie wielu rdzeni procesora.

Poniższa odpowiedź poprzedza ten system. Nadal będzie działać, ale mogą być lepsze opcje dostępne w nowoczesnej Unity, w zależności od potrzeb. W szczególności wydaje się, że system zadań rozwiązuje niektóre z ograniczeń, do których ręcznie tworzone wątki mogą bezpiecznie uzyskać dostęp, opisanych poniżej. Na przykład programiści eksperymentujący z raportem podglądu wykonujący raycast i tworzący równolegle siatki .

Zapraszam użytkowników z doświadczeniem w korzystaniu z tego systemu zadań do dodawania własnych odpowiedzi odzwierciedlających bieżący stan silnika.


W przeszłości korzystałem z wątków do zadań ciężkich w Unity (zwykle przetwarzanie obrazów i geometrii) i nie różni się drastycznie od używania wątków w innych aplikacjach C #, z dwoma zastrzeżeniami:

  1. Ponieważ Unity używa nieco starszego podzbioru .NET, istnieje kilka nowszych funkcji wątków i bibliotek, których nie możemy używać od razu po wyjęciu z pudełka, ale podstawy są już dostępne.

  2. Jak zauważa Almo w powyższym komentarzu, wiele typów Jedności nie jest bezpiecznych dla wątków i spowoduje wyjątki, jeśli spróbujesz je skonstruować, użyć, a nawet porównać z głównym wątkiem. O czym należy pamiętać:

    • Jednym z typowych przypadków jest sprawdzenie, czy referencja GameObject lub Monobehaviour jest zerowa przed próbą uzyskania dostępu do jej członków. myUnityObject == nullwywołuje przeciążonego operatora dla wszystkiego, co wywodzi się z UnityEngine.Object, ale System.Object.ReferenceEquals()działa w tym zakresie do pewnego stopnia - pamiętaj tylko, że GameObject z Destroy () porównuje wartość równą zeru przy użyciu przeciążenia, ale nie jest jeszcze ReferenceEqual do null.

    • Odczytywanie parametrów z typów Unity jest zwykle bezpieczne w innym wątku (w tym sensie, że nie od razu wyrzuci wyjątek, o ile będziesz uważnie sprawdzać wartości zerowe jak wyżej), ale zwróć uwagę na ostrzeżenie Filipa, że główny wątek może modyfikować stan podczas gdy to czytasz. Musisz być zdyscyplinowany, kto może modyfikować, co i kiedy, aby uniknąć odczytania niespójnego stanu, co może prowadzić do błędów, które mogą być diabelnie trudne do wyśledzenia, ponieważ zależą one od mniej niż milisekundowych czasów między wątkami, które możemy rozmnażają się do woli.

    • Statyczne elementy losowe i czasowe nie są dostępne. Utwórz instancję System.Random dla wątku, jeśli potrzebujesz losowości, i System.Diagnostics.Stopwatch, jeśli potrzebujesz informacji o taktowaniu.

    • Funkcje Mathf, struktury wektorowe, macierzowe, czwartorzędowe i kolorowe działają dobrze w wątkach, dzięki czemu większość obliczeń można wykonać osobno

    • Tworzenie GameObjects, dołączanie Monobehaviours lub tworzenie / aktualizowanie tekstur, siatek, materiałów itp. - wszystko to musi się zdarzyć w głównym wątku. W przeszłości, kiedy musiałem z nimi pracować, tworzyłem kolejkę producent-konsument, w której mój wątek roboczy przygotowuje surowe dane (takie jak duży wektory / kolory do zastosowania na siatce lub teksturze), a Aktualizacja lub Coroutine w głównym wątku sonduje dane i stosuje je.

Z tymi notatkami na bok, oto wzór, którego często używam do pracy w wątkach. Nie gwarantuję, że jest to styl najlepszych praktyk, ale spełnia swoje zadanie. (Komentarze lub zmiany do ulepszenia są mile widziane - Wiem, że wątki to bardzo głęboki temat, którego znam tylko podstawy)

using UnityEngine;
using System.Threading; 

public class MyThreadedBehaviour : MonoBehaviour
{

    bool _threadRunning;
    Thread _thread;

    void Start()
    {
        // Begin our heavy work on a new thread.
        _thread = new Thread(ThreadedWork);
        _thread.Start();
    }


    void ThreadedWork()
    {
        _threadRunning = true;
        bool workDone = false;

        // This pattern lets us interrupt the work at a safe point if neeeded.
        while(_threadRunning && !workDone)
        {
            // Do Work...
        }
        _threadRunning = false;
    }

    void OnDisable()
    {
        // If the thread is still running, we should shut it down,
        // otherwise it can prevent the game from exiting correctly.
        if(_threadRunning)
        {
            // This forces the while loop in the ThreadedWork function to abort.
            _threadRunning = false;

            // This waits until the thread exits,
            // ensuring any cleanup we do after this is safe. 
            _thread.Join();
        }

        // Thread is guaranteed no longer running. Do other cleanup tasks.
    }
}

Jeśli nie potrzebujesz ściśle dzielić pracy na wątki, aby uzyskać szybkość, a szukasz sposobu, aby nie blokować, aby reszta gry wciąż działała, lekkim rozwiązaniem w Unity są Coroutines . Są to funkcje, które mogą wykonać trochę pracy, a następnie przywrócić kontrolę nad silnikiem, aby kontynuować to, co robi, i płynnie wznowić w późniejszym czasie.

using UnityEngine;
using System.Collections;

public class MyYieldingBehaviour : MonoBehaviour
{ 

    void Start()
    {
        // Begin our heavy work in a coroutine.
        StartCoroutine(YieldingWork());
    }    

    IEnumerator YieldingWork()
    {
        bool workDone = false;

        while(!workDone)
        {
            // Let the engine run for a frame.
            yield return null;

            // Do Work...
        }
    }
}

Nie wymaga to żadnych specjalnych rozważań dotyczących czyszczenia, ponieważ silnik (o ile wiem) pozbywa się dla ciebie korupcji ze zniszczonych obiektów.

Cały lokalny stan metody jest zachowywany, gdy ustępuje i wznawia się, więc dla wielu celów działa tak, jakby działał nieprzerwanie w innym wątku (ale masz wszystkie wygody działania w głównym wątku). Musisz tylko upewnić się, że każda iteracja jest wystarczająco krótka, aby nie spowolnić twojego głównego wątku w nieuzasadniony sposób.

Zapewniając, że ważne operacje nie są oddzielone wydajnością, możesz uzyskać spójność zachowania jednowątkowego - wiedząc, że żaden inny skrypt lub system w głównym wątku nie może modyfikować danych, nad którymi pracujesz.

Linia zwrotu z zysku daje kilka opcji. Możesz...

  • yield return null wznowić po aktualizacji następnej ramki ()
  • yield return new WaitForFixedUpdate() wznowić po następnej FixedUpdate ()
  • yield return new WaitForSeconds(delay) wznowić po upływie określonego czasu gry
  • yield return new WaitForEndOfFrame() wznowić po zakończeniu renderowania GUI
  • yield return myRequestgdzie myRequestjest instancja WWW , która jest wznawiana po zakończeniu ładowania żądanych danych z sieci lub dysku.
  • yield return otherCoroutinegdzie otherCoroutinejest instancja Coroutine , którą można wznowić po otherCoroutinezakończeniu. Jest to często używane w formularzu yield return StartCoroutine(OtherCoroutineMethod())do łączenia wykonania z nową korupcją, która sama może ustąpić, kiedy chce.

    • Eksperymentalnie, pomijanie drugiego StartCoroutinei pisanie po prostu yield return OtherCoroutineMethod()osiąga ten sam cel, jeśli chcesz wykonać łańcuch w tym samym kontekście.

      Zawijanie wewnątrz StartCoroutinemoże być nadal przydatne, jeśli chcesz uruchomić zagnieżdżoną coroutine w połączeniu z drugim obiektem, takim jakyield return otherObject.StartCoroutine(OtherObjectsCoroutineMethod())

... w zależności od tego, kiedy chcesz, aby coroutine wzięła kolejną turę.

Lub, yield break;aby zatrzymać coroutine, zanim dotrze do końca, sposób, w jaki możesz użyć return;do wczesnego wyjścia z konwencjonalnej metody.

DMGregory
źródło
Czy istnieje sposób użycia coroutines do uzyskania wyników dopiero po tym, jak iteracja zajmie więcej niż 20 ms?
DarkDestry
@DarkDestry Możesz użyć instancji stopera, aby zmierzyć czas spędzony w wewnętrznej pętli i przejść do zewnętrznej pętli, w której ustąpisz, zanim zresetujesz stoper i wznowisz wewnętrzną pętlę.
DMGregory
1
Dobra odpowiedź! Chcę tylko dodać, że jeszcze jednym powodem, dla którego warto używać coroutines, jest to, że jeśli kiedykolwiek będziesz musiał budować dla webgl, będzie to działać, co nie zadziała, jeśli użyjesz wątków. Siedzę teraz z bólem głowy w wielkim projekcie: P
Mikael Högström
Dobra odpowiedź i dzięki. Zastanawiam się, co jeśli będę
musiał
@flankechen Uruchomienie i porzucenie wątku to stosunkowo kosztowne operacje, więc często wolimy, aby wątek był dostępny, ale uśpiony - używając takich rzeczy jak semafory lub monitory do sygnalizowania, kiedy mamy świeżą pracę do zrobienia. Chcesz opublikować nowe pytanie szczegółowo opisujące Twój przypadek użycia, a ludzie mogą zasugerować skuteczne sposoby jego osiągnięcia?
DMGregory
0

Możesz umieścić swoje ciężkie obliczenia w innym wątku, ale interfejs API Unity nie jest bezpieczny dla wątku, musisz wykonać je w głównym wątku.

Możesz wypróbować ten pakiet w Asset Store, który ułatwi ci korzystanie z wątków. http://u3d.as/wQg Możesz po prostu użyć tylko jednego wiersza kodu, aby uruchomić wątek i bezpiecznie uruchomić interfejs Unity API.

Qi Haoyan
źródło
0

@DMGregory wyjaśnił to bardzo dobrze.

Można używać zarówno wątków, jak i coroutines. Poprzednio w celu odciążenia głównego wątku, a później w celu przywrócenia kontroli do głównego wątku. Pchanie ciężkiego podnoszenia do oddzielnego gwintu wydaje się bardziej rozsądne. Dlatego kolejki zadań mogą być tym, czego szukasz.

Na Unity Wiki jest naprawdę dobry przykładowy skrypt JobQueue .

IndieForger
źródło