La risposta è sempre di usare un array o std :: vector. Tipi come un elenco collegato o una std :: map sono di solito assolutamente orrendi nei giochi e questo include sicuramente casi come raccolte di oggetti di gioco.
È necessario memorizzare gli oggetti stessi (non i puntatori ad essi) nell'array / vettore.
Si desidera memoria contigua. Lo vuoi davvero davvero. L'iterazione su tutti i dati nella memoria non contigua impone un sacco di errori di cache in generale e rimuove la capacità del compilatore e della CPU di eseguire un prefetching della cache efficace. Questo da solo può uccidere le prestazioni.
Si desidera inoltre evitare allocazioni di memoria e deallocazioni. Sono molto lenti, anche con un allocatore di memoria veloce. Ho visto i giochi ottenere un bump 10x FPS semplicemente rimuovendo alcune centinaia di allocazioni di memoria per ogni frame. Non sembra che dovrebbe essere così male, ma può essere.
Infine, la maggior parte delle strutture di dati che ti interessano per la gestione degli oggetti di gioco possono essere implementate in modo molto più efficiente su un array o un vettore di quanto possano fare con un albero o un elenco.
Ad esempio, per la rimozione di oggetti di gioco, è possibile utilizzare swap-and-pop. Facilmente implementabile con qualcosa come:
std::swap(objects[index], objects.back());
objects.pop_back();
Puoi anche contrassegnare gli oggetti come eliminati e mettere il loro indice in un elenco gratuito per la prossima volta che devi creare un nuovo oggetto, ma fare lo scambio e pop è meglio. Ti permette di fare un semplice ciclo per tutti gli oggetti vivi senza ramificarsi a parte il ciclo stesso. Per l'integrazione della fisica dei proiettili e simili, questo può essere un significativo aumento delle prestazioni.
Ancora più importante, è possibile trovare oggetti con una semplice coppia di ricerche di tabelle da un unico stabile che utilizza la struttura della mappa di slot.
I tuoi oggetti di gioco hanno un indice nella loro matrice principale. Possono essere cercati in modo molto efficiente solo con questo indice (molto più veloce di una mappa o persino di una tabella hash). Tuttavia, l'indice non è stabile a causa dello scambio e del pop durante la rimozione di oggetti.
Una mappa di slot richiede due livelli di riferimento indiretto, ma entrambi sono semplici ricerche di array con indici costanti. Sono veloci . Davvero veloce.
L'idea di base è che hai tre array: il tuo elenco di oggetti principale, il tuo elenco di riferimenti indiretti e un elenco gratuito per l'elenco di riferimenti indiretti. Il tuo elenco di oggetti principale contiene i tuoi oggetti reali, dove ogni oggetto conosce il proprio ID univoco. L'ID univoco è composto da un indice e un tag di versione. L'elenco indiretto è semplicemente un array di indici all'elenco oggetti principale. L'elenco gratuito è una pila di indici nell'elenco indiretto.
Quando si crea un oggetto nell'elenco principale, si trova una voce non utilizzata nell'elenco indiretto (utilizzando l'elenco gratuito). La voce nell'elenco indiretto punta a una voce non utilizzata nell'elenco principale. Inizializzi il tuo oggetto in quella posizione e imposti il suo ID univoco sull'indice della voce dell'elenco di riferimenti indiretti che hai scelto e il tag della versione esistente nell'elemento dell'elenco principale, più uno.
Quando distruggi un oggetto, esegui lo scambio e il pop normalmente, ma aumenti anche il numero di versione. Quindi aggiungere anche l'indice dell'elenco di riferimenti indiretti (parte dell'ID univoco dell'oggetto) all'elenco libero. Quando si sposta un oggetto come parte di swap-and-pop, si aggiorna anche la sua voce nell'elenco indiretto nella nuova posizione.
Esempio di pseudo-codice:
Object:
int index
int version
other data
SlotMap:
Object objects[]
int slots[]
int freelist[]
int count
Get(id):
index = indirection[id.index]
if objects[index].version = id.version:
return &objects[index]
else:
return null
CreateObject():
index = freelist.pop()
objects[count].index = id
objects[count].version += 1
indirection[index] = count
Object* object = &objects[count].object
object.initialize()
count += 1
return object
Remove(id):
index = indirection[id.index]
if objects[index].version = id.version:
objects[index].version += 1
objects[count - 1].version += 1
swap(objects[index].data, objects[count - 1].data)
Il livello di riferimento indiretto consente di avere un identificatore stabile (l'indice nel livello di riferimento indiretto, in cui le voci non si spostano) per una risorsa che può spostarsi durante la compattazione (l'elenco di oggetti principale).
Il tag versione consente di archiviare un ID in un oggetto che potrebbe essere eliminato. Ad esempio, hai l'id (10,1). L'oggetto con indice 10 viene eliminato (ad esempio, il proiettile colpisce un oggetto e viene distrutto). L'oggetto in quella posizione di memoria nell'elenco principale degli oggetti ha quindi il suo numero di versione bloccato, dandogli (10,2). Se si tenta di cercare nuovamente (10,1) da un ID non aggiornato, la ricerca restituisce quell'oggetto tramite l'indice 10, ma può vedere che il numero di versione è cambiato, quindi l'ID non è più valido.
Questa è la struttura dati più veloce in assoluto che puoi avere con un ID stabile che consente agli oggetti di spostarsi in memoria, il che è importante per la localizzazione dei dati e la coerenza della cache. Questo è più veloce di qualsiasi implementazione di una tabella hash possibile; una tabella di hash deve almeno calcolare un hash (più istruzioni di una ricerca di tabella) e quindi deve seguire la catena di hash (o un elenco collegato nel caso orribile di std :: unordered_map o un elenco aperto in qualsiasi implementazione non stupida di una tabella hash), e quindi deve fare un confronto di valore su ciascuna chiave (non più costoso, ma forse meno costoso, rispetto al controllo del tag di versione). Una tabella hash molto buona (non quella in nessuna implementazione dell'STL, poiché l'STL impone una tabella hash che ottimizza per diversi casi d'uso rispetto al gioco per un elenco di oggetti di gioco) potrebbe salvare su una indiretta,
Ci sono vari miglioramenti che puoi apportare all'algoritmo di base. Usare qualcosa come std :: deque per la lista degli oggetti principale, per esempio; un ulteriore livello di riferimento indiretto, ma consente l'inserimento di oggetti in un elenco completo senza invalidare i puntatori temporanei acquisiti dalla slotmap.
Puoi anche evitare di memorizzare l'indice all'interno dell'oggetto, poiché l'indice può essere calcolato dall'indirizzo di memoria dell'oggetto (questo - oggetti), e anche meglio è necessario solo quando rimuovi l'oggetto nel qual caso hai già l'ID dell'oggetto (e quindi indice) come parametro.
Scuse per la redazione; Non credo sia la descrizione più chiara che potrebbe essere. È tardi ed è difficile da spiegare senza passare più tempo di quello che ho sugli esempi di codice.