PC Games

Orb
Lasagne Monsters
Three Guys Apocalypse
Water Closet
Blob Wars : Attrition
The Legend of Edgar
TBFTSS: The Pandoran War
Three Guys
Blob Wars : Blob and Conquer
Blob Wars : Metal Blob Solid
Project: Starfighter
TANX Squadron

Android Games

DDDDD
Number Blocks
Match 3 Warriors

Tutorials

2D shoot 'em up
2D top-down shooter
2D platform game
Sprite atlas tutorial
Working with TTF fonts
2D adventure game
Widget tutorial
2D shoot 'em up sequel
2D run and gun
Roguelike
SDL 1 tutorials (outdated)

Latest Updates

SDL2 Rogue tutorial
Wed, 29th September 2021

SDL2 Gunner tutorial
Thu, 26th August 2021

SDL2 Shooter 2 tutorial
Tue, 13th July 2021

SDL2 Widget tutorial
Fri, 18th June 2021

SDL2 Adventure tutorial
Tue, 8th June 2021

All Updates »

Tags

android (3)
battle-for-the-solar-system (9)
blob-wars (9)
brexit (1)
code (6)
edgar (6)
games (37)
lasagne-monsters (1)
making-of (5)
match3 (1)
numberblocksonline (1)
orb (2)
site (1)
tanx (4)
three-guys (3)
three-guys-apocalypse (3)
tutorials (8)
water-closet (3)

Books

« Back to tutorial listing

— Creating a simple roguelike —
Part 2: First Monster

Note: this tutorial assumes knowledge of C, as well as prior tutorials.

Introduction

We're now ready to add in our first monster. To begin with, our monster, a Micro Mouse, will be competely static, not moving or attacking. One could almost consider it a wall. However, we're going to use this opportunity to lay the ground work for fully supporting our monsters fully later down the line.

Extract the archive, run make, and then use ./rogue02 to run the code. You will see a window open like the one above, showing our main character in a dungeon environment. Use the same controls as before to move around. Notice how the mouse blocks not only our movement, but also our line of sight. The mouse's placement is random each time a dungeon is generated. Once you're finished, close the window to exit.

Inspecting the code

Adding our static mouse is quite simple, due to everything we prepared in part one. To begin with, let's update defs.h:


enum {
	ET_UKNOWN,
	ET_PLAYER,
	ET_MONSTER
};

We've added ET_MONSTER to our enums, to represent a monster. Just a one line change. We've also done the same with structs.h:


struct Entity {
	int id;
	int type;
	char name[MAX_NAME_LENGTH];
	int x;
	int y;
	int solid;
	int facing;
	AtlasImage *texture;
	Entity *next;
};

We've added in a field called `solid` to our Entity struct. This field will be used to specify whether an entity is solid and therefore blocks movement and line of sight. We've also added in a new field to Dungeon:


typedef struct {
	int entityId;
	Entity entityHead, *entityTail;
	Entity *player, *currentEntity;
	MapTile map[MAP_WIDTH][MAP_HEIGHT];
	SDL_Point camera;
} Dungeon;

currentEntity will be used to track the entity that we are currently processing in the dungeon, whether that be the player or a monster.

Now, let's move onto the file we've created to handle our monsters: monsters.c. This is where we'll create our monsters, process them, and all the rest. There are already a number of function here, so we'll work through them one at a time.

The first function is doMonsters:


void doMonsters(void)
{
	int processing;

	processing = 1;

	do
	{
		nextMonster();

		processing = dungeon.currentEntity != dungeon.player;
	}
	while (processing);
}

We'll be using this function to process our monsters when it's their turn. In a roguelike, a player will take their turn and then the monsters will do so. Once all the monsters have taken their turns (moving, attacking, etc), control will return to the player. We're doing just that in this function, albeit without the monsters taking any actions themselves.

We're setting a variable named `processing` to 1 and then setting up a do-loop. We first call a function named nextMonster (more on this in a bit) and then update our `processing` flag. `processing` will be set to 1 if dungeon's currentEntity is not the player. Our do-while loop won't exit until `processing` is 0. In other words, we want to keep calling nextMonster until we reach the player and have been through all the monsters in our dungeon.

If we move onto nextMonster, this will make a bit more sense:


void nextMonster(void)
{
	int found;

	do
	{
		dungeon.currentEntity = dungeon.currentEntity->next;

		if (dungeon.currentEntity == NULL)
		{
			dungeon.currentEntity = dungeon.entityHead.next;
		}

		found = (dungeon.currentEntity->type == ET_MONSTER || dungeon.currentEntity->type == ET_PLAYER);
	}
	while (!found);

We setup a do-while loop and then move to the next entity in the dungeon, by assigning dungeon's currentEntity to the existing currentEntity's `next`. If our currentEntity is now NULL it means we have reached the end of the list. We therefore want to move back to the top of the list, and so set dungeon's currentEntity to entityHead's `next`, in effect wrapping back around. We'll then test the type of currentEntity and assign the result to a variable called `found`. If `type` is either ET_MONSTER or ET_PLAYER, `found` will become 1. Otherwise, it will be 0. Our while-loop will only exit if `found` is 1 (in other words, we've found a monster or the player).

While right now we only have ET_MONSTER and ET_PLAYER in our dungeon, we'll later on have other types (such as items), and so we're just ensuring now that we only consider these two types to be valid.

So, our doMonsters function will call nextMonster to process all our monsters, stopping when it comes back around to the player. As you can see by now, when it comes to handling the monsters, we're merely looping through all the monsters in the dungeon until we return back to the player. Remember, however, that right now the monsters are doing nothing..!

Our next function is addMonsters. There's not much to it right now:


void addMonsters(void)
{
	addEntityToDungeon(initEntity("Micro Mouse"));
}

This function merely adds a single monster to the dungeon, by calling addEntityToDungeon. The addEntityToDungeon function takes just an entity as an argument, in this case, the entity returned by initEntity. We'll see more on addEntityToDungeon in a bit.

This next function is createMonster:


static void createMonster(Entity *e)
{
	e->type = ET_MONSTER;
	e->solid = 1;
}

This is just a function to help with setting up common monster attributes. For the entity that has been passed into the function (`e`), we're setting its `type` as ET_MONSTER and its `solid` flag to 1. Later on, this function will do much more!

We call createMonster in our initMicroMouse function:


void initMicroMouse(Entity *e)
{
	createMonster(e);

	STRCPY(e->name, "Micro Mouse");
	e->texture = getAtlasImage("gfx/entities/microMouse.png", 1);
}

Being an init function, initMicroMouse takes an entity as an argument (passed across from the entity factory). We first call createMonster, passing the entity over, then set the entity's `name` to "Micro Mouse", and then grab the `texture` that is needed, using getAtlasImage. Quite simple, really.

Now let's look at entities.c. We've made quite a number of additions and updates to this file, so we'll work our way through them from top to bottom. Starting with addEntityToDungeon:


void addEntityToDungeon(Entity *e)
{
	int x, y, ok;

	do
	{
		x = 1 + rand() % (MAP_WIDTH - 2);
		y = 1 + rand() % (MAP_HEIGHT - 2);

		ok = dungeon.map[x][y].tile > TILE_HOLE && dungeon.map[x][y].tile < TILE_WALL && isOccupied(x, y) == NULL;
	}
	while (!ok);

	e->x = x;
	e->y = y;
}

For the given entity (`e`), this function will find a suitable place in the dungeon to insert it. We start with a do-while loop, and then set two variables, `x` and `y`, to a random place in the dungeon (less the edges). We then test whether the tile at this point in the map is a floor tile (> TILE_HOLE and < TILE_WALL) and also whether that square is not already occupied by another entity, by calling isOccupied and passing in the `x` and `y`. We assign the result of this condition check to a variable called `ok`. Our do-while loop will continue until `ok` is 1. With that done, we assign the entity's `x` and `y` as the values of `x` and `y`.

Our isOccupied function is simple enough:


static int isOccupied(int x, int y)
{
	Entity *e;

	for (e = dungeon.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->x == x && e->y == y)
		{
			return 1;
		}
	}

	return 0;
}

We're iterating through all the entities in the dungeon, to see if there's an entity at the position specified (the entity's `x` and `y` are equal to the `x` and `y` passed into the function). If so, we'll return 1. Otherwise, we'll return 0.

Next, we've made an update to moveEntity:


void moveEntity(Entity *e, int dx, int dy)
{
	int x, y;

	x = e->x + dx;
	y = e->y + dy;

	if (dx < 0)
	{
		e->facing = FACING_LEFT;
	}
	else if (dx > 0)
	{
		e->facing = FACING_RIGHT;
	}

	if (x >= 0 && y >= 0 && x < MAP_WIDTH && y < MAP_HEIGHT && dungeon.map[x][y].tile >= TILE_GROUND && dungeon.map[x][y].tile < TILE_WALL && !isBlocked(e, x, y))
	{
		e->x = x;
		e->y = y;
	}
}

As well as testing that the location the entity wishes to move is within the map and a ground tile, we're making a call to a function called isBlocked, passing in the entity and the `x` and `y` position we want to move to. isBlocked needs to return 0 (false) in order for us to be able to move.

isBlocked is, for now, a basic function:


static int isBlocked(Entity *e, int x, int y)
{
	Entity *other;

	for (other = dungeon.entityHead.next ; other != NULL ; other = other->next)
	{
		if (other->x == x && other->y == y)
		{
			switch (other->type)
			{
				case ET_PLAYER:
				case ET_MONSTER:
					return 1;

				default:
					break;
			}
		}
	}

	return 0;
}

Like isOccupied, we're looping through all the entities in our dungeon, assigning them to a variable called `other`, and looking for one in the same square we wish to move to. If `other`'s `x` and `y` is the same as the square we want to move to, we'll test the `type` of `other`. If it's an ET_PLAYER or an ET_MONSTER, we'll return 1 (meaning the square is blocked). If we don't find a match, we'll return 0. This function might look odd right now, but this is because we'll be expanding it in future and reacting more appropriately to the type of entity we've encountered. For example, encountering ET_PLAYER or ET_MONSTER might result in an attack happening, while walking into an item could result in it being picked up, and not blocking movement.

drawEntities is the last function that has been tweaked:


void drawEntities(void)
{
	Entity *e;
	int x, y;

	for (e = dungeon.entityHead.next ; e != NULL ; e = e->next)
	{
		if (dungeon.map[e->x][e->y].visible)
		{
			x = (e->x - dungeon.camera.x) * MAP_TILE_SIZE;
			y = (e->y - dungeon.camera.y) * MAP_TILE_SIZE;

			blitAtlasImage(e->texture, x + MAP_RENDER_X, y + MAP_RENDER_Y, 0, e->facing == FACING_RIGHT ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL);
		}
	}
}

We've made one small change, and are now testing whether the dungeon tile the entity is on (using their `x` and `y`) is `visible`. If so, we'll draw the entity as normal. Otherwise, they'll be hidden. You can see this effect for yourself in some of the dungeons by standing somewhere where the Micro Mouse is in a dark tile. The mouse will vanish from sight until you move into a position where it is visble once more.

Now onto the fogOfWar.c updates. We've changed both function here, to support our solid entities blocking our line of sight. Starting with updateFogOfWar:


void updateFogOfWar(void)
{
	int x, y, mx, my;
	Entity *e;

	for (x = 0 ; x < MAP_WIDTH ; x++)
	{
		for (y = 0 ; y < MAP_HEIGHT ; y++)
		{
			hasSolid[x][y] = 0;

			dungeon.map[x][y].visible = 0;
		}
	}

	for (e = dungeon.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e != dungeon.player && e->solid)
		{
			hasSolid[e->x][e->y] = 1;
		}
	}

	for (y = -VIS_DISTANCE ; y <= VIS_DISTANCE ; y++)
	{
		for (x = -VIS_DISTANCE ; x <= VIS_DISTANCE ; x++)
		{
			mx = dungeon.player->x + x;
			my = dungeon.player->y + y;

			if (getDistance(dungeon.player->x, dungeon.player->y, mx, my) <= VIS_DISTANCE)
			{
				if (mx >= 0 && my >= 0 && mx < MAP_WIDTH && my < MAP_HEIGHT)
				{
					if (!dungeon.map[mx][my].visible && hasLOS(dungeon.player, mx, my))
					{
						dungeon.map[mx][my].revealed = dungeon.map[mx][my].visible = 1;
					}
				}
			}
		}
	}
}

We've declared a multi-dimensional array called hasSolid (as static within the file), of size MAP_WIDTH and MAP_HEIGHT. Basically, this array is the same size as our dungeon and will be used to mark squares that contains something solid. Now, as well as marking each square in our map as not being visible, we're also clearing our hasSolid array at the same position. With that done, we're then looping through all the entities in our dungeon and testing their `solid` flag. If the entity is solid (1), we're updating the hasSolid array at the entity's `x` and `y` coordinates, and marking it as 1, to say a solid entity exists at that position. The rest of the function remains the same.

We're making use of this hasSolid array in our hasLOS function:


int hasLOS(Entity *src, int x2, int y2)
{
	int x1, y1, dx, dy, sx, sy, err, e2;

	x1 = src->x;
	y1 = src->y;

	dx = abs(x2 - x1);
	dy = abs(y2 - y1);

	sx = (x1 < x2) ? 1 : -1;
	sy = (y1 < y2) ? 1 : -1;
	err = dx - dy;

	while (1)
	{
		e2 = 2 * err;

		if (e2 > -dy)
		{
			err -= dy;
			x1 += sx;
		}

		if (e2 < dx)
		{
			err += dx;
			y1 += sy;
		}

		if (x1 == x2 && y1 == y2)
		{
			return 1;
		}

		if (dungeon.map[x1][y1].tile >= TILE_WALL || hasSolid[x1][y1])
		{
			return 0;
		}
	}

	return 0;
}

Before, we would return 0 if the map tile at `x1` and `y1` is a wall tile. Now, we're also return 1 if there is a solid entity at `x1` and `y1`, by testing the value of hasSolid. This is what leads to our Micro Mouse blocking the line of sight and darkening the tiles behind it.

Those are the changes to fogOfWar.c complete, and with it the bulk of our changes. The remaining changes are less significant. We'll start with player.c, and doPlayer:


void doPlayer(void)
{
	int dx, dy;

	moveDelay = MAX(moveDelay - app.deltaTime, 0);

	if (moveDelay == 0)
	{
		dx = dy = 0;

		if (app.keyboard[SDL_SCANCODE_W])
		{
			dy = -1;
		}

		if (app.keyboard[SDL_SCANCODE_S])
		{
			dy = 1;
		}

		if (app.keyboard[SDL_SCANCODE_A])
		{
			dx = -1;
		}

		if (app.keyboard[SDL_SCANCODE_D])
		{
			dx = 1;
		}

		if (dx != 0 || dy != 0)
		{
			moveEntity(dungeon.player, dx, dy);

			moveDelay = MOVE_DELAY;

			updateFogOfWar();

			nextMonster();
		}
	}
}

We've added a call to nextMonster after our updateFogOfWar code. This means that once the player has moved, it will be the turn of the monsters (but as we've already seen, the monsters won't be doing much to begin with).

Moving now to dungeon.c, we've updated createDungeon:


static void createDungeon(void)
{
	initEntities();

	initEntity("Player");

	if (1)
	{
		generateMap();
	}
	else
	{
		generateEmptyMap();
	}

	addMonsters();

	updateFogOfWar();

	dungeon.currentEntity = dungeon.player;
}

Notice to begin with that we've added in an if-statement around two functions: generateMap and generateEmptyMap. This statement exists to aid with our testing and development. Basically, changing the if from 1 to 0 will call generateEmptyMap instead of generateMap. The latter function creates an empty map that is easier to navigate and test with; it is useful to add in monsters, items, etc. and have them available in a small space, rather than go hunting for them. Flip the value from 1 to 0 as you desire, to create a maze or empty map. We're also making a call to addMonsters, to add in our monsters, and assigning dungeon's currentEntity as the player, so that the player can make their turn first (and also ensuring that our game doesn't crash, due to currentEntity being NULL).

`logic` has been updated next:


static void logic(void)
{
	if (dungeon.currentEntity == dungeon.player)
	{
		doPlayer();
	}
	else
	{
		doMonsters();
	}

	doCamera();
}

We simply test who (or what) dungeon's currentEntity is. If it's the player, we'll call doPlayer. Otherwise, we'll call doMonsters, to drive the monsters.

In case you're wondering what the code for our generateEmptyMap looks like, it can be found in map.c:


void generateEmptyMap(void)
{
	int x, y;

	for (x = 0 ; x < MAP_WIDTH ; x++)
	{
		for (y = 0 ; y < MAP_HEIGHT ; y++)
		{
			memset(&dungeon.map[x][y], 0, sizeof(MapTile));

			dungeon.map[x][y].tile = TILE_WALL;
		}
	}

	for (x = 1 ; x < MAP_RENDER_WIDTH ; x++)
	{
		for (y = 1 ; y < MAP_RENDER_HEIGHT ; y++)
		{
			dungeon.map[x][y].tile = TILE_GROUND;

			dungeon.map[x][y].revealed = 1;
		}
	}

	dungeon.player->x = MAP_RENDER_WIDTH / 2;

	dungeon.player->y = MAP_RENDER_HEIGHT / 2;
}

As with our other map generation, we're first filling the entire map with wall tiles (TILE_WALL). Next, we're using two for-loops to convert all the tiles of MAP_RENDER_WDITH and MAP_RENDER_HEIGHT (plus 1, to keep the edges walls) to ground tiles (TILE_GROUND). We're also setting the tile's `revealed` flag to 1, so that the map is uncovered. Finally, we're setting the player's position to the middle of the empty map, assigning MAP_RENDER_WIDTH / 2 and MAP_RENDER_HEIGHT / 2 to the player's `x` and `y`, respectively.

To finish off, we turn to entityFactory.c, where we've added in the init function for our Micro Mouse:


void initEntityFactory(void)
{
	memset(&head, 0, sizeof(InitFunc));
	tail = &head;

	addInitFunc("Player", initPlayer);
	addInitFunc("Micro Mouse", initMicroMouse);
}

All we need to do is add in a call to addInitFunc, passing "Micro Mouse" and initMicroMouse as the arguments. We can now add a Micro Mouse monster to the dungeon by calling initEntity with "Micro Mouse".

So, we now have a dungeon where we can add in a monster. It doesn't do a lot right now, but that will change in a little way down the line. Before that, we're going to make a start on combat. Since our Micro Mouse can't move, it will make an ideal target to practice against ...

Purchase

The source code for all parts of this tutorial (including assets) is available here:

It is also available as part of the SDL2 tutorial bundle (with on-going updates):

If you do not wish to create an itch.io account, you can also purchase the tutorial bundle using PayPal. This method will be slower, however, as it will require manual verification of the transaction.

Comments

Share your comments and thoughts below. All comments are anonymous and cannot be edited.

 

Mobile site