This is part two of my series of posts about the game engine we’re building. Part one was about where we started and how we’re working. Before that I had already posted a blog post about our motivations and motives.
Here’s the thing to remember: We’re a team of just two coders working on this engine. Also, bear in mind that we’re currently at a very early stage. Everything can still be changed – which is the main reason why I’m writing publicly about the engine in the first place. Keep the feedback coming, tell me all I’m doing wrong!
The Component System
The core of the Ginkgo engine is a Component System. Instead of using a fat Game Object class and inheriting other game objects, we assemble Components to aggregate a game object. The main point of a Component System is to replace (especially multiple) inheritance with aggregation. Components are allocated independently of the actual game objects and assembled according to requirements. Components are vertical slices of game objects. Each one encapsulates specific functions and a specific set of data. They are updated (their update() function is invoked) in consecutive order. Our system assembles the game object aggregates from templates defined in XML. Our components are stored in contiguous pools. One component pool is updated at one update stage. Updating a component pool means that all components are updated. Thus, a game object is updated several times during one main loop iteration – part for part, component for component.
Let’s look at a typical component. The player Game Object currently consists of 9 components. PlayerController collects and manages a number of other components. PlayerStateMachine is a state machine for the player. It is used to trigger different states (e.g. flying, falling, crashed, …) and thus different player character behavior. PlayerInput is a component that translates the input of the player to specific properties. These are interpreted by the PlayerMovement component. The Box2dBody component wraps a physical body that is actually part of the physics subsystem. The Spatial2d component stores a location and orientation in space. It is written by the Box2dBody and read by the RenderManager. The SpriteRenderer sends rendering information to the RenderManager in every frame. The TickInterpolator component interpolates the draw position and orientation in order to sync the tick-based physics to the frame-based rendering.
Additionally, this player has a particle system attached that consist of two components, the system itself and a particle renderer component. The particles are stored in a different tight contiguous pool. More about that in a later post.
The Components
All components are derived from the base Component class. It stores administrative data and receives a number of events. Components are declared and defined using a number of macros. The most important one is GIN_DECLARE_COMPONENT, which declares ObjectPool and ComponentPool as friends, adds a declaration of an update() function that is not virtual but thus still present in all components. The GIN_DECLARE_COMPONENT_HELPERS are mostly comprised of shortcuts to static management information and typed allocation and free functions.
#define GIN_DECLARE_COMPONENT(className)
GIN_DECLARE_COMPONENT_FRIENDS
private:
GIN_DECLARE_COMPONENT_DECONSTRUCTORS(className)
GIN_DECLARE_COMPONENT_MEMBERS(className)
void update(UpdateStage::Enum stage);
public:
GIN_DECLARE_COMPONENT_HELPERS(className)
GIN_DECLARE_INITIALIZE
Each component has a class, a name and an internal type. By supplying the class name to the macro, we circumvent error prone RTTI methods and can still query each component for its class and type.
There are three important callbacks in each component:
- update(UpdateStage::Enum stage) gets called with a specific update stage. In this function the main runtime work of the component is done. See the runtime section below for further information on the different update stages. Update is called once per frame/tick, so making it non-virtual is crucial.
- onOwnerModified(bool added, ComponentType type, Component* component) is a callback that gets triggered once the owner (usually a game object) is modified, i.e. a component is added or removed.
- loadConfiguration(TiXmlElement* element) is a callback that is issued by the level loader. We use tinyXML for loading component configurations. We’ve initially planned to allow different initialization data, but there was no reason to implement that yet.
Component Pools
Components are stored in component pools. Each pool stores exactly one type (class) of components. It has a capacity that is specified at level load time. The full capacity is allocated once and all components are pre-initialized at that point by calling each component’s init() function.
Component classes set up their pools in static initialization methods pre-main. The pool is first allocated and then registered in the ComponentManager. The ComponentManager class is the main entry point for all component operations, allocating, initializing, updating and terminating components. It manages all pools. Once the pool is valid, the staticInitialise() function of the component class is called. This function initializes e.g. exported component properties for the GUI and component loading. The two-stage init is just necessary because the preMain() call is wrapped in a macro that always makes sure the pool is created:
GIN_IMPLEMENT_INITIALIZE(className)
void className::preMain() {
_pool = new ComponentPool;
ComponentManager::instance().registerType(&_info, static_cast(_pool), stages);
staticInitialise();
}
As you can see in the registerType() function above, the pool is created with a template of the component class. Every concrete pool object is a subclass of the abstract IComponentPool interface:
class IComponentPool
{
public:
virtual ~IComponentPool() { }
virtual Component* allocateBase(void *prius, GameObject *owner) = 0;
virtual Component* getComponentBase(ComponentHandle handle) = 0;
virtual void freeComponentBase(ComponentHandle handle) = 0;
virtual void recreate(ComponentHandle newSize) = 0;
virtual void updateComponents(UpdateStage::Enum stage) = 0;
virtual Component** getActiveComponents(u32& numActiveComponents) = 0;
virtual ComponentArray getActiveComponents() = 0;
virtual void pauseComponents (bool paused) = 0;
};
Let’s go through these functions one by one. recreate() deletes all Components in the pool (physically) and creates #newSize new ones. It is to be used at level or scene changes. updateComponents calls the non-virtual update function of all components, whose state is set to INITIALISED. All components start in state DEAD. getActiveComponents() returns a temporary array of Component* or a vector of Component* containing only initialized components. We have implemented a component-level pause function that can be called via pauseComponents().
The allocateBase() function allocates a component and returns it with the actual type it has. It calls pool->allocate() and simply casts the component before returning it. The allocate function of the pool first retrieves a new component handle. Then it sets the state of the new component to INITIALISED, sets the game object that called the allocate function as the owner of the component and calls the component’s init.
The ComponentManager
Usually you do not interact with component pools directly. Instead, ComponentManager acts as a one-stop shop for all accesses to component pools. This singleton class also manages the actual pools. The functionality of the ComponentManager is split into two areas: Its internal functions for managing the pools, its external functions for querying the pool for specific components.
The manager maintains an array of actual pools. It also stores all necessary runtime information for the pools and has preprocessed lookup functions for retrieving the internal type and the name of a component class. Most lookup functions are based on each component pool’s ComponentInfo struct. The ComponentInfo is stored as a static struct for each component class:
struct ComponentInfo
{
ComponentType type;
FixedSymbol name;
ComponentType baseType;
FixedSymbol baseName;
FixedSymbolArray requiredComponentsAtStartup;
};
The system can look up type information and name of the component class and the same for the base class of the component. It also stores a list of required components describing all dependencies of this component, should there be any. We have implemented a fail-early approach for dependencies. The other option would have been to use a visitor pattern. In that case, a component would always act as if its dependency is sufficed and broadcast messages in the hope that the required component picks it up. In our solution, the required component needs to be present once the whole list of components for the game object is assembled. Otherwise the system exits with an error.
The component manager has the ability to call all component update functions per pool. Since the data of the components is stored in consecutive order, iterating over them and invoking their respective update functions should be very cache-friendly. The update stage for each component class is set at the definition of the class via the macro GIN_IMPLEMENT_COMPONENT(className, stages). We currently support one update stage per pool but a limited number of stages in total.
Next up: The Game Loop
In the next post about the Ginkgo engine I’ll write about the runloop and how (and when) the different subsystems and component pools are updated.
It’s great to have your own tech but it also means that you have to make a lot of decisions. I know we’re building something quite experimental – and a lot of things could go wrong. I’d highly welcome feedback!