Fat Old Yeti

Fat Old Yeti

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

08 Feb 2021

Roguelike Tutorial 10

Roguelike in Go - Part 10 (Adding Monsters)

All the code for this tutorial can be gotten from here.

So, we have a player. We have movement. We even have a field of view working. I guess it’s time to add monsters. Just as an FYI, I’ve updated the graphics with a couple that I put together in the course of about five minutes. Nothing great, but much easier to look at. It also works great with the way I’ve implemented the revealed tiles currently. If you grab the code (either from the link above or from my github), the new assets are there. We have added a sprite for our monster, calling it skelly.png, so if you are making your own, you will want to add that one.

We will start by adding a new component. Open components.go and add a type for Monster.

type  Monster  struct{}

Now in level.go, let’s add an enum for tile type. For now, it’s a wall or a floor. More will certainly come later, but this is a start.

type TileType int

const (
	WALL TileType = iota
	FLOOR
)

Next, exit MapTile so that it records TileType. At the bottom of the Type definition, add this:

TileType TileType

Now we need to go into createRoom, createHorizontalTunnel and createVerticalTunnel and look for the line:

level.Tiles[index].Blocked = false

Right under that line, in all three functions, add this:

level.Tiles[index].TileType = FLOOR

The fact that we are doing the same thing in three functions tells me we need to do a refactor and code cleanup soon. For now, just do this.

Going into createTiles, where we create a new MapTile, change the code to this:

tile := MapTile{
	PixelX: x * gd.TileWidth,
	PixelY: y * gd.TileHeight,
	Blocked: true,
	Image: wall,
	IsRevealed: false,
	TileType: WALL,
}

So, now we have split the concept of the tile being blocked with the concept of it blocking our view (and walls currently block our view).

In the IsOpaque function, remember when we added a note that we would have to change this? It’s time. Change the following line in that function:

return level.Tiles[idx].Blocked

to this:

return level.Tiles[idx].TileType  ==  WALL

Now our vision is only blocked by walls and not some random object sitting on the floor.

Let’s add our first monster to the world. In world.go open the InitializeWorld function. Up at the top we declare all the components. Let’s add the following to add a Monster to our world.

monster := manager.NewComponent()

Now right under where we load the Player Image, let’s load an image for our monster:

skellyImg, _, err := ebitenutil.NewImageFromFile("assets/skelly.png")
if err != nil {
	log.Fatal(err)
}

Now right after we add the player entity to the world, let’s add our monsters. We want to add one in every room except the one the player is in (at least for now). So we can put one in the center of each room. Between the code where we create and add the player entity, and the code creating the tag views, add the following:

//Add a Monster in each room except the player's room
for _, room := range startingLevel.Rooms {
	if room.X1 != startingRoom.X1 {
		mX, mY := room.Center()
		manager.NewEntity().
			AddComponent(monster, Monster{}).
			AddComponent(renderable, &Renderable{
			Image: skellyImg,
		}).
		AddComponent(position, &Position{
			X: mX,
			Y: mY,
		})
	}
}

Since our render system is already in place, and our monsters are renderable with a position, they will render now. One problem:

badrender

The Skeletons are drawn regardless of whether or not the player can see them. This is because we didn’t take player fov into account in our rendering. It didn’t matter before, because all we rendered was our player and they are always in their fov.

So, let’s open up render_system.go and change the ProcessRenderables system function.

func ProcessRenderables(g *Game, level Level, screen *ebiten.Image) {
	for _, result := range g.World.Query(g.WorldTags["renderables"]) {
		pos := result.Components[position].(*Position)
		img := result.Components[renderable].(*Renderable).Image

		if level.PlayerVisible.IsVisible(pos.X, pos.Y) {
			index := level.GetIndexFromXY(pos.X, pos.Y)
			tile := level.Tiles[index]
			op := &ebiten.DrawImageOptions{}
			op.GeoM.Translate(float64(tile.PixelX), float64(tile.PixelY))
			screen.DrawImage(img, op)
		}
	}
}

So now, even though we draw tiles the player has previously seen, we only draw monsters if they are currently in the player’s line of sight.
Note that the player can actually walk right through these monsters, as we aren’t updating blocked for them yet, but we will be getting to that next.

So, we have now seen how easy it is to add monsters to the world and how with an ECS in place, the simple fact of giving them a set of components can already add them to the rules of your game.

If you have any questions, please feel free to contact me at fatoldyeti@gmail.com or @idiotcoder on the gophers slack. Also, as previously mentioned, there is a discord button too.