Slide 1

Slide 1 text

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

Slide 57

Slide 57 text

Connected position turret.Pos.Base = &tank.Pos 57

Slide 58

Slide 58 text

Absolute pos container.Pos.Offset = Vec2{X: x, Y: y} 58

Slide 59

Slide 59 text

Pos with offset button1.Pos = container.Pos.WithOffset(ox, oy) button2.Pos = button1.Pos.WithOffset(ox, 0) 59

Slide 60

Slide 60 text

func (p Pos) WithOffset(ox, oy float64) Pos { ox += p.Offset.X oy += p.Offset.Y return Pos{ Base: p.Base, Offset: Vec2{X: ox, Y: oy}, } } 60

Slide 61

Slide 61 text

Signals & Slots 61

Slide 62

Slide 62 text

Why signals & slots? ● Reduces objects coupling 62

Slide 63

Slide 63 text

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

Slide 72

Slide 72 text

func (e *Event[T]) Connect(conn connection, slot func(T)) { e.handlers = append(e.handlers, eventHandler[T]{ c: conn, f: slot, }) } 72

Slide 73

Slide 73 text

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

Slide 79

Slide 79 text

EventAllianceChanged signals.Event[???] -> * signals.Event2[*Vessel, int] * signals.Event[tuple.Value2[*Vessel, int]] 79

Slide 80

Slide 80 text

EventN vs Event[tuple] With EventN multiple-arg signals are more elegant. With tuples we get more code reusability. 80

Slide 81

Slide 81 text

func ConnectOneShot[T any](e *Event[T], c connection, f func(T)) { oneshot := &oneshotConnector[T]{conn: c} e.Connect(oneshot, func(arg T) { oneshot.fired = true f(arg) }) } 81

Slide 82

Slide 82 text

type oneshotConnector[T any] struct { conn connection fired bool } func (c *oneshotConnector[T]) IsDisposed() bool { if c.fired { return true } return c.conn != nil && c.conn.IsDisposed() } 82

Slide 83

Slide 83 text

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

Slide 93

Slide 93 text

func (v Value2[T1, T2]) Fields() (T1, T2) { return v.First, v.Second } func (v Value3[T1, T2, T3]) Fields() (T1, T2, T3) { return v.First, v.Second, v.Third } 93

Slide 94

Slide 94 text

// Tuple creation tup := tuple.New2(10, "quasilyte") // Tuple destructuring id, username := tup.Fields() 94

Slide 95

Slide 95 text

Controls mapping (keymaps) 95

Slide 96

Slide 96 text

Keymap requirements ● Input-device agnostic 96

Slide 97

Slide 97 text

Keymap requirements ● Input-device agnostic ● Supports N-to-M action-key mapping 97

Slide 98

Slide 98 text

Keymap requirements ● Input-device agnostic ● Supports N-to-M action-key mapping ● Multi-device support (gamepads, etc.) 98

Slide 99

Slide 99 text

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

Slide 110

Slide 110 text

const ( ActionNone Action = iota ActionForward ActionRotateLeft ActionRotateRight // ... ) 110

Slide 111

Slide 111 text

var keymap = Keymap{ ActionForward: { KeyW, KeyArrowUp, KeyGamepadUp, }, } inputHandler := isys.NewHandler(0, keymap) 111

Slide 112

Slide 112 text

func (h *Handler) ActionIsJustPressed(action Action) bool { keys, ok := h.keymap[action] if !ok { return false } for _, k := range keys { if h.keyIsJustPressed(k) { return true } } return false } 112

Slide 113

Slide 113 text

func (h *Handler) keyIsJustPressed(key Key) bool { switch key.kind { case keyGamepad: return h.gamepadKeyIsPressed(key) case keyMouse: return h.mouseKeyIsJustPressed(key) default: return h.keyboardKeyIsJustPressed(key) } } 113

Slide 114

Slide 114 text

func (h *Handler) gamepadKeyIsJustPressed(key Key) bool { return inpututil.IsStandardGamepadButtonJustPressed( ebiten.GamepadID(h.id), ebiten.StandardGamepadButton(key.code), ) } 114

Slide 115

Slide 115 text

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