a friend of mine and me are building a game. Therefore we decided to use a Entity Component system.
What do we want to achieve?
- Cache friendliness
- Fast iteration times
- Code as small and clean as possible
Our overall design we got until now:
- Handles instead of raw pointers
- Entites are small classes with an array of componenthandles
- Components are just data structs
- Systems that got one job
To get this done we got a so called SlotMap that holds the components internally in a inner array linearly packed. You can get the component pointers via one lookup into this structure from outside. To work on the component data the systems just have to iterate over this inner array.
I want to share the main ressources where we got our inspiration from:
- http://bitsquid.blogspot.de/2011/09/managing-decoupling-part-4-id-lookup.html
- http://molecularmusings.wordpress.com/2013/07/24/adventures-in-data-oriented-design-part-3c-external-references/
Some Code:
struct Entity
{
ComponentHandle componentIDs[64];
template<typename T>
ComponentHandle getComponent()
{
return componentIDs[TypeID::value<T>()];
}
template<typename T>
void addComponent(ComponentHandle ID)
{
componentIDs[TypeID::value<T>()] = ID;
}
};
This is the entity class. The systems get registered via the scene and can get back via the TyeID template.
class Scene
{
public:
Scene();
~Scene();
EntityID& createEntity();
void deleteEntity(EntityID ID);
template<typename T>
void registerSystem(void* dataStorage)
{
m_systems[TypeID::value<T>()] = dataStorage;
}
template<typename T>
ComponentHandle createComponent()
{
SlotMap<T, ComponentHandle>* storage = static_cast<SlotMap<T, ComponentHandle>*>(m_systems[TypeID::value<T>()]);
return storage->add();
}
template<typename T>
T* getComponent(ComponentHandle id)
{
SlotMap<T, ComponentHandle>* storage = static_cast<SlotMap<T, ComponentHandle>*>(m_systems[TypeID::value<T>()]);
return storage->get(id);
}
Entity& getEntity(EntityID ID);
private:
void* m_systems[64];
ECS::FreeListTable<Entity, MAX_ENTITIES>* m_entities;
};
There we got the scene. Here comes my first question: Is it a good idea to store the Systems this way as pointers and have them to register in the scene or could the be a better way via some macros or templates?
typedef SlotMap<Mesh, ComponentHandle> MeshMap;
class MeshRenderSystem
{
public:
MeshRenderSystem(Renderer* renderer, Scene* scene) : m_renderer(renderer)
{
m_meshArray = new MeshMap();
scene->registerSystem<Mesh>(m_meshArray);
m_effect = new GreenDrawEffect(m_renderer);
}
~MeshRenderSystem()
{
}
void update(RenderMatrixMap& matrices)
{
MeshMap::iterator i = m_meshArray->begin();
MeshMap::iterator j = m_meshArray->end();
RenderMatrixMap::iterator k = matrices.begin();
for (; i != j; i++)
{
m_renderer->m_paramManager->setWorldMatrixParameter(&k->worldMatrix);
m_renderer->m_paramManager->setShaderResourceParameter(L"objTexture", i->texture.resourceView);
m_renderer->m_paramManager->setSamplerStateParameter(L"samplerState", i->texture.samplerState);
m_renderer->drawIndexed(i->indexCount, i->vertexBuffer, i->indexBuffer, m_effect);
k++;
}
}
private:
MeshMap* m_meshArray;
Renderer* m_renderer;
GreenDrawEffect* m_effect;
};
There we got a system example. You can see the system iterates linearly over the data it has to render.
Okay so now, what do we want to ask?
- Is it a good idea to have many of this small systems that all got one job and prepare the data for the next system?
- Sometimes i think there are systems that need a big load of lookups to work on special data. That could kill the performance, when we got a lot of data. For example: we want to calculate the worldmatrices for the rendering. So we need the positions of the meshes. Therefore we can see which entities got meshes and lookup for these the positions and calculate the worldmatrix from. That way we got the number of meshes as lookups every frame.
- The Slotmap holds the innerarray with the data that is packed and two indices array that are 32 bit per index. So the arrays are everytime (4 * 2 + sizeof(componentData)) * 2 ^ handle::index (usually 16 bit). even if there is maybe just the half of the components used. Would it be a good idea to make this arrays dynamically sizeable?
Thanks for reading! I'm very excited to see your opinions. I'm willing to share all knowledge we got until now, so if you want to know something that goes into this topic feel free to ask.
Alex