Component Based Game Objects In C++
Overview:
This is the solution that I implemented for the core architecture of Derelict, and later refined for the core architecture of Conglopoly 2.
I designed this system around the notion that accessing component data is a primary functionality in a game engine, so I wanted to make it as efficient as possible. I started with the idea of storing all instances of component types in an array which can be indexed to retrieve the component data. The main challenge of this was to allow for the user to define arbitrary component types and easily have them be integrated into the engine. I achieved this by using templates and the pre-processor to store component types and set up the appropriate data structures at compile time.
Goal: To provide the user with a system which allows them to do the following.
There are three primary pieces to this system.
Lets start by defining the GameObject interface.
GameObject.h:
This is the solution that I implemented for the core architecture of Derelict, and later refined for the core architecture of Conglopoly 2.
I designed this system around the notion that accessing component data is a primary functionality in a game engine, so I wanted to make it as efficient as possible. I started with the idea of storing all instances of component types in an array which can be indexed to retrieve the component data. The main challenge of this was to allow for the user to define arbitrary component types and easily have them be integrated into the engine. I achieved this by using templates and the pre-processor to store component types and set up the appropriate data structures at compile time.
Goal: To provide the user with a system which allows them to do the following.
- Define arbitrary components and integrate them into the engine easily.
- Create, initialize, and destroy Game objects which are compositions of these arbitrary components.
- Access a game object's components in constant time.
There are three primary pieces to this system.
- Game Objects - Provides an interface to Add, Remove, and access component data.
- Components - Provides arbitrary units of data to create game objects.
- Game Object Manager - Provides an interface for creating, initializing, and destroying game objects.
Lets start by defining the GameObject interface.
GameObject.h:
The implementations of the Has, AddComponent, RemoveComponent, and operator bool depend on the object manager, so we will get back to them later.
Lets define the interface for Components.
Component.h:
Lets define the interface for Components.
Component.h:
We will use the Object Manager to connect these two pieces together into a working system. The basic public interface for the Object Manager is as follows.
ObjectManager.h:
ObjectManager.h:
The private implementation of ObjectManager is where most of the magic happens. As mentioned previously, we want component data to exist in an array so we can have constant time component access. We can do this with a template class.
ObjectManager.h:
ObjectManager.h:
Now we have a templated component factory which can be used to store all of our arbitrary component data.
There are still a few more issues that need to be worked out here.
At first this sounds like a lot of upkeep, but we can cut down on the work by making use of the pre-processor so that we only have two simple header files to maintain when creating new component types.
To show how this works we first need to make a component.
ExampleComponent.h:
There are still a few more issues that need to be worked out here.
- How can we make sure a component factory for each component type gets created before the object manager starts using them?
- When the object manager creates or deletes an object, it needs to be able to access the component factory for each component type in order to allocate, initialize, or de-initialize data.
- Component needs to be friends with the component factory for each component type in order for ComponentFactory to access 'flag' and destructor
- GameObject needs to have forward declarations of each component type in order for the Has, AddComponent, and RemoveComponent template functions to work.
At first this sounds like a lot of upkeep, but we can cut down on the work by making use of the pre-processor so that we only have two simple header files to maintain when creating new component types.
To show how this works we first need to make a component.
ExampleComponent.h:
Now, we can create two header files. One to store the names of the component types, and the other to contain all of the component definitions.
ComponentNames.h:
ComponentNames.h:
ComponentDefinitions.h:
For example, if the user created a new component, MyComponent, in order for it to be part of game objects he would need to add
REGISTER_COMPONENT_NAME(MyComponent)
to ComponentNames.h and
#include "MyComponent.h"
to ComponentDefinitions.h
ComponentNames.h allows us to use the pre-processor to generate code for each component type name. Lets try this out by solving problem #3 and #4 stated above. For problem #3, Component needs to be friends with the component factory for each component type in order for ComponentFactory to access 'flag' and Component's destructor.
Component.h:
REGISTER_COMPONENT_NAME(MyComponent)
to ComponentNames.h and
#include "MyComponent.h"
to ComponentDefinitions.h
ComponentNames.h allows us to use the pre-processor to generate code for each component type name. Lets try this out by solving problem #3 and #4 stated above. For problem #3, Component needs to be friends with the component factory for each component type in order for ComponentFactory to access 'flag' and Component's destructor.
Component.h:
As you can see, each entry of REGISTER_COMPONENT_NAME in ComponentNames.h expands to the code defined in the macro. After including ComponentNames.h, it is important to un-define that macro for later use.
For problem #4, GameObject needs to have forward declarations of each component type in order for the Has, AddComponent, and RemoveComponent template functions to compile properly.
GameObject.h:
For problem #4, GameObject needs to have forward declarations of each component type in order for the Has, AddComponent, and RemoveComponent template functions to compile properly.
GameObject.h:
We must also make sure the static instance in each component factory exists and is instantiated and created (problem #1).
ObjectManager.h:
ObjectManager.h:
ObjectManager.cpp:
Our final issue (problem #2) is to make sure the Object Manager can access each separate component factory for creating and destroying objects. We can do this by maintaining a table of pointers to each component factory instance. In order for that to work, we will define an abstract base class for ComponentFactory called BaseComponentFactory. This will allow us to maintain a table of pointers to BaseComponentFactory which can be looped through each time it needs to add or remove a component. We will call this ComponentFactoryTable.
ObjectManager.h
ObjectManager.h
To populate the table, we add an extra line to ObjectManager::Initialize() like so:
ObjectManager.cpp:
ObjectManager.cpp:
Now we can implement the functions in ComponentFactoryTable, ObjectManager, and the template functions in GameObject.
ObjectManager.h:
ObjectManager.h:
ObjectManager.cpp:
GameObject.cpp:
One last thing that we can add is a way for the user to loop through all of the components of a specific type.
ObjectManager.h:
ObjectManager.h:
And here is an example of how to use the system.
main.cpp:
main.cpp:
Final Thoughts:
Constant time component access is achieved here and the design worked great for the purposes that I needed it. However, there are some drawbacks to this design in certain situations.
Constant time component access is achieved here and the design worked great for the purposes that I needed it. However, there are some drawbacks to this design in certain situations.
- Wasted memory on unused components: In a memory constrained environment, this design would not be acceptable. Also, if your game world has a large amount of objects for only a few seconds, then deletes most of them, you will have extra unused memory lying around. This can be potentially avoided with ClearAllObjects_FreeMemory, however. A possible change in the design would be to maintain component handles for each object in ObjectManager, and use those to access the component data. In that case, the component data could be stored in any data structure that suits your needs.
- Looping through components can waste time: This limits the amount of game objects this system can support. Consider a scenario when you have 1000 objects in your game world and only 20 of them have a TextureComponent. The graphics system in your game needs to loop over all of the TextureComponents to draw each texture. The graphics system would then have to loop over 1000 components every frame. A naive fix for this would be to keep a count of how many active components there are in each ComponentFactory, but the worst case scenario remains the same. A better fix would be the one mentioned in the previous bullet, since this issue stems from the same problem of wasted memory.
- Users can access unused components without any warnings: Component has a bool operator for this reason, but there are no protections in place in this design to warn them if they do not check components before accessing them.
- ObjectManager and ComponentFactory are singletons: It's not possible to have multiple instances of ObjectManager if you needed to maintain multiple different game worlds in memory. This could be fixed by removing the singleton parts of these structures and designing a method of generating contexts for ObjectManager. GameObject would then need another private variable to link it to its owning context.