When I started my current project, I was in much of a "hardcode things" mindset. I imagined that the project would be relatively small, so it wouldn't be too bad to write a C++ class for each object in the game. I started out with a fairly standard object-oriented approach. I had Actors, which were general purpose objects that were updated once per frame. Derived from that, I had GameActors, which had a few extra features, such as being able to access the game's state more easily. From that was derived NetworkActor (which, as the name implies, was an actor synced on the network). DynamicPhysicsActor was derived from that, and then came more more specific classes, such as Player, Enemy (from which things like BatEnemy would be derived), etc.
Like many others, as I tried to maintain this system, I discovered that even with just a few actor types, it very quickly started to become messy, unmanageable, and worst of all, restrictive. For example, what if I wanted an enemy that didn't have a standard physical component? I shouldn't derive it from Enemy, since Enemy derives from DynamicPhysicsActor! I'd have to derive it from NetworkActor and rewrite the base Enemy code. Or maybe I'd make an option to disable physics in DynamicPhysicsActor, but that's equally nasty. What if I wanted to use a different networking model for an object? NetworkActor is up pretty high in the hierarchy, so in order to not use it I'd have to make a completely new hierarchy.
In addition to these issues, I was getting tired of hardcoding resource names into my classes and having to update a ton of references every time I added or changed a resource. Clearly I needed an alternative to the mess that I had coded myself into.
It was at this point that I remembered the "component-entity-system" design pattern. I started doing some research and realized that this design would be much better suited to my requirements. There isn't any one "right way" to implement a component-entity-system design, so I made a list of features I wanted and designed the system to fit. These features include:
- No hardcoding entities in C++: entities should be loaded from a file at startup, and each entity consists of a list of components (components are "hardcoded" in the engine).
- Dynamically adding/removing components from entities is not required.
- Components should be able to communicate in some "generic" way. E.g. a PositionComponent and a DynamicPhysicsComponent should both be able to provide a Vector3 position value.
I'll now describe the solution I came up with to meet these requirements.
Components:
Each component consists of a C++ class derived from the Component base class. Unlike many implementations, logic is implemented in the components themselves, as opposed to just storing data. Each class derived from Component must contain the following:
- A nested class called Settings, which has a constructor that reads the settings for a component from part of a file.
- A static method getDescription() which returns a ComponentDescription, described below.
At startup, all components are registered with the ComponentEntityManager class. This is done using a templated method registerComponent <T>(). This component calls the getDescription() method on T to obtain a ComponentDescription containing the following:
- A string representing the name of the component
- The size/alignment requrements of the component
- A list of message handlers for the component, described below (side note: actually two lists, one for the client and one for the server)
- The network state table variables for the component. I won't go into detail about this here, but I've implemented a system similar to Valve's.
- A few more "thunky" functions for constructing and destroying the component
Message Handlers:
Components communicate using a messaging system. Messages are sent to entities and are forwarded to a message handler for each interested component. The message handler should return a bool value: true indicates that the message has been "consumed" and should not be forwarded to the remaining components. Messages consist of structures of the following form:
struct SomeMessage {
static const uint ID = 100;
int pieceOfData;
float moreData;
};
Note the ID field: each message type must have a unique value. Each component of type T registers a number of message handlers. A message handler consists of an integer "priority", used to determine the order in which components receive a message, and a function pointer to handle the message, of the form: bool T::someMessageHandler( SomeMessage * message )
Using the power of templates, when registering message handlers, all we need to do is provide a pointer to the message handling function and the message ID value will be determined from the message type argument in the handler. This means we never have to deal with the IDs once we've declared them, just the message types. Message handlers are encapsulated using the following implementation (which uses boost and fastdelegate):
struct MessageHandlerDescription {
uint messageId;
int priority;
fastdelegate::FastDelegate2 <Component*, void*, bool> handler;
// slightly ugly - space for the member function pointer to the message handler
char handlerPointer[16];
// another option that would simplify this would be to convert the "handler" argument to a template argument
// unfortunately, the downside is that ComponentType and MessageType can't be deduced that way
template <typename ComponentType, typename MessageType>
ComponentDescription::MessageHandlerDescription ComponentDescription::MessageHandlerDescription::create( bool (ComponentType::*handler)( MessageType* ), int priority ) {
BOOST_STATIC_ASSERT( (boost::is_base_of <Component, ComponentType>::value) );
// thunk to call the message handler
struct Thunk {
// pointerToHandlerPointer is a pointer to a buffer containing the handler function pointer
static bool thunk( Component * ptr, void * pointerToHandlerPointer, void * message ) {
// cast pointer to correct type
ComponentType * tPtr = (ComponentType*)ptr;
// cast handler function pointer to correct type
typedef bool (ComponentType::*THandler)( MessageType* );
THandler tHandler = *((THandler*)pointerToHandlerPointer);
return (tPtr->*tHandler)( (MessageType*)message );
}
};
MessageHandlerDescription desc;
desc.messageId = MessageType::ID;
desc.priority = priority;
desc.handler = &Thunk::thunk;
// copy the handler function pointer to the buffer
BOOST_STATIC_ASSERT( sizeof( handler ) <= sizeof( desc.handlerPointer ) );
memset( desc.handlerPointer, 0, sizeof( desc.handlerPointer ) );
memcpy( desc.handlerPointer, &handler, sizeof( handler ) );
return desc;
}
}
Entities:
Finally, we have entities, which bring order to the components and message handlers. Entities are loaded from the entity description file, entities.txt. Each entity type E provides a list of components, and each component C listed contains a list of settings. This is where the Component::Settings object receives its data from. So each instance of C within entities of type E gets those settings applied.
Since entities contain only a static list of components, we can do a lot of preprocessing when first loading the entities file. Similar to components, we store this preprocessed data in the EntityDescription struct. Components are constructed in-place in an array for each entity, so we determine the layout of each component within that array using using the size and alignment data determined in ComponentDescription.
Each component contains a hash table mapping message IDs to a list of component message handlers sorted by the message handler priorities previously provided. This means that when sending a message to an entity, we only need to forward that message to the list of components that actually care about it. Message handlers are where the component logic lies. For example, there are PreUpdate and PostUpdate messages which are sent to all entities each frame.
The implementation for receiveMessage() is once again templated to avoid explicit use of message IDs and is as follows:
template <typename M> bool Entity::receiveMessage( M * message ) {
// check if there are handler(s) registered
EntityDescription::MessageHandlerList list;
if (entityDescription.messageHandlerMap.get( messageId, list )) {
// pass the message to each handler
// if any of them return true, then the message has been "consumed", so we return
for (size_t i = 0; i < list.count; ++i) {
const EntityDescription::ComponentMessageHandler & cmh =
entityDescription.messageHandlers[list.start + i];
Component * component = getComponent( cmh.componentIndex );
if (cmh.messageHandler( component, data ))
// if returned true, message consumed, so return true immediately
return true;
}
}
return false;
}
We also use the network state tables in each ComponentDescription to construct a single network state table for the entity.