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

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 Red Road

For Joe Crosthwaite, surviving school was about to become more than just a case of passing his exams ...

Click here to learn more and read an extract!

« Back to tutorial listing

— Simple 2D adventure game —
Part 13: The Escape Room

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

Introduction

Our final room is an Escape Room. Upon entering, the player will be locked in, with no clear means of escape. The icon will also be nowhere to be seen. However, the way out is simple: fake walls. By circling the room and pushing against the walls, the player will reveal a way out (and also the hidden icon). This might sound hard to do, but with our entity and touch system, it was in fact very easy.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./adventure13 to run the code. The usual controls apply. The escape room is located in the south-east corner of the store room. A signpost offering Free Cake will mark the entrance. Following the route into the escape room, then hug the walls to find the icon and the way out. Close the window to exit.

Inspecting the code

You will notice how a wall comes up block our exit once we enter the escape room. This was achieved very easily. The wall is raised when the player walks on an invisible entity, known as a Trigger. This is defined in structs.h:


typedef struct {
	char target[MAX_NAME_LENGTH];
} Trigger;

A simple structure, it contains just one variable called target. This will be used to hold the name of the entity we want to connect to. To facilitate this, we've added a new function pointer to our Entity struct, called activate:


struct Entity {
	unsigned long id;
	int x;
	int y;
	char name[MAX_NAME_LENGTH];
	int facing;
	int alive;
	int solid;
	AtlasImage *texture;
	void (*data);
	void (*touch)(Entity *self, Entity *other);
	void (*load)(Entity *e, cJSON *root);
	void (*activate)(Entity *self);
	Entity *next;
};

We'll see more on how this works in a little bit. For now, let's return to our Trigger. Our Trigger functions are defined in trigger.c. It contains three functions. We'll start with initTrigger:


void initTrigger(Entity *e)
{
	Trigger *t;

	t = malloc(sizeof(Trigger));
	memset(t, 0, sizeof(Trigger));

	e->data = t;

	e->touch = touch;
	e->load = load;
}

At this point, you can see that this is a regular init function for an Entity. Note, however, that we're not assigning a texture. We want it to be invisible, so not assigning one will cause it not to render (we've updated entities.c to accomodate this). Our load function isn't anything out of the ordinary, either:


static void load(Entity *e, cJSON *root)
{
	Trigger *t;

	t = (Trigger*) e->data;

	STRCPY(t->target, cJSON_GetObjectItem(root, "target")->valuestring);
}

We're just loading in the target data for the Trigger, in much the same way as we load in other entity extended data. Our touch function is more interesting:


static void touch(Entity *self, Entity *other)
{
	Trigger *t;

	if (other == player)
	{
		t = (Trigger*) self->data;

		activateEntities(t->target);

		self->alive = ALIVE_DEAD;
	}
}

After confirming the thing that has touched the Trigger is the player, we're calling a new function called activateEntities, and passing over the Trigger's target to it. After this, we're marking the trigger as dead, to remove it from the dungeon and stop it from being activated again.

Our activateEntities function is defined in entities.c, and takes one parameter - the name of the entity we wish to interact with:


void activateEntities(char *name)
{
	Entity *e;

	for (e = dungeon.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->activate != NULL && strcmp(e->name, name) == 0)
		{
			e->activate(e);
		}
	}
}

In this function, we'll loop through all the entities in our dungeon and look for any that have a name matching that one we passed into the function. We also want to make sure that they have an activate function set. If so, we call it. To see what this looks like in action, let's consider our Wall entity.

Our Wall entity is what it suggests - an entity to represent a wall. It's just a solid (or not) entity with a wall tutorial. It is defined in wall.c, a file that contains 4 functions. We'll go from the top, starting with initWall:


void initWall(Entity *e)
{
	e->activate = activate;
	e->load = load;
}

Nothing special here, as we're just setting the function pointers. However, our activate function is what we're most interested in:


static void activate(Entity *e)
{
	e->solid = !e->solid;

	updateSolid(e);
}

The activate function here will toggle the entity's solid state, making it either solid or non-solid. How this all ties into the activateEntities function is simple. Consider a trigger with a target of "CakeWall". We then name our Wall entity "CakeWall". Upon the player touching the Trigger entity, activateEntities will be called, passing over "CakeWall" as its argument. Our Wall named "CakeWall" will be found in the dungeon's entity list, and the Wall's activate function (above) will be called. In effect, we've made it so that a stepping on our invisible Trigger will make a once non-solid wall become solid and visible (and visa-versa).

With our solid state changed, we call a function called updateSolid, to change the entity's texture:


static void updateSolid(Entity *e)
{
	if (e->solid)
	{
		e->texture = getAtlasImage("gfx/tiles/40.png", 1);
	}
	else
	{
		e->texture = NULL;
	}
}

If it's solid, we want to set its texture as a wall. Otherwise, we set it to NULL, to make it invisible. We've separated this out as its own function, so that the logic can be shared by the load function:


static void load(Entity *e, cJSON *root)
{
	e->solid = cJSON_GetObjectItem(root, "solid")->valueint;

	updateSolid(e);
}

Walls can either be solid or non-solid to begin with, represented in the JSON as a 1 or 0.

We have also created a fake wall. This is simply an entity that looks like a wall, but disappears once the player touches it. It's defined in fakeWall.c. There are just two functions, initFakeWall and touch. Starting with initFakeWall:


void initFakeWall(Entity *e)
{
	e->texture = getAtlasImage("gfx/tiles/40.png", 1);
	e->solid = SOLID_SOLID;

	e->touch = touch;
}

Our fake wall is just a basic entity, without any extra entity data. Notice how the fake wall is solid, even though it will die upon the player touching it. This is because we want the wall to block the player's line of sight, and not give away its existence. The touch function is next, and is likely just as you might expect:


static void touch(Entity *self, Entity *other)
{
	if (other == player)
	{
		setInfoMessage("A secret is revealed!");

		self->alive = ALIVE_DEAD;
	}
}

Once touched, the fake wall's alive is set to ALIVE_DEAD to remove it from the dungeon, and an info message is set (with a reference to DOOM..!). With the wall gone, the player will be able to proceed into the area once disguised from them.

Once again, we need to add the new entities to our initEntityFactory function in order to use them:


void initEntityFactory(void)
{
	memset(&initFuncHead, 0, sizeof(InitFunc));
	initFuncTail = &initFuncHead;

	addInitFunc("player", initPlayer);
	addInitFunc("item", initItem);
	addInitFunc("chest", initChest);
	addInitFunc("gold", initGold);
	addInitFunc("silver", initSilver);
	addInitFunc("signpost", initSignpost);
	addInitFunc("torch", initTorch);
	addInitFunc("goblin", initGoblin);
	addInitFunc("door", initDoor);
	addInitFunc("dungeonMistress", initDungeonMistress);
	addInitFunc("merchant", initMerchant);
	addInitFunc("blacksmith", initBlacksmith);
	addInitFunc("bat", initBat);
	addInitFunc("wall", initWall);
	addInitFunc("fakeWall", initFakeWall);
	addInitFunc("trigger", initTrigger);
}

And that's it for the escape room! We only needed to create some entities to disguise things from the player and make them blend in with their surroundings. Before we wrap up, let's look at some other tweaks, starting with drawEntities in entities.c:


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

	for (e = dungeon.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->alive != ALIVE_DEAD && e->texture != NULL)
		{
			x = e->x - dungeon.camera.x;
			y = e->y - dungeon.camera.y;

			if (x >= 0 && y >= 0 && x < MAP_RENDER_WIDTH && y < MAP_RENDER_HEIGHT)
			{
				x = (x * TILE_SIZE) + (TILE_SIZE / 2);
				y = (y * TILE_SIZE) + (TILE_SIZE / 2);

				x += dungeon.renderOffset.x;
				y += dungeon.renderOffset.y;

				blitAtlasImage(e->texture, x, y, 1, e->facing == FACING_LEFT ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL);
			}
		}
	}
}

As well as testing if the entity isn't dead before drawing it, we're also testing if a texture has been set. We don't want to pass over NULL data to our blitAtlasImage, as it could cause us grief.

We've also updated the Dungeon Mistress's touch logic, since we're now able to return her all 4 of the Icons she wants:


static void touch(Entity *self, Entity *other)
{
	DungeonMistress *d;
	Prisoner *p;

	if (other == player)
	{
		self->facing = (other->x > self->x) ? FACING_RIGHT : FACING_LEFT;

		d = (DungeonMistress*) self->data;
		p = (Prisoner*) other->data;

		if (hasInventoryItem("Icon"))
		{
			removeInventoryItem("Icon");

			d->iconsFound++;

			switch (d->iconsFound)
			{
				case 1:
					addMessageBox("Prisoner", "I got one of the icons!", p->mbColor.r, p->mbColor.g, p->mbColor.b);
					addMessageBox("Dungeon Mistress", "You found one? Beginner's luck, I guess. Well, don't expect the others to come so easily. I'll just take that from you ...", mbColor.r, mbColor.g, mbColor.b);
					break;

				case 2:
					addMessageBox("Prisoner", "Here you go ...", p->mbColor.r, p->mbColor.g, p->mbColor.b);
					addMessageBox("Dungeon Mistress", "Another one? No, you're cheating. This has got to be a fake. I'll have it checked ...", mbColor.r, mbColor.g, mbColor.b);
					break;

				case 3:
					addMessageBox("Prisoner", "another one.gif", p->mbColor.r, p->mbColor.g, p->mbColor.b);
					addMessageBox("Dungeon Mistress", "What the flip?! Stop looking up the answers on the internet!", mbColor.r, mbColor.g, mbColor.b);
					addMessageBox("Prisoner", "See you again in a bit.", p->mbColor.r, p->mbColor.g, p->mbColor.b);
					break;

				case 4:
					addMessageBox("Prisoner", "Look! I found the last one!", p->mbColor.r, p->mbColor.g, p->mbColor.b);
					addMessageBox("Dungeon Mistress", "No! That's not possible! It's ... no.", mbColor.r, mbColor.g, mbColor.b);
					addMessageBox("Prisoner", "So, can I go home now?", p->mbColor.r, p->mbColor.g, p->mbColor.b);
					addMessageBox("Dungeon Mistress", "Ugh.", mbColor.r, mbColor.g, mbColor.b);
					addMessageBox("Dungeon Mistress", "Fine, fine, get out of here. The exit's unlocked. Leave. I never want to see you again.", mbColor.r, mbColor.g, mbColor.b);

					activateEntities("ExitDoor");
					break;

				default:
					break;
			}
		}
		else
		{
			switch (d->iconsFound)
			{
				case 0:
					addMessageBox("Dungeon Mistress", "Not found any yet? Aw, poor baby. Going to be here at while, aren't you? Heh heh heh!", mbColor.r, mbColor.g, mbColor.b);
					break;

				case 1:
					addMessageBox("Dungeon Mistress", "Don't get excited, hon. You've only found one icon so far.", mbColor.r, mbColor.g, mbColor.b);
					break;

				case 2:
					addMessageBox("Dungeon Mistress", "Halfway there, but you'll never find the rest. You'll starve to death down here. Ha ha ha!", mbColor.r, mbColor.g, mbColor.b);
					break;

				case 3:
					addMessageBox("Dungeon Mistress", "I've had the WiFi password changed, so you can't keep cheating. You're not going to break my winning streak.", mbColor.r, mbColor.g, mbColor.b);
					break;

				case 4:
					addMessageBox("Dungeon Mistress", "Go away. I'm not talking to you any more.", mbColor.r, mbColor.g, mbColor.b);
					break;

				default:
					break;
			}
		}
	}
}

With all four Icons returns, the Dungeon Mistress is less than pleased that we've passed her test, and will grant the Prisoner his freedom. A locked door in the started area will be unlocked via a call to activateEntities, passing in "ExitDoor" as an argument. Our Door entity has also received an update for this (in door.c):


static void activate(Entity *e)
{
	Door *door;

	door = (Door*) e->data;

	door->locked = !door->locked;
}

When called, the Door's activate will toggle the state of the door between locked and unlocked.

Our game is very nearly finished! Hurrah! All we need to do now is add some finishing touches: a title screen, an ending screen, and some music and sound effects. We'll tackle these in the final part of the tutorial and then stick a fork in it. It's been quite a journey, eh?

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