Honeycomb Engine: Devblog #5 ~ A Component-Based Architecture

The backbone of a game engine is its architecture. When and how you update the game state can have massive performance implications; as such, a large chunk of the time taken while writing your main loop will be spent making trade-offs that make the engine more complicated but ensure maximum performance. The main loop is something you absolutely need to get right if your game engine stands any chance of running smoothly. Prepare yourself for a particularly wordy post all about Honeycomb’s inner workings!

Many game engines opt for an architecture in which every conceptual object that can go into a game – the player, a chair, an explosion – is modelled by a GameObject. By itself, a GameObject doesn’t do a great deal except hold a list of all the useful snippets of behaviour that actually define what the object does – those are usually called Components. It’s probably the most straightforward way of reasoning about the objects in your game, and that’s part of the reason this programming pattern, aptly called the Component design pattern, is used – GameObjects and Components are intuitive.

If you think of a person in real life, you can easily think up a whole host of behaviours they have and how those behaviours interact with each other – they use their legs to move, their eyes to see and their brain to work out a path from point A to point B. In a virtual environment in which the person were an NPC, the legs would be a movement component and the brain and eyes make up the AI component. But the person also has to interact with a physics component to make sure it doesn’t clip through walls, which would require a collider component to define the physical shape of the person. The movement component modifies the transform component (I’ll talk about those later), then the player’s orientation gets fed to the renderer component so that the game knows how to display them. The person also probably has to deal with an animation component that details exactly how it walks and maybe a sound component if it needs to speak. It’s easy to see how the number of components on a GameObject can snowball!

Usually, the only component that is actually required is called the Transform component. The Transform defines the position, rotation and scaling of an object, which is all data passed to the renderer each frame to calculate part of what’s known as the model-view-projection matrix. Without going into much detail, that matrix is responsible for taking a model as defined relative to itself (how you’d see a model in your modelling package) and transforming its vertices onto your screen. You can see why it’s important – everything has a position and orientation! You could theoretically merge the Transform component into the base GameObject class, although later on in this post I’ll go into a reason you might not want to.

Imagine a world without components. Instead, we just throw all of the ideas I just mentioned into one monolithic class called Player. You can probably already see a couple problems with that, such as:

  • You end up with a lot of code to trundle through if you want to modify any of the person’s behaviour, since all of the person’s behaviour is clumped together.
  • Every single piece of behaviour is coupled to every other piece of behaviour. Physics engine code relies on audio code, which relies on the renderer code, and so on. If you want to change how the renderer works, you might need to think about how the change affects the audio engine, which is obviously counter-intuitive.
  • If you want two different types of person that differ only in one small aspect, for example they use different AI components but the rest of their behaviour is the same, you’d have to copy and paste almost all of the code and change the tiny bit that differs. You’re not exploiting the features of object-oriented programming very well.
  • It’s really difficult for a different object to hold references to some part of Player. For example, if we have an Enemy class too, the enemy probably needs to know where the player is. With components, we’d just hold a reference to the Transform component, which holds all data related to the position and orientation of the Player. But without components, all we can do is hold a reference to the entire Player and hope there’s a sensible way hidden in the tangled mess to find just the position data.

By using components, we eliminate these problems. We can use the object-oriented principle of inheritance to make two components that are mostly similar and differ only slightly, then we can use polymorphism to interact with the common portions of instances of those objects as if they were one of the same. We can hold a reference to just the AI component of a Player GameObject and be reasonably sure it has the relevant functions you’d hope to find in an AI. And best of all, you don’t need to think about the audio engine while you’re writing rendering code, since they’re nicely encapsulated in separate components.

It’s worth noting that organising your game this way may incur a performance hit; however, it’s a reasonable trade-off to ensure that your game is easy to program and extensible later on. This is just one of the many instances of needing to make compromises in programming. And like any programming, there are ways to minimise the performance cost or even leverage features of your hardware to your advantage.

One way in which the hardware can actively make your program more efficient when using a component-based system is by utilising the processor cache. I won’t go into a great amount of detail here, but whenever you access some address in memory, the processor will grab not only the memory it needs, but a bunch of memory adjacent to it too, and it gets put in the cache. The cache is essentially ultra-fast memory – it is built directly onto the processor chip – so as you can imagine, anything in the cache gets processed blindingly quickly.

If your components are organised in memory contiguously, when you access one component’s memory, a bunch of other components stored next to it will get pulled into the cache too; hence, by storing all of one type of component together, we can process them all super-fast when we iterate over them serially. That’s much better than having all of our behaviour lumped into monolithic classes, because in that case we’d have to pull a lot of redundant data into the cache every time we call update() on a component and we’d get a cache miss when we try to access the next component. This is also a reason you’d want to separate out the Transform component from the base GameObject class, because you might want to do something with all of the GameObjects that doesn’t involve their Transform data. By ignoring the Transform component, you can exploit the data locality of the GameObjects fully.

This is just one of the things I’ve discovered while developing Honeycomb. Modern software development contains so many more considerations to make than I would have ever imagined! Who knew that the order in which you create objects could have such a profound impact on the overall performance of the software, or that random-access memory isn’t really random-access any more? Stuff like this makes software development complicated and ridiculous, but that’s also what makes it fun for me. This post is rather lacking in colour and is just a 1,300-word block of text – apologies for that, hopefully the next devblog has some nice screenshots and diagrams in it!