How I Structure My GameObjects: Component-based GameObjects and Mixin-based Inheritance
Thursday, November 2nd 2017
Component-based GameObjects are similar to the concept of mixins/traits in some Object-Oriented langauges. In this article, I’ll discuss the relative advantages and disadvantages of both patterns, then describe how they can be unified in a way that takes the best of both patterns.
A Quick Review of Component-based Game Objects
Many of you who have used Unity or other game engines are familiar with the component-based model of GameObjects. You can skip ahead to the next section if you are. If you don’t know what it is or want a review, then keep reading!
Many people write their games using traditional single-inheritance object heirarchies, but it turns out single-inheritance can rarely describe all of the ways one might want to reuse code in a game.
Suppose you have a simple game with a player and some enemies. All players have collision detection, meaning that they cannot pass through walls. Only some enemies have collision detection, while others are ghosts that can pass through walls. You would like to reuse the collision detection code across the entities that use it, as well as share code across enemy entities, but is this even possible using single inheritance? Take a look at the two attempts at a hierarchy below. Grey blocks represent abstract classes.
If you split GameObjects into Players and Enemies, then you will end up with
CollidingEnemy duplicating collision code. If you split GameObjects into Colliding and non-colliding entities, then
GhostEnemy will end up sharing enemy-related code.
The solution is to simply create a
CollidingEnemy will have
CollisionComponent objects as members, and can call into that object to perform operations related to collision. 1
This model is explored in more detail (with C++ code) in Game Programming Patterns by Bob Nystrom.
Problems with Component-based Objects
The component-based GameObject pattern has some problems. Some architectures like Unity are so pure that GameObjects cannot be subclassed at all, and will always be an empty container for components. This is not ideal, as you end up having many one-off components that are only used by a single object.
If you allow both models - subclassing GameObjects and adding in Components, you can create a development flow whereby you first write behavior in a GameObject subclass. Then, when you find yourself writing similar code across multiple game objects, you can refactor it into a component, which you can then add in to your GameObject and use.
Another issue with Components is that they are members, so you have to call
this.component.DoSomething() anytime any interesting action should occur. Furthermore, calling from one component into another component on the same object requires something like
Mixin-based Game Objects
I first encountered Mixins as an alternative to components in this blog post. In this case, you write your reusable components as mixins, which are then included into a class 2. Here’s a concrete example taken from that post:
Dust = class('Dust', Entity) Dust:include(PhysicsRectangle) Dust:include(Timer) Dust:include(Fader) Dust:include(Visual) function Dust:init(world, x, y, settings) Entity.init(self, world, x, y, settings) self:physicsRectangleInit(self.world.world, x, y, 'dynamic', 16, 16) self:timerInit() self:faderInit(self.fade_in_time, 0, 160) self:visualInit(dust, Vector(0, 0)) // ... end function Dust:draw() if debug_draw then self:physicsRectangleDraw() end self:faderDraw() self:visualDraw() end
The mixins, such as
Timer add functions onto the object that can then be called using
self:mixinFunc(...) as if they were defined in the class. More details can be found in the blog post. This solves the problems mentioned above with the component-based model, as you can still use inheritance,
this.DoSomethingElse(). However, we’ve introduced some new problems.
- In the component pattern, you can add in two components of the same type. For example, I can add two ImageComponents to an object - let’s say one for a character’s body, and another image for a character’s head. Only one mixin of a certain type can be added to an entity.
- You don’t have a “constructor” for the mixin. In the example above, each mixin is simply given a uniquiely named init function that is called in the object constructor. I’m not a fan of the way this reads.
- In the component pattern, we typically add components to a list. Then, in the GameObject’s update and draw calls, we simply iterate through those lists. Adding a mixin does not typically update a list, and furthermore Mixins that both implement the
drawmethod will conflict at Mixin time. So we need to manually call some uniquely named update and draw functions.
- Mixins are not isolated from one another. If two mixins try to set some field on
selfwith the same name, they will conflict with each other. Components on the other hand are isolated because they are their own objects.
Unifying Component-based Objects and Mixins
It turns out you can have your cake and eat it too - effectively getting the best of both worlds. Let’s start with a component-based implementation of GameObjects.
function GameObject:addComponent(comp) comp:setGameObject(self) -- Set the owning GameObject of this component. table.insert(self.components, comp) -- Add to components table. return comp -- Return the component for chaining. end -- Draw and update simply call out to the components. function GameObject:draw() for _, comp in ipairs(self.components) do comp:draw() end end function GameObject:update(dt) for _, comp in ipairs(self.components) do comp:update(dt) end end
Now take a look at this
--[[ 'Installs' a component into the current object by delegating all methods to it. Returns the component for compositional purposes. ]]-- function GameObject:includeComponent(comp) self:addComponent(comp) -- Add the component, so that it's draw, update, etc. functions will be called. for key, value in pairs(comp.class.__instanceDict) do if type(value) == "function" and not string.starts(key, "__") and key ~= "draw" and key ~= "update" and key ~= "initialize" then self[key] = function(_, ...) return comp[key](comp, ...) end end end return comp -- Return the component. end
What this does is two-fold:
- It adds the component to the list of components.
- For every function in the component that is not “draw” or “update”, we create a function on the Entity that simply delegates to the component.
Now, for every component, we can either “add” it, or “install” it. “Installed” components can be accessed using functions directly on the current object itself, as if it was a mixin. But unlike a mixin, we can still deal with the case where we need two components of the same type.
Heres some code that constructs a guard out of a few components.
ShotgunGuard = class('ShotgunGuard', Entity) function ShotgunGuard:initialize(layer, pos) Entity.initialize(self, 'enemy', layer, pos) self.image = self:addComponent(ImageComponent(loader.Image.guard)) self.shotgun = self:addComponent(ImageComponent(loader.Image.shotgun)) self.unit = self:includeComponent(UnitComponent(8)) self.healthBar = self:includeComponent(HealthBarComponent(vector(0, -75))) end
So let’s recap what we have…
- We can have multiple components of the same type on one GameObject.
- We have actual constructors that we can call, instead of trying to make uniquely named init functions.
update()functions are automatically called on our components
- Components have separate fields from each other so they don’t accidentally conflict.
- We can access certain components using
self:DoSomething()instead of having to write
- We can still subclass GameObjects and write code in the subclasses, rather than writing tons of one-off components.
Right now, this is how I structure my GameObjects, and I feel like it’s a strong way going forward for using components but also limiting the verbosity of calls into those components.