Sincronizzazione tra thread della logica di gioco e thread di rendering


16

In che modo una logica di gioco e un rendering separati? So che sembrano esserci già domande qui che chiedono esattamente questo, ma le risposte non sono soddisfacenti per me.

Da quello che ho capito finora il punto di separarli in diversi thread è in modo che la logica di gioco possa iniziare immediatamente a correre per il prossimo tick invece di aspettare il prossimo vsync in cui il rendering ritorna finalmente dalla chiamata dello swapbuffer che sta bloccando.

Ma in particolare quali strutture di dati vengono utilizzate per prevenire le condizioni di competizione tra il thread della logica di gioco e il thread di rendering. Presumibilmente il thread di rendering necessita dell'accesso a varie variabili per capire cosa disegnare, ma la logica di gioco potrebbe aggiornare queste stesse variabili.

Esiste una tecnica standard di fatto per gestire questo problema. Forse come copiare i dati necessari al thread di rendering dopo ogni esecuzione della logica di gioco. Qualunque sia la soluzione, l'overhead della sincronizzazione o qualsiasi altra cosa sarà inferiore alla semplice esecuzione di tutto il thread singolo?


1
Odio spammare un link, ma penso che sia un'ottima lettura e che dovrebbe rispondere a tutte le tue domande: altdevblogaday.com/2011/07/03/threading-and-your-game-loop
Roy T.


1
Quei link danno il tipico risultato finale che si vorrebbe, ma non approfondire come farlo. Copieresti l'intero grafico della scena per ogni fotogramma o qualcos'altro? Le discussioni sono troppo alte e vaghe.
user782220

Pensavo che i collegamenti fossero abbastanza espliciti su quanto stato copiato in ciascun caso. per esempio. (dal 1 ° link) "Un batch contiene tutte le informazioni necessarie per disegnare un frame, ma non contiene altri stati del gioco." o (dal secondo collegamento) "I dati devono ancora essere condivisi, ma ora invece che ciascun sistema acceda a una posizione di dati comune per dire, ottenere dati di posizione o orientamento, ogni sistema ha la propria copia" (Vedi in particolare 3.2.2 - Stato Manager)
DMGregory

Chiunque abbia scritto l'articolo Intel non sembra sapere che il threading di alto livello sia una pessima idea. Nessuno fa qualcosa di così stupido. Improvvisamente l'intera applicazione deve comunicare attraverso canali specializzati e ci sono blocchi e / o enormi scambi di stato coordinati ovunque. Per non parlare del fatto che non sarà possibile stabilire quando i dati inviati verranno elaborati, quindi è estremamente difficile ragionare su ciò che fa il codice. È molto più facile copiare i dati di scena rilevanti (immutabili come puntatori contati ref., Mutabili - per valore) in un punto e lasciare che il sottosistema li risolva come vuole.
snake5,

Risposte:


1

Ho lavorato sulla stessa cosa. L'ulteriore preoccupazione è che OpenGL (e per quanto ne sappia, OpenAL) e un certo numero di altre interfacce hardware, sono effettivamente macchine a stati che non vanno d'accordo con il fatto di essere chiamate da più thread. Non penso che il loro comportamento sia nemmeno definito, e per LWJGL (forse anche JOGL) spesso genera un'eccezione.

Quello che ho finito per fare è stato creare una sequenza di thread che implementavano un'interfaccia specifica e caricarli sullo stack di un oggetto di controllo. Quando quell'oggetto riceveva un segnale per chiudere il gioco, passava attraverso ogni thread, chiamava un metodo ceaseOperations () implementato e aspettava che si chiudessero prima di chiudersi. I dati universali che potrebbero essere rilevanti per il rendering del suono, della grafica o di qualsiasi altro dato vengono conservati in una sequenza di oggetti che sono volatili o universalmente disponibili per tutti i thread ma mai conservati nella memoria dei thread. C'è una leggera penalità prestazionale lì, ma usato correttamente, mi ha permesso di assegnare in modo flessibile l'audio a un thread, la grafica a un altro, la fisica a un altro, e così via senza legarli al tradizionale (e temuto) "loop di gioco".

Quindi, di regola, tutte le chiamate OpenGL passano attraverso il thread Graphics, tutte OpenAL tramite il thread Audio, tutti gli input attraverso il thread Input e tutto ciò di cui il thread di controllo organizzativo deve preoccuparsi è la gestione dei thread. Lo stato del gioco si svolge nella classe GameState, a cui tutti possono dare un'occhiata come devono. Se mai decidessi che, diciamo, JOAL è stato datato e voglio usare invece la nuova edizione di JavaSound, ho appena implementato un thread diverso per Audio.

Spero che tu veda quello che sto dicendo, ho già qualche migliaio di righe su questo progetto. Se vuoi che provi a mettere insieme un campione, vedrò cosa posso fare.


Il problema che alla fine dovrai affrontare è che questa configurazione non si adatta particolarmente bene su una macchina multi-core. Sì, ci sono aspetti di un gioco che generalmente sono meglio serviti nel proprio thread come l'audio, ma gran parte del resto del loop di gioco può effettivamente essere gestito in serie insieme alle attività del pool di thread. Se il tuo pool di thread supporta maschere di affinità, puoi facilmente fare la fila per dire che le attività di rendering devono essere eseguite sullo stesso thread e che il tuo programmatore di thread gestisca le code di lavoro dei thread e faccia rubare se necessario fornendo supporto multi-thread e multi-core.
Naros,

1

Di solito, la logica che si occupa del rendering della grafica passa (e la loro pianificazione, e quando verranno eseguiti, ecc.) È gestita da un thread separato. Tuttavia quel thread è già implementato (attivo e funzionante) dalla piattaforma che usi per sviluppare il tuo loop di gioco (e gioco).

Quindi, al fine di ottenere un loop di gioco in cui la logica di gioco si aggiorna indipendentemente dal programma di aggiornamento della grafica non è necessario creare thread aggiuntivi, basta toccare il thread già esistente per detti aggiornamenti grafici.

Questo dipende dalla piattaforma che stai utilizzando. Per esempio:

  • se lo stai facendo nella maggior parte delle piattaforme correlate a Open GL ( GLUT per C / C ++ , JOLG per Java , azione correlata a OpenGL ES di Android ) di solito ti daranno un metodo / funzione che viene periodicamente chiamato dal thread di rendering e che tu può integrarsi nel tuo loop di gioco (senza far dipendere le iterazioni del gameloop da quando viene chiamato quel metodo). Per GLUT usando C, fai qualcosa del genere:

    glutDisplayFunc (myFunctionForGraphicsDrawing);

    glutIdleFunc (myFunctionForUpdatingState);

  • in JavaScript, puoi usare Web Workers poiché non esiste il multi-threading (che puoi raggiungere a livello di programmazione) , puoi anche utilizzare il meccanismo "requestAnimationFrame" per ricevere una notifica quando verrà pianificato un nuovo rendering grafico e fare gli aggiornamenti dello stato del gioco di conseguenza .

Fondamentalmente quello che vuoi è un loop di gioco a fasi miste: hai del codice che aggiorna lo stato del gioco e che viene chiamato all'interno del thread principale del tuo gioco e vuoi anche attingere periodicamente (o essere richiamato da) il già thread di rendering della grafica esistente per informazioni su quando è il momento di aggiornare la grafica.


0

In Java c'è la parola chiave "sincronizzata", che blocca le variabili che le passi per renderle thread-safe. In C ++ puoi ottenere la stessa cosa usando Mutex. Per esempio:

Giava:

synchronized(a){
    //code using a
}

C ++:

mutex a_mutex;

void f(){
    a_mutex.lock();
    //code using a
    a_mutex.unlock();
}

Il blocco delle variabili assicura che non cambino durante l'esecuzione del codice che lo segue, quindi le variabili non vengono modificate dal thread di aggiornamento durante il rendering (in realtà cambiano, ma dal punto di vista del thread di rendering non lo fanno ' t). Devi stare attento con la parola chiave sincronizzata in Java, poiché assicura solo che il puntatore alla variabile / Oggetto non cambi. Gli attributi possono ancora cambiare senza cambiare il puntatore. Per riflettere su questo, potresti copiare l'oggetto da solo o chiamare sincronizzato su tutti gli attributi dell'oggetto che non vuoi cambiare.


1
I mutex non sono necessariamente la risposta qui perché l'OP non dovrebbe solo disaccoppiare la logica e il rendering del gioco, ma vuole anche evitare qualsiasi stallo della capacità di un thread di andare avanti nella sua elaborazione indipendentemente da dove potrebbe trovarsi l'altro thread nella sua elaborazione ciclo continuo.
Naros,

0

Quello che ho visto generalmente per gestire la comunicazione con thread di logica / rendering è triplicare il buffer dei dati. In questo modo il thread di rendering ha detto bucket 0 da cui legge. Il thread logico utilizza il bucket 1 come sorgente di input per il frame successivo e scrive i dati del frame nel bucket 2.

Nei punti di sincronizzazione, gli indici del significato di ciascuno dei tre bucket vengono scambiati in modo che i dati del frame successivo vengano dati al thread di rendering e che il thread logico possa continuare in avanti.

Ma non c'è necessariamente un motivo per dividere il rendering e la logica nei rispettivi thread. Puoi infatti mantenere il loop di gioco seriale e disaccoppiare la frequenza dei fotogrammi di rendering dal passaggio logico usando l'interpolazione. Per sfruttare i processori multi-core che utilizzano questo tipo di installazione, è necessario disporre di un pool di thread che opera su gruppi di attività. Queste attività possono essere semplicemente cose come piuttosto che iterare un elenco di oggetti da 0 a 100, si esegue l'iterazione dell'elenco in 5 bucket da 20 su 5 thread aumentando efficacemente le prestazioni ma non complicando eccessivamente il ciclo principale.


0

Questo è un vecchio post, ma viene ancora visualizzato quindi volevo aggiungere i miei 2 centesimi qui.

Prima elenca i dati che dovrebbero essere memorizzati nell'interfaccia utente / thread di visualizzazione vs thread logico. Nel thread dell'interfaccia utente potresti includere mesh 3d, trame, informazioni sulla luce e una copia dei dati di posizione / rotazione / direzione.

Nel thread della logica di gioco potresti aver bisogno di dimensioni degli oggetti di gioco in 3d, primitive di delimitazione (sfera, cubo), dati mesh 3D semplificati (ad esempio per collisioni dettagliate), tutti gli attributi che influenzano il movimento / comportamento, come la velocità dell'oggetto, il rapporto di virata, ecc., e anche i dati di posizione / rotazione / direzione.

Se si confrontano due elenchi, si può vedere che solo la copia dei dati di posizione / rotazione / direzione deve essere passata dalla logica al thread dell'interfaccia utente. Potrebbe anche essere necessario un tipo di ID di correlazione per determinare a quale oggetto di gioco appartengono questi dati.

Il modo in cui lo fai dipende dalla lingua con cui stai lavorando. In Scala è possibile utilizzare Software Transactional Memory, in Java / C ++ una sorta di blocco / sincronizzazione. Mi piacciono i dati immutabili, quindi tendo a restituire un nuovo oggetto immutabile per ogni aggiornamento. Questo è un po 'di spreco di memoria, ma con i computer moderni non è un grosso problema. Tuttavia, se si desidera bloccare strutture di dati condivise, è possibile farlo. Dai un'occhiata alla classe Exchanger in Java, l'uso di due o più buffer può velocizzare le cose.

Prima di iniziare a condividere dati tra thread, capire quanti dati è effettivamente necessario passare. Se hai un ottetto che divide il tuo spazio 3d e puoi vedere 5 oggetti di gioco su 10 oggetti in totale, anche se la tua logica ha bisogno di aggiornare tutti i 10 devi ridisegnare solo i 5 che stai vedendo. Per ulteriori informazioni, consulta questo blog: http://gameprogrammingpatterns.com/game-loop.html Non si tratta di sincronizzazione, ma mostra come la logica di gioco è separata dal display e quali sfide devi superare (FPS). Spero che sia di aiuto,

marchio

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.