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
2D quest game
SDL 1 tutorials (outdated)

Latest Updates

SDL2 Quest game tutorial
Wed, 7th May 2025

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

All Updates »

Tags

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

Books


Project Starfighter

In his fight back against the ruthless Wade-Ellen Asset Protection Corporation, pilot Chris Bainfield finds himself teaming up with the most unlikely of allies - a sentient starfighter known as Athena.

Click here to learn more and read an extract!

« Back to tutorial listing

— Simple 2D quest game —
Part 15: Fetch quest

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

Introduction

We have our first real quest implemented - a delivery quest. This was a great start, but we should add in a few more. In this part, we're going to look into adding in a fetch quest (a staple of all RPGs).

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./quest15 to run the code. As before, you can move around with the WASD control scheme. Play the game as normal. Every now again, a Resident will request that you find an item they have misplaced. This might be in a town or on an island, out in the open. The Quest Log can help you to locate the town or island. With the item found, return to the requester to complete the quest. Once done, the item will be removed from your inventory. When you're finished, close the window to exit.

Inspecting the code

Adding in our fetch quest is very easy, as all the work is done in quests.c; we simply need to support a bunch of new commands, and do some quest setup.

So, first over to generateQuest:


void generateQuest(Entity *requester)
{
	Quest *q;

	// snipped

	addQuestStep(q, formattedString("INTERACT %d", q->requester->id));
	addQuestStep(q, "START_FETCH_QUEST");

	// snipped
}

Since we're only handling fetch quests in this part, we're going to make the first quest step START_FETCH_QUEST, replacing START_DELIVERY_QUEST.

Next, we've updated executeQuestSteps:


static void executeQuestSteps(Quest *q)
{
	// snipped

	do
	{
		q->currentStep = q->currentStep->next;

		if (q->currentStep != NULL)
		{
			sscanf(q->currentStep->line, "%s", command);

			// snipped

			else if (strcmp(command, "START_FETCH_QUEST") == 0)
			{
				doStartFetchQuest(q);
			}
			else if (strcmp(command, "UPDATE_DESCRIPTION") == 0)
			{
				doUpdateDescription(q);
			}
			else if (strcmp(command, "SET_TOWN") == 0)
			{
				doSetTown(q);
			}

			// snipped

			else
			{
				SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_CRITICAL, "Unrecognised command '%s'", command);
				exit(1);
			}
		}
		else
		{
			done = 1;
		}
	} while (!done);
}

As our fetch quest will have some new command steps, we've added support for these. We'll be handling START_FETCH_QUEST, UPDATE_DESCRIPTION, and SET_TOWN. We'll look at how START_FETCH_QUEST is handled first, in doStartFetchQuest:


static void doStartFetchQuest(Quest *q)
{
	Entity *item;
	char   *lastLocationName;

	if (rand() % 2 == 0)
	{
		item = doStartFetchQuestTown(q);
	}
	else
	{
		item = doStartFetchQuestOverworld(q);
	}

	addQuestStep(q, formattedString("INTERACT %d", item->id));
	addQuestStep(q, formattedString("UPDATE_DESCRIPTION Return to %s", q->requester->name));
	addQuestStep(q, formattedString("SET_TOWN %d", ((Resident *)q->requester->data)->town->id));
	addQuestStep(q, formattedString("PULSE_ENTITY %d 1", q->requester->id));
	addQuestStep(q, formattedString("INTERACT %d", q->requester->id));
	addQuestStep(q, formattedString("PULSE_ENTITY %d 0", q->requester->id));
	addQuestStep(q, formattedString("MSG_BOX %s;You found it! Thanks so much, I was worried it was gone for good.", q->requester->name));
	addQuestStep(q, formattedString("MSG_BOX %s;If I have any other tasks for you, I'll let you know.", q->requester->name));
	addQuestStep(q, formattedString("REMOVE_INVENTORY_ITEM %d", item->id));
	addQuestStep(q, "COMPLETE_QUEST");

	doActivateQuest(q);

	if (q->town != NULL)
	{
		lastLocationName = q->town->name;
	}
	else
	{
		lastLocationName = q->island->name;
	}

	addMessageBox(q->requester->name, "Oh, hello there! If you've got a moment, I have a favour to ask.");
	addMessageBox(q->requester->name, expiringFormattedString("I've lost my %s, and can't find it no matter how hard I look.", item->name));
	addMessageBox(q->requester->name, expiringFormattedString("I last had it when I was visiting %s.", lastLocationName));
	addMessageBox(q->requester->name, "If you're over that way and happen to find it, I'd be grateful if you could bring it back.");
}

As you will have seen, the lost item can be found either in a town or out in the open, on an island. There is a 50/50 chance we'll call either doStartFetchQuestTown or doStartFetchQuestOverworld, depending on where the item is to be located. Next, we prepare all the quest items, activate the quest, and then setup the message boxes. Of note in the quest steps are the two new commands: UPDATE_DESCRIPTION and SET_TOWN. UPDATE_DESCRIPTION will, as the name implies, update the quest's `description` with the given text. SET_TOWN will set the quest's `town`, so that we can see where we need to go in the Quest Log. This allows us to first point to a town where the item was lost, and then point back to the original town where the requester lives. This change is triggered once the item is found. Very, very handy! Note that the requester will mention where they last saw the item, by naming either the town or the island (lastLocationName).

An example of a fully complete fetch script from this part might look like this:

INTERACT 56
START_FETCH_QUEST
INTERACT 173
UPDATE_DESCRIPTION Return to Summertown
SET_TOWN 7
PULSE_ENTITY 56 1
INTERACT 56
PULSE_ENTITY 56 0
MSG_BOX John Kreese;You found it! Thanks so much, I was worried it was gone for good.
MSG_BOX John Kreese;If I have any other tasks for you, I'll let you know.
REMOVE_INVENTORY_ITEM 173
COMPLETE_QUEST

Translated, line by line, this script would mean:

- Wait for the player to interact with entity #56.
- Setup the fetch quest.
- Wait for the player to interact with entity #173.
- Update the quest description, with the text "Return to Summertown".
- Set the Quest's town to the one with id #7
- Make entity #56 pulse
- Wait for the player to interact with entity #56
- Make entity #56 stop pulsing
- Add a message box, for John Kreese, saying "You found it! Thanks so much, I was worried it was gone for good."
- Add a message box, for John Kreese, saying "If I have any other tasks for you, I'll let you know."
- Remove item #173 from the player's inventory
- Complete the quest

Quite a lot going on here, but as you can see it flows as one would expect for such a quest.

Next, we'll look at doStartFetchQuestTown:


static Entity *doStartFetchQuestTown(Quest *q)
{
	Entity *item;
	Entity *town;
	Map    *oldMap;
	int     x, y, ok;

	do
	{
		town = towns[rand() % numTowns];
	} while (town == game.town);

	oldMap = game.map;

	game.map = &((Town *)town->data)->map;

	item = initEntity("Item");

	do
	{
		x = rand() % game.map->width;
		y = rand() % game.map->height;

		ok = game.map->data[x][y] == 0 && getEntityAt(x, y) == NULL;
	} while (!ok);

	item->x = x;
	item->y = y;
	((Item *)item->data)->quest = q;

	game.map = oldMap;

	sprintf(q->title, "Find lost %s", item->name);
	sprintf(q->description, "Search town of %s", town->name);
	q->town = town;

	return item;
}

This is the function we'll call when we want to create a fetch quest for an item that is found in a town. To begin with, we randomly select a town to place the item in (from our `towns` array). Note that this won't be the same town as we currently occupy (Game's `town`); we want it to be a completely different town! Next, we create an Item. Note how we switch Game's `map` to the target town's `map` before we create the item. This, again, is because our game's entity system works within a context, so we need to ensure we are in the correct place before spawning a new entity. With that done, we randomly place the item within the town, ensuring it does not get embedded in a wall or overlaps another entity, and then assign the Item's `quest` to the current quest (`q`). Finally, we restore Game's `map` to the current town, and then set the quest's `title` and `description`, and the `town`.

If we now compare this to setting up a fetch quest in the overworld, in doStartFetchQuestOverworld, you'll notice some similarities, but also a bunch of differences:


static Entity *doStartFetchQuestOverworld(Quest *q)
{
	Entity *item;
	Map    *oldMap;
	Island *is;
	int     x, y, ok, n;

	oldMap = game.map;

	game.map = &game.overworld.map;

	item = initEntity("Item");

	do
	{
		x = rand() % game.map->width;
		y = rand() % game.map->height;

		n = game.map->data[x][y] / TILE_TYPE_RANGE;

		ok = n > MT_SHALLOWS && n < MT_FOREST && getEntityAt(x, y) == NULL;
	} while (!ok);

	item->x = x;
	item->y = y;
	((Item *)item->data)->quest = q;

	is = getIslandAt(x, y);

	game.map = oldMap;

	sprintf(q->title, "Find lost %s", item->name);
	sprintf(q->description, "Search island of %s", is->name);
	q->island = is;

	return item;
}

While both functions create an item, placing the item in the overworld requires us to ensure that it can be reached by the player; we want the item to be found on an island, not in the sea, and also need it to not spawn in forest or mountains, and not overlap another entity. With that done, we set the quest's `island` to the island at the item's location (`x` and `y`). This is why our game will abort if getIslandAt doesn't find an island at the given location. This shouldn't happen, as we always want to be able to point the player to the island they need to search.

Both doStartFetchQuestTown and doStartFetchQuestOverworld return the item that was created, so it can be used by further by doStartFetchQuest.

That should all be quite clear and expected, so we'll look finally at the three other command handling functions we've added. First, we have doRemoveInventoryItem:


static void doRemoveInventoryItem(char *line)
{
	unsigned int id;

	sscanf(line, "%*s %d", &id);

	removeInventoryItem(id);
}

As expected, this function removes the item with the given id from the player's inventory. In this quest, when we return the item to the requester it is removed from our inventory.

Next, we have doUpdateDescription:


static void doUpdateDescription(Quest *q)
{
	sscanf(q->currentStep->line, "%*s %[^\n]", q->description);

	addHudMessage(expiringFormattedString("Quest updated - %s", q->description), 255, 220, 0);
}

This function updates the `description` of the quest. As well as this, it will display a HUD message with the new description text.

Finally, we have doSetTown:


static void doSetTown(Quest *q)
{
	int i, townId;

	sscanf(q->currentStep->line, "%*s %d", &townId);

	for (i = 0; i < numTowns; i++)
	{
		if (towns[i]->id == townId)
		{
			q->town = towns[i];
		}
	}

	q->island = NULL;
}

Here, we're setting the quest's `town` to that with the id in the command. To find the town, we loop through all the towns in our towns array, searching for the one with the matching `id`. If we didn't have access to the towns array, we could've also fallen back on getEntityById. Note that we also clear the quest's `island` pointer, so that we don't end up highlighting both the town and the island in our quest log, which would look a bit messy.

And that's it! We've added in our fetch quest. Far easier than you might have been expecting. Again, our quest step system grants us a great amount of flexability, and we need only order our steps as we need, and implement functions to support them.

We'll be adding in one more quest, to keep things interesting - a Dispose quest. This will involve the player needing to get rid of an item that they are presented with. Kind of like a reverse fetch quest! As you shall see, this won't be any more difficult than what we did in this part.

Purchase

The source code for all parts of this tutorial (including assets) is available for purchase, as part of the SDL2 tutorials bundle:

From itch.io

Mobile site