Welcome to the blog of

Josh Deprez

⬅️ Previous postNext post ➡️ Permalink link

Published 28 August 2021

Making a Game in Go, part 1

Oh, poor neglected blog!

I’m making a game.

Why Go?

I’m doing it this way because:

  • I’m very familiar with Go.
  • I’ve done it before. Awakeman episodes 27 and 40 were written in Go.
  • Go can target Windows, Linux, Mac, the web (WebAssembly), and even iOS and Android.
  • I have some ideas about engine programming in Go that might not be interesting to anyone else, but entertain me nonetheless.

Headwinds

There are probably lots of reasons for me not use Go for making a game.

  • Limited resources for doing it, because everybody else is using Unity or Unreal or Godot or löve or …
  • Something something garbage collector
  • Something something generics
  • Something something C++
  • I’m an inexperienced game developer, with less than a handful of games made, and no formal education in game making, so what would I know!

ebiten

ebiten is a library for making 2D games in Go. It takes care of the following:

  • Graphics across the varied platforms
  • Input handling, including game controllers
  • Telling your code to layout, update, and draw

But there is a conceptual gap between being able to draw stuff in a window, and using that to make an entire game - even a “simple” platformer or top-down adventure.

How to draw an owl. Step 1: draw some circles. Step 2: Draw the rest of the fucking owl.

So in this post I wanted to start writing about the thing I’m building now (an engine) that will provide a bunch of tools for building a thing on top of it later (a game).

Diagram showing layers of a game. From the top: game, game engine, graphics and input library, runtime, and operating system/web browser.

Engine

ebiten.RunGame requires we pass in something implementing ebiten.Game, which looks like this (comments removed, which you can read at the ebiten godoc):

1
2
3
4
5
type Game interface {
	Update() error
	Draw(screen *Image)
	Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int)
}

Basically, ebiten calls these methods over and over. Update is supposed to read the input state and then update the game state by 1 “tick,” and Draw is where the game should do all the graphics (screen.DrawImage, for example). This is a reasonably straightforward application of a Go interface.

Up the other end of a game engine is the “game”, that is, whatever is special or unique about this particular game being made. A good engine library should be useful for making lots of different games and provide a good selection of common parts, but also be flexible enough to allow custom parts to be provided by the game maker.

The subproblems the author of a game engine needs to solve are therefore:

  1. How to represent the various parts of various games
  2. Organising the parts of the current game in a useful way
  3. Updating each part of the game that needs updating
  4. Drawing each part of the game that needs drawing (in the right order)

Representing and organising the parts of a game

It occurred to me that the different parts of a game (let’s call them components from now on) need to do different things. Some components need to accept user input and update state over time. Some components need to be shown on the screen.

One approach would be to require all the components to have both Draw and Update methods, and for each component to have a stub do-nothing implementation when it doesn’t need to do either drawing or updating.

Another approach, that makes sense in Go, is to look at each component to see if it supports a behaviour, before calling it. This can be done with type assertions, which are fairly fast. An incredibly tiny game engine (though one that doesn’t provide much value to the game maker) might therefore look like this:

 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
34
35
36
37
38
package engine

import "github.com/hajimehoshi/ebiten/v2"

type Game struct {
    Components []interface{}
}

func (g *Game) Draw(screen *ebiten.Image) {
    // Find all the components that can be drawn, and draw them.
    for _, c := range g.Components {
        if d, ok := c.(Drawer); ok {
            d.Draw(screen)
        }
    }
}

func (g *Game) Update() error {
    // Find all the components that can be updated, and update them.
    for _, c := range g.Components {
        if u, ok := c.(Updater); ok {
            if err := u.Update(); err != nil {
                return err
            }
        }
    }
    return nil
}

// And here's what Drawer and Updater look like:

type Drawer interface {
    Draw(*ebiten.Image)
}

type Updater interface {
    Update() error
}

Need to change the order of things being drawn? Reorder the components in the g.Components slice.

Or, instead: the engine could make it easier on the game-maker by letting them opt-in to some mechanism to figure out the draw order based on some number provided by each component:

 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
34
35
type DrawOrderer interface {
    DrawOrder() float64
}

func (g *Game) Update() error {
    // Find all the components that can be updated, and update them.
    ...
    
    // Check if any of the components are now out of order - if so, sort them.
    cz := -math.MaxFloat64 // fun fact: this is the minimum value of float64
    for _, c := range g.Components {
        if do, ok := c.(DrawOrderer); ok {
            if z := do.DrawOrder(); z < cz {
                g.sortByDrawOrder()
                return nil
            }
            cz = z
        }
    }
    return nil
}

func (g *Game) sortByDrawOrder() {
    // Use a stable sort to preserve the ordering of components with equal
    // DrawOrder values.
    sort.SliceStable(g.Components, func(i, j int) bool {
        a, aok := g.Components[i].(DrawOrderer)
        b, bok := g.Components[j].(DrawOrderer)
        if aok && bok {
            return a.DrawOrder() < b.DrawOrder()
        }
        return !aok && bok
    })
}

Problems

This approach is probably less efficient than it could be.

Firstly, however fast each interface type assertion is (i.e. c.(Drawer), c.(Updater), c.(DrawOrderer)), it will happen for every component on every call to Draw and Update. That could mean a lot of repetitious, unnecessary type assertions when they could all be done “up front”. Suppose Game keeps track of whether it has done this processing or not, and performs it if needed:

 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
type Game struct {
    Components []interface{}
    
    processed bool
    drawers []Drawer
    updaters []Updater
}

func (g *Game) Update() error {
    if !g.processed {
        // Do type assertions once
        for _, c := range g.Components {
            if d, ok := c.(Drawer); ok {
                g.drawers = append(g.drawers, d)
            }
            if u, ok := c.(Updater); ok {
                g.updaters = append(g.updaters, u)
            }
        }
        g.processed = true
    }
    
    for _, u := range g.updaters {
        if err := u.Update(); err != nil {
            return err
        }
    }
    
    // ... Do draw-ordering check...
    return nil
}

A drawback to this is that instead of having to edit just one slice to change components in the game, as many as three would have to be updated. (Additional books leads to additional bookkeeping.)

Secondly, if the draw ordering changes a lot, that could mean a lot of sorting, which may mean a bit of memory churn depending on how the Go stable sort is implemented. Optimising this would probably require knowing a bit more about the game being made - for example, are changes to draw order being made smoothly, so there are at most a few “inversions” per frame?

To begin with, though, these aren’t necessarily problems for implementing a game. The game engine just needs to be fast enough, and as long as the number of components is small enough (millions might still be fine, even on slow computers - I haven’t benchmarked it), then the basic approach seeems acceptable.