Fat Old Yeti

Fat Old Yeti

Being a blog of thoughts and tutorials from a hobby game developer.

06 Feb 2021

Roguelike Tutorial Part 2 (Refactor)

Roguelike in Go - Part 2 (Refactor)

So, now that we have the basics, it’s time to do a little bit of refactoring so that we can begin the next lesson. As before, if you want the code from this tutorial, it can be found here.

GameData Let’s start by moving the GameData struct and it’s constructor into its own file. Name this file gamedata.go

Remove the GameData Struct and Constructor from level.go and add it to gamedata.go. It should now look like this.

package main

//GameData holds the values for the size of elements within the game
type GameData struct {
	ScreenWidth int
	ScreenHeight int
	TileWidth int
	TileHeight int
}

  

//NewGameData creates a fully populated GameData Struct.
func NewGameData() GameData {
	g := GameData{
		ScreenWidth: 80,
		ScreenHeight: 50,
		TileWidth: 16,
		TileHeight: 16,
	}
	return g
}

Next we will create a file named dungeon.go. A dungeon will consist of a name (indicating what the dungeon is) and a slice of levels (the generated levels for the dungeon). To create this, add the following code to dungeon.go:

package main

//Dungeon is a container for all the levels that make up a particular dungeon in the world.
type Dungeon struct {
	Name string
	Levels []Level
}

I’m pretty sure this file is self-evident. However, we have not defined what a Level is yet. We should do that next.

Go back to level.go. We removed GameData from it, but we still have a few more changes to make (like defining level in the first place).

A level will be a struct which contains a slice of MapTiles. Add the following to the file.

//Level holds the tile information for a complete dungeon level.
type Level struct {
	Tiles []MapTile
}

I’m a sucker for a good constructor function (or its go equivalent, so let’s add one.

//NewLevel creates a new game level in a dungeon.
func NewLevel() Level {
	l := Level{}
	tiles := l.CreateTiles()
	l.Tiles = tiles
	return l
}

Now all we have left to do in this file is to change the function signatures to make them methods of the Level struct.

Change the function signatures to match the ones below:

func (level *Level) CreateTiles() []MapTile {

and

func (level *Level) GetIndexFromXY(x int, y int) int {

With this change, we have successfully refactored level.go

The GameMap

We now have a Level, which is a collection of Map Tiles. We have a Dungeon, which is a collection of levels. What we need is a Game Map, which would be a collection of dungeons. This will allow for us to create multiple dungeons, each with a distinct look and feel, and even an overland. Create a file called map.go and let’s add a struct to contain our Game Map. Add the following to the file.

package main

//GameMap holds all the level and aggregate information for the entire world.
type GameMap struct {
	Dungeons []Dungeon
}

Then let’s add a constructor to create this easily:

//NewGameMap creates a new set of maps for the entire game.
func NewGameMap() GameMap {
	//Return a new game map of a single level for now
	l := NewLevel()
	levels := make([]Level, 0)
	levels = append(levels, l)
	d := Dungeon{Name: "default", Levels: levels}
	dungeons := make([]Dungeon, 0)
	dungeons = append(dungeons, d)
	gm := GameMap{Dungeons: dungeons}
	return gm
}

For now, we will be creating only one level and one dungeon in our game. However, this sets the groundwork for later expansion.

So, all that’s left is to make some changes to main.go.

Let’s start with the Layout function. Previously we hardcoded the values for the window size in it. Let’s make this dynamic so that just by changing the values in GameData, we will be able to change our entire game.

//Layout will return the screen dimensions.
func (g *Game) Layout(w, h int) (int, int) {
	gd := NewGameData()
	return gd.TileWidth * gd.ScreenWidth, gd.TileHeight * gd.ScreenHeight
}

This is a simple change, but much better than what we had.

Game and its associated constructor function will need to change to use a GameMap instead of a Level.

//Game holds all data the entire game will need.
type Game struct {
	Map GameMap
}

//NewGame creates a new Game Object and initializes the data
func NewGame() *Game {
	g := &Game{}
	g.Map = NewGameMap()
	return g
}

Lastly, we need to make changes to our Draw function in order for it to adapt to the changes we made in this refactor. The logic, however, will remain the same.

//Draw is called each draw cycle and is where we will blit.
func (g *Game) Draw(screen *ebiten.Image) {
	//Draw the Map
	gd := NewGameData()
	level := g.Map.Dungeons[0].Levels[0]
	for x := 0; x < gd.ScreenWidth; x++ {
		for y := 0; y < gd.ScreenHeight; y++ {
			tile := level.Tiles[level.GetIndexFromXY(x, y)]
			op := &ebiten.DrawImageOptions{}
			op.GeoM.Translate(float64(tile.PixelX), float64(tile.PixelY))
			screen.DrawImage(tile.Image, op)
		}
	}
}

At this point, running the program will give you the same output as we got at the end of the first tutorial. However, this code is a lot more future-proof.

If you have issues, compare your code to the code linked above. If you have questions, please feel free to contact me. fatoldyeti@gmail.com or @idiotcoder on the gophers slack.