Aggiornamento: Nel 2018, Unity sta lanciando un sistema di lavoro C # come un modo per scaricare il lavoro e utilizzare più core della CPU.
La risposta seguente precede questo sistema. Funzionerà comunque, ma potrebbero esserci opzioni migliori disponibili in Unity moderna, a seconda delle tue esigenze. In particolare, il sistema di lavoro sembra risolvere alcune delle limitazioni a cui i thread creati manualmente possono accedere in modo sicuro, descritti di seguito. Gli sviluppatori che sperimentano il rapporto di anteprima eseguendo raycast e costruendo mesh in parallelo , ad esempio.
Inviterei gli utenti con esperienza nell'uso di questo sistema di lavoro ad aggiungere le proprie risposte che riflettono lo stato attuale del motore.
Ho usato il threading per attività pesanti in Unity in passato (in genere elaborazione di immagini e geometria) e non è drasticamente diverso dall'uso di thread in altre applicazioni C #, con due avvertenze:
Poiché Unity utilizza un sottoinsieme di .NET un po 'più vecchio, ci sono alcune nuove funzionalità di threading e librerie che non possiamo usare immediatamente, ma le basi sono tutte lì.
Come osserva Almo in un commento sopra, molti tipi di Unity non sono sicuri per i thread e genereranno eccezioni se si tenta di costruirli, utilizzarli o addirittura confrontarli con il thread principale. Cose da tenere a mente:
Un caso comune è verificare se un riferimento GameObject o Monobehaviour è null prima di provare ad accedere ai suoi membri. myUnityObject == null
chiama un operatore sovraccaricato per qualsiasi cosa discenda da UnityEngine.Object, ma System.Object.ReferenceEquals()
funziona in questo modo fino a un certo punto - ricorda solo che un Destroy () ed GameObject confronta come uguale a null usando il sovraccarico, ma non è ancora ReferenceEqual su null.
La lettura dei parametri dai tipi Unity è generalmente sicura su un altro thread (in quanto non genererà immediatamente un'eccezione fintanto che stai attento a verificare la presenza di null come sopra), ma nota l'avvertenza di Philipp che il thread principale potrebbe modificare lo stato mentre lo leggi. Dovrai essere disciplinato su chi è autorizzato a modificare cosa e quando, al fine di evitare la lettura di uno stato incoerente, che può portare a bug che possono essere diabolicamente difficili da rintracciare perché dipendono da tempistiche sub-millisecondi tra i thread che possiamo si riproducono a piacimento.
I membri statici casuali e temporali non sono disponibili. Crea un'istanza di System.Random per thread se hai bisogno di casualità e System.Diagnostics.Stopwatch se hai bisogno di informazioni sui tempi.
Le funzioni Mathf, Vector, Matrix, Quaternion e Color funzionano tutte bene tra i thread, quindi puoi eseguire la maggior parte dei tuoi calcoli separatamente
La creazione di GameObjects, il collegamento di Monobehaviours o la creazione / aggiornamento di trame, mesh, materiali, ecc. Devono avvenire sul thread principale. In passato, quando avevo bisogno di lavorare con questi, ho creato una coda produttore-consumatore, in cui il mio thread di lavoro prepara i dati grezzi (come una vasta gamma di vettori / colori da applicare a una mesh o trama), e un aggiornamento o Coroutine sui thread di thread principali per i dati e lo applica.
Con quelle note fuori mano, ecco uno schema che uso spesso per il lavoro a thread. Non garantisco che sia uno stile best practice, ma fa il suo lavoro. (I commenti o le modifiche da migliorare sono i benvenuti - So che il threading è un argomento molto profondo di cui conosco solo le basi)
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.
}
}
Se non hai la necessità di dividere il lavoro tra i thread per la velocità e stai solo cercando un modo per renderlo non bloccante in modo che il resto del gioco continui a ticchettare, una soluzione più leggera in Unity è Coroutine . Queste sono funzioni che possono fare un po 'di lavoro, quindi restituire il controllo al motore per continuare quello che sta facendo e riprendere senza problemi in un secondo momento.
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...
}
}
}
Questo non ha bisogno di particolari considerazioni di pulizia, dal momento che il motore (per quanto posso dire) si sbarazza delle coroutine dagli oggetti distrutti per te.
Tutto lo stato locale del metodo viene preservato quando si produce e riprende, quindi per molti scopi è come se fosse in esecuzione ininterrotta su un altro thread (ma hai tutte le comodità di essere eseguito sul thread principale). Devi solo assicurarti che ogni iterazione sia abbastanza breve da non rallentare irragionevolmente il tuo thread principale.
Assicurando che le operazioni importanti non siano separate da un rendimento, è possibile ottenere la coerenza del comportamento a thread singolo, sapendo che nessun altro script o sistema sul thread principale può modificare i dati su cui si sta lavorando.
La riga di ritorno del rendimento offre alcune opzioni. Puoi...
yield return null
riprendere dopo l'aggiornamento del frame successivo ()
yield return new WaitForFixedUpdate()
riprendere dopo il prossimo FixedUpdate ()
yield return new WaitForSeconds(delay)
riprendere dopo un certo periodo di tempo di gioco
yield return new WaitForEndOfFrame()
per riprendere al termine del rendering della GUI
yield return myRequest
dove si myRequest
trova un'istanza WWW , per riprendere una volta terminato il caricamento dei dati richiesti dal Web o dal disco.
yield return otherCoroutine
dove otherCoroutine
si trova un'istanza Coroutine , da riprendere dopo il otherCoroutine
completamento. Questo è spesso usato nella forma yield return StartCoroutine(OtherCoroutineMethod())
per concatenare l'esecuzione a un nuovo coroutine che può da solo cedere quando vuole.
Sperimentalmente, saltare il secondo StartCoroutine
e scrivere semplicemente yield return OtherCoroutineMethod()
raggiunge lo stesso obiettivo se si desidera eseguire l'esecuzione in serie nello stesso contesto.
L'avvolgimento all'interno di a StartCoroutine
può essere comunque utile se si desidera eseguire la coroutine nidificata in associazione con un secondo oggetto, ad esempioyield return otherObject.StartCoroutine(OtherObjectsCoroutineMethod())
... a seconda di quando vuoi che la coroutine faccia il suo turno successivo.
O yield break;
per fermare la coroutine prima che raggiunga la fine, il modo in cui potresti usare return;
per uscire presto da un metodo convenzionale.