Come può un gioco gestire tutti i personaggi contemporaneamente?


31

Questa domanda è solo per conoscere come un gioco può gestire così tanti personaggi contemporaneamente. Sono nuovo di gioco, quindi chiedo scusa in anticipo.

Esempio

Sto creando un gioco di difesa della torre in cui ci sono 15 slot per torri in cui sono costruite le torri e ogni torre espelle un proiettile ad una certa velocità; diciamo che ogni secondo, 2 proiettili vengono creati da ciascuna delle torri e ci sono nemici che marciano sul campo di battaglia, diciamo 70 (ognuno con 10 tipi di attributi come HP, mana, ecc., che cambieranno mentre si muovono attorno al campo di battaglia).

Sommario

Numero di torri = 15
proiettili creati da ogni torre al secondo = 2
Numero totale di proiettili creati al secondo = 30
unità nel conteggio dei campi di battaglia = 70

Ora, il gioco gestisce quei 30 proiettili e 70 unità gestendoli su 100 thread diversi (che è troppo per un PC) o 1 thread che li sposta tutti, riduce il loro valore, ecc. (Che sarà un po 'lento , Credo)?

Non ne ho idea, quindi qualcuno può guidarmi su come funzionerà?


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
MichaelHouse

Aggiungendo alle altre risposte ... un esempio di alcuni giochi enormi. Skyrim ha avuto la maggior parte dell'aggiornamento della logica di gioco su un singolo thread. Il modo in cui lo gestisce così bene è che gli NPC distanti (NPC distanti miglia) sono approssimati secondo il loro programma. La maggior parte degli MMO aggiorna la logica di gioco su un singolo thread, MA ogni porzione di una mappa esiste su un thread o un server rack diverso.
moonshineTheleocat,

Risposte:


77

Ora in che modo il gioco gestisce quei 30 proiettili e 70 unità gestendoli su 100 thread diversi

No, non farlo mai. Non creare mai un nuovo thread per risorsa, ciò non si adatta alle reti, né all'aggiornamento delle entità. (Qualcuno ricorda i tempi in cui avevi un thread per leggere per socket in Java?)

1 thread che li sposta tutti riduce il loro valore ecc.?

Sì, per cominciare, questa è la strada da percorrere. I "grandi motori" dividono un po 'di lavoro tra i thread, ma questo non è necessario per iniziare un gioco semplice come un gioco di difesa della torre. Probabilmente c'è ancora più lavoro da fare per ogni tick che farai anche in questo thread. Oh sì, e il rendering ovviamente.

(che sarà un po 'lento penso)

Bene ... Qual è la tua definizione di lento ? Per 100 entità, non dovrebbe richiedere più di mezzo millisecondo, probabilmente anche meno, a seconda della qualità del codice e della lingua con cui stai lavorando. E anche se ci vogliono due millisecondi completi, è comunque abbastanza buono da raggiungere i 60 tps (tick al secondo, in questo caso non si parla di frame).


34
Più come mezzo microsecondo, a meno che tu non stia facendo qualcosa di strano. Ma soprattutto, suddividere il lavoro su più thread peggiorerà tutto , non meglio. Per non parlare del fatto che il multithreading è estremamente difficile.
Luaan,

13
+1 La maggior parte dei moderni motori di gioco sta eseguendo il rendering di migliaia o addirittura decine di migliaia di poligoni in tempo reale, il che è molto più intenso del tracciamento del movimento di soli 100 oggetti in memoria.
phyrfox,

1
"Un gioco semplice come un gioco di difesa della torre." Hmm ... hai mai giocato a Defense Grid: The Awakening o al suo sequel?
Mason Wheeler,

4
"Non creare mai un nuovo thread per risorsa, questo non si adatta alle reti ..." ehm alcune architetture molto scalabili fanno esattamente questo!
NPSF3000

2
@BarafuAlbino: che cosa strana da dire. Esistono molte valide ragioni per creare più thread rispetto ai core disponibili. È un compromesso di complessità / prestazioni / ecc. Come qualsiasi altra decisione di progettazione.
Dietrich Epp,

39

La regola numero uno del multithreading è: non utilizzarla a meno che non sia necessario parallelizzare su più core della CPU per prestazioni o reattività. Un requisito "xey dovrebbe avvenire contemporaneamente dal punto di vista degli utenti" non è ancora una ragione sufficiente per utilizzare il multithreading.

Perché?

Il multithreading è difficile. Non hai alcun controllo su quando ogni thread viene eseguito, il che può comportare tutti i tipi di problemi impossibili da riprodurre ("condizioni di gara"). Esistono metodi per evitarlo (blocchi di sincronizzazione, sezioni critiche), ma questi presentano un proprio insieme di problemi ("deadlock").

Di solito i giochi che trattano un numero così basso di oggetti come poche centinaia (sì, non è molto nello sviluppo del gioco) di solito li elaborano in modo seriale ogni tick di logica usando un forloop comune .

Anche le CPU per smartphone relativamente più deboli possono funzionare miliardi di istruzioni al secondo. Ciò significa che anche quando la logica di aggiornamento dei tuoi oggetti è complessa e richiede circa 1000 istruzioni per oggetto e tick, e stai mirando a generosi 100 tick al secondo, hai una capacità della CPU sufficiente per decine di migliaia di oggetti. Sì, questo è un calcolo del back-of-the-envelope notevolmente semplificato, ma ti dà un'idea.

Inoltre, la saggezza comune nello sviluppo del gioco è che le logiche del gioco sono molto raramente il collo di bottiglia di un gioco. La parte critica per le prestazioni è quasi sempre la grafica. Sì, anche per i giochi 2D.


1
"La regola numero uno del multithreading è: non utilizzarla a meno che non sia necessario parallelizzare su più core della CPU per prestazioni o reattività." Potrebbe essere vero per lo sviluppo del gioco (ma ne dubito persino). Quando si lavora con sistemi in tempo reale, il motivo principale per aggiungere discussioni è rispettare le scadenze e per semplicità logica.
Sam,

6
@Sam Il rispetto delle scadenze nei sistemi in tempo reale è un caso in cui è necessario il multithreading per la reattività. Ma anche lì la semplicità logica che apparentemente raggiungi attraverso il threading è spesso insidiosa, perché crea complessità nascosta sotto forma di deadlock, condizioni di razza e fame di risorse.
Philipp,

Sfortunatamente, molte volte ho visto la logica del gioco impantanare l'intero gioco se ci sono problemi di pathfinding.
Loren Pechtel,

1
@LorenPechtel Ho visto anche questo. Ma di solito era risolvibile non eseguendo calcoli di percorsi non necessari (come ricalcolare ogni singolo percorso su ogni singolo segno di spunta), memorizzando nella cache percorsi richiesti di frequente, utilizzando la ricerca di percorsi su più livelli e utilizzando algoritmi di ricerca di percorsi più appropriati. Questo è qualcosa in cui un abile programmatore può trovare molto potenziale di ottimizzazione.
Philipp,

1
@LorenPechtel Ad esempio in un gioco di difesa della torre, potresti usare il fatto che in genere ci sono solo una manciata di punti di destinazione. Quindi potresti eseguire l'algoritmo di Dijkstra per ogni destinazione per calcolare una mappa di direzione che guida tutte le unità. Anche in un ambiente dinamico in cui è necessario ricalcolare queste mappe ogni frame, questo dovrebbe essere comunque conveniente.
Codici A Caos il

26

Le altre risposte hanno gestito il threading e il potere dei computer moderni. Per rispondere alla domanda più grande, quello che stai cercando di fare qui è evitare situazioni "n quadrate".

Ad esempio, se hai 1000 proiettili e 1000 nemici, l'ingenua soluzione è semplicemente controllarli tutti l'uno contro l'altro.

Questo significa che finisci con p * e = 1,000 * 1,000 = 1,000,000 di assegni diversi! Questo è O (n ^ 2).

D'altra parte, se organizzi meglio i tuoi dati, puoi evitarlo molto.

Ad esempio, se elenchi su ogni quadrato della griglia quali sono i nemici in quel quadrato, puoi scorrere tra i tuoi 1000 proiettili e controllare il quadrato sulla griglia. Ora devi solo controllare ogni proiettile contro il quadrato, questo è O (n). Invece di un milione di controlli per ogni fotogramma, ne servono solo un migliaio.

Pensare all'organizzazione dei dati e all'elaborazione in modo efficiente grazie a quell'organizzazione è la più grande ottimizzazione singola che tu possa mai fare.


1
In alternativa alla memorizzazione dell'intera griglia in memoria solo per tenere traccia di alcuni elementi, è possibile utilizzare anche b-tree, uno per ogni asse, per cercare rapidamente possibili candidati per collisioni, ecc. Alcuni motori lo fanno anche automaticamente " "; specifichi le regioni colpite e chiedi un elenco di collisioni e la libreria te lo fornisce. Questo è uno dei tanti motivi per cui gli sviluppatori dovrebbero usare un motore invece di scrivere da zero (quando possibile, ovviamente).
phyrfox,

@phyrfox Certamente, ci sono molti modi diversi per farlo - a seconda del tuo caso d'uso che è meglio varierà sostanzialmente.
Tim B

17

Non creare thread per risorsa / oggetto ma per sezione della logica del programma. Per esempio:

  1. Thread per aggiornare unità e proiettili - thread logico
  2. Discussione per il rendering dello schermo - Thread GUI
  3. Thread per rete (es. Multiplayer) - IO thread

Il vantaggio di ciò è che la tua GUI (es. Pulsanti) non si blocca necessariamente se la tua logica è lenta. L'utente può ancora mettere in pausa e salvare il gioco. È anche utile per preparare il tuo gioco al multiplayer, ora che separi la grafica dalla logica.


1
Per un principiante non consiglierei l'uso di thread grafici e logici separati, poiché a meno che non copiate i dati richiesti, il rendering dello stato del gioco richiede l'accesso in lettura allo stato del gioco, quindi non è possibile modificare lo stato del gioco mentre lo si disegna.
Codici InCos

1
Non disegnare troppo spesso (ad es. Più di 50 volte al secondo) è un po 'importante e questa domanda riguardava le prestazioni. Il programma di divisione è la cosa più semplice da fare per un reale vantaggio in termini di prestazioni. È vero che ciò richiede una certa conoscenza dei thread, ma acquisire la conoscenza è utile.
Tomáš Zato - Ripristina Monica il

Dividere un programma in più thread è la cosa più difficile da fare per un programmatore. I bug più fastidiosi derivano dal multi-threading ed è una seccatura enorme e la maggior parte delle volte non ne vale la pena - Prima regola: controlla se hai un problema di prestazioni, ALLORA ottimizza. E ottimizza proprio dove si trova il collo di bottiglia. Forse un singolo thread esterno per un certo algoritmo complesso. Ma anche allora devi pensare a come avanzerà la logica del tuo gioco quando questo algoritmo impiegherà 3 secondi per finire ...
Falco,

@Falco Stai supervisionando i vantaggi a lungo termine di questo modello, sia per il progetto che per l'esperienza del programmatore. La tua affermazione che è più difficile pensare che non possa davvero essere affrontata, è solo un'opinione. Per me il design della GUI è molto più terrificante. Tutti i linguaggi evoluti (C ++, Java) hanno modelli multithreading abbastanza chiari. E se non sei davvero sicuro, puoi usare un modello di attore che non soffre di bug multithread per principianti. Sai c'è un motivo per cui la maggior parte delle applicazioni sono progettate come da me proposto, ma sentiti libero di discuterne ulteriormente.
Tomáš Zato - Ripristina Monica il

4

Persino Space Invaders gestiva dozzine di oggetti interagenti. Mentre la decodifica di un fotogramma di video HD H264 comporta centinaia di milioni di operazioni aritmetiche. Hai molto potenza di elaborazione disponibile.

Detto questo, puoi ancora rallentare se lo sprechi. Il problema non è tanto il numero di oggetti quanto il numero di prove di collisione eseguite; il semplice approccio di verificare ogni oggetto uno contro l'altro quadra il numero di calcoli richiesti. Testare 1001 oggetti per collisioni in questo modo richiederebbe un milione di confronti. Spesso questo viene risolto, ad esempio, non controllando la collisione dei proiettili.


2
Non sono sicuro che Space Invaders sia il miglior confronto da fare. Il motivo per cui inizia lentamente e accelera mentre uccidi i nemici non è perché è stato progettato in questo modo, ma perché l'hardware non è in grado di gestire il rendering di così tanti nemici contemporaneamente. en.wikipedia.org/wiki/Space_Invaders#Hardware
Mike Kellogg

Che dire, ogni oggetto mantiene un elenco di tutti gli oggetti abbastanza vicini da potersi scontrare con loro nel secondo successivo, aggiornati una volta al secondo o ogni volta che cambiano direzione?
Casuale 832,

Dipende da cosa stai modellando. Le soluzioni di partizionamento dello spazio sono un altro approccio comune: suddividere il mondo in regioni (ad esempio BSP che potrebbe essere necessario eseguire comunque per scopi di rendering o quadrifoglio), quindi è possibile scontrarsi solo con oggetti nella stessa regione.
pjc50,

3

Non sono d'accordo con alcune delle altre risposte qui. I thread logici separati non sono solo una buona idea, ma sono estremamente utili per la velocità di elaborazione, se la logica è facilmente separabile .

La tua domanda è un buon esempio di logica che è probabilmente separabile se puoi aggiungere qualche logica aggiuntiva sopra di essa. Ad esempio, è possibile eseguire diversi thread di rilevamento dei risultati bloccando i thread in specifiche aree dello spazio o disattivando l'audio degli oggetti coinvolti.

Probabilmente NON vuoi un thread per ogni possibile collisione, solo perché è probabile che impantani lo scheduler; c'è anche un costo associato alla creazione e alla distruzione dei thread. Meglio creare un numero di thread attorno ai core del sistema (o utilizzare una metrica come la vecchia #cores * 2 + 4), quindi riutilizzarli al termine del processo.

Tuttavia, non tutta la logica è facilmente separabile. A volte le tue operazioni possono raggiungere contemporaneamente tutti i dati di gioco, il che renderebbe il threading inutile (in effetti, dannoso, perché dovresti aggiungere controlli per evitare problemi di threading). Inoltre, se più fasi della logica dipendono fortemente l'una dall'altra che si verificano in ordini specifici, dovrai controllare l'esecuzione dei thread in modo tale da garantire che non fornisca risultati dipendenti dall'ordine. Tuttavia, quel problema non viene eliminato non usando i thread, i thread lo esasperano.

La maggior parte dei giochi non lo fa semplicemente perché è più complessa di quanto lo sviluppatore di giochi medio sia disposto / in grado di gestire per prima cosa ciò che di solito non è il collo di bottiglia. La stragrande maggioranza dei giochi è limitata alla GPU, non alla CPU. Mentre migliorare la velocità della CPU può aiutare nel complesso, di solito non è l'obiettivo.

Detto questo, i motori fisici utilizzano spesso più thread e posso nominare diversi giochi che penso avrebbero beneficiato di più thread logici (i giochi Paradox RTS come HOI3 e simili, ad esempio).

Sono d'accordo con altri post sul fatto che probabilmente non avresti bisogno di utilizzare discussioni in questo esempio specifico, anche se potrebbe essere utile. Il threading dovrebbe essere riservato ai casi in cui si ha un carico eccessivo della CPU che non può essere ottimizzato tramite altri metodi. È un'impresa enorme e influenzerà la struttura fondamentale di un motore; non è qualcosa su cui puoi attingere dopo il fatto.


2

Penso che le altre risposte manchino una parte importante della domanda, concentrandosi troppo sulla parte di discussione della domanda.

Un computer non gestisce affatto tutti gli oggetti in un gioco. Li gestisce in sequenza.

Un gioco per computer procede in fasi temporali discrete. A seconda del gioco e della velocità del PC, questi passaggi sono in genere 30 o 60 passi al secondo, o quanti / pochi passi è possibile calcolare dal PC.

In uno di questi passaggi, un computer calcola cosa farà ciascuno degli oggetti di gioco durante quel passaggio e li aggiorna di conseguenza, uno dopo l'altro. Potrebbe anche farlo in parallelo, usando i thread per essere più veloce, ma come vedremo presto la velocità non è affatto una preoccupazione.

Una CPU media dovrebbe essere di 2 GHz o più veloce, il che significa 10 9 cicli di clock al secondo. Se calcoliamo 60 Timesteps al secondo, che le foglie 10 9 cicli / 60 cicli di clock = 16.666.666 orologio per volta passo. Con 70 unità, restano ancora circa 2.400.000 di cicli di clock per unità. Se dovessimo ottimizzare, potremmo essere in grado di aggiornare ogni unità in appena 240 cicli, a seconda della complessità della logica di gioco. Come puoi vedere, il nostro computer è circa 10.000 volte più veloce di quanto sia necessario per questa attività.


0

Disclaimer: il mio tipo di gioco preferito in assoluto è basato sul testo e lo scrivo come programmatore di lunga data di un vecchio MUD.

Penso che una domanda importante che devi porti sia questa: hai bisogno di discussioni? Capisco che un gioco grafico abbia probabilmente un maggiore utilizzo di MT ma penso che dipenda anche dalla meccanica del gioco. (Potrebbe anche valere la pena considerare che con GPU, CPU e tutte le altre risorse che abbiamo oggi sono molto più potenti, il che rende le tue preoccupazioni di risorse tanto problematiche come potrebbero sembrare; in effetti 100 oggetti sono praticamente zero). Dipende anche da come definisci "tutti i personaggi contemporaneamente". Intendi allo stesso tempo? Non lo avrai, come giustamente sottolinea Peter, quindi tutto in una volta è irrilevante in senso letterale; sembra solo così.

Supponendo che andrai con i thread: sicuramente non dovresti considerare 100 thread (e non ho nemmeno intenzione di capire se è troppo per la tua CPU o no; mi riferisco solo alle complicazioni e alla praticità di esso).

Ma ricorda questo: il threading multiplo non è facile (come sottolinea Philipp) e ha molti problemi. Altri hanno molta più esperienza (di gran lunga) di me con MT ma direi che anche loro suggerirebbero la stessa cosa (anche se sarebbero più capaci di me, specialmente senza pratica da parte mia).

Alcuni sostengono che non sono d'accordo sul fatto che i thread non siano utili e alcuni sostengono che ogni oggetto dovrebbe avere un thread. Ma (e di nuovo questo è tutto il testo, ma anche se si considera più di un thread non è necessario - e non si dovrebbe - considerarlo per ogni oggetto) poiché Philipp sottolinea che i giochi tendono a scorrere tra gli elenchi. Ma non è solo (come suggerisce anche se mi rendo conto che sta solo rispondendo ai tuoi parametri di così pochi oggetti) per così pochi oggetti. Nel MUD sono un programmatore perché abbiamo le seguenti (e questa non è tutta l'attività che si svolge in tempo reale, quindi tienilo a mente):

(Il numero di istanze varia ovviamente - superiore e inferiore)

Mobiles (NPC cioè personaggio non giocante): 2614; prototipi: 1360 Oggetti: 4457; prototipi: 2281 sale: 7983; prototipi: 7983. Ogni stanza ha la sua istanza di solito ma abbiamo anche stanze dinamiche, vale a dire stanze all'interno di una stanza; o stanze all'interno di un cellulare, ad esempio lo stomaco di un drago; o stanze negli oggetti, ad esempio si inserisce un oggetto magico). Tieni presente che queste stanze dinamiche esistono per oggetto / stanza / mobile che le ha effettivamente definite. Sì, questo è molto simile a quello di World of Warcraft (non lo gioco ma un amico mi ha fatto giocare quando avevo una macchina Windows, per un po ') idea di istanze tranne che ce l'avevamo molto prima che esistesse World of Warcraft.

Script: 868 (attualmente) (stranamente il nostro comando statistico non mostra quanti prototipi abbiamo, quindi lo aggiungerò). Tutti questi sono tenuti in aree / zone e ne abbiamo 103. Abbiamo anche procedure speciali che si svolgono in momenti diversi. Abbiamo anche altri eventi. Quindi abbiamo anche prese collegate. I cellulari si muovono, svolgono diverse attività (oltre al combattimento), interagiscono con i giocatori e così via. (Così fanno altri tipi di entità).

Come gestiamo tutto questo senza alcun ritardo?

  • socket: select (), code (input, output, eventi, altro), buffer (input, output, altro), ecc. Vengono sottoposti a polling 10 volte al secondo.

  • personaggi, oggetti, stanze, combattimenti, tutto: tutto in un ciclo centrale su impulsi diversi.

Inoltre (la mia implementazione basata sulla discussione tra il fondatore / altro programmatore e me stesso) disponiamo di un ampio monitoraggio dell'elenco collegato e di test di validità del puntatore e abbiamo risorse gratuite più che sufficienti se ne avessimo effettivamente bisogno. Tutto questo (tranne che abbiamo ampliato il mondo) esisteva anni fa quando c'erano meno RAM, potenza della CPU, spazio sul disco rigido, ecc. E infatti anche allora non abbiamo avuto problemi. Nei loop descritti (gli script lo causano così come i ripristini / ripopolazioni delle aree come altre cose) mostri, oggetti (oggetti) e altre cose vengono creati, liberati e così via. Anche le connessioni vengono accettate, eseguite il polling e tutto ciò che ci si aspetterebbe.

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.