Implementing Component-Entity-Systems

来源:互联网 发布:和淘宝一样的平台 编辑:程序博客网 时间:2024/05/10 16:30

Implementing Component-Entity-Systems


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
The purpose of this article is to show how to implement the architecture that I described in an efficient way, and to provide solutions for some sample problems. All the code samples that I provide will be written in C.

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:

  1. Displacement (x, y)
  2. Velocity (x, y)
  3. Appearance (name)

Here's the sample of code defining the Displacement component. It is a simple struct with two members that define its vector components.

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 aDisplacement 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<n> 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] // && ...

Source Files


I've zipped up the source code here. Main.c runs a sample program that creates three entities and runs each system once.

Attached File  CES.zip   2.41KB   108 downloads 


Real-World Problems


This implementation works very well in the small scope of my program and can easily be extended to use more components and systems. It can also easily be extended to run in a main loop, and extended to read entities from data files with some work.

This section will tackle some problems with transferring gameplay mechanics and advanced features over to my implementation of CES.

Power-ups and collision filtering


This problem was pitched to me by Krohm in a comment on the original article. He was asking about gameplay-specific behaviours in general, but provided the specific example of a power-up that stopped collisions with a certain entity type.

Dynamic components to the rescue! Let's create a component, say GhostBehaviour, that has a list of qualifiers for determining which entities an object can pass through. For example, a list of component masks, or possibly material indices. Any component can be added or removed (technically, enabled or disabled) from any entity at any time, simply by changing that entity's component mask. When the player grabs the powerup, theGhostBehaviour component will be added. It could also have a built-in timer to automatically remove itself after a few seconds.

To actually disable the necessary collisions, the typical collision response in a physics engine can be exploited. In most physics engines, there is first a step to detect collisions and produce contacts, and then a step to actually apply the contact forces to each body. Let's say that each of those steps is implemented in a system, and that there is a component that keeps track of each entity's collision contacts (Collidable). To permit the desired behaviour, each contact should store the index of the other entity.

By injecting a system that operates on a GhostBehaviour and a Collidable in between the two physics steps, the contacts between the entities that should pass through each other can be deleted before they are acted upon by the physics engine. This will have the effect of a disabled collision. The same system can also disable the GhostBehaviour after a few seconds.

A similar approach can be used to perform a certain action upon collision. There could be a unique system for every action, or the same system could govern all the actions. In any case, the system would read the collision contacts to determine whether the entity collided with some other entity and then act accordingly. In fact, this would be needed to give the player the power-up in the first place.

The Big F***ing Spell - Destroying all monsters


Another problem I received was how to kill all monsters with a spell. Thanks to smorgasbord for this one!

The key to solving this one is that a system can be run somewhere outside of the top-level main loop, and that any entity that is a monster, according to CES rules, satisfies the same component mask. For example, every entity with both Health and AI is a monster, and this can be described with a component mask.

Remember how a system is just a function and a component mask? Let's define the "kill all monsters" spell as a system. The function, at its core, isdestroyEntity, but could also create particle effects or play a sound. The component mask of the system can be COMPONENT_HEALTH | COMPONENT_AI.

In terms of actually casting the spell, I mentioned in the previous article that each entity can have one or more input components, which store boolean or real values that map to various inputs, including AI and networked players. Let's create a MagicInputComponent that has a boolean value that says when the entity should cast the spell, and an ID corresponding to the spell that should be cast.

Each spell has a unique ID, which is actually a key in a lookup table. This lookup table maps a spell ID to the function that "casts" that spell. In this case, the function would run the system..

Conclusion


Remember that this is just a sample implementation. It works for the test program, but is probably not ready for a full game. However, I hope that the design principles have been made clear and that it is easy to understand and implement in your own way in a different language.

If you have any more problems you'd like me to solve you can write a comment or send me a message. I'll update this article periodically with my solutions.

Keep in touch for the next article!


原创粉丝点击