Fat Old Yeti

Fat Old Yeti

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

08 Feb 2021

Roguelike Tutorial 11

Roguelike in Go - Part 11 (Monster Reacting to Player)

All the code in this tutorial can be found here.

So, adding monsters and rendering them is all well and good, but they should also react to the player. We finally get to use the fact that we build this to be turn-based!

Let’s start by giving the monster a name. Open components.go and change the Monster struct to add Name to it.

type Monster struct {
	Name string
}

Now we need to make some changes in world.go so we can more easily use our component. First, where we have declared components outside function scope (globally), add the following:

var  monster  *ecs.Component

This lets us access this component as a component across the entire application. As a potential refactor, we may end up moving these declarations to our components file so they are all in a single place. But that’s later.

Now, where we add the monster component to our monster entity in InitializeWorld, we change it to the following:

AddComponent(monster, &Monster{
	Name: "Skeleton",
}).

And then we want to create a view just for monsters for faster access. Right before the return statement in InitializeWorld, add the following:

monsters := ecs.BuildTag(monster, position)
tags["monsters"] = monsters

There is nothing here we haven’t done already, so it should be evident what it’s doing.

Now create a new file named monster_system.go and add the following code to it:

package main

import (
	"log"
	"github.com/norendren/go-fov/fov"
)

func UpdateMonster(game *Game) {
	l := game.Map.CurrentLevel
	playerPosition := Position{}

	for _, plr := range game.World.Query(game.WorldTags["players"]) {
		pos := plr.Components[position].(*Position)
		playerPosition.X = pos.X
		playerPosition.Y = pos.Y
	}

	for _, result := range game.World.Query(game.WorldTags["monsters"]) {
		pos := result.Components[position].(*Position)
		mon := result.Components[monster].(*Monster)
		monsterSees := fov.New()
		monsterSees.Compute(l, pos.X, pos.Y, 8)
		if monsterSees.IsVisible(playerPosition.X, playerPosition.Y) {
			log.Printf("%s shivers to its bones.", mon.Name)
		}
	}
	game.Turn = PlayerTurn
}

This system will allow the monsters to take actions. The first action we will take is that they will “shiver to their bones” whenever they see the player. Yeah, kind of silly, but it shows a reaction in an easy fashion.

So, first we get the current level. We need this for the FOV calculation for the monster. Currently we are allowing the monster to see 8 squares out, just like the player. This can be made to differ between monsters. Next we get the player’s position and we save it. Now that we have the players position and the map, we loop through the monsters, calculate each monster’s field of view, and if the player happens to be visible, we log the statement “Skeleton shivers to its bones”. We can make this much more robust in the future, but this is enough to show how to do it.

Lastly, we set things back to being the player’s turn.

So…we have an UpdateMonsters system, let’s do something to call it. In main.go look in the update file and replace this:

//Obviously just for now
g.Turn = PlayerTurn

with this:

if g.Turn == MonsterTurn {
	UpdateMonster(g)
}

That’s it. Now when you walk into the field of view for a monster, it will shiver to its bones.

If you have any questions feel free to ask me at fatoldyeti@gmail.com or @idiotcoder on the gopher slack. I can also be found at the Discord server linked in the phone icon above.