The Developer’s Cry

Yet another blog by a hobbyist programmer

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.