La memorizzazione di tutti gli oggetti di gioco in un unico elenco è un design accettabile?


24

Per ogni gioco che ho realizzato, finisco per mettere tutti i miei oggetti di gioco (proiettili, macchine, giocatori) in un unico elenco di array, che cerco per disegnare e aggiornare. Il codice di aggiornamento per ogni entità è memorizzato nella sua classe.

Mi chiedevo, è questo il modo giusto di affrontare le cose o è un modo più rapido ed efficiente?


4
Ho notato che hai detto "elenco array", supponendo che sia .Net, dovresti invece passare a un Elenco <T>. È quasi identico, tranne per il fatto che un Elenco <T> è di tipo forte e per questo motivo non dovrai digitare cast ogni volta che accedi a un elemento. Ecco il documento: msdn.microsoft.com/en-us/library/6sh2ey19.aspx
John McDonald

Risposte:


26

Non esiste un modo giusto. Ciò che stai descrivendo funziona ed è presumibilmente abbastanza veloce da non importare dal momento che sembra che ti stia chiedendo perché sei preoccupato per il design e non perché ti ha causato problemi di prestazioni. Quindi è un modo giusto, certo.

Potresti certamente farlo in modo diverso, ad esempio mantenendo un elenco omogeneo per ogni tipo di oggetto o assicurando che tutto nel tuo elenco eterogeneo sia raggruppato in modo tale da avere una maggiore coerenza per il codice che è nella cache o consentire aggiornamenti paralleli. È difficile dire se vedresti miglioramenti apprezzabili in questo modo senza conoscere l'ambito e le dimensioni dei tuoi giochi.

Potresti anche considerare ulteriormente le responsabilità delle tue entità - sembra che le entità si aggiornino e si rendano disponibili al momento - per seguire meglio il Principio di Responsabilità Unica. Ciò può aumentare la manutenibilità e la flessibilità delle singole interfacce disaccoppiandole l'una dall'altra. Ma ancora una volta, se vedresti un beneficio dal lavoro extra che comporterebbe è difficile per me sapere sapendo quello che so dei tuoi giochi (che è praticamente nulla).

Consiglierei di non sottolinearne troppo, e tieni nella parte posteriore della testa che potrebbe essere una potenziale area di miglioramento se noti mai problemi di prestazioni o di manutenibilità.


16

Come altri hanno già detto, se è abbastanza veloce, allora è abbastanza veloce. Se ti stai chiedendo se esiste un modo migliore, la domanda da porsi è

Mi ritrovo a scorrere tutti gli oggetti ma a operare solo su un sottoinsieme di essi?

Se lo fai, significa che potresti voler avere più elenchi contenenti solo riferimenti pertinenti. Ad esempio, se si esegue il looping di tutti gli oggetti di gioco per renderli, ma è necessario renderizzare solo un sottoinsieme di oggetti, può valere la pena tenere un elenco di rendering separato solo per quegli oggetti che devono essere resi.

Ciò ridurrebbe il numero di oggetti attraverso cui si esegue il ciclo e salta i controlli non necessari perché si sa già che tutti gli oggetti nell'elenco devono essere elaborati.

Dovresti profilarlo per vedere se stai ottenendo un notevole miglioramento delle prestazioni.


Sono d'accordo con questo, in genere ho sottoinsiemi del mio elenco per gli oggetti che devono essere aggiornati ogni fotogramma e ho considerato di fare lo stesso per gli oggetti che rendono. Tuttavia, quando tali proprietà possono cambiare al volo, la gestione di tutti gli elenchi può essere complessa. Quando e l'oggetto passa da renderizzabile a non renderizzabile, o aggiornabile a non aggiornabile, e ora li stai aggiungendo o rimuovendo da più elenchi contemporaneamente.
Nic Foster,

8

Dipende quasi interamente dalle tue esigenze di gioco specifiche. Può funzionare perfettamente per un gioco semplice, ma fallire su un sistema più complesso. Se funziona per il tuo gioco, non preoccuparti o prova a progettarlo eccessivamente fino a quando non è necessario.

Per quanto riguarda il motivo per cui l'approccio semplice potrebbe non riuscire in alcune situazioni, è difficile sintetizzarlo in un singolo post, poiché le possibilità sono infinite e dipendono interamente dai giochi stessi. Altre risposte hanno citato, ad esempio, che potresti voler raggruppare gli oggetti condividendo le responsabilità in un elenco separato.

Questo è uno dei cambiamenti più comuni che potresti fare a seconda del design del tuo gioco. Ne approfitterò per descrivere alcuni altri (più complessi) esempi, ma ricordo che ci sono ancora molte altre possibili ragioni e soluzioni.

Per i principianti, farò notare che l'aggiornamento di alcuni oggetti di gioco e il loro rendering possono avere requisiti diversi in alcuni giochi e pertanto devono essere gestiti separatamente. Alcuni possibili problemi con l'aggiornamento e il rendering degli oggetti di gioco:

Aggiornamento di oggetti di gioco

Ecco un estratto da Game Engine Architecture che consiglierei di leggere:

In presenza di dipendenze tra oggetti, la tecnica di aggiornamento graduale sopra descritta deve essere leggermente adattata. Questo perché le dipendenze tra oggetti possono portare a regole contrastanti che regolano l'ordine di aggiornamento.

In altre parole, in alcuni giochi è possibile che gli oggetti dipendano l'uno dall'altro e richiedano un ordine specifico di aggiornamento. In questo scenario, potrebbe essere necessario escogitare una struttura più complessa di un elenco per archiviare gli oggetti e le loro interdipendenze.

inserisci qui la descrizione dell'immagine

Rendering di oggetti di gioco

In alcuni giochi, scene e oggetti creano una gerarchia di nodi genitore-figlio e devono essere resi nell'ordine corretto e con trasformazioni relative ai loro genitori. In questi casi, potresti aver bisogno di una struttura più complessa come un grafico di scena (una struttura ad albero) invece di un singolo elenco:

inserisci qui la descrizione dell'immagine

Altri motivi potrebbero essere, ad esempio, l'utilizzo di una struttura di dati di partizionamento spaziale per organizzare gli oggetti in modo da migliorare l'efficienza dell'esecuzione dell'eliminazione della frustum della vista.


4

La risposta è "sì, va bene." Quando ho lavorato su grandi simulazioni di ferro in cui le prestazioni non erano negoziabili, sono rimasto sorpreso di vedere tutto in un array globale di grandi dimensioni (BIG e FAT). Successivamente ho lavorato su un sistema OO e ho potuto confrontare i due. Sebbene fosse molto brutto, la versione "un array per domarli tutti" nella vecchia C semplice a thread singolo compilata in debug era parecchie volte più veloce del "grazioso" sistema OO multithread compilato -O2 in C ++. Penso che un po 'abbia avuto a che fare con l'eccellente localizzazione della cache (sia nello spazio che nel tempo) nella versione big-fat-array.

Se vuoi prestazioni, un array C globale di grandi dimensioni sarà il limite superiore (per esperienza).


2

Fondamentalmente quello che vuoi fare è creare elenchi di cui hai bisogno. Se ridisegna gli oggetti terreno tutti in una volta, un elenco di solo loro potrebbe essere utile. Ma per questo vale la pena mentre ne avresti bisogno di decine di migliaia e migliaia di cose che non sono terreno. Altrimenti sfogliare la tua lista di tutto e usare un "if" per estrarre gli oggetti del terreno è abbastanza veloce.

Ho sempre avuto bisogno di una lista principale, indipendentemente da quante altre liste ho avuto. Di solito lo trasformo in una mappa, una tabella con chiave o un dizionario, di qualche tipo in modo da poter accedere in modo casuale ai singoli elementi. Ma un semplice elenco è molto più semplice; se non accedi casualmente agli oggetti di gioco non hai bisogno della Mappa. E la ricerca sequenziale occasionale attraverso alcune migliaia di voci per trovare quella giusta non richiederà molto tempo. Quindi direi, qualunque altra cosa tu faccia, pensi di restare nella lista principale. Prendi in considerazione l'utilizzo di una mappa se diventa grande. (Anche se se puoi usare gli indici invece delle chiavi, puoi rimanere con le matrici e avere qualcosa di molto meglio di una Mappa. Ho sempre finito con grandi lacune tra i numeri di indice, quindi ho dovuto rinunciare.)

Una parola sugli array: sono incredibilmente veloci. Ma quando si alzano intorno a 100.000 elementi, iniziano a dare i giusti Garbage Collector. Se puoi allocare lo spazio una volta e non toccarlo in seguito, bene. Ma se si espande continuamente l'array, si allocano e si liberano continuamente grossi blocchi di memoria e si tende a far bloccare il gioco per alcuni secondi alla volta e anche a fallire con un errore di memoria.

Così più veloce ed efficiente? Sicuro. Una mappa per l'accesso casuale. Per elenchi davvero di grandi dimensioni, un Elenco collegato batterà un array se e quando non è necessario un accesso casuale. Più mappe / elenchi per organizzare gli oggetti in vari modi, a seconda delle necessità. È possibile eseguire il multithreading dell'aggiornamento.

Ma è tutto molto lavoro. Quel singolo array è veloce e semplice. Puoi aggiungere altri elenchi e cose solo quando necessario e probabilmente non ne avrai bisogno. Basta essere consapevoli di ciò che può andare storto se il gioco diventa grande. Potresti voler avere una versione di prova con un numero di oggetti 10 volte maggiore rispetto alla versione reale per darti un'idea di quando sei diretto ai guai. (Fallo solo se il tuo gioco lo è diventando grande.) (E non provare il multithreading senza pensarci troppo. Tutto il resto è facile da iniziare e puoi fare tutto o quanto vuoi e tornare indietro in qualsiasi momento. Con il multi-threading pagherai un grande prezzo per entrare, le quote per rimanere sono alte ed è difficile tornare indietro.)

In altre parole, continua a fare quello che stai facendo. Il tuo limite più grande è probabilmente il tuo tempo, e tu stai facendo il miglior uso possibile.


Non credo davvero che un elenco collegato superi le prestazioni di un array di qualsiasi dimensione, a meno che non si aggiungano o rimuovano spesso elementi dal centro.
Zan Lynx,

@ZanLynx: E anche allora. Penso che le nuove ottimizzazioni di runtime aiutino molto gli array, non che ne avessero bisogno. Ma se si allocano e liberano grandi quantità di memoria ripetutamente e rapidamente, come può accadere con grandi array, è possibile legare il cestino della spazzatura in nodi. (Anche in questo caso la quantità totale di memoria è un fattore. Il problema è in funzione della dimensione dei blocchi, della frequenza di libero e di allocazione e della memoria disponibile.) Il guasto si verifica improvvisamente, con il programma sospeso o in crash. Di solito c'è molta memoria libera; il GC non può usarlo. Le liste collegate evitano questo problema.
RalphChapin,

D'altra parte, gli elenchi collegati hanno una probabilità significativamente maggiore di perdere cache sull'iterazione a causa del fatto che i nodi non sono contigui nella memoria. Non è così semplice. È possibile alleviare i problemi di riallocazione non farlo (allocando in anticipo se possibile, perché spesso lo è) e è possibile alleviare i problemi di coerenza della cache in un elenco collegato allocando i nodi (anche se si ha ancora il costo di riferimento del riferimento , è relativamente banale).
Josh,

Sì, ma anche in un array, non stai semplicemente memorizzando i puntatori sugli oggetti? (A meno che non sia un array di breve durata per un sistema particellare o qualcosa del genere.) In tal caso, ci saranno ancora molti errori nella cache.
Todd Lehman,

1

Ho usato un metodo un po 'simile per i giochi semplici. La memorizzazione di tutto in un array è molto utile quando sono vere le seguenti condizioni:

  1. Hai un numero limitato o un massimo noto di oggetti di gioco
  2. Preferisci la semplicità rispetto alle prestazioni (es .: strutture di dati più ottimizzate come gli alberi non valgono il problema)

In ogni caso, ho implementato questa soluzione come un array di dimensioni fisse che contiene un gameObject_ptr struttura. Questa struttura conteneva un puntatore all'oggetto di gioco stesso e un valore booleano che indica se l'oggetto era "vivo" o meno.

Lo scopo del booleano è velocizzare le operazioni di creazione e cancellazione. Invece di distruggere gli oggetti di gioco quando vengono rimossi dalla simulazione, imposterò semplicemente la bandiera "viva" su false.

Quando si eseguono calcoli su oggetti, il codice scorre ciclicamente attraverso l'array, eseguendo calcoli su oggetti contrassegnati come "vivi" e vengono renderizzati solo gli oggetti contrassegnati come "vivi".

Un'altra semplice ottimizzazione è quella di organizzare l'array per tipo di oggetto. Tutti i proiettili, ad esempio, potrebbero vivere nelle ultime n celle dell'array. Quindi, memorizzando un int con il valore dell'indice o un puntatore all'elemento array, è possibile fare riferimento a tipi specifici a un costo ridotto.

Va da sé che questa soluzione non è adatta ai giochi con grandi quantità di dati, poiché l'array sarà inaccettabilmente grande. Inoltre, non è adatto a giochi con vincoli di memoria che vietano agli oggetti non utilizzati di occupare memoria. La linea di fondo è che se hai un limite superiore noto sul numero di oggetti di gioco che avrai in un determinato momento, non c'è nulla di sbagliato nel memorizzarli in un array statico.

Questo è il mio primo post! Spero che risponda alla tua domanda!


primo post! Sìì!
Tili,

0

È accettabile, anche se insolito. Termini come "più veloce" e "più efficiente" sono relativi; di solito organizzi gli oggetti di gioco in categorie separate (quindi un elenco di proiettili, un elenco di nemici, ecc.) per rendere il lavoro di programmazione più organizzato (e quindi più efficiente) ma non è come se il computer eseguisse il ciclo di tutti gli oggetti più velocemente .


2
Abbastanza vero in termini di maggior parte dei giochi XNA, ma l'organizzazione dei tuoi oggetti può effettivamente farli scorrere più velocemente: gli oggetti dello stesso tipo hanno maggiori probabilità di colpire le istruzioni e la cache di dati della tua CPU. I cache miss sono costosi, specialmente sulle console. Ad esempio, su Xbox 360, una mancanza di cache L2 ti costerà ~ 600 cicli. Questo sta lasciando molte prestazioni sul tavolo. Questo è un punto in cui il codice gestito presenta un vantaggio rispetto al codice nativo: anche gli oggetti allocati vicini nel tempo rimarranno vicini nella memoria e la situazione migliora con la garbage collection (compattazione).
Programmatore di giochi cronici,

0

Dici array e oggetti singoli.

Quindi (senza il tuo codice da guardare) immagino siano tutti collegati a 'uberGameObject'.

Quindi forse due problemi.

1) Potresti avere un oggetto "divino": fa tonnellate di cose, non realmente segmentate da ciò che fa o è.

2) Esamini questo elenco e esegui il casting per digitare? o unboxing ciascuno. Questo ha un grosso dispendio di prestazioni.

considera la possibilità di suddividere diversi tipi (proiettili, cattivi, ecc.) nelle loro liste fortemente tipizzate.

List<BadGuyObj> BadGuys = new List<BadGuyObj>();
List<BulletsObj> Bullets = new List<BulletObj>();

 //Game 
OnUpdate(gameticks gt)
{  foreach (BulletObj thisbullet in Bullets) {thisbullet.Update();}  // much quicker.

Non è necessario eseguire il cast di tipi se esiste una sorta di classe base o interfaccia comune che ha tutti i metodi che verranno chiamati nel ciclo di aggiornamento (ad es. update()).
bummzack,

0

Posso proporre un compromesso tra un unico elenco generale e elenchi separati per ciascun tipo di oggetto.

Mantenere il riferimento a un oggetto in più elenchi. Questi elenchi sono definiti da comportamenti di base. Ad esempio, MarineSoldieroggetto farà riferimento a PhysicalObjList, GraphicalObjList, UserControlObjList, HumanObjList. Quindi, quando si elabora la fisica, si scorre attraverso PhysicalObjList, quando il processo danneggia il veleno - HumanObjList, se il Soldato muore - lo rimuove HumanObjListma resta dentro PhysicalObjList. Per far funzionare questo meccanismo è necessario organizzare l'inserimento / la rimozione automatica dell'oggetto quando viene creato / eliminato.

Vantaggi dell'approccio:

  • organizzazione tipizzata di oggetti (no AllObjectsListcon casting in aggiornamento)
  • gli elenchi si basano su logiche, non su classi di oggetti
  • nessun problema con oggetti con abilità combinate ( MegaAirHumanUnderwaterObjectverrebbero inseriti negli elenchi corrispondenti invece di creare un nuovo elenco per il suo tipo)

PS: Per quanto riguarda l'auto-aggiornamento, incontrerai un problema quando più oggetti interagiscono e ciascuno di essi dipende dagli altri. Per risolvere questo, dobbiamo influenzare l'oggetto esternamente, non all'interno dell'autoaggiornamento.

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.