« Back to tutorial listing

— Simple 2D adventure game —
Part 10: The Grumpy Goblin

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

Introduction

It's now time to start our game proper; we've done a lot of ground work, so that we have all the components and logic we'll need (aside from some tweaks and updates to come). The plot behind our game is that the prisoner will need to find 4 magical "icons" and return them to the Dungeon Mistress. Each icon will be attached to a puzzle or challenge that the player must overcome. To start with, we'll have the player need to get past a goblin to get the icon.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./adventure10 to run the code. The usual controls apply. The goal is to get to the goblin's lair and get the magic icon. To do this, you will need to find a key, then use it to open a chest, and a then finally make your way to the goblin and get the icon. It's a completely new map, and I'll leave it up to you to explore the dungeon and find the things you need (it is meant to be an adventure game, after all!). Close the window to exit.

Inspecting the code

This tutorial hasn't had a great deal added to it, since we already created the functions we needed earlier. We've added a three major new entities, the goblin, a torch, and a door. Let's start with the goblin, by looking at structs.h.


typedef struct {
	int state;
} Goblin;

We've declared a Goblin struct, that will hold just its state. This will be used to determine what the goblin will say when we're talking to him (by walking into him). The goblin's logic lives in goblin.c. It's a short file, consisting of just 2 functions. We'll start with initGoblin:


void initGoblin(Entity *e)
{
	Goblin *goblin;
	goblin = malloc(sizeof(Goblin));
	memset(goblin, 0, sizeof(Goblin));

	e->texture = getAtlasImage("gfx/entities/goblin.png", 1);
	e->solid = SOLID_SOLID;
	e->data = goblin;

	e->touch = touch;

	mbColor.r = 0;
	mbColor.g = 64;
	mbColor.b = 0;
}

We malloc and zero a Goblin struct, and assign it to the Entity's data variable. We're also grabbing the texture to use, declaring the goblin as being solid, and assigning the touch function. Nothing out of the ordinary. mbColor is an SDL_Color object, used to hold the color of the Goblin's message box. We're just doing this so that we don't have to change the colors in multiple places if we don't like it (this will be extended to the player themselves at some point).

The touch function is where the bulk of the logic for our goblin comes into play:


static void touch(Entity *self, Entity *other)
{
	Goblin *g;

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

		g = (Goblin*) self->data;

		switch (g->state)
		{
			case STATE_INIT:
				addMessageBox("Prisoner", "Excuse me, do you mind if I just squeeze past?", 64, 64, 64);
				addMessageBox("Goblin", "Go away! I'm meant to be guarding this magical 'House' icon from the contestant. But I'm in a bad mood because I left my lunch in the fridge today, and I'm really hungry.", mbColor.r, mbColor.g, mbColor.b);
				addMessageBox("Goblin", "Now I'm going to have to wait until I get home, but I'm on an extended shift here so that's hours away. Stupid job, stupid contract.", mbColor.r, mbColor.g, mbColor.b);
				addMessageBox("Goblin", "So, unless you've got something to eat, you can just push off.", mbColor.r, mbColor.g, mbColor.b);
				g->state = STATE_WANT_FOOD;
				break;

			case STATE_WANT_FOOD:
				if (hasInventoryItem("Cheese"))
				{
					addMessageBox("Prisoner", "Will this do?", 64, 64, 64);
					addMessageBox("Goblin", "Cheese! Wow, and it's a big lump, too!", mbColor.r, mbColor.g, mbColor.b);
					addMessageBox("Goblin", "Thanks, stranger, I'm going to go and enjoy this. Back soon.", mbColor.r, mbColor.g, mbColor.b);

					removeInventoryItem("Cheese");

					self->alive = ALIVE_DEAD;
				}
				else
				{
					addMessageBox("Goblin", "Unless you've got some food, I don't want to talk to you.", mbColor.r, mbColor.g, mbColor.b);
				}
				break;

			default:
				break;
		}
	}
}

The first thing we want to do is find out what's touched the goblin. If it's the player, we want to take action. The first thing we do is make sure the goblin is facing the player, by testing the player's x position vs the goblin's, and changing his facing appropriately. Next, we grab the Goblin's data and test his state to see what he's going to do. Our states are defined in goblin.c, where STATE_INIT is 0 and STATE_WANT_FOOD is 1. Our goblin will initially be in STATE_INIT. When the player touches the goblin, he'll react with a load of message boxes, before setting his state to STATE_WANT_FOOD. This will mean that the next time we touch the goblin, he'll respond differently.

When in STATE_WANT_FOOD mode, we'll test the player's inventory to see if they are carrying Cheese. If so, we'll show some message boxes expressing the goblin's joy, then remove the cheese from the player's inventory, as well as removing the goblin from the dungeon itself. This will effectively allow the player to walk by and get the House icon. A note on the logic: you'll notice while playing that the goblin vanishes immediately, rather than waiting for the message boxes to all disappear. This is because the message boxes go into a queue, and the rest of the code execute as normal. If we wanted to tie these events together, so that the goblin only vanished after his speech, we'd need to develop a callback system to invoke a function after a message has been displayed. It's not a pressing thing to do in our case, so we're good to leave things as they are.

Finally, if the player doesn't have any cheese, the goblin responds in the negative.

You've probably noticed that we're just calling removeInventoryItem once, without mucking about with the returned object afterwards. This is a change to the previous behaviour, as I figured it wasn't needed. Rather than change the previous tutorial, I thought I'd leave it as is, so one can see that such updates are all part of the development process. The updated removeInventoryItem is below:


void removeInventoryItem(char *name)
{
	Prisoner *p;
	Entity *e;
	int i;

	p = (Prisoner*) player->data;

	for (i = 0 ; i < NUM_INVENTORY_SLOTS ; i++)
	{
		if (p->inventorySlots[i] != NULL && strcmp(p->inventorySlots[i]->name, name) == 0)
		{
			e = p->inventorySlots[i];

			p->inventorySlots[i] = NULL;

			e->alive = ALIVE_DEAD;

			addEntityToDungeon(e);

			return;
		}
	}
}

As you can see, after removing the inventory item from the slot, we're destroying it as part of the function itself. We're also calling return as soon as we've removed the item, to ensure we don't remove all the items with the same name (e.g., all Rusty keys when opening a single chest).

That's our goblin done! Not a lot to it, eh? Now, let's look over the other entities we've added to the dungeon, namely the door and the torch. We'll start with the door, defined in structs.h:


typedef struct {
	int locked;
} Door;

A simple struct. It just contains a single variable called locked, which will be used to determine whether the door is passable. The door's functions live in door.c, which contains just three functions. We'll start with initDoor:


void initDoor(Entity *e)
{
	Door *door;

	door = malloc(sizeof(Door));
	memset(door, 0, sizeof(Door));

	e->texture = getAtlasImage("gfx/entities/door.png", 1);
	e->solid = SOLID_SOLID;
	e->data = door;

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

The initDoor function sets up our door, creating a Door data object, and grabbing the texture. The door is solid, which means at the very least it will block our line of sight; rooms blocked by doors won't reveal their fog of war until we open them. Our touch function comes next:


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

	if (other == player)
	{
		door = (Door*) self->data;

		if (door->locked)
		{
			setInfoMessage("The door's locked.");
		}
		else
		{
			self->alive = ALIVE_DEAD;
		}
	}
}

When the player touches the door, we'll test the door to see if it's locked. If so, we'll show a message saying so. Otherwise, we'll "open" the door by setting its alive flag to ALIVE_DEAD, to remove it from the dungeon. We might not want to do this in future, but for now it'll do. The last function is load:


static void load(Entity *e, cJSON *root)
{
	Door *door;

	door = (Door*) e->data;

	door->locked = cJSON_GetObjectItem(root, "locked")->valueint;
}

We're merely grabbing the "locked" value from the JSON object passed in, to determine whether the door is locked.

Our torch is a simple object, as we'll now see by looking at torch.c:


void initTorch(Entity *e)
{
	e->texture = getAtlasImage("gfx/entities/torch.png", 1);
	e->solid = SOLID_SOLID;

	updateFogOfWar(e, VIS_DISTANCE);
}

Just one function - initTorch. This just grabs the texture for the torch and declares it as solid. However, we're doing one other thing. Our torch is a light source, and therefore removes some of the fog of war. We do this by calling updateFogOfWar, passing through the torch itself and the visibility distance. This is a tweak to our updateFogOfWar function, that before always assumed it was the player we were using, with a fixed visibility distance. The changes are simple, so we won't cover them. You need only know that instead of hardcoding the player and the distance to view, we're allowing the source entity and distance to be specified.

With our goblin, door, and torch defined, we just need to add them initEntityFactory function, in entityFactory.c, so that they can be created when the dungeon is loaded.


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

	addInitFunc("player", initPlayer);
	addInitFunc("item", initItem);
	addInitFunc("chest", initChest);
	addInitFunc("gold", initGold);
	addInitFunc("signpost", initSignpost);
	addInitFunc("torch", initTorch);
	addInitFunc("goblin", initGoblin);
	addInitFunc("door", initDoor);
}

That's the first part of our dungeon done, and we can now solve the puzzle of how to get the House icon from the goblin. There's nothing we can do with it yet, but that will change in the next tutorial, when we introduce the Dungeon Mistress and the Merchant.

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:

Desktop site