I tuoi ID dovrebbero essere una combinazione di indice e versione . Ciò ti consentirà di riutilizzare gli ID in modo efficiente, utilizzare l'ID per trovare rapidamente i componenti e rendere molto più semplice l'implementazione della tua "opzione 2" (sebbene l'opzione 3 possa essere resa molto più appetibile con un po 'di lavoro).
struct entity {
uint16 version;
/* and other crap that doesn't belong in components */
};
std::vector<entity> pool;
std::vector<uint16> freelist;
typedef uint32 entity_id; /* this shoudl be a wrapper class */
entity_id createEntity()
{
uint16 index;
if (!freelist.empty())
{
pool.push_back(entity());
freelist.push_back(pool.size() - 1);
}
index = freelist.pop_back();
return (pool[id].version << 16) | index;
}
void deleteEntity(entity_id id)
{
uint16 index = id & 0xFFFF;
++pool[index].version;
freelist.push_back(index);
}
entity* getEntity(entity_id id)
{
uint16 index = id & 0xFFFF;
uint16 version = id >> 16;
if (index < pool.size() && pool[index].version == version)
return &pool[index];
else
return NULL;
}
Ciò assegnerà un nuovo numero intero a 32 bit che è una combinazione di un indice univoco (che è univoco tra tutti gli oggetti attivi) e un tag di versione (che sarà univoco per tutti gli oggetti che hanno mai occupato quell'indice).
Quando si elimina un'entità, si aumenta la versione. Ora, se hai dei riferimenti a quell'ID fluttuante, non avrà più lo stesso tag di versione dell'entità che occupa quel posto nel pool. Eventuali tentativi di chiamata getEntity
(o uno isEntityValid
o quello che preferisci) falliranno. Se si alloca un nuovo oggetto in quella posizione, i vecchi ID continueranno a fallire.
Puoi usare qualcosa del genere per la tua "opzione 2" per assicurarti che funzioni senza preoccupazioni per i riferimenti alle entità precedenti. Si noti che non è mai necessario memorizzarne uno entity*
poiché potrebbero spostarsi ( pool.push_back()
potrebbero riallocare e spostare l'intero pool!) E utilizzare invece solo entity_id
per riferimenti a lungo termine. Utilizzare getEntity
per recuperare un oggetto ad accesso più rapido solo nel codice locale. std::deque
Se lo desideri, puoi anche utilizzare a o simili per evitare l'invalidazione del puntatore.
La tua "opzione 3" è una scelta perfettamente valida. Non c'è nulla di intrinsecamente sbagliato nell'uso world.foo(e)
anziché nell'uso e.foo()
, soprattutto perché probabilmente si desidera world
comunque il riferimento e non è necessariamente migliore (anche se non necessariamente peggio) archiviare quel riferimento nell'entità stessa.
Se vuoi davvero che la e.foo()
sintassi rimanga intorno, considera un "puntatore intelligente" che lo gestisce per te. Costruendo il codice di esempio che ho rinunciato sopra, potresti avere qualcosa del tipo:
class entity_ptr {
world* _world;
entity_id _id;
public:
entity_ptr() : _id(0) { }
entity_ptr(world& world, entity_id id) : _world(&world), _id(id) { }
bool empty() const { return _world != NULL && _world->getEntity(_id) != NULL; }
void clear() { _world = NULL; _id = 0; }
entity* get() { assert(!empty()); return _world->getEntity(_id); }
entity* operator->() { return get(); }
entity& operator*() { return *get(); }
// add const method where appropriate
};
Ora hai un modo per memorizzare un riferimento a un'entità che utilizza un ID univoco e che può utilizzare l' ->
operatore per accedere alla entity
classe (e qualsiasi metodo che crei su di essa) in modo abbastanza naturale. Il _world
membro potrebbe essere anche singleton o globale, se preferisci.
Il codice utilizza semplicemente un entity_ptr
posto al posto di qualsiasi altro riferimento di entità e va. Puoi anche aggiungere il conteggio dei riferimenti automatici alla classe se lo desideri (un po 'più affidabile se aggiorni tutto quel codice in C ++ 11 e usi la semantica di spostamento e i riferimenti di valore) in modo da poterlo usare entity_ptr
ovunque e non pensare più pesantemente su riferimenti e proprietà. Oppure, e questo è ciò che preferisco, crearne uno separato owning_entity
e weak_entity
digitare solo con i precedenti conteggi dei riferimenti di gestione in modo da poter utilizzare il sistema dei tipi per distinguere tra handle che mantengono in vita un'entità e quelli che lo fanno solo riferimento fino alla sua distruzione.
Si noti che il sovraccarico è molto basso. La manipolazione dei bit è economica. La ricerca extra nel pool non è un costo reale se accedi comunque ad altri campi entity
subito dopo. Se le tue entità sono veramente solo ID e nient'altro, potrebbe esserci un po 'di sovraccarico in più. Personalmente, l'idea di un ECS in cui le entità sono solo ID e nient'altro mi sembra un po '... accademica per me. Ci sono almeno alcune bandiere che vorrai memorizzare sull'entità generale, e i giochi più grandi probabilmente vorranno una raccolta di componenti dell'entità di qualche tipo (elenco collegato incorporato se non altro) per strumenti e supporto per la serializzazione.
Come nota piuttosto finale, non ho inizializzato intenzionalmente entity::version
. Non importa Qualunque sia la versione iniziale, purché la incrementiamo ogni volta che andiamo bene. Se finisce vicino 2^16
allora si avvolge. Se si finisce per spostarsi in modi che rendono validi i vecchi ID, passare a versioni più grandi (e ID a 64 bit se necessario). Per sicurezza, dovresti probabilmente cancellare entity_ptr ogni volta che lo controlli ed è vuoto. Potresti empty()
farlo per te con un mutevole _world_
e _id
, stai solo attento con il threading.
owning_entity
eweak_entity
?