This is the follow-up to my article from April, Understanding Component-Entity-Systems. If you haven't read that article yet, I suggest looking it over because it explains the theory behind what I am about to show you. To summarize what was written:
- Components represent the data a game object can have
- Entities represent a game object as an aggregation of components
- Systems provide the logic to operate on components and simulate the game
Implementation
Components
I wrote in the last article that a component is essentially a C struct: plain old data, so that's what I used to implement them. They're pretty self-explanatory. I'll implement three types of component here:- Displacement (x, y)
- Velocity (x, y)
- Appearance (name)
typedef struct
{
float x;
float y;
} Displacement;
The Velocity component is defined the same way, and the Appearance has a single string member.
In addition to the concrete data types, the implementation makes use of an enum for creating "component masks", or bit fields, that identify a set of components. Each entity and system has a component mask, the use of which will be explained shortly.
typedef enum
{
COMPONENT_NONE = 0,
COMPONENT_DISPLACEMENT = 1 << 0,
COMPONENT_VELOCITY = 1 << 1,
COMPONENT_APPEARANCE = 1 << 2
} Component;
Defining a component mask is easy. In the context of an entity, a component mask describes which components the entity has. If the entity has a Displacement and a Appearance, the value of its component mask will be COMPONENT_DISPLACEMENT | COMPONENT_APPEARANCE.
Entities
The entity itself will not be defined as a concrete data type. In accordance with data-oriented-design (DOD) principles, having each entity be a structure containing each of its components, creating an "array of structs", is a no-no. Therefore, each component type will be laid out contiguously in memory, creating a "struct of arrays". This will improve cache coherency and facilitate iteration. In order to do this, the entity will be represented by an index into each component array. The component found at that index is considered as part of that entity. I call this "struct of arrays" the World. Along with the components themselves, it stores a component mask for each entity.
typedef struct
{
int mask[ENTITY_COUNT];
Displacement displacement[ENTITY_COUNT];
Velocity velocity[ENTITY_COUNT];
Appearance appearance[ENTITY_COUNT];
} World;
ENTITY_COUNT is defined in my test program to be 100, but in a real game it will likely be much higher. In this implementation, the maximum number of entities is constrained to this value. I prefer to use stack-allocated memory to dynamic memory, but the world could also be implemented as a number of C++-style vectors, one per component.
Along with this structure, I have defined a couple of functions that are able to create and destroy specific entities.
unsigned int createEntity(World *world)
{
unsigned int entity;
for(entity = 0; entity < ENTITY_COUNT; ++entity)
{
if(world->mask[entity] == COMPONENT_NONE)
{
return(entity);
}
}
printf("Error! No more entities left!\n");
return(ENTITY_COUNT);
}
void destroyEntity(World *world, unsigned int entity)
{
world->mask[entity] = COMPONENT_NONE;
}
The first does not "create" an entity per se, but instead returns the first "empty" entity index, i.e. for the first entity with no components. The second simply sets an entity's component mask to nothing. Treating an entity with an empty component mask as "non-existent" is very intuitive, because no systems will run on it.
I've also created a few helper functions to create a fully-formed entity from initial parameters such as displacement and velocity. Here is the one that creates a tree, which has a Displacement and an Appearance.
unsigned int createTree(World *world, float x, float y)
{
unsigned int entity = createEntity(world);
world->mask[entity] = COMPONENT_DISPLACEMENT | COMPONENT_APPEARANCE;
world->displacement[entity].x = x;
world->displacement[entity].y = y;
world->appearance[entity].name = "Tree";
return(entity);
}
In a real-world engine, your entities would likely be defined using external data files, but that is beyond the scope of my test program. Even so, it is easy to see how flexible the entity creation system is.
Systems
The systems are easily the most complex part of the implementation. Each system is a generic function which is mapped to a certain component mask. This is the second use of a component mask: to define which components a certain system operates on.
#define MOVEMENT_MASK (COMPONENT_DISPLACEMENT | COMPONENT_VELOCITY)
void movementFunction(World *world)
{
unsigned int entity;
Displacement *d;
Velocity *v;
for(entity = 0; entity < ENTITY_COUNT; ++entity)
{
if((world->mask[entity] & MOVEMENT_MASK) == MOVEMENT_MASK)
{
d = &(world->displacement[entity]);
v = &(world->velocity[entity]);
v->y -= 0.98f;
d->x += v->x;
d->y += v->y;
}
}
}
Here is where the component mask becomes really powerful. It makes it trivial to select an entity based on whether or not it has certain components, and it does it quickly. If each entity was a concrete structure with a dictionary or set to show which components it has, it would be a much slower operation.
The system itself adds the effect of gravity and then moves any entity with both a Displacement and a Velocity. If all entities are initialized properly, every entity processed by this function is guaranteed to have a valid Displacement and Velocity.
The one downside of the component mask is that the number of possible components is finite. In this implementation it is 32 because the default integer type is 32 bits long. C++ provides the std::bitset class, which is N bits long, and I'm sure other languages provide similar facilities. In C, the number of bits can be extended by using multiple component masks in an array and checking each one independently, like this:
(EntityMask[0] & SystemMask[0]) == SystemMask[0] && (EntityMask[1] & SystemMask[1]) == SystemMask[1] // && ...
Great article!
I would suggest moving the looping through all entities and masking inside each system function. Try to follow Mike Acton's tip "Where there's one, there's more than one."
Before you know it, with the current implementation you will be calling each system function thousands of times every frame.
You don't even need a System struct, it can simply be a function.
And then again you can also remove the runSystem function and simply run each system directly, but since you probably won't have that many systems the perfomance impact will be smaller.