Come non congelare il thread principale in Unity?


33

Ho un algoritmo di generazione di livello che è pesante dal punto di vista computazionale. Pertanto, chiamarlo provoca sempre il blocco della schermata di gioco. Come posso posizionare la funzione su un secondo thread mentre il gioco continua a visualizzare una schermata di caricamento per indicare che il gioco non è bloccato?


1
È possibile utilizzare il sistema di threading per eseguire altri processi in background ... ma potrebbero non chiamare nulla nell'API Unity poiché tale roba non è thread-safe. Sto solo commentando perché non l'ho fatto e non posso darti rapidamente un codice di esempio.
Almo,

Uso Listdi azioni per memorizzare le funzioni che si desidera chiamare nel thread principale. blocca e copia l' Actionelenco nella Updatefunzione all'elenco temporaneo, cancella l'elenco originale, quindi esegui il Actioncodice in quello Listsul thread principale. Vedi UnityThread dal mio altro post su come farlo. Ad esempio, per chiamare una funzione sul thread principale, UnityThread.executeInUpdate(() => { transform.Rotate(new Vector3(0f, 90f, 0f)); });
Programmatore

Risposte:


48

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:

  1. 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ì.

  2. 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 == nullchiama 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 myRequestdove si myRequesttrova un'istanza WWW , per riprendere una volta terminato il caricamento dei dati richiesti dal Web o dal disco.
  • yield return otherCoroutinedove otherCoroutinesi trova un'istanza Coroutine , da riprendere dopo il otherCoroutinecompletamento. 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 StartCoroutinee 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 StartCoroutinepuò 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.


C'è un modo per usare le coroutine per produrre solo dopo che l'iterazione richiede più di 20 ms?
DarkDestry,

@DarkDestry È possibile utilizzare un'istanza del cronometro per calcolare il tempo impiegato nel ciclo interno e passare a un ciclo esterno in cui si cede prima di ripristinare il cronometro e riprendere il ciclo interno.
DMGregory

1
Buona risposta! Voglio solo aggiungere che un motivo in più per usare le coroutine è che se mai hai bisogno di costruire per webgl funzionerà che non funzionerà se usi i thread. Seduto con quel mal di testa in un grande progetto in questo momento: P
Mikael Högström

Buona risposta e grazie Mi chiedo solo cosa succede se devo interrompere e riavviare il thread più volte, provo a interrompere e avviare, mi fa errore
flankechen

@flankechen L'avvio e lo smontaggio del thread sono operazioni relativamente costose, quindi spesso preferiremo mantenere il thread disponibile ma inattivo - usando cose come semafori o monitor per segnalare quando abbiamo del lavoro da fare. Vuoi pubblicare una nuova domanda che descriva in dettaglio il tuo caso d'uso e la gente può suggerire modi efficaci per raggiungerlo?
DMGregory

0

Puoi inserire il tuo calcolo pesante in un altro thread, ma l'API di Unity non è thread-safe, devi eseguirli nel thread principale.

Bene, puoi provare questo pacchetto su Asset Store ti aiuterà a usare il threading più facilmente. http://u3d.as/wQg Puoi semplicemente usare solo una riga di codice per avviare un thread ed eseguire l'API Unity in modo sicuro.



0

@DMGregory lo ha spiegato molto bene.

È possibile utilizzare sia la filettatura che le coroutine. Precedentemente per scaricare il thread principale e successivamente per restituire il controllo al thread principale. Spingere il sollevamento pesante su filo separato sembra più ragionevole. Pertanto, le code di lavoro potrebbero essere ciò che stai cercando.

C'è un ottimo script di esempio di JobQueue su Unity Wiki.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.