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:
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.
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 == null
wywoł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 myRequest
gdzie myRequest
jest instancja WWW , która jest wznawiana po zakończeniu ładowania żądanych danych z sieci lub dysku.
yield return otherCoroutine
gdzie otherCoroutine
jest instancja Coroutine , którą można wznowić po otherCoroutine
zakoń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 StartCoroutine
i 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 StartCoroutine
moż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.