Component-based programming (for games)
Arcade games center around a few basic types: a player, monsters, and bullets. I used to write behaviors for different kinds of monsters in plain C by using structs that have pointers to functions—as if having methods in a non-object oriented language. In C++ we can use inheritance and leverage polymorphism. This will work as long as you keep things simple. As often in C++, things don’t remain simple, and quickly become complicated. It turns out that a strict class hierarchy is difficult to work with in game codes. A more flexible model is component-based programming.
The troubles with tribbles
After all you’ve learned in school about object-oriented programming, you would think that the classic class hierarchy is the only right way. But then when you go and actually do it, you find that it is just not all that, and it’s the class hierarchy itself that gets in the way.
At some level things are clean and simple: alien ships move, get hit,
and explode. But aside from alien ships, we want the game world to contain
turret towers, power-ups, and invisible triggers. Because these entities are
so different from each other, it becomes difficult to unify them under a
grandparent GameObject
class. Unifying them is important because we would
really like to be able to store all game objects into a list (or array, or
a grid, or a quad tree or whatever data structure you are using), and ideally
we want C++’s polymorphism feature work it all out.
Breaking it down
Game objects have certain capabilities. A moving object can move. A living object is alive and has a heartbeat. A trigger can be triggered. These capabilities ask for certain methods, as well as state kept in member variables. A moving object has speed and a direction, and a living object may have hit points. These capabilities may be combined; a monster in the game is both moving and living. The capabilities are components for constructing game objects.
Pseudo-code:
class GameObject {
MovingObject *moving;
LivingObject *living;
void move(void) {
if (moving) {
moving->move();
}
}
void heartbeat(void) {
if (living) {
living->heartbeat();
}
}
};
The GameObject
only has placeholders that you can plug components into.
The MovingObject
keeps speed and direction, and the LivingObject
holds
timer ticks so that it can heartbeat on the right time.
While this is a very small and simple example, it would already be a hassle to implement in a class hierarchy without using multiple inheritance.
Now we want to make a variation, an object that moves in zig-zags. It changes direction every now and then.
class ZigZagMover : public MovingObject {
...
};
The ZigZagMover
component inherits MovingObject
, and plugs into
GameObject
without issues by virtue of C++ polymorphism.
Purists will note that MovingObject
should be an abstract base class
providing only an interface. The speed
and direction
members should be in
a derived Mover
class.
Me, myself, and I
We normalize GameObject
, taking as many members as possible out of the class
and moving them into components. The only things all game objects have in
common are the component placeholders and an x,y,z
world position.
The moving
component must be able to access it (otherwise it can’t update
the object’s position). Therefore we pass the GameObject
instance to the
component code as parameter self
:
class GameObject {
Vec3 pos;
MovingObject *moving;
void move(void) {
if (moving) {
moving->move(*this);
}
}
};
class Mover : public MovingObject {
float speed;
Vec3 direction;
void move(GameObject& self) {
self.pos += speed * direction;
}
};
You can choose whether you want to pass a reference or a pointer (it doesn’t
make much of a difference), but the point here is to refer to the underlying
GameObject
as self
. The component now has access to the object it is
plugged in to.
There is an iffy bit about coding things this way, as you can see it leads
to some spaghetti-like constructs. On the other hand, it is quite wonderful
that the model allows this. For example, suppose we wanted the zigzag mover
to change direction based on heartbeat, then its ‘living’ code should
have knowledge of the ‘mover’ code. You can now interconnect components
via the self
parameter.
Put all the component codes for a certain game entity in a single file, and
you won’t get lost in the source code.
Animated animation
Next, we want to add animation. Some game entities will have animation, while others won’t. We can easily add a component for that:
class GameObject {
MovingObject *moving;
LivingObject *living;
AnimatedObject *anim;
void animate(long timedelta) {
if (anim) {
anim->animate(*this, timedelta);
}
}
};
class AnimatedObject {
int curr_frame, anim_speed;
const std::array<TextureID> *frames;
void animate(const GameObject& self, long timedelta) {
...
}
};
We now have objects that can do animation. Note that this component can be
applied to any kind of object, so for example we might have non-moving objects
that have animation, and living objects that don’t.
It’s making these combinations freely that isn’t possible in a clean way
when using a strict class hierarchy.
Typing the types
All game objects have certain static properties like width, height (well,
unless they are elastic and have varying width and height), texture id,
etcetera. This depends on what type of game object it is. Because these
properties are static, they do not need to be a member variable in each and
every instance of the object. Therefore they can be coded as const
:
struct ObjectProps {
int width, height;
TextureID tex;
};
class GameObject {
const ObjectProps *props;
MovingObject *moving;
LivingObject *living;
AnimatedObject *anim;
...
};
Note how this is much like having dynamic typing with prototypes.
We are free to morph any object in the game during runtime.
The best example of why this is useful is replacing the player’s input
component with one that replays a previously recorded game for a demo
mode. Or you could have some kind of bonus in which the player
temporarily turns into a monster.
Initializing initialization
All these component classes need to be constructed properly. What I do is simply use inline factory functions to create the game objects:
inline GameObject* new_Alien(void) {
return new GameObject(&alien_props,
new MoveAlien(),
new LivingAlien(),
new AnimAlien());
}
As you can see, it uses new
and delete
a lot. You may want to optimize
performance by using a caching memory allocation code.
Practice makes perfect
Having a handful of pointers in GameObject
is almost like hand-coding a
virtual table. With the component-based approach we never derive from
GameObject
, however tempting it may be. All capability and accompanying
state (member variables) are kept in components. Components are self-contained
in the sense that they bring only the state and methods that are needed
for that component. We derive from component base classes to implement
the exact functionality we want to have.
Following this model, we can construct game objects from components without
getting strung up in the web that multiple inheritance is.
I learned about component-based programming from on an online book by
professional game programmer. Even though he explains really well, I did get
stuck on the first attempt and went back to a class hierarchy—at least,
for a day or two. Of course that didn’t work out, and finally re-did the
whole thing by means of components.
While you can make a game that uses a class hierarchy (you can simply
ignore any members that you don’t need for a particular kind of entity),
the component-based model is, in a way, much more elegant.
It takes some plotting in the design phase, but works really well once
you get it bootstrapped, and build from there.