↓Skip to main content

Go Rogue! - Chapter 1

Drawing the '@' Symbol and Moving It Around

·11 mins

Cover image: By FooWho - Own work, CC BY-SA 4.0

If you prefer to jump straight to the working Chapter 1 state instead of setting up the files by hand, and you have previously cloned the repository, checkout the chapter-1 branch or the v0.1.0 tag:

git checkout v0.1.0
go run .

This checks out the exact “Chapter 1” project state and runs the project.

In the first chapter, I am sticking to a fairly literal translation of Part 1 from Roguelike Tutorials. One side effect is that it gives my app an odd aspect ratio. This is because in Ebitengine, the closest approximation to the console printing functionality in tcod is the DebugPrintAt method from ebitenutil. This method uses a built-in font (or at least I did not spot a way to change the font). The glyphs in the font used by Roguelike Tutorials are 10x10. The glyphs in the font being used by Ebitengine are 6x16, making the glyphs narrower and taller. I have corrected this by changing the window size to 800x500 in the call to ebiten.SetWindowSize() in main.go. This does result in the game’s canvas being pillarboxed in the window, but this will be fixed in the next chapter when we change the rendering method and the font.

The goal of this chapter is to get the ‘@’ onto the screen and move it around. The Ebitengine game loop is started by calling RunGame(game Game). Game is an interface that requires we implement three methods:

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

We want to have a game screen that is 80 columns wide and 50 rows high. As mentioned above, the font that we are going to get by calling ebitenutil.DebugPrintAt is a fixed-width 6x16 font. We can get started by setting up the structure that will represent our game and implement the required interface. Right now, we have four things that we know we are going to need: the number of columns and rows in our game screen and the width and height of a character. These can be the first fields of our structure.

type Game struct {
    gridCols   int
    gridRows   int
    charWidth  int
    charHeight int
}

This is also what we need to be able to implement one of the three required methods, Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int). We can define this method next:

func (g *Game) Layout(outsideWidth int, outsideHeight int) (screenWidth int, screenHeight int) {
	screenWidth = g.gridCols * g.charWidth
	screenHeight = g.gridRows * g.charHeight

	return screenWidth, screenHeight
}

Our main.go file should look like this now:

package main

import (
	"log"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)

type Game struct {
	gridCols   int
	gridRows   int
	charWidth  int
	charHeight int
}

func (g *Game) Update() error {
	return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
	ebitenutil.DebugPrint(screen, "Hello, World!")
}

func (g *Game) Layout(outsideWidth int, outsideHeight int) (screenWidth int, screenHeight int) {
	screenWidth = g.gridCols * g.charWidth
	screenHeight = g.gridRows * g.charHeight

	return screenWidth, screenHeight
}

func main() {
	game := Game{
		gridCols:   80,
		gridRows:   50,
		charWidth:  6,
		charHeight: 16,
	}
	// This line sets the window to match the logical screen pixel for pixel.
	// This produces the awkward aspect ratio.
	// ebiten.SetWindowSize(game.gridCols*game.charWidth, game.gridRows*game.charHeight)
	// This sets the physical window to a landscape 800x500 size. Because Layout()
	// returns 480x800, Ebitengine scales the logical screen to fit vertically and
	// pillarboxes the left and right sides. We'll fix this properly in Chapter 2
	// when we switch to a square 10x10 tileset font.
	ebiten.SetWindowSize(800, 500)
	ebiten.SetWindowTitle("Go Rogue!")
	err := ebiten.RunGame(&game)
	if err != nil && err != ebiten.Termination {
		log.Fatal(err)
	}
}

We have started adding useful fields into our Game struct. We have a Layout() method that calculates our screen size based on our desired grid size (80x50) and the size of our glyphs for representing things in the game world (6x16). Finally, we have updated the title of the window to reflect the game we are working on, “Go Rogue!”.

The next thing we want to do is get the glyph for the player, ‘@’, onto the screen. To do this, we will need to change our Draw() method. Currently, we have the Draw() method print “Hello, World!” to the screen. Let’s have the ‘@’ start at the center of the screen. We need to make a couple of changes to support this. We are going to need to keep track of where the player is located, so let’s add playerX and playerY to our Game struct. It will now look like this:

type Game struct {
	gridCols   int
	gridRows   int
	charWidth  int
	charHeight int
	playerX    int
	playerY    int
}

Then we need to calculate our initial location when we initialize the Game struct:

game := Game{
	gridCols:   80,
	gridRows:   50,
	charWidth:  6,
	charHeight: 16,
	// 80 columns, each 6 pixels wide. The center column is thus (80 / 2) * 6
	playerX: (80 / 2) * 6,
	// 50 rows, each 16 pixels tall. The center row is thus (50 / 2) * 16
	playerY: (50 / 2) * 16,
}

We want the player to begin in the center column and center row. We have 80 columns with each column being 6 pixels wide and we have 50 rows, with each row 16 pixels tall. Now, we can update the Draw() method to put the ‘@’ glyph at the player’s location.

func (g *Game) Draw(screen *ebiten.Image) {
	ebitenutil.DebugPrintAt(screen, "@", g.playerX, g.playerY)
}

The final piece to complete this chapter is to get the player’s glyph to move around the screen in response to keyboard inputs. We could put all of this into our main.go file, but we are going to follow the example from Roguelike Tutorials and split our project up into some additional files. Right now, we want to get our rogue moving, but eventually we will want our rogue to be able to do lots of other things as well. So let’s create a new file, actions.go, where we can define things the rogue can do. In Roguelike Tutorials the tutorial starts with a base class, Action, and derives two subclasses, MovementAction and EscapeAction, from it. Go is not an object-oriented language, at least not in the way Python is. In Go, the basic type for encapsulation of data is the struct type, like the Game struct we have defined. Go allows us to define methods with a receiver argument that binds the method to a struct. Go also allows us to define an interface, which is a set of methods that need to be supported. Then, any struct with methods to match each method signature specified in the interface is said to implement that interface. What this means for us is that we are going to define an interface, Action and have two different structs, MovementAction and EscapeAction that implement the methods required by the Action interface. This allows us to use either a MovementAction or an EscapeAction anywhere we want to use an Action. Let’s start filling out our actions.go file:

package main

type Action interface {
}

type MovementAction struct {
}

type EscapeAction struct {
}

Now we have stubs for our Action interface and our MovementAction and EscapeAction structs. We want our MovementAction to be able to move the player’s glyph in response to keyboard input and we want our EscapeAction to shut down the game if the player hits the escape key.

Let’s start with MovementAction. We are going to start off allowing the player to move in the four cardinal directions and move one tile at a time. We can represent this movement as a delta on the player’s current location:

type MovementAction struct {
	dx int
	dy int
}

So, if the player wants to move up, we would need a MovementAction that has a delta of -1 for the MovementAction.dy field: MovementAction{dx: 0, dy: -1}. Similarly, if the player wanted to move to the east, or right, we would need a MovementAction with a delta of 1 for the MovementAction.dx field: MovementAction{dx: 1, dy: 0}.

The EscapeAction doesn’t actually need to store any data. Its existence is all the data that is needed. For our Action interface, we didn’t come up with any specific functions we wanted, so we will leave it empty as well for now.

Finally, we are going to define two helper functions to generate these structures for us when we need them:

func NewEscapeAction() *EscapeAction {
	return &EscapeAction{}
}

func NewMovementAction(dx int, dy int) *MovementAction {
	return &MovementAction{dx: dx, dy: dy}
}

If you are looking at my finished code for chapter 1, you may have also noticed the Interface Guards in a few places. I have labeled all of them with a comment // Interface Guard. Here is an example of one:

// Interface Guard
var _ Action = (*MovementAction)(nil)

In Go, a struct implements an interface if it implements all of the interface’s required methods. Since you don’t explicitly say that a certain struct is going to implement a particular interface, it can become easy to change something in your code and now your struct no longer implements the interface. Having an interface guard helps you spot this immediately, if you are using an editor like VS code or any other editor connected to a Go language server. If we make a change and our struct no longer implements an interface we have a guard for, that guard will immediately get flagged as an error and will even tell us what method is missing. Since the Action interface does not specify any required methods, any struct would currently pass this guard, so it isn’t particularly useful now, but it is a good habit to have interface guards for interfaces that might become complicated (as this interface might in future chapters).

Let’s tackle getting the input from the keyboard next. Create a new file, inputHandlers.go. We are going to create a receiver for a Game to get input from the keyboard. We can then use this in our Game.Update() method to update the state of the game with either the player’s new location, or by shutting down the game.

package main

import (
	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/inpututil"
)

func (g *Game) EventHandler() Action {
	keys := inpututil.AppendJustPressedKeys(nil)

	if len(keys) == 0 {
		return nil
	}

	switch keys[0] {
	case ebiten.KeyArrowUp, ebiten.KeyW:
		action := NewMovementAction(0, -1)
		return action
	case ebiten.KeyArrowDown, ebiten.KeyS:
		action := NewMovementAction(0, 1)
		return action
	case ebiten.KeyArrowLeft, ebiten.KeyA:
		action := NewMovementAction(-1, 0)
		return action
	case ebiten.KeyArrowRight, ebiten.KeyD:
		action := NewMovementAction(1, 0)
		return action
	case ebiten.KeyEscape:
		action := NewEscapeAction()
		return action
	}
	return nil
}

Now, we see why we needed the Action interface. When we read the keyboard input, we need to be able to handle either a movement key or an escape key. So our EventHandler method is going to return anything that implements the Action interface. Right now, that is anything. We didn’t define any required methods for the Action interface, so everything implements it. In our EventHandler() method, we get a slice containing any keys that were pressed since the last game tick. In theory, there could be multiple key presses, but given the frequency with which this method will be called, it is pretty unlikely. In any event, we are only going to handle the first key press. If one of the arrow keys or wasd were pushed, we create a MovementAction to capture that intent. If the escape key was pushed, we create an EscapeAction to capture that intent. We will return the Action that was created for the player’s intent, or nil to indicate that there was no input from the player.

Back in main.go, our Update() method calls EventHandler() and receives the player’s intent. We then either move the player, shut down the game, or do nothing.

func (g *Game) Update() error {
	action := g.EventHandler()
	if action == nil {
		return nil
	}
	switch v := action.(type) {
	case *MovementAction:
		g.playerX += (v.dx * g.charWidth)
		g.playerY += (v.dy * g.charHeight)
	case *EscapeAction:
		return ebiten.Termination
	}
	return nil
}

When the game state has possibly been updated, we want to redraw the screen. Our Draw() method is very simple, since all we handle at the moment is drawing the ‘@’ for the character at the correct location on the screen.

func (g *Game) Draw(screen *ebiten.Image) {
	ebitenutil.DebugPrintAt(screen, "@", g.playerX, g.playerY)
}

Finally, our Layout() method is unchanged:

func (g *Game) Layout(outsideWidth int, outsideHeight int) (screenWidth int, screenHeight int) {
	screenWidth = g.gridCols * g.charWidth
	screenHeight = g.gridRows * g.charHeight

	return screenWidth, screenHeight
}

We calculate the width of the screen by multiplying the number of columns by the width of our glyphs and the height of the screen by multiplying the number of rows by the height of our glyphs. This allows us to easily change the sizes if we used a different font or some other representation (which we will be doing in later chapters).

Our program now consists of three Go files: main.go, actions.go, and inputHandlers.go. We should be able to run the program in the same way as before:

go run .

If you have also downloaded the code from my Git repository, you can look through my code at this point by checking out the chapter-1 branch or the v0.1.0 tag:

git checkout v0.1.0

If you are using git and your own repository, now is a good time to commit.

git add .
git commit -m "Chapter 1 - Drawing the '@' symbol and moving it around"