Facebook Linkedin Twitter
Posted Sat Dec 10, 2022 •  Reading time: 15 minutes

2D Game Development in Go

Once in a while a video game comes out that just takes over your world. It’s all you can think about, and you can’t wait to get home from work/school. Flash back to 1999, Pokémon Red and Blue were released in EU, and I was hooked. I took it everywhere, collected everything, nothing could stop me and my Venusaur. Wait, this is a post about Golang, right? Yes! Let’s talk about how we can make a Pokémon clone in Go.

A quick disclaimer: by day I write Go for a living, but I’m not a professional game developer. So, feel free to disagree with some of what’s written here, if anything let’s have a discussion!

Getting up to speed

I think for this blog post to be effective, we need to be on a level playing ground. We’ll assume a basic understanding of Go, but we’ll also assume no knowledge of game development (because that’s what I have/had).

The basics

I guess the real basic question is: what makes a game? There are definitely philosophical essays we could write about this, but to keep it simple I think a game is; a set of rules, some input, and some output. So that’s what we’ll run with.

The simple game

As always, I think we should break this down into the simplest possible parts. Before we can make a triple a game, we need to make a small indie style game, before that let’s even try to make some CLI game. Have y’all played hangman in the CLI? Let’s make that.

First, we need some setup - a hangman game needs a word to guess. After that, we need a loop where we read a guess letter from the user, and then print out the word with the guessed letters filled in. We also need to display the number of guesses remaining, and if we’re feeling really fancy, the drawing of the state of the game.

So, let’s get started!

Get yourself a list of words to guess, since we’re going to make a pokemon clone, I’m going to make a list of the first 151 pokemon names. We’re going to keep this brief, so we’ll call out to functions to handle the guts of the game. We’ll just focus on the overall structure of the game.

func main() {
	word := getWord()

	solved := false
	incorrectGuesses := 0
	guesses := make(map[rune]bool)
	for !finished() {
		guess := getInput()
		handleInput(guess, word, guesses, &incorrectGuesses)
		printWord(word, guesses)
	}

	printOutcome(solved, word)
}

Ok, confession time, I actually haven’t written this game (yet). But I think it’s a good starting point for us to start thinking about how we can make a game in Go.

The two things I really want to get across before we dive into a game engine and how to write a game in Go are; the setting up of the initial state in the first few lines, and the loop that runs the game. As you can see, we create the word to guess, which is to say we set up the state for the original game. The we loop until the game is finished, handling input and responding to it (including letting the user see the game), and lastly allowing the user to see the outcome of the game.

A game engine

If you’ve toyed with the idea of making a game before, you’ve probably heard of Unity, Unreal, or Godot. These are all game engines, and they’re a great way to get started making a game. So what exactly is a game engine? Great question, they are a framework for making games, this includes anything from the physics engine, to the rendering engine, to the audio engine, and everything in between. They also include a way to make a game, by providing a way to make a game loop, and a way to handle input.

The options above are all great options, but we’re here for Go, so my advice - as always - is to check awesome-go. Sweet, we’ve got options (actually more than I remember). We’re going to use Ebitengine, as imo for a 2D game it’s the most complete solution out there, it’s super easy to use, and the community around it is amazing.

Making a prototype

Ok, so we’ve covered the basics, we’ve picked ourselves a game engine, and we have some inspiration. Time to make a prototype.

Let’s be honest, any large project has to be broken down into smaller parts. I can see two major chunks of work here; the battle system and the overworld/map system. Of course, there are other areas we could split out, but I think that will get us started.

Project layout

We’re going to layout this project like I would most other CLIs. We’ll have a cmd directory, and in it a main.go file, since we’re only making one binary, we’ll keep it simple. Let’s just dump out a code snipper and talk about it after.

package main

import (
	"errors"
	"fmt"
	"log"
	"math/rand"
	"os"
	"time"

	"github.com/alexanderjophus/fakemon"
	"github.com/alexanderjophus/fakemon/pkg/scenes/menu"
	ebiten "github.com/hajimehoshi/ebiten/v2"
)

func main() {
	rand.Seed(time.Now().UnixNano())
	if len(os.Args) != 1 {
		log.Fatal("Usage: fakemon")
	}
	ebiten.SetWindowTitle("Fakemon")
	ebiten.SetFullscreen(true)
	g, err := fakemon.NewGame()
	if err != nil {
		log.Fatal(fmt.Errorf("failed to create game: %w", err))
	}
	if err := ebiten.RunGame(g); err != nil {
		if errors.Is(err, menu.ErrQuit) {
			return
		}
		log.Fatal(fmt.Errorf("failed to run game: %w", err))
	}
}

I didn’t feel the need to use anything like urfavecli, or cobra - though you certainly can. We’re just going to use the standard library to parse the command line arguments, and then we’re going to use ebiten to run the game. The bulk of the work will be done in ebiten.RunGame, you’ll see that we’re passing in a fakemon.Game struct, this must implement the ebiten.Game interface. Those functions are Update() error, Draw(screen *Image), and Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int). Update and Draw will be called every tick, and Layout will be called when the window is resized.

Let’s look at our implementation of those functions. But first, let’s look at the NewGame() function.

//go:embed map.ldtk
var level []byte

func NewGame() (*game, error) {
	ldtkProject, err := ldtkgo.Read(level)
	if err != nil {
		return nil, err
	}
    r := renderer.NewEbitenRenderer(renderer.NewDiskLoader(""))
    level := ldtkProject.Levels[0]
    pe := level.LayerByIdentifier("Entities").EntityByIdentifier("Player")
    player := entities.NewPlayer(0, entities.WithOverworld(
        h,
        static.MustLoadImage("characters.png"),
        0, 0, pe.Position[0], pe.Position[1],
    ))

	g := &game{
		currentScene: sceneTypeOverworld,
		project:      ldtkProject,
        overworld:    overworld.NewOverworld(player, level, r)
		renderer:     r,
		keymap:       controls.Keymap,
	}
	g.inputSystem.Init(input.SystemConfig{
		DevicesEnabled: input.AnyInput,
	})
	return g, nil
}

There’s a lot to unpack here. Firstly, we’re embedding out map into the level variable, this allows us to port the game around without having to worry about where the map is too much. Next we use the ldtkgo package from earlier to read in our level, this allows us to get the level data from the map, this includes the entities, the tiles, etc. The renderer package we call next I took from here, and it’s a simple way to render the map. We then create a player, and I wasn’t too sure how to best do this. I used the options pattern to allow myself to create a player for the overworld, and a player for the battle - this just allows us to have a bit more flexibility. Lastly, we create the game struct, and initialise the input library. The input library allows us to easily map keyboard, mouse, and gamepad inputs to actions, and then we can use those actions in our game.

Next up is the Update() function.

func (g *game) Update() error {
	g.inputSystem.Update()
	switch g.currentScene {
	case sceneTypeOverworld:
		return g.overworld.Update()
	}
	return nil
}

This looks verbose and overly complex - the goal I’m aiming for here is to make it easy to add new scenes, and to make it easy to switch between scenes. If we’re in the overworld, we call the overworld’s update function, otherwise we return nil (I’m debating changing this to error, but I’m not sure yet). The overworld functions for now are just stubs, but we’ll get to them soon.

Next up is the Draw() function, which looks very similar

func (g *game) Draw(screen *ebiten.Image) {
	switch g.currentScene {
	case sceneTypeOverworld:
		g.overworld.Draw(screen)
	}
}

Lastly, we have the Layout() function, which is pretty simple.

func (g *game) Layout(_, _ int) (int, int) {
	return ScreenWidth, ScreenHeight
}

ScreenWidth and ScreenHeight are constants we can define at the top of the file, and they’re just the width and height of the screen.

The overworld system

To me this is the easier part to implement (when compared to the battle system), and perhaps the more fun in terms of feedback. Let’s start with loose goals, we want; a player character, a way to move the player character, and a way to interact with the world. For this we need some assets, so here you can either create some, or head on over to itch.io or humble bundle and get some. Or if you’re really creative, make your own! Warning: easier said than done.

Next up, we need a tool to create a map, I’m going to use ldtk, the main goal here is to easily create a map, and then export it as a json file. There’s probably other options, but here’s where we get into the Go side of things - there’s go bindings, I told you this was a Go post. I’m not going to cover how to use ldtk (or the map editor of your choice), there’s plenty of tutorials out there, but I will cover how to use the Go bindings.

After some effort, you’ll end up with a json looking .ldtk file, and you’ll want to embed it into your Go code for portability. This may be fiddly as it uses relative imports, I tend to just open the ldtk file at the root of my game directory. I’m sure there are cleaner ways, but this works for me.

Before we dive into the code, let’s talk about what we want to achieve. I would like to create a game where the user can walk around an overworld as freely as they like, but as soon as they interact with a signpost, they’ll be given a monster and then when they walk around there’s a 10% chance they’ll encounter a monster.

Let’s explore the NewOverworld function we called earlier.

func NewOverworld(player *entities.Player, level *ldtkgo.Level, r *renderer.EbitenRenderer) *Overworld {
	r.Render(level)

	return &Overworld{
		currentScene: overworldSceneTypeIdle,
		player:       player,
		tileset:      static.MustLoadImage("village.png"),
		level:        level,
		renderer:     r,
	}
}

We’re passing in the player, the level, and the renderer. First thing to do is render the level, this is done by calling the renderer’s Render() function, which is a simple wrapper around the ldtkgo package. After that we create the overworld struct, and return it. The only thing I woulc like to call out here is the currentScene, this is a subscene within the scene. Honestly, I think this is tech debt, learn from my mistakes. My motivation for including it stems from the battle system which got complicated quickly, and I wanted a way to control the flow of the battle, with the overworld I think splitting this out makes the game harder to seem ‘fluid’.

In my implementation for example, when you’re idle that’s one scene, when you’re walking that’s another. Let’s hypothetically say we want an npc to move around a particular path, I then need to start moving them independently of the scenes, unless we want them to move when the player moves. In short, be careful of using scenes within scenes, don’t try to be clever up front.

Next up is the Update() function.

func (o *Overworld) Update(battleScene func(player *entities.Player, monster *entities.Monster)) error {
	switch o.currentScene {
	case overworldSceneTypeIdle:
		o.startTime = time.Now()
		hasMoved := o.handleMovement(o.player)
		if hasMoved {
			o.currentScene = overworldSceneTypeMove
			// has 10% chance of finding fakemon
			var encounter *entities.Monster
			if o.player.GetActiveMonster() != nil && rand.Intn(9) == 0 {
				encounters, err := entities.LoadMonsters()
				if err != nil {
					panic(err)
				}
				encounter = encounters[rand.Intn(len(encounters))]
				encounter.Level = 1
				encounter.Health = 2
			}
			o.currentScene = overworldSceneTypeMove
			o.encounter = encounter
		}
		if o.player.Input.ActionIsPressed(controls.ActionSelect) {
			// check collision with signpost
			if o.playerMeetsEntity(o.player, "Signpost") {
				// give player monster
				monster, err := entities.LoadMonster("Eleafant")
				if err != nil {
					return err
				}
				o.player.GiveMonster(monster)
			}
		}
	case overworldSceneTypeMove:
		if time.Since(o.startTime) >= time.Millisecond*500 {
			if o.encounter != nil {
				battleScene(o.player, o.encounter)
				o.encounter = nil
			} else {
				o.currentScene = overworldSceneTypeIdle
			}
		}
	}
	return nil
}

I’m not going to dive into each of the functions this calls, instead we’ll talk at a high level about them and leave the implementation to the reader. Structurally, this is very similar to the Update() function in the game struct, we have a switch statement, and we call the appropriate function based on the current subscene.

In the idle scene, we set a timer - this allows us to control a few things such as walk animation, fluid movement of the sprite, etc. handleMovement then handles the users movements with respect to boundaries and obstacles in the game, and returns a boolean indicating if the user has moved. If they have, we set the current scene to overworldSceneTypeMove, and we also have a 10% chance of finding a monster (provided they have a monster), if they do find a monster we set an encounter. This just allows us to do the movement of the user into the tile they desired before starting the battle. Lastly, if the user presses the action button, we check if they’re colliding with a signpost, and if they are, we give them a monster.

In the movement we wait until a timer has elapsed before taking the user to either a battle scene if there was an encounter, or back to the idle scene otherwise.

Next up is the Draw() function.

func (o *Overworld) Draw(screen *ebiten.Image) {
	screen.Fill(o.level.BGColor) // We want to use the BG Color when possible
	for _, layer := range o.renderer.RenderedLayers {
		screen.DrawImage(layer.Image, &ebiten.DrawImageOptions{})
	}

	// draw the character in the scene
	switch o.currentScene {
	case overworldSceneTypeIdle:
		// draw player
		op := &ebiten.DrawImageOptions{}
		op.GeoM.Translate(float64(o.player.X), float64(o.player.Y))
		var spriteModifier image.Rectangle
		switch o.player.Orientation {
		case entities.OrientationUp:
			spriteModifier = spriteBackward1
		case entities.OrientationDown:
			spriteModifier = spriteForward1
		case entities.OrientationLeft:
			spriteModifier = spriteLeft1
		case entities.OrientationRight:
			spriteModifier = spriteRight1
		}
		screen.DrawImage(o.player.Sprite().SubImage(spriteModifier).(*ebiten.Image), op)
	case overworldSceneTypeMove:
		oldX := o.player.X
		oldY := o.player.Y
		switch o.player.Orientation {
		case entities.OrientationUp:
			oldY += spriteSize
		case entities.OrientationDown:
			oldY -= spriteSize
		case entities.OrientationLeft:
			oldX += spriteSize
		case entities.OrientationRight:
			oldX -= spriteSize
		}

		// draw smooth movement from old to new coords
		ts := float64(time.Since(o.startTime).Milliseconds()) / 500.0
		tx := float64(oldX) + float64(o.player.X-oldX)*ts
		ty := float64(oldY) + float64(o.player.Y-oldY)*ts

		op := &ebiten.DrawImageOptions{}
		op.GeoM.Translate(tx, ty)

		screen.DrawImage(o.player.Sprite().SubImage(getSpriteAnimation(o.player.Orientation, ts)).(*ebiten.Image), op)
	}
}

Here we draw the background and any layers that we created in our ldtk file. The we either draw the player in the idle scene, or we draw the player in the move scene, with smooth movement. There’s also extra logic for direction as well as which sprite to draw (we have 3 for the walking animation).

The eagle eyed among you may notice we never defined the battleScene function we pass into update. We need to make some minor ammendments to the Game package to allow this to work.

case sceneTypeOverworld:
    battleScene := func(player *entities.Player, monster *entities.Monster) {
        g.currentScene = sceneTypeBattle
        cpu := entities.NewPlayer(1, entities.WithBattle(
            monster,
            true,
        ))
        retScene := func() {
            g.currentScene = sceneTypeOverworld
        }
        g.battle = battle.NewBattle(player, cpu, retScene)
    }
    return g.overworld.Update(battleScene)

We’re able to change the game scene, even though we’re in the overworld scene, because we’re using higher order functions. Next we set the cpu player to be the monster we found. We also set a function to return to the overworld scene when the battle is over. Finally we create a new battle with the player and the monster, and set the game battle to this.

I think at this point I owe an apology. I pass into a function another function which also has another function. We could simplify this by chucking everything into the game package, and for a prototype we probably should have.

We’ll go over the battle system next.

The battle system

Last bit of this little prototype is the battle system.

As always, let’s start with modifying the game package. We’ll include a new scene type and extend our Update() and Draw() functions.

func (g *game) Update() error {
    switch g.currentScene {
	...
    case sceneTypeBattle:
		return g.battle.Update()
    }
}

func (g *game) Draw(screen *ebiten.Image) {
	switch g.currentScene {
    ...
	case sceneTypeBattle:
		g.battle.Draw(screen)
	}
}

After this, we need to create and implement the battle package.

func NewBattle(p1, p2 *entities.Player, retScene func()) *Battle {
	font, err := truetype.Parse(goregular.TTF)
	if err != nil {
		panic(err)
	}

	return &Battle{
		background:   static.MustLoadImage("background.png"),
		currentScene: battleSceneTypeChooseMove,
        chooseMove:   newChooseMove(b.player1, b.player2),
		player1:      p1,
		player2:      p2,
		retScene:     retScene,
		font: truetype.NewFace(font, &truetype.Options{
			Size: 40,
		}),
	}
}

Here’s our ‘constructor’ function. We load the background image, set the current scene to the choose move scene, and create the intro scene. p1 and p2 are the players in the battle, and retScene is the function we’ll call when the battle is over.

Next, let’s explore the updates battle function

func (b *Battle) Update() error {
	switch b.currentScene {
	case battleSceneTypeChooseMove:
		nextScene := func(p1Move, p2Move int) {
			b.currentScene = battleSceneTypeMoveResult
			b.moveResult = newMoveResult(b.player1, b.player2, p1Move, p2Move)
		}
		return b.chooseMove.Update(nextScene)
	case battleSceneTypeMoveResult:
		nextScene := func() {
			b.currentScene = battleSceneTypeChooseMove
			b.chooseMove = newChooseMove(b.player1, b.player2)
		}
		if b.isFinished() {
			nextScene = func() {
				b.currentScene = battleSceneTypeBattleOver
				var condition GameOverCondition
				if b.player1.GetActiveMonster().Health == 0 {
					condition = GameOverConditionPlayer2Wins
				} else {
					condition = GameOverConditionPlayer1Wins
				}
				b.battleOver = newBattleOver(condition)
			}
		}
		return b.moveResult.Update(nextScene)
	case battleSceneTypeBattleOver:
		nextScene := b.retScene
		return b.battleOver.Update(nextScene)
	}
	return nil
}

We’ve created three different subscenes here, the goal is to go back and forth between the choose move and move result scenes until a win/lose condition is met, when we’ll go to the battle over scene. We also use (or abuse, depending on your stance) higher order functions to pass in the next scene function. I found the subscene system worked out quite well here as it allowed me to break out what was happening in different files/areas of code. In my particular vision of a battle system we also don’t need to worry about actions happening independently of the user (like NPCs walking around in the overworld), so we can get away with this. I’m going to avoid showing all the code, and will leave the new...() functions to the reader to explore.

Lastly let’s look at the draw function.

func (b *Battle) Draw(screen *ebiten.Image) {
	screen.DrawImage(b.background, nil)
	switch b.currentScene {
	case battleSceneTypeChooseMove:
		b.chooseMove.Draw(screen)
	case battleSceneTypeMoveResult:
		b.moveResult.Draw(screen)
	case battleSceneTypeBattleOver:
		b.battleOver.Draw(screen)
	}
}

Ok, I’ll grant you this ones a little cheeky, but the key takeaway here is we draw the background, then switch on top of that for the current scene. I found this kept the code clean, and allowed me to focus on what was important in that particular subscene. Was it drawing out the move options, or was it drawing effects once the move had been selected? Again, I’ll leave this to you, the reader, to think of how to implement.

Summary

Key Takeaways:

  • Games are just loops
  • Explore the awesome-go list, find some projects that spark your interest
  • Higher order functions have their place (hint: it’s not absolutely everywhere)
  • Don’t be afraid to break out your code into different packages
  • It’s to make games, you should have fun making it