La soluzione più semplice che evita di scendere ai bit e ai byte dei raggi X e di perdere tutta la sicurezza del tipo nell'implementazione intermedia: non astrarre / mixare lo storage sul singolo componente, astrarre / mixare nel container livello. Invece di BaseComponent
puoi usare BaseComponents
(avviso plurale) per la tua astrazione. Ovviamente questo significa che quando si inserisce un componente concreto (magari attraverso una sorta di factory), è necessario downcast il relativo contenitore astratto (sebbene questo sia generalmente inevitabile con un ECS, specialmente se non è possibile utilizzare tutte le chicche in fase di compilazione , ma è centralizzato in un punto nell'implementazione ECS) con le informazioni sul tipo corrispondenti per inserire il sottotipo del componente concreto in base al valore.
Ciò è sfumato rispetto all'implementazione (posso aiutarlo se necessario), ma questa è la strategia fondamentale. Il modo più semplice per memorizzare tipi misti di elementi in cui gli elementi dello stesso sottotipo sono memorizzati in modo contiguo è astratto a livello del contenitore e il contenitore polimorfico è in realtà un contenitore di contenitori.
Un esempio molto semplice (frettoloso e sgradevole, ma minimalista), che non copre le campane e i fischi di associare componenti alle entità, o consentire la registrazione di componenti di runtime non codificati, ma mostra un modo fondamentale per mantenere contiguità nei sottotipi del componente:
struct FooComponent
{
enum {id = 0};
int x;
int y;
};
struct BarComponent
{
enum {id = 1};
float z;
};
struct Components
{
virtual ~Components() {}
};
template <class T>
struct ComponentsT: public Components
{
// All components of a given type are stored contiguously,
// by value.
std::vector<T> comps;
};
struct Ecs
{
Ecs()
{
// Insert concrete sequences/containers for Foo and Bar components.
comps.emplace_back(new ComponentsT<FooComponent>);
comps.emplace_back(new ComponentsT<BarComponent>);
}
template <class T>
void insert(const T& comp)
{
// Fetch appropriate container based on associated type ID to T.
typedef ComponentsT<T> SubComponents;
SubComponents* sub_comps = dynamic_cast<SubComponents*>(comps[T::id].get());
// Insert component of type T.
sub_comps->comps.push_back(comp);
}
template <class T>
T* get(int idx)
{
// Fetch appropriate container based on associated type ID to T.
typedef ComponentsT<T> SubComponents;
SubComponents* sub_comps = dynamic_cast<SubComponents*>(comps[T::id].get());
// Return component of type T at specified index.
return &sub_comps->comps[idx];
}
// A container of containers of different component types.
std::vector<std::unique_ptr<Components>> comps;
};
In fase di runtime
Ora essere in grado di fare cose come registrare nuovi tipi di componenti al volo in fase di runtime diventa davvero coinvolgente con un'implementazione ECS in piena regola.
Ma la cosa fondamentale da ricordare è che hai bisogno di un'implementazione in fase di compilazione per costruire e distruggere un certo tipo, T
. Quindi devi chiamare una funzione che ha quel tipo di informazioni per essere in grado di fare queste cose che richiedono tali informazioni di tipo.
Ma ovviamente se chiami tali funzioni da Lua, non hai informazioni di questo tipo. Quindi la strategia generale è che Lua usa, ad esempio, un ID di tipo (potrebbe essere solo un numero intero, o una stringa, o qualsiasi altra cosa galleggi la tua barca) per dire indirettamente all'ECS quali funzioni chiamare. E quando registri nuovi tipi di componenti, puoi fornire i metadati ECS su come costruire e distruggere quel particolare tipo di componente (es: puntatori di funzione o un'interfaccia virtuale per costruire e distruggere BarComponent
) e associarlo a questo tipo ID / chiave. I metadati potrebbero effettivamente memorizzare ComponentsT<BarComponent>
come parte di esso insieme ai puntatori di funzione / funzioni virtuali per recuperare e costruire e distruggere e inserire% istanze diBarComponent
.
Semplice esempio di registrazione runtime
Va bene, per noia ho incluso un esempio molto minimalista e frettoloso di come fare quanto detto sopra usando chiavi di stringa (non consiglio le chiavi di stringa normalmente in un ECS - le stringhe internate potrebbero essere un'alternativa decente, ma sono semplici).
#include <vector>
#include <map>
#include <string>
#include <memory>
#include <iostream>
struct FooComponent
{
int x;
int y;
};
struct BarComponent
{
float z;
};
// Abstract container.
struct Components
{
virtual ~Components() {}
virtual void insert() = 0;
virtual void erase(int idx) = 0;
virtual int size() const = 0;
virtual void* get(int idx) = 0;
};
// Container subtype.
template <class T>
struct ComponentsT: public Components
{
virtual void insert() override
{
comps.push_back(T());
}
virtual void erase(int idx) override
{
comps[idx] = comps[comps.size() - 1];
comps.pop_back();
}
virtual int size() const override
{
return static_cast<int>(comps.size());
}
virtual void* get(int idx) override
{
return &comps[idx];
}
// Stores all components of type T contiguously by value.
std::vector<T> comps;
};
struct Ecs
{
// Registers a new type of component and associates the type to
// a string key.
template <class T>
void register_type(const std::string& key)
{
Components* comps_t = new ComponentsT<T>;
comps[key] = std::unique_ptr<Components>(comps_t);
}
// Fetch the container of components associated to string key
// or nullptr if no such type is registered.
Components* fetch_components(const std::string& key)
{
auto pos = comps.find(key);
return pos != comps.end() ? pos->second.get(): nullptr;
}
// Associates a container of component subtypes to a string key.
std::map<std::string, std::unique_ptr<Components>> comps;
};
int main()
{
using namespace std;
Ecs ecs;
// Register FooComponents to a string key.
ecs.register_type<FooComponent>("FooComponent");
// Register BarComponents to a string key.
ecs.register_type<BarComponent>("BarComponent");
// Fetch contiguous FooComponents container by string key.
Components* foo_comps = ecs.fetch_components("FooComponent");
// Insert a FooComponent to scene.
foo_comps->insert();
// Retrieve a FooComponent and set its fields.
FooComponent* foo = static_cast<FooComponent*>(foo_comps->get(0));
foo->x = 123;
foo->y = 456;
// Fetch contiguous BarComponents container by string key.
Components* bar_comps = ecs.fetch_components("BarComponent");
// Insert some new BarComponents to scene.
for (int j=0; j < 3; ++j)
bar_comps->insert();
// Output how many FooComponents there are in the scene.
cout << foo_comps->size() << " FooComponents" << endl;
// Output how many BarComponents there are in the scene.
cout << bar_comps->size() << " BarComponents" << endl;
}