Partiamo dall'approccio di base sistemi-componenti-entità .
Creiamo assemblaggi (termine derivato da questo articolo) semplicemente per informazioni sui tipi di componenti . Viene eseguito in modo dinamico in fase di runtime, proprio come aggiungeremmo / rimuovere componenti a un'entità uno per uno, ma chiamiamolo più precisamente poiché si tratta solo di informazioni sul tipo.
Quindi costruiamo entità specificando l' assemblaggio per ognuna di esse. Una volta creata l'entità, il suo assemblaggio è immutabile, il che significa che non possiamo modificarla direttamente sul posto, ma possiamo comunque ottenere la firma dell'entità esistente su una copia locale (insieme al contenuto), apportare le modifiche appropriate e creare una nuova entità di esso.
Ora per il concetto chiave: ogni volta che un'entità viene creata, viene assegnata a un oggetto chiamato bucket di assemblaggio , il che significa che tutte le entità della stessa firma saranno nello stesso contenitore (ad es. In std :: vector).
Ora i sistemi passano in rassegna ogni secchio di loro interesse e fanno il loro lavoro.
Questo approccio presenta alcuni vantaggi:
- i componenti sono memorizzati in pochi (precisamente: numero di bucket) blocchi di memoria contigui - questo migliora la compatibilità della memoria ed è più facile scaricare l'intero stato del gioco
- i sistemi elaborano i componenti in modo lineare, il che significa una migliore coerenza della cache - ciao ciao dizionari e salti di memoria casuali
- creare una nuova entità è facile come mappare un assemblaggio su un bucket e riportare i componenti necessari al suo vettore
- cancellare un'entità è facile come una chiamata a std :: move per scambiare l'ultimo elemento con quello cancellato, perché l'ordine non ha importanza in questo momento
Se disponiamo di molte entità con firme completamente diverse, i vantaggi della coerenza della cache diminuiscono, ma non penso che accadrà nella maggior parte delle applicazioni.
C'è anche un problema con l'invalidazione del puntatore dopo che i vettori sono stati riallocati - questo potrebbe essere risolto introducendo una struttura come:
struct assemblage_bucket {
struct entity_watcher {
assemblage_bucket* owner;
entity_id real_index_in_vector;
};
std::unordered_map<entity_id, std::vector<entity_watcher*>> subscribers;
//...
};
Quindi ogni volta che per qualche ragione nella nostra logica di gioco vogliamo tenere traccia di un'entità appena creata, all'interno del bucket registriamo un entity_watcher e una volta che l'entità deve essere std :: spostata durante la rimozione, cerchiamo i suoi watcher e aggiorniamo loro real_index_in_vector
a nuovi valori. Il più delle volte questo impone una sola ricerca nel dizionario per ogni eliminazione di entità.
Ci sono altri svantaggi di questo approccio?
Perché la soluzione non è menzionata da nessuna parte, nonostante sia abbastanza ovvia?
EDIT : sto modificando la domanda per "rispondere alle risposte", poiché i commenti non sono sufficienti.
perdi la natura dinamica dei componenti innestabili, creati appositamente per allontanarti dalla costruzione di classi statiche.
Io non. Forse non l'ho spiegato abbastanza chiaramente:
auto signature = world.get_signature(entity_id); // this would just return entity_id.bucket_owner->bucket_signature or so
signature.add(foo_component);
signature.remove(bar_component);
world.delete_entity(entity_id); // entity_id would hold information about its bucket owner
world.create_entity(signature); // automatically assigns new entity to an existing or a new bucket
È semplice come prendere la firma dell'entità esistente, modificarla e caricarla di nuovo come nuova entità. Natura innestabile e dinamica ? Ovviamente. Qui vorrei sottolineare che esiste solo un "assemblaggio" e una classe "secchio". I bucket sono guidati dai dati e creati in fase di esecuzione in quantità ottimale.
dovresti esaminare tutti i bucket che potrebbero contenere un target valido. Senza una struttura di dati esterna, il rilevamento delle collisioni potrebbe essere altrettanto difficile.
Bene, questo è il motivo per cui abbiamo le strutture di dati esterne sopra menzionate . La soluzione alternativa è semplice come l'introduzione di un iteratore nella classe System che rileva quando passare al bucket successivo. Il salto sarebbe puramente trasparente alla logica.