Update: Unity führt 2018 ein C # -Jobsystem ein, um die Arbeit auszulagern und mehrere CPU-Kerne zu nutzen.
Die folgende Antwort geht diesem System voraus. Es wird immer noch funktionieren, aber in modernen Unity gibt es je nach Ihren Anforderungen möglicherweise bessere Optionen. Insbesondere scheint das Auftragssystem einige der Einschränkungen zu beseitigen, auf die manuell erstellte Threads sicher zugreifen können (siehe unten). Entwickler, die mit dem Vorschaubericht experimentieren, führen beispielsweise Raycasts durch und konstruieren parallel Netze .
Ich möchte Benutzer mit Erfahrung in der Verwendung dieses Job-Systems einladen, ihre eigenen Antworten hinzuzufügen, die den aktuellen Status der Engine widerspiegeln.
Ich habe in der Vergangenheit Threading für Schwergewichtsaufgaben in Unity verwendet (normalerweise Bild- und Geometrieverarbeitung), und es unterscheidet sich nicht wesentlich von der Verwendung von Threads in anderen C # -Anwendungen, mit zwei Einschränkungen:
Da Unity eine etwas ältere Teilmenge von .NET verwendet, gibt es einige neuere Threading-Funktionen und -Bibliotheken, die wir nicht sofort verwenden können, aber die Grundlagen sind alle vorhanden.
Wie Almo in einem Kommentar oben bemerkt, sind viele Unity-Typen nicht threadsicher und lösen Ausnahmen aus, wenn Sie versuchen, sie außerhalb des Hauptthreads zu konstruieren, zu verwenden oder sogar zu vergleichen. Dinge zu beachten:
Ein häufiger Fall ist die Überprüfung, ob eine GameObject- oder Monobehaviour-Referenz null ist, bevor versucht wird, auf ihre Mitglieder zuzugreifen. myUnityObject == null
ruft einen überladenen Operator für alles auf, was von UnityEngine.Object abstammt, System.Object.ReferenceEquals()
funktioniert jedoch bis zu einem gewissen Grad. Denken Sie daran, dass ein Destroy () ed-GameObject mit der Überladung gleich null ist, aber noch nicht ReferenceEqual to null ist.
Das Lesen von Parametern aus Unity-Typen ist normalerweise in einem anderen Thread sicher (insofern wird nicht sofort eine Ausnahme ausgelöst, solange Sie darauf achten, wie oben beschrieben auf Nullen zu prüfen). Beachten Sie jedoch die Warnung von Philipp, dass der Haupt-Thread möglicherweise den Status ändert während du es liest. Sie müssen sich darüber im Klaren sein, wer was und wann ändern darf, um zu vermeiden, dass ein inkonsistenter Status gelesen wird. Dies kann zu Fehlern führen, die sich nur schwer aufspüren lassen, da sie von Zeitabständen im Millisekundenbereich zwischen den Threads abhängen, die wir können nicht nach Belieben reproduzieren.
Zufällige und zeitstatische Elemente sind nicht verfügbar. Erstellen Sie eine Instanz von System.Random pro Thread, wenn Sie Zufälligkeiten benötigen, und System.Diagnostics.Stopwatch, wenn Sie Zeitinformationen benötigen.
Mathf-Funktionen, Vektor-, Matrix-, Quaternions- und Farbstrukturen funktionieren alle innerhalb von Threads einwandfrei, sodass Sie die meisten Berechnungen separat ausführen können
Das Erstellen von GameObjects, das Anhängen von Monobehaviours oder das Erstellen / Aktualisieren von Texturen, Meshes, Materialien usw. muss im Haupt-Thread erfolgen. In der Vergangenheit, als ich damit arbeiten musste, habe ich eine Produzenten-Konsumenten-Warteschlange eingerichtet, in der mein Arbeitsthread die Rohdaten aufbereitet (wie eine große Anzahl von Vektoren / Farben, die auf ein Netz oder eine Textur angewendet werden sollen). und ein Update oder eine Coroutine im Haupt-Thread fragt nach Daten und wendet diese an.
Mit diesen Notizen aus dem Weg, hier ist ein Muster, das ich oft für Zwirnarbeiten verwende. Ich kann nicht garantieren, dass es sich um einen Best-Practice-Stil handelt, aber er erledigt die Arbeit. (Kommentare oder Änderungen zur Verbesserung sind willkommen - ich weiß, dass Threading ein sehr tiefes Thema ist, von dem ich nur die Grundlagen kenne.)
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.
}
}
Wenn Sie die Arbeit aus Geschwindigkeitsgründen nicht unbedingt auf mehrere Threads aufteilen müssen und nur nach einer Möglichkeit suchen, sie blockierungsfrei zu machen, damit der Rest Ihres Spiels weiter läuft, ist Coroutines eine leichtere Lösung . Dies sind Funktionen, die einige Arbeit erledigen können, um dann die Steuerung an den Motor zurückzugeben, um fortzufahren, was er tut, und zu einem späteren Zeitpunkt nahtlos fortzufahren.
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...
}
}
}
Dies erfordert keine besonderen Reinigungsüberlegungen, da der Motor (soweit ich das beurteilen kann) Coroutinen von zerstörten Objekten für Sie entfernt.
Der gesamte lokale Status der Methode bleibt erhalten, wenn sie ausgegeben und fortgesetzt wird. In vielen Fällen ist es also so, als würde sie auf einem anderen Thread ohne Unterbrechung ausgeführt (Sie verfügen jedoch über alle Annehmlichkeiten, um auf dem Hauptthread ausgeführt zu werden). Sie müssen nur sicherstellen, dass jede Iteration kurz genug ist, damit Ihr Haupt-Thread nicht unangemessen verlangsamt wird.
Indem Sie sicherstellen, dass wichtige Vorgänge nicht durch einen Ertrag getrennt sind, können Sie die Konsistenz des Singlethread-Verhaltens erzielen - mit dem Wissen, dass kein anderes Skript oder System im Hauptthread Daten ändern kann, an denen Sie gerade arbeiten.
Die Yield Return-Linie bietet Ihnen einige Möglichkeiten. Sie können...
yield return null
wird nach dem Update des nächsten Frames fortgesetzt ()
yield return new WaitForFixedUpdate()
wird nach dem nächsten FixedUpdate () fortgesetzt
yield return new WaitForSeconds(delay)
nach Ablauf einer bestimmten Spielzeit wieder aufzunehmen
yield return new WaitForEndOfFrame()
wird fortgesetzt, nachdem die GUI das Rendern beendet hat
yield return myRequest
Wo myRequest
ist eine WWW- Instanz, um fortzufahren, sobald die angeforderten Daten aus dem Web oder von der CD geladen wurden?
yield return otherCoroutine
Wo otherCoroutine
ist eine Coroutine-Instanz , die nach otherCoroutine
Abschluss fortgesetzt werden soll ? Dies wird häufig in der Form verwendet, yield return StartCoroutine(OtherCoroutineMethod())
um die Ausführung an eine neue Koroutine zu ketten, die selbst nachgeben kann, wenn sie dies möchte.
Experimentell kann durch Überspringen der zweiten StartCoroutine
und einfaches Schreiben yield return OtherCoroutineMethod()
das gleiche Ziel erreicht werden, wenn Sie die Ausführung im gleichen Kontext verketten möchten.
Das Einschließen in ein StartCoroutine
kann dennoch nützlich sein, wenn Sie die verschachtelte Coroutine in Verbindung mit einem zweiten Objekt ausführen möchten, zyield return otherObject.StartCoroutine(OtherObjectsCoroutineMethod())
... je nachdem, wann die Coroutine ihre nächste Runde machen soll.
Oder yield break;
um die Coroutine zu stoppen, bevor sie das Ende erreicht, wie Sie es return;
von einer herkömmlichen Methode gewohnt sind.