Grogue: A Roguelike Tutorial in Go (Part 4)
Tags: roguelike, go, tutorial, gamedev
Published: 2024-03-12 18:58:09

---
title: Grogue: A Roguelike Tutorial in Go (Part 4)
published: true
description: Adding FOV to our Game
tags: #roguelike #go #tutorial #gamedev
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/icmpoo6orxbiw3oduuqx.png
# Use a ratio of 100:42 for best results.
# published_at: 2023-10-13 15:07 +0000
---

Happy New Year and welcome back to part 4 of my Rougelike tutorial in Go! (**EDIT:** It's been 2024 for a while now, as I've been busy and wasn't able to get this published when I initially intended.)

We now have a dungeon that we can move around, but we're not really exploring it if we can see it all from start. We should implement a "field of view" for the player to allow only a limited range around them to be seen.

This can be a rather complicated thing to code, but luckily [norendren](https://github.com/norendren/go-fov) had already created a library that will do the hard work for us.

Let's start by adding the following import to `level.go`:

```go
	"github.com/norendren/go-fov/fov"
```

Then add an `*fov.View` to the Level structure.

```go
	PlayerView *fov.View
```

We also need to instantiate this View, so in `NewLevel()` add the following:

```go
	l.PlayerView = fov.New()
```

Save the file and run `go mod tidy` to pull down the new library.

The next thing to do is to modify our Level's `Draw()` to only render what the player can see. We'll do this by wrapping the four lines inside the nested loops in an `if` statement, as seen below.

```go
// Draw the current level to 'screen'.
func (level *Level) Draw(screen *ebiten.Image) {
	gd := NewGameData()
	for x := 0; x < gd.ScreenWidth; x++ {
		for y := 0; y < gd.ScreenHeight; y++ {  // NEW
			if level.PlayerView.IsVisible(x, y) {
				tile := level.Tiles[GetIndexFromCoords(x, y)]
				op := &ebiten.DrawImageOptions{}
				op.GeoM.Translate(float64(tile.PixelX), float64(tile.PixelY))
				screen.DrawImage(tile.Image, op)
			} // NEW
		}
	}
}
```

For this to work, we need to tell the event system to calculate the player's field of view. Add the new line to the `if !tile.Blocked {}` in `HandleInput()` in `event.go`:

```go
	if !tile.Blocked {
		g.Player.X += dx
		g.Player.Y += dy
		g.CurrentLevel.PlayerView.Compute(g.CurrentLevel, g.Player.X, g.Player.Y, 8)  // NEW
	}
```

At this time, you may notice that your editor (or the go CLI, if you tried to run) complains about `g.CurrentLevel` in the call to `Compute()`.

> ./event.go:27:37: cannot use g.CurrentLevel (variable of type *Level) as fov.GridMap value in argument to g.CurrentLevel.PlayerView.Compute: *Level does not implement fov.GridMap (missing method InBounds)

This library needs us to add a few functions to `Level{}`, so let's go back to `level.go` and add them now.

```go
// Determine if tile coordinates are on the screen.
func (level Level) InBounds(x int, y int) bool {
	gd := NewGameData()
	if x < 0 || x > gd.ScreenWidth || y < 0 || y > gd.ScreenHeight {
		return false
	}
	return true
}

// Determines if a tile at a given coordinate can be seen through.
func (level Level) IsOpaque(x int, y int) bool {
	return level.Tiles[GetIndexFromCoords(x, y)].Opaque
}
```

Now if we run the game, it should look something like this.

![FOV Active](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uh5tf78m7y3p26gqhpgl.png)

# Drawing the Past

It would be cool if we showed the map tiles that we've already seen (though not the monsters that may have moved into those rooms). In order to implement such a change, we should add a flag to our `MapTile{}` to indicate whether or not it has been seen.

```go
	Seen    bool
```

This should be set to `false` by default in `NewTile()`.

```go
	tile := MapTile{
		PixelX:  x,
		PixelY:  y,
		Blocked: blocked,
		Opaque:  opaque,
		Seen:    false,   // NEW
		Image:   image,
	}
```

We'll want to render these seen-but-out-of-sight tiles in a different color, so we'll use the [colorm](https://pkg.go.dev/github.com/hajimehoshi/ebiten/v2/colorm) portion of the Ebitengine. Add the following line to your imports in `level.go`:

```go
	"github.com/hajimehoshi/ebiten/v2/colorm"
```

Refactor the inside of the nested `for` loops in `Draw()` again.

```go
			idx := GetIndexFromCoords(x, y)
			tile := level.Tiles[idx]
			if level.PlayerView.IsVisible(x, y) {
				op := &ebiten.DrawImageOptions{}
				op.GeoM.Translate(float64(tile.PixelX), float64(tile.PixelY))
				screen.DrawImage(tile.Image, op)
				level.Tiles[idx].Seen = true
			} else if tile.Seen {
				op := &colorm.DrawImageOptions{}
				var colorM colorm.ColorM
				op.GeoM.Translate(float64(tile.PixelX), float64(tile.PixelY))
				colorM.Translate(0, 0, 50, 0.75)
				colorm.DrawImage(screen, tile.Image, colorM, op)
			}
```

We move the assignment of the `tile` variable outside of the `if` statement, as we need it in both conditions. The visible condition should look the same, except that we set the tile's `Seen` flag to `true`.

The `else if` part is more interesting. We use the `colorm` package to do the GeoM transformation (placing it in the right part of the screen), to perform a color matrix transformation (tinting it blue), and to draw the tinted image.

Run the game and you'll see something like this after moving around a bit.

![Explored rooms are drawn](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cdgmm6qaartu94yjw99q.png)

## Slowing Down

With FOV done, we should solve the main issue we currently have: the player moves too fast. Instead of moving the player on each call to `Game.Update()`, let's ensure some time has passed first.

In `main.go` add the following to our `Game struct`:

```go
	TickCount    int
```

and set it to zero in `NewGame()`:

```go
	g.TickCount = 0
```

Now, modify `Game.Update()` to increment `TickCount` and only call `HandleInput()` when `TickCount` is greater than five.

```go
func (g *Game) Update() error {
	g.TickCount++

	if g.TickCount > 5 {
		HandleInput(g)
		g.TickCount = 0
	}
	return nil
}
```

When you run the game now, the player should move at a much more reasonable rate.

You can view the complete source code [here](https://github.com/seancallaway/Grogue/tree/part04) and if you have any questions, please feel free to ask them in the comments.