OOP Part 3: Composition

Last time, we looked at the inheritance style of object-oriented programming in detail. Now let’s look at the alternative: composition.

We’ll use another example from Icefall to illustrate the differences. Icefall has a central ‘world view’ that shows the player and their immediate surroundings (items, monsters, terrain, etc). The world view also shows animations (e.g. fireballs, weapons swinging, speech bubbles, etc) and it’s the animation code that we’ll be looking at in detail today.

I designed this feature around each animation being a class instance, with the class methods and properties making it easy for the game engine to control the details (lifetime, location, save/load etc etc). I knew I’d have several different types of animation, so my initial implementation chucked all that ‘management’ functionality into a TAnimation class, and left virtual methods for  things like Process and Draw for descendents to implement. After creating a few basic animations, my class hierarchy looked like this:

TAnimation
   TAnimText
      TAnimSpeech
      TAnimTexturedText
   TAnimTexture
      TAnimMissile
      TAnimExplosion
      TAnimMelee

Animations with text (i.e. picking up an item) were a subclass, and speech bubbles (or text with a textured background) were subclasses of that. Missiles (like fireballs) were a subclass where the animation moved from one place to another (and was rotated to face the right direction), and Explosions were in one place but had a series of animation frames over time. Melee animations showed a weapon and some motion-blur, sweeping out to attack a monster/player.

The problem with this system, which I discovered after adding TAnimTexturedText (specificially to show damage-dealt as a number on a bloodsplat-graphic background) was that most of the subtypes only varied in very small ways, and the overhead of naming, constructing and using each subtype took more effort than defining the subtype itself. Also, the ‘event’ system I was setting up for Icefall was supposed to allow for animations to be defined dynamically in response to any spell or event (and new events added in data files without recompiling the game) – but the code that supported this was going to be very complex and ugly if it had to choose between all of the different constructors each time and pass a specific set of parameters to make it happen.

So, I rewrote the animation subsystem to bring any functionality that I could possibly re-use into the parent class: now, all animations had a Texture property, a Rotate property, Frames, Movement, Sound… which could all be left at Nil or 0 if not needed. My new, composited animation hierarchy looked like this:

TAnimation
   TAnimText
      TAnimSpeech

As soon as I did this, I found that it was not only easier to manage the sources, but that features of the parent class ended up getting used more often than I anticipated: e.g. giving an explosion a random value for Rotate made each explosion look slightly different, and I could apply a small Movement value to Text to let it ‘float’ upwards from where it first appeared: new and useful functionality essentially for ‘free’.

Note that I kept AnimText separate from the parent, because there is some substantially different functionality: it has text (which directly contributes to the size and position of the anim), which is also dynamic, and there is some special logic to ensure two text animations don’t overlap each other. It made sense to move this code out into a specific class where it could be closely tweaked. Speech is different again, because there is a “speaker” creature that it it linked to: the speech bubble needs to move around with it’s speaker, disappear if they die, etc.

In summary, now that the TAnimation class is composited, it’s significantly ‘better’ code than before:

  • Fewer types to worry about
  • Much easier to follow the logic of the code in a single class
  • Can trivially re-use functionality developed for one subtype in others
  • Much easier for other code to utilise this class effectively

So, my conclusion in this small series is:

Use composition in your classes, unless there is substantially different behaviour, or external code needs to interact with a specific subtype in a special way (e.g. TAnimation itself is inherited from TWorldObject, because the world view needs to interact with animations in a specific way).

Next time, I’ll find something different to ramble on about.

Leave a Reply

Your email address will not be published. Required fields are marked *