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
Medals (Achievements)
2D turn-based strategy game
2D isometric game
2D map editor
2D mission-based shoot 'em up
2D Santa game
2D split screen game
SDL 1 tutorials (outdated)

Latest Updates

SDL2 Versus game tutorial
Wed, 20th March 2024

Download keys for SDL2 tutorials on itch.io
Sat, 16th March 2024

The Legend of Edgar 1.37
Mon, 1st January 2024

SDL2 Santa game tutorial 🎅
Thu, 23rd November 2023

SDL2 Shooter 3 tutorial
Wed, 15th February 2023

All Updates »

Tags

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

Books


The Attribute of the Strong (Battle for the Solar System, #3)

The Pandoran War is nearing its end... and the Senate's Mistake have all but won. Leaving a galaxy in ruin behind them, they set their sights on Sol and prepare to finish their twelve year Mission. All seems lost. But in the final forty-eight hours, while hunting for the elusive Zackaria, the White Knights make a discovery in the former Mitikas Empire that could herald one last chance at victory.

Click here to learn more and read an extract!

« Back to tutorial listing

— Creating a simple roguelike —
Part 14: Doors

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

Introduction

In this part, we're finally going to be making use of those keys we've been finding lying around. To do this, we're going to insert some doors into the dungeon, some of which will be open, others locked. The locked doors will require the player to have a key in their inventory to open. We're also going to be shifting items such as weapons, armour, and microchips, to alcoves into the dungeon, rather than have them sitting out in the middle of the floor.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./rogue14 to run the code. You will see a window open displaying the player character in a small room, with just a stair case (leading up). Play the game as normal. If you come across a door, a red light at the bottom will indicate that it requires a key to open it. Walk into it to use the key (which will be lost). Doors with a green light do not require a key to open and can be walked into normally. Once you're finished, close the window to exit.

Inspecting the code

Adding the doors to our dungeon is a bit more complicated than one might think, due to the need to identify where they should be placed. We'll see how this is done when we come to creating them.

To begin with, let's look at the changes we've made to defs.h:


enum {
	ET_UKNOWN,
	ET_PLAYER,
	ET_MONSTER,
	ET_ITEM,
	ET_WEAPON,
	ET_ARMOUR,
	ET_MICROCHIP,
	ET_DOOR,
	ET_STAIRS
};

We've added in a new ET_ type - ET_DOOR will identify the entity as a door.

Turning next to structs.h, we've added in a new struct:


typedef struct {
	int locked;
} Door;

We've created a Door struct, with a single field - `locked`. `locked` will specify whether the door is locked (1) or unlocked (0). Easy.

To handle our doors, we've added in a file called doors.c. As always, there are plenty of functions to get through, though the coding pattern should be quite familiar by now. Starting with initDoor:


static Entity *initDoor(Entity *e, int locked)
{
	Door *d;

	d = malloc(sizeof(Door));
	memset(d, 0, sizeof(Door));
	d->locked = locked;

	e->type = ET_DOOR;
	e->data = d;
	e->solid = 1;
	e->alwaysVisible = 1;

	e->touch = touch;

	return e;
}

As we've seen a few times before, this is a helper function for creating Doors. It handles all the common data setup. The function takes two parameters - the Entity (`e`) and an int called `locked`. `locked` will tell us whether the Door we create is locked. We start by mallocing and memsetting a Door (as `d`), and setting the value of its `locked` field to the value of `locked` we passed into the function. Folowing that, we set `e`'s type to ET_DOOR, set its `data` field to `d`, and also set `e`'s `solid` and alwaysVisbile values to 1. Like with stairs, we don't want our doors to vanish from sight if the player's LOS is obscured. Finally, we set `e`'s `touch` to the `touch` function in doors.c and return `e`.

If we now look at initDoorNormal, we can see how we're using initDoor:


void initDoorNormal(Entity *e)
{
	STRCPY(e->name, "Door");
	STRCPY(e->description, "The light is green, meaning it's unlocked.");
	e->texture = getAtlasImage("gfx/entities/door.png", 1);

	initDoor(e, 0);
}

initDoorNormal is an init function that creates an unlocked door. We're setting `e`'s `name`, `description`, and `texture`, and then calling initDoor, passing over `e` and 0, to say that the door is unlocked.

If we look now at initDoorLocked, we can see we're doing largely the same:


void initDoorLocked(Entity *e)
{
	STRCPY(e->name, "Door (Locked)");
	STRCPY(e->description, "The red light is on. It'll require a key to open.");
	e->texture = getAtlasImage("gfx/entities/doorLocked.png", 1);

	initDoor(e, 1);
}

Other than the different `name`, `description`, and `texture`, we're passing 1 over to initDoor, to say that this door is locked.

The `touch` function is next and is where things get a bit more interesting:


static void touch(Entity *self, Entity *other)
{
	Door *d;
	Entity *key;

	if (other == dungeon.player)
	{
		d = (Door*) self->data;

		if (!d->locked)
		{
			self->dead = 1;
		}
		else
		{
			key = getInventoryItem("Key");

			if (key != NULL)
			{
				removeFromInventory(key);

				dungeon.deadTail->next = key;

				dungeon.deadTail = key;

				d->locked = 0;

				addHudMessage(HUD_MSG_NORMAL, "Unlocked door. Key lost.");
			}
			else
			{
				addHudMessage(HUD_MSG_NORMAL, "The door is locked. Key required.");

				clearInput();
			}
		}
	}
}

Like many other entities, we're testing if the thing (`other`) that has touched the door (`self`) is the player. If so, we're extracting the Door from `self`'s `data` field (assigning to `d`), and then checking the `locked` state. If `d`'s `locked` is 0, we're going to set `self`'s `dead` flag to 1, to remove it from the dungeon. If the door is locked, however, we're going to do some other things.

First, we're going to call a new function named getInventoryItem and pass over "Key", and assign the result to an entity variable called `key`. This function will basically search our inventory for an item called "Key". If `key` is not NULL (we found a Key in our inventory), we'll call removeFromInventory and pass `key` over. Next, we'll add `key` to our dungeon's dead list, to remove it fully from our dungeon (and also ensure the data is fully removed), set `d`'s `locked` state to 0, and add a HUD message to say the door has been unlocked. So, in short, we'll check if we have a key in our inventory and use it to open the door.

If, however, we don't have a key, we'll add a HUD message to say that the door is locked and a key is required. We'll also call a new function named clearInput. What this function does is clear all the current input and cancels our keyboard and mouse controls. We're doing this so that a player doesn't push against the door and fill their HUD with messages that the door is locked. While they can still tap the keyboard or mouse to achieve the same, this does help to mitigate the issue.

Moving on, we have a function called addDoors:


void addDoors(void)
{
	Entity *e;
	int x, y, dx, dy, mx, my, steps;

	for (x = 1 ; x < MAP_WIDTH - 2 ; x++)
	{
		for (y = 1 ; y < MAP_HEIGHT - 2 ; y++)
		{
			if (dungeon.map[x][y].tile > TILE_HOLE && dungeon.map[x][y].tile < TILE_WALL && countWalls(x, y) >= 7)
			{
				steps = 0;

				dx = 1;
				dy = 0;

				mx = x;
				my = y;

				do {
					if (dungeon.map[mx + dx][my + dy].tile > TILE_HOLE && dungeon.map[mx + dx][my + dy].tile < TILE_WALL)
					{
						mx += dx;
						my += dy;

						steps++;
					}
					else
					{
						dx = dx + dy;
						dy = dx - dy;

						dx = dx - dy;
						dx = -dx;
					}

				} while (countWalls(mx, my) >= 6);

				if (steps >= 3)
				{
					if (rand() % 3 == 0)
					{
						e = initEntity("Door (Locked)");
					}
					else
					{
						e = initEntity("Door");
					}

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

It's fairly long, as you can see. What this function does is search the map for alcoves (floor tiles that are surrounded by at least 7 walls) and the follows the route out into the open (when it finds a ground square surrounded by fewer than 6 walls). Once there, it places a door. In short, it places a door at the entrance to a passageway that ends in a deadend (which might be a location where we've placed an item - we'll come to that a bit later on).

To start with, we setup two for-loops, to traverse the map. Note that we're keeping 1 square away from the edges of the map. We're testing each tile that we come to, to see if it's a ground tile, and also whether it is surrounded by more than 7 walls (using countWalls). If so, we've found an alcove in the map. We'll set a variable called `steps` to 0 and variable called `dx` to 1 and a variable called `dy` to 0. We'll also assign two variables called `mx` and `my` to the current values of `x` and `y`, respectively.

We're then setting up a do-loop, to test the square at `mx` + `dx` and `my` + `dy`, to see if it's a ground tile. If so, we'll add `dx` and `dy` to `mx` and `my`, and then increment `steps`. If it's not a ground tile, we'll be rotating through 90 degrees clockwise, to try a different direction (you'll have to trust me on this one that this is what the code is doing, since this is essentially a 2D matrix transform ..!). We'll continue this do-loop while the call to countWalls at `mx` and `my` is greater than 6. A count of 6 means we're still in the corridor, and will continue to follow it until we exit. This allows us to follow straight corridors, as well as ones that twist and turn (through 90 degrees).

Upon exiting, we'll test the value of `steps`, to find out how long our corridor is. If it's greater than 3, we'll add a door. Using rand, there's a one in three chance that the door will be locked. Otherwise, we'll add in normal (unlocked door). We'll call the relevant init function and assign the result to `e`. We'll then set `e`'s `x` and `y` values to those of `mx` and `my` (which will be the end of the corridor).

We're now able to add in doors at the entrances to corridors, that can potentially seal off access to items that the player might find useful, unless they have a key with them. We'll see shortly how we add our items to these alcoves.

Next, let's look at the changes to inventory.c, where we've added in a new function - getInventoryItem:


Entity *getInventoryItem(char *name)
{
	Entity *e;

	for (e = game.inventoryHead.next ; e != NULL ; e = e->next)
	{
		if (strcmp(e->name, name) == 0)
		{
			return e;
		}
	}

	return NULL;
}

This function is quite straightforward - we just loop through our inventory, searching for an entity with a `name` that matches the one passed into the function. Once we find one, we return it. Otherwise, the function will return NULL.

Moving over to items.c, we've updated our addItems function:


void addItems(void)
{
	Entity *e;
	int x, y, i, n;

	n = rand() % 3;

	for (i = 0 ; i < n ; i++)
	{
		if (rand() % 2 == 0)
		{
			addEntityToDungeon(initEntity("Key"), 0);
		}
		else
		{
			addEntityToDungeon(initEntity("Health Pack"), 0);
		}
	}

	for (x = 1 ; x < MAP_WIDTH - 2 ; x++)
	{
		for (y = 1 ; y < MAP_HEIGHT - 2 ; y++)
		{
			if (dungeon.map[x][y].tile < TILE_WALL)
			{
				if (countWalls(x, y) >= 7 && rand() % 3 != 0)
				{
					e = createRandomItem();
					e->x = x;
					e->y = y;
				}
			}
		}
	}
}

To begin with, we're now adding between 0 and 2 keys or health packs to the dungeon, anywhere about the floor. Next, we're looping through all our map tiles, in the same way as we did when adding in the doors, searching for alcoves (a floor tile with 7 or more walls surrounding it). Upon finding one, there's a 2 in 3 chance that we'll call a function named createRandomItem at the current position (`x` and `y`). We'll assign the result of call (an Entity) to `e` and then set `e`'s `x` and `y` to the values of `x` and `y` in the loop. Basically, we're looking for alcoves and then adding random items to them.

createRandomItem is a simple function:


static Entity *createRandomItem(void)
{
	switch (rand() % 6)
	{
		case 0:
			return initEntity("Health Pack");

		case 1:
			return initEntity("Crowbar");

		case 2:
			return initEntity("Stun Baton");

		case 3:
			return initEntity("Biker Jacket");

		case 4:
			return initEntity("Bulletproof Vest");

		case 5:
			return initEntity("Microchip");

		default:
			break;
	}

	return NULL;
}

We're just performing a switch against a random of 6. Based on the result, we're calling initEntity for a named item and returning it. Little more needs saying about the function. Note that we'll never hit NULL due to all our cases being handled; we're just doing so to silence the compiler warnings.

We're almost done with our changes. We just have some simple updates to make. Turning to entities.c, we've updated isBlocked:


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

	prev = &dungeon.entityHead;

	for (other = dungeon.entityHead.next ; other != NULL ; other = other->next)
	{
		hasNext = other->next != NULL;

		if (other->x == x && other->y == y)
		{
			switch (other->type)
			{
				case ET_PLAYER:
				case ET_MONSTER:
					doMeleeAttack(e, other);
					return 1;

				case ET_ITEM:
				case ET_WEAPON:
				case ET_ARMOUR:
				case ET_MICROCHIP:
				case ET_STAIRS:
				other->touch(other, e);
					break;

				case ET_DOOR:
					other->touch(other, e);
					return 1;

				default:
					break;
			}

			if (hasNext && other->next == NULL)
			{
				other = prev;
			}
		}

		prev = other;
	}

	return 0;
}

We've added ET_DOOR to our switch statement. Like with some of the other entities, we're calling the door's `touch` function. Notice, however, that we're returning 1 to say that the door is blocking our movement. For a locked door, we'll not proceed any further. For an unlocked door, this will result in the door disappearing before we can then proceed.

Next, we turn to dungeon.c, to update createDungeon:


static void createDungeon(void)
{
	int oldFloor;
	char text[MAX_DESCRIPTION_LENGTH];

	oldFloor = dungeon.floor;

	dungeon.floor = dungeon.newFloor;

	initEntities();

	if (dungeon.player == NULL)
	{
		initEntity("Player");
	}
	else
	{
		dungeon.player->next = NULL;

		dungeon.entityTail->next = dungeon.player;
		dungeon.entityTail = dungeon.player;
	}

	generateMap();

	if (dungeon.floor > 0 && dungeon.floor < MAX_FLOORS)
	{
		addMonsters();

		addItems();
	}

	addStairs(oldFloor);

	addDoors();

	updateFogOfWar();

	dungeon.currentEntity = dungeon.player;

	sprintf(text, "Entering floor #%d", dungeon.floor);

	addHudMessage(HUD_MSG_NORMAL, text);
}

We've added in a call to addDoors. Something we've also done is taken away the calls to addWeapons, addArmour, and addMicrochips. This is because these functions have now been removed from the source code, due to addItems now handling everything.

And, of course, the last thing we need to do is update entityFactory.c, to add in our door init functions. Two new lines in initEntityFactory is all that's required:


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

	addInitFunc("Player", initPlayer);
	addInitFunc("Micro Mouse", initMicroMouse);
	addInitFunc("Neo Mouse", initNeoMouse);
	addInitFunc("Tough Mouse", initToughMouse);
	addInitFunc("Rabid Mouse", initRabidMouse);
	addInitFunc("Meta Mouse", initMetaMouse);
	addInitFunc("Key", initKey);
	addInitFunc("Health Pack", initHealthPack);
	addInitFunc("Crowbar", initCrowbar);
	addInitFunc("Stun Baton", initStunBaton);
	addInitFunc("Biker Jacket", initBikerJacket);
	addInitFunc("Bulletproof Vest", initBulletproofVest);
	addInitFunc("Microchip", initMicrochip);
	addInitFunc("Stairs (Up)", initStairsUp);
	addInitFunc("Stairs (Down)", initStairsDown);
	addInitFunc("Door", initDoorNormal);
	addInitFunc("Door (Locked)", initDoorLocked);
}

Our doors can now be created as needed!

Another part down, but we still have several to go. Building roguelikes can take a while, as one can see. Our next part is going to focus on the one thing that we have been ignoring up until now - handling the player's death. No longer will our brave tech be able to swan around with -22012 hit points. We'll also throw in a highscore table, to keep track of each attempt to slay the Mouse King!

Purchase

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

From itch.io

It is also available as part of the SDL2 tutorial bundle:

Mobile site