Making a Game in Go, part 2
In Part 1 of this series of posts, I sketched a simple game engine written in Go. The proposed engine was overly simplistic, but could conceivably be used as a starting point for simple 2D games.
In this post, I want to discuss a critical design decision that affects how easy it is to make more complicated 2D games.
Structure
Recall from the previous post that the main type for the overly-simple game engine looked like this:
1 2 3 |
|
And, when time came to update the game state or draw the components, Game
delegated the relevant work to each component that had either of those
behaviours.
The flat nature of the Components
slice implies that all the components in the
game sit at some equal level in a flat hierarchy, with one special component
(Game
) delegating work. It’s kind of like the false utopia of the theoretical
“Valve Organizational Chart”:

Whatever your opinion on company org-charts, game design often benefits from additional structure. Each component might itself consist of some other components. Compare this screenshot from Commander Keen 4, and a hypothetical hierarchy of components that might be driving the game under the hood:

- Level 1 (Border Village)
- Background
- Forest
- House
- Background tile
- Background tile
- …
- Midground tilemap
- Ground tile
- Ground tile
- …
- Sprites
- Mimrock
- Billy Blaze (Keen)
- Bounder
- Circling stars
- Poison Slug
- Lifewater droplet
- Lifewater droplet
- …
- Foreground tilemap
- Tile
- Tile
- …
- Scorecard
- Score
- Digit
- Digit
- …
- Lives count
- Helmet icon
- Digit
- Digit
- Ammo count
- Blaster icon
- Digit
- Digit
- Score
- Background
It seems clear that a game tree is a more natural way of describing a game than flattening all the components into a single list.
Fork in the road
There are two ways to adapt the game engine to handle more complicated game structures. These are:
- Don’t. Make every component responsible for calling
Update
andDraw
on all its direct subcomponents. In turn, each subcomponent calls those methods on their direct subcomponents, and so on. - Alternatively, each component and subcomponent (and so on) registers
with
Game
, andGame
does some book-keeping to track them all and the relationships between them. ThenGame
figures out when to callUpdate
andDraw
on all components.
The rest of this post will be about Approach #1. I will write about Approach #2 in the next post.
Commander Keen 4 looks like the kind of game that could be implemented easily with approach #1. It is a platformer which could be described as a “tilemap sandwich”: background tiles, midground tiles and sprites, and foreground tiles. This makes the top layer of the game tree “fixed”; there is never a need to reorder the layers.
Scenery
Approach #1 has a number of benefits for the engine programmer. In particular,
the work is delegated from the top level Game
object down through each
subcomponent, so there is minimal book-keeping in the top-level object. Also,
each component can easily influence its subcomponents.
Let’s implement a component that might be useful in such an engine. Here’s one
called Scene
, which is intended to be a bare-bones container of other
components:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Note that *Scene
implements both the Drawer
and Updater
interfaces itself,
so when it is nested in Game
or another Scene
(e.g. the below), its methods
get called as intended, and in turn delegates work to its subcomponents:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
But wait! Scene
is almost exactly the same as Game
! It’s doing the same
things: loop over the subcomponents, and delegate work to those that have the
applicable behaviour. It would even make sense to copy the draw-order sorting
in Update
from Game
into Scene
too, because a Scene
could contain
subcomponents that change draw order over time.
This suggests a refactor of Game
:
1 2 3 |
|
By embedding Scene
, Game
now has the methods of Scene
(Draw
and
Update
), and calling these is equivalent to calling them on game.Scene
. The
only other thing from ebiten.Game
is Layout
, and for now,
that isn’t useful for Scene
to implement (it could be, though), so Game
remains useful as a separate type.
Let’s make Scene
more useful, though. Some things we might want to do
during a game are:
- Hide everything in a
Scene
, e.g. hide level 1 and show level 2 - Stop updates to everything in a
Scene
, e.g. pause the game
This is easy to do in the Approach #1 paradigm. This could be implemented in
Scene
as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Drawing stuff and relative positioning
To draw a sprite onto the screen in ebiten, one typically needs code like this:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Given the Scene
implemented above, it would be inconvenient if, say, we wanted
to translate everything in a Scene
by the same offset - each component would
need its position updated. This in turn means storing two different positions
for each component: its draw position and its real position, and keeping them in
sync somehow. This is a mess.
A similar but related problem would be to recolour all the components in a
Scene
, e.g. suppose we want to use a ColorM
to adjust the colouring in a
consistent way.
Relative positioning and recolouring and all sorts of fun is unblocked if we
alter the Drawer
interface:
1 2 3 |
|
This diverges from ebiten.Game
. Game
would be the logical place to interop
between ebiten.Game
and Drawer
, by supply the initial opts
value to
Scene
’s Draw
method:
1 2 3 4 |
|
Each parent component such as Scene
can modify opts
before delegating
drawing to its subcomponents:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
And each component can use the opts
provided from its parent component in
basically the same way:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Problems
Approach #1 gets a long way, especially for platformers and other tilemap sandwiches, because it is straightforward and the logical structure matches the draw order. There are a few well-defined layers, drawn in a fixed order, and each component in the hierarchy can affect its subcomponents directly because it is responsible for delegating work to them.
The limits of the approach are hit as soon as subcomponents in different parts of the game tree need to be drawn in different orders. Suppose we are implementing a top-down game with walls or other obstacles with “height”. They must partially obscure objects behind them, and are partially obscured by objects in front of them. (Spoilers: you are now making a 3D game.)

Logically, all the “wall parts” might belong as subcomponents of a single “wall” component, especially if the parent component holds common properties of each part like “size” and “sprite sheet”.
But having all the wall parts be subcomponents of one wall would mean all the wall parts are drawn in one layer: the “wall layer”. The wall parts now can’t intermingle with other components in a dynamic way, because draw ordering happens for each layer, not across layers. If the draw ordering is fixed (i.e. the game consists of nothing but fixed walls) then there is hardly a problem, but this prohibits having a sprite that sometimes walks in front of walls and sometimes walks behind them, because the ordering between them cannot change.
This necessitates a compromise: the wall parts must be immediate subcomponents
of all the other components they need to be sorted amongst. The parent Scene
can then sort them as the draw ordering changes. Great!
Unfortunately this sucks, because now there is no single “wall”, just lots of horrible little pieces that are separate from one another, and if we need to change some common property of them, we must either:
- Find them all and change each one individually
- Do book-keeping to ensure each one has a pointer to some shared struct, and update that
- There is no 3.
Compromise #2 is the more sensible option, but it becomes gets harder if the game tree needs to be serialised and deserialised. If the pointer to the shared struct is serialised along with the wall part, then the deserialiser might decide to allocate each wall part its own copy of the “shared” struct! If the pointer is not serialised along with the wall part, then each wall part has to somehow be told about, or independently locate, the shared struct to use after being inflated.
The benefits of a hierarchy are lost. It seems some amount of book-keeping is actually necessary, so why not go back to the beginning and implement Approach #2? Let’s rip the bandaid off and make complex 2D games easier! Approach #2 will be written about in Part 3 of this series.