Allocazione di entità all'interno di un sistema di entità


9

Non sono sicuro di come dovrei allocare / assomigliare le mie entità all'interno del mio sistema di entità. Ho varie opzioni, ma la maggior parte sembra avere dei contro associati. In tutti i casi, le entità sono simili a un ID (numero intero) e possibilmente hanno una classe wrapper associata. Questa classe wrapper ha metodi per aggiungere / rimuovere componenti da / verso l'entità.

Prima di menzionare le opzioni, ecco la struttura di base del mio sistema di entità:

  • Entità
    • Un oggetto che descrive un oggetto all'interno del gioco
  • Componente
    • Utilizzato per archiviare i dati per l'entità
  • Sistema
    • Contiene entità con componenti specifici
    • Utilizzato per aggiornare entità con componenti specifici
  • Mondo
    • Contiene entità e sistemi per il sistema di entità
    • Può creare / distruggere entità e avere sistemi aggiunti / rimossi da / ad essa

Ecco le mie opzioni a cui ho pensato:

Opzione 1:

Non archiviare le classi del wrapper Entity e archiviare solo l'ID / gli ID eliminati successivi. In altre parole, le entità verranno restituite per valore, in questo modo:

Entity entity = world.createEntity();

Questo è molto simile a entityx, tranne che vedo alcuni difetti in questo design.

Contro

  • Possono esserci classi duplicate del wrapper di entità (poiché è necessario implementare il copy-ctor e i sistemi devono contenere entità)
  • Se un'entità viene distrutta, le classi wrapper entità duplicate non avranno un valore aggiornato

Opzione 2:

Memorizzare le classi del wrapper di entità all'interno di un pool di oggetti. cioè le Entità verranno restituite da un puntatore / riferimento, in questo modo:

Entity& e = world.createEntity();

Contro

  • Se sono presenti entità duplicate, quando un'entità viene distrutta, lo stesso oggetto entità può essere riutilizzato per allocare un'altra entità.

Opzione 3:

Utilizzare ID grezzi e dimenticare le classi di entità wrapper. La rovina di questo, penso, è la sintassi che sarà richiesta per questo. Sto pensando di farlo come sembra il più semplice e facile da implementare. Non ne sono abbastanza sicuro, a causa della sintassi.

cioè Per aggiungere un componente con questo disegno, sarebbe simile a:

Entity e = world.createEntity();
world.addComponent<Position>(e, 0, 3);

Come allegato a questo:

Entity e = world.createEntity();
e.addComponent<Position>(0, 3);

Contro

  • Sintassi
  • ID duplicati

Risposte:


12

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 isEntityValido 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_idper riferimenti a lungo termine. Utilizzare getEntityper recuperare un oggetto ad accesso più rapido solo nel codice locale. std::dequeSe 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 worldcomunque 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 entityclasse (e qualsiasi metodo che crei su di essa) in modo abbastanza naturale. Il _worldmembro potrebbe essere anche singleton o globale, se preferisci.

Il codice utilizza semplicemente un entity_ptrposto 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_ptrovunque e non pensare più pesantemente su riferimenti e proprietà. Oppure, e questo è ciò che preferisco, crearne uno separato owning_entitye weak_entitydigitare 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 entitysubito 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^16allora 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.


Perché non contenere l'ID all'interno della struttura dell'entità? Sono abbastanza confuso. Inoltre potresti usare std :: shared_ptr / weak_ptr per owning_entitye weak_entity?
miguel.martin,

È possibile invece contenere l'ID, se lo si desidera. L'unico punto è che il valore dell'ID cambia quando un'entità nello slot viene distrutta mentre l'ID contiene anche l'indice dello slot per una ricerca efficiente. Puoi usare shared_ptre, weak_ptrma tieni presente che sono pensati per oggetti allocati individualmente (anche se possono avere deleter personalizzati per alterarlo) e quindi non sono i tipi più efficienti da usare. weak_ptrin particolare potrebbe non fare quello che vuoi; impedisce a un'entità di essere completamente deallocata / riutilizzata fino a quando non weak_ptrviene ripristinata ogni operazione weak_entity.
Sean Middleditch,

Sarebbe molto più semplice spiegare questo approccio se avessi una lavagna o non fossi troppo pigro per elaborarlo in Paint o qualcosa del genere. :) Penso che visualizzare la struttura lo renda estremamente chiaro.
Sean Middleditch,

gamesfromwithin.com/managing-data-relationships Questo articolo sembra presentare un po 'quello che hai detto nella tua risposta, è questo che intendi?
miguel.martin,

1
Sono l'autore di EntityX e il riutilizzo degli indici mi ha infastidito per un po '. Sulla base del tuo commento, ho aggiornato EntityX per includere anche una versione. Grazie @SeanMiddleditch!
Alec Thomas,

0

Al momento sto lavorando a qualcosa di simile e sto usando una soluzione più vicina al tuo numero 1.

Ho EntityHandleistanze restituite dal World. Ognuno EntityHandleha un puntatore al World(nel mio caso, lo chiamo solo EntityManager) e i metodi di manipolazione / recupero dei dati in EntityHandlesono in realtà chiamate a World: ad esempio per aggiungere un Componenta un'entità, è possibile chiamare EntityHandle.addComponent(component), che a sua volta chiamerà World.addComponent(this, component).

In questo modo le Entityclassi wrapper non vengono archiviate e si evita l'overhead aggiuntivo nella sintassi che si otterrebbe con l'opzione 3. Si evita anche il problema di "Se un'entità viene distrutta, le classi wrapper entità duplicate non avranno un valore aggiornato ", perché indicano tutti gli stessi dati.


Cosa succede se si crea un altro EntityHandle per assomigliare alla stessa entità e quindi si tenta di eliminare uno degli handle? L'altro handle avrà comunque lo stesso ID, il che significa che "gestisce" un'entità morta.
miguel.martin,

È vero, gli altri handle rimanenti indicheranno quindi l'ID che non "detiene" più un'entità. Naturalmente, le situazioni in cui si elimina un'entità e quindi si tenta di accedervi da altrove dovrebbero essere evitate. Ad Worldesempio, potrebbero generare un'eccezione quando si tenta di manipolare / recuperare dati associati a un'entità "morta".
vijoc

Mentre è meglio evitare, nel mondo reale questo accadrà. Gli script si aggrappano ai riferimenti, gli oggetti di gioco "intelligenti" (come la ricerca di missili) si aggrappano ai riferimenti, ecc. Hai davvero bisogno di un sistema che sia in grado di gestire correttamente riferimenti stantii o che rintracci e azzeri debole Riferimenti.
Sean Middleditch,

Il mondo potrebbe, ad esempio, generare un'eccezione quando tenta di manipolare / recuperare dati associati a un'entità "morta" .
miguel.martin,
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.