Go gamedev patterns
@quasilyte for Golang United, 2022
1
Slide 2
Slide 2 text
Prerequisite
✅ Go 1.18 (we need generics)
✅ Ebitengine
✅ Legendary tolerance for my English
Please ⭐ both Ebitengine and Godot projects :3
2
Slide 3
Slide 3 text
About $username
● Compiler engineer during the day
● Game developer during the night
● I also stream random indie games from itch io
● Played video games since ~2000
I worked with Game Maker, Godot and Ebiten.
I also tried Defold and Phaser.
3
Slide 4
Slide 4 text
My Ebitengine games
4
Slide 5
Slide 5 text
5
Slide 6
Slide 6 text
6
Slide 7
Slide 7 text
7
Slide 8
Slide 8 text
8
Slide 9
Slide 9 text
9
Slide 10
Slide 10 text
10
Slide 11
Slide 11 text
11
Slide 12
Slide 12 text
12
Slide 13
Slide 13 text
13
Slide 14
Slide 14 text
14
Slide 15
Slide 15 text
My published games
● Autotanks
● Retrowave City
● Decipherism (GameOff 2022 game jam submission)
15
Slide 16
Slide 16 text
Go gamedev overview
16
Slide 17
Slide 17 text
Ebitengine vs Godot impressions
Cons:
17
Slide 18
Slide 18 text
Ebitengine vs Godot impressions
Cons:
● It’s much harder to become productive
18
Slide 19
Slide 19 text
Ebitengine vs Godot impressions
Cons:
● It’s much harder to become productive
● Much smaller community
19
Slide 20
Slide 20 text
Ebitengine vs Godot impressions
Cons:
● It’s much harder to become productive
● Much smaller community
● Ebiten provides no “framework” to shape your game
20
Slide 21
Slide 21 text
Ebitengine vs Godot impressions
Cons:
● It’s much harder to become productive
● Much smaller community
● Ebiten provides no “framework” to shape your game
● The ecosystem is not mature enough
21
Slide 22
Slide 22 text
Ebitengine vs Godot impressions
Pros:
22
Slide 23
Slide 23 text
Ebitengine vs Godot impressions
Pros:
● Go as a first-class language
23
Slide 24
Slide 24 text
Ebitengine vs Godot impressions
Pros:
● Go as a first-class language
● Full go toolchain benefits: tests, benchmarks, profiling
24
Slide 25
Slide 25 text
Ebitengine vs Godot impressions
Pros:
● Go as a first-class language
● Full go toolchain benefits: tests, benchmarks, profiling
● You can help the ecosystem to get better
25
Slide 26
Slide 26 text
Ebitengine game development guide
26
Slide 27
Slide 27 text
You need more than just Ebitengine
Ebitengine
Input
Nodes/ECS Res. manager
Math utils Collisions
Slots/signals
UI
Layers
Camera
Tiles
Scripting
Prem
ium
content! $$$
27
Slide 28
Slide 28 text
Some core ideas
● Port the code and ideas from Godot (C++ -> Go)
28
Slide 29
Slide 29 text
Some core ideas
● Port the code and ideas from Godot (C++ -> Go)
● Start from the simpler, small games
29
Slide 30
Slide 30 text
Some core ideas
● Port the code and ideas from Godot (C++ -> Go)
● Start from the simpler, small games
● Browse awesome-ebiten for packages and inspiration
30
Slide 31
Slide 31 text
Why many Go gamedev libs are not mature?
In my opinion:
● They’re created to solve a specific task at the
moment of the game creation
● Most lib authors are interested in creating games,
not libraries
31
Slide 32
Slide 32 text
Some other issues
● Libraries don’t play well together
● It’s hard to become the #1 library
32
Slide 33
Slide 33 text
Some other issues
● Libraries don’t play well together
● It’s hard to become the #1 library
33
Slide 34
Slide 34 text
Vectors & Math
34
Slide 35
Slide 35 text
Vec2 use cases
● Velocity, force and other vector-like units
35
Slide 36
Slide 36 text
Vec2 use cases
● Velocity, force and other vector-like units
● Simple positions, points in space (X, Y)
36
Slide 37
Slide 37 text
Vec2 use cases
● Velocity, force and other vector-like units
● Simple positions, points in space (X, Y)
● Normalized (unit) vectors for directions
37
Slide 38
Slide 38 text
type Vec2 struct {
X float64
Y float64
}
38
Slide 39
Slide 39 text
Why float64
● Float literals in Go imply float64 type
● Go math library works with float64
● Most ebiten APIs work with float64
Only GPU-related things may require float32
39
Slide 40
Slide 40 text
Why float64
● Float literals in Go imply float64 type
● Go math library works with float64
● Most ebiten APIs work with float64
Only GPU-related things may require float32
40
Slide 41
Slide 41 text
Why float64
● Float literals in Go imply float64 type
● Go math library works with float64
● Most ebiten APIs work with float64
Only GPU-related things may require float32
41
Slide 42
Slide 42 text
Vec2 representation
kvartborg/vector uses slices to represent vectors.
This is not efficient because most vectors in 2D
game have exactly 2 components.
Godot is open-source, you can roll the
implementation based on its source code.
42
Slide 43
Slide 43 text
Vec2 representation
kvartborg/vector uses slices to represent vectors.
This is not efficient because most vectors in 2D
game have exactly 2 components.
Godot is open-source, you can roll the
implementation based on its source code.
43
Slide 44
Slide 44 text
gmath library
github.com/quasilyte/gmath
Vec2, radians and other gamedev math helpers.
Inspired by Godot math library.
44
Slide 45
Slide 45 text
Positions / data binding
45
Slide 46
Slide 46 text
type Button struct {
Pos Vec2
}
type Container struct {
Pos Vec2
}
How about simple vectors for
UI elements position?
46
Slide 47
Slide 47 text
Offset relative to another element
Container
Button1 Button1
47
Slide 48
Slide 48 text
type Tank struct {
Pos Vec2
}
type Turret struct {
Pos Vec2
}
And for the other game objects
as well...
48
Slide 49
Slide 49 text
The turret follows its hull: their positions are related
49
Slide 50
Slide 50 text
Position requirements
● Small size
● Can express a position with offset
● Can automatically follow other position (parent)
Some positions are still going to be Vec2
50
Slide 51
Slide 51 text
Position requirements
● Small size
● Can express a position with offset
● Can automatically follow other position (parent)
Some positions are still going to be Vec2
51
Slide 52
Slide 52 text
Position requirements
● Small size
● Can express a position with offset
● Can automatically follow other position (parent)
Some positions are still going to be Vec2
52
Slide 53
Slide 53 text
Pointers for the (one-sided) data binding
● Data source: a value owner, read+write
● Pointer to data: read-only
We can combine both to get a more flexible concept.
53
Slide 54
Slide 54 text
type Pos struct {
Base *Vec2 // The position origin or (0, 0) if nil
Offset Vec2 // The offset in relation to the origin
}
func (p Pos) Resolve() Vec2 {
if p.Base == nil {
return p.Offset
}
return p.Base.Add(p.Offset)
}
54
Slide 55
Slide 55 text
type Pos struct {
Base *Vec2 // The position origin or (0, 0) if nil
Offset Vec2 // The offset in relation to the origin
}
func (p Pos) Resolve() Vec2 {
if p.Base == nil {
return p.Offset
}
return p.Base.Add(p.Offset)
}
55
Slide 56
Slide 56 text
type Tank struct {
Pos Vec2
}
type Turret struct {
- Pos Vec2
+ Pos Pos
}
56
Why signals & slots?
● Reduces objects coupling
● An elegant event listener solution for Go
63
Slide 64
Slide 64 text
Why signals & slots?
● Reduces objects coupling
● An elegant event listener solution for Go
● Familiar concept (Godot, Phaser, Qt, …)
64
Slide 65
Slide 65 text
In Go terms
● Signal = a field inside a struct
65
Slide 66
Slide 66 text
In Go terms
● Signal = a field inside a struct
● Slot = a function bound to a signal
66
Slide 67
Slide 67 text
In Go terms
● Signal = a field inside a struct
● Slot = a function bound to a signal
● Disconnect = remove bound function
67
Slide 68
Slide 68 text
In Go terms
● Signal = a field inside a struct
● Slot = a function bound to a signal
● Disconnect = remove bound function
● Emit = call all bound functions
68
Slide 69
Slide 69 text
type Event[T any] struct {
handlers []eventHandler[T]
}
type eventHandler[T any] struct {
c connection
f func(T)
}
type connection interface {
IsDisposed() bool
}
69
Slide 70
Slide 70 text
type Event[T any] struct {
handlers []eventHandler[T]
}
type eventHandler[T any] struct {
c connection
f func(T)
}
type connection interface {
IsDisposed() bool
}
70
Slide 71
Slide 71 text
type Event[T any] struct {
handlers []eventHandler[T]
}
type eventHandler[T any] struct {
c connection
f func(T)
}
type connection interface {
IsDisposed() bool
}
71
func (e *Event[T]) Disconnect(conn connection) {
for i, h := range e.handlers {
if h.c == conn {
e.handlers[i].c = theRemovedConnection
break
}
}
}
73
Slide 74
Slide 74 text
type removedConnection struct{}
func (r *removedConnection) IsDisposed() bool {
return true
}
var theRemovedConnection = &removedConnection{}
74
Slide 75
Slide 75 text
func (e *Event[T]) Emit(arg T) {
handlers := e.handlers[:0]
for _, h := range e.handlers {
if h.c != nil && h.c.IsDisposed() {
continue // effectively removes the handler
}
h.f(arg)
handlers = append(handlers, h)
}
e.handlers = handlers
}
75
Slide 76
Slide 76 text
type Vessel struct {
hp float64
EventDestroyed signals.Event[*Vessel]
EventAllianceChanged signals.Event[???]
}
76
Slide 77
Slide 77 text
func (s *state) NewVessel() *Vessel {
v := newVessel()
v.EventDestroyed.Connect(s.onVesselDestroyed)
return v
}
func (s *state) onVesselDestroyed(v *Vessel) {}
77
Slide 78
Slide 78 text
func (v *Vessel) destroy() {
// ...
v.EventDestroyed.Emit(v)
}
78
type Void struct{}
Event[Void]
event.Emit(Void{})
83
Slide 84
Slide 84 text
Implementation properties
● Tiny overhead and no allocs with 0 connections
84
Slide 85
Slide 85 text
Implementation properties
● Tiny overhead and no allocs with 0 connections
● Statically typed signatures without interface{}
85
Slide 86
Slide 86 text
Implementation properties
● Tiny overhead and no allocs with 0 connections
● Statically typed signatures without interface{}
● Fast connect+emit
86
Slide 87
Slide 87 text
Implementation properties
● Tiny overhead and no allocs with 0 connections
● Statically typed signatures without interface{}
● Fast connect+emit
● Not thread-safe
87
Slide 88
Slide 88 text
Tuples
88
Slide 89
Slide 89 text
Why tuples?
● Make things like Event[T] viable
● Abstract away the arity in generic code
Go doesn’t have variadic templates, so
we need a way to overcome this limitation
89
Slide 90
Slide 90 text
Why tuples?
● Make things like Event[T] viable
● Abstract away the arity in generic code
Go doesn’t have variadic templates, so
we need a way to overcome this limitation
90
Slide 91
Slide 91 text
type Value2[T1 any, T2 any] struct {
First T1
Second T2
}
func New2[T1 any, T2 any](a T1, b T2) Value2[T1, T2] {
return Value2[T1, T2]{a, b}
}
91
Slide 92
Slide 92 text
type Value3[T1 any, T2 any, T3 any] struct {
First T1
Second T2
Third T3
}
func New3[T1 any, T2 any, T3 any](...) Value2[T1, T2, T3] {
return Value3[T1, T2, T3]{a, b, c}
}
92
Keymap requirements
● Input-device agnostic
● Supports N-to-M action-key mapping
● Multi-device support (gamepads, etc.)
● Can be conveniently reconfigured at run time
99
Slide 100
Slide 100 text
if inpututil.IsKeyJustPressed(ebiten.KeyW) {
obj.moveForward()
}
100
Slide 101
Slide 101 text
if inpututil.IsKeyJustPressed(ebiten.KeyW) {
obj.moveForward()
}
if obj.input.IsActionJustPressed(MoveForward) {
obj.moveForward()
}
The exact key and input
method is abstracted away
101
Slide 102
Slide 102 text
if inpututil.IsKeyJustPressed(ebiten.KeyW) {
obj.moveForward()
}
if obj.input.IsActionJustPressed(MoveForward) {
obj.moveForward()
}
ID-bound input handlers instead
of global input system
102
Slide 103
Slide 103 text
Action can be associated with multiple input events
Key W
Arrow Up
ActionForward
D-Pad Up
103
Slide 104
Slide 104 text
Every input handler has its ID (useful for gamepads)
Player 2 Gamepad 2
Player 1 Gamepad 1
Handler{ID:1}
Handler{ID:0}
104
Slide 105
Slide 105 text
All handlers are connected to some shared “global input system”
InputSystem
Handler{ID:1}
Handler{ID:0}
105
Slide 106
Slide 106 text
type Handler struct {
id int
keymap Keymap
sys *System
}
func (sys *System) NewHandler(id int, k Keymap) *Handler {
return &Handler{
id: id,
keymap: k,
sys: sys,
}
}
106
Slide 107
Slide 107 text
type Action uint32
type Keymap map[Action][]Key
type Key struct {
code int
kind keyKind
}
107
Slide 108
Slide 108 text
type Action uint32
type Keymap map[Action][]Key
type Key struct {
code int
kind keyKind
}
type keyKind uint8
const (
keyKeyboard keyKind = iota
keyGamepad
keyMouse
)
108
Slide 109
Slide 109 text
var KeyW = Key{
code: int(ebiten.KeyW),
kind: keyKeyboard,
}
var KeyGamepadUp = Key{
code: int(ebiten.StandardGamepadButtonLeftTop),
kind: keyGamepad,
}
109
ebitengine-input library
github.com/quasilyte/ebitengine-input
Implements everything mentioned above + more.
Inspired by Godot too.
115
Slide 116
Slide 116 text
Resources & loaders
116
Slide 117
Slide 117 text
Resource loader requirements
● Convenient to use to get resources
117
Slide 118
Slide 118 text
Resource loader requirements
● Convenient to use to get resources
● Efficient resource lookup and caching
118
Slide 119
Slide 119 text
Resource loader requirements
● Convenient to use to get resources
● Efficient resource lookup and caching
● API that can prevent some mistyped keys, etc.
119
Slide 120
Slide 120 text
Resource loader requirements
● Convenient to use to get resources
● Efficient resource lookup and caching
● API that can prevent some mistyped keys, etc.
● Can preload resources (+ optionally unload them)
120
Slide 121
Slide 121 text
Typed, integer resource keys
type AudioID int
type ImageID int
type FontID int
121
Slide 122
Slide 122 text
App enumerates all its resources as enums
const (
ImageNone resource.ImageID = iota
ImageTank
ImageTurret
)
122
Slide 123
Slide 123 text
...and then associates metadata with them
imageResources := map[resource.ImageID]resource.ImageInfo{
ImageTank: {Path: "sprites/tank.png"},
ImageTurret: {Path: "sprites/turret.png"},
}
123
Slide 124
Slide 124 text
Getting a resource is easy
tankImage := loader.GetImage(ImageTank)
turretImage := loader.GetImage(ImageTurret)
124
Slide 125
Slide 125 text
Resource lookup
1. Find associated metadata
a. Return from cache, if possible
2. Using path info, use asset finder to get raw data
3. Decode data into requested resource
4. Cache the result
5. Return the result
125
Slide 126
Slide 126 text
Asset finder can work with embed data
loader.OpenAsset = func(p string) io.ReadCloser {
f, err := gameAssets.Open("_assets/" + p)
if err != nil {
// handle error
}
return f
}
126
Slide 127
Slide 127 text
Use embed.FS when embedding assets
//go:embed all:_assets
var gameAssets embed.FS
127
Slide 128
Slide 128 text
Resource preload
Just call Get() method on the resource needed
and ignore the results.
The next time it’ll be returned from cache.
128
Slide 129
Slide 129 text
Explicit state passing
129
Slide 130
Slide 130 text
Why bother and avoid global state?
● Easier to test your code
130
Slide 131
Slide 131 text
Why bother and avoid global state?
● Easier to test your code
● Can substitute dependencies easier
131
Slide 132
Slide 132 text
Why bother and avoid global state?
● Easier to test your code
● Can substitute dependencies easier
● Scene transitions become trivial
132
Slide 133
Slide 133 text
if inpututil.IsKeyJustPressed(ebiten.KeyW) {
obj.moveForward()
}
if obj.input.IsActionJustPressed(MoveForward) {
obj.moveForward()
}
Remember this?
How obj gets its input handler?
133
Slide 134
Slide 134 text
main()
runGame(state)
newMenuController(state)
newBattleController(state)
newPlayerTank(tank, state)
Creates the gameState
Accepts gameState as argument
All scene controllers hold the gameState
On scene change, gameState is passed
The scene controller passes it to objects
134
Slide 135
Slide 135 text
type gameState struct {
player1input *input.Handler
player2input *input.Handler
// + other common game state
}
135
Slide 136
Slide 136 text
// This is just a simplified example.
func main() {
p1keymap, p2keymap := readKeymaps()
state := newGameState()
state.player1input = input.NewHandler(0, p1keymap)
state.player2input = input.NewHandler(1, p2keymap)
runGame(state)
}
136
Slide 137
Slide 137 text
Maps, tiles
137
Slide 138
Slide 138 text
138
Slide 139
Slide 139 text
Tiled use cases
Tiled can be your graphical level editor!
● Object layers to deploy game entities
● Tile layers to draw the level landscape
● Can also be used to prototype the UI layouts
● Tilesets for auto-tiling
139
Slide 140
Slide 140 text
140
Slide 141
Slide 141 text
Tiled + modders
Non-programmers can use tiled to create levels for
your game.
A special triggers tileset can be used to apply visual
programming, leveraging the Tiled limitations.
141
Slide 142
Slide 142 text
Closing words
142
Slide 143
Slide 143 text
Things I still try to figure out
● Layers and rendering order abstractions
● Viewports and cameras with Ebitengine
● Smooth rendering of geometrical shapes
● Etc, etc.
143
Slide 144
Slide 144 text
How to discover cool Ebitengine packages?
awesome-ebitengine by sedyh
144
Slide 145
Slide 145 text
Additional resources
● sfxr (or jsfxr) - generate game sound effects
● ccmixter - find CC licensed music for your games
I want to try using neural network generated images
and music in one of my next games. :)
145
Slide 146
Slide 146 text
Go gamedev patterns
@quasilyte for Golang United, 2022
146