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


The Battle for the Solar System (Complete)

The Pandoran war machine ravaged the galaxy, driving the human race to the brink of destruction. Seven men and women stood in its way. This is their story.

Click here to learn more and read an extract!

« Back to tutorial listing

— Simple 2D quest game —
Part 17: Finishing touches

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

Introduction

Our game is more or less finished. What we need to do now is just add in some sound and music, to bring it more to life. We also need to ensure that our quests are randomly selected. In this final part, we'll add these missing bits and bobs.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./quest17 to run the code. Play the game as normal. Note the sound effects that play in various places, such as when a HUD message displays, when a door is opened, etc. Notice, also, how the sound effects change as the player walks on different terrain - grass, desert, the town, etc. Even sailing the boat plays a unique sound. When you're finished, close the window to exit.

Inspecting the code

In this final part, we'll be putting the finishing touches to our game - sound, music, and random quests. We've seen adding sound and music a number of times already, so we'll only focus on the more interesting aspects in this part.

To begin with, let's like at quests.c, where we've updated generateQuest:


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

	// snipped

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

	switch (rand() % QT_MAX)
	{
		case QT_DELIVER:
			addQuestStep(q, "START_DELIVERY_QUEST");
			break;

		case QT_FETCH:
			addQuestStep(q, "START_FETCH_QUEST");
			break;

		case QT_DISPOSE:
			addQuestStep(q, "START_DISPOSE_QUEST");
			break;

		default:
			break;
	}

	// snipped
}

Now, instead of hardcoding the quest type, we're selecting one at random from QT_DELIVER, QT_FETCH, or QT_DISPOSE (defined in defs.h), to create a delivery, fetch, or dispose quest.

Next, we want when our game starts for our robot adventurer to display some error messages, to explain why it is here. The backstory is basically that our robot has re-activated in this world, with its memory circuits wiped. It needs to now restore that knowledge by visiting islands, towns, and interacting with people. So, over in generating.c, we've updated `logic`:


static void logic(void)
{
	loading += (0.4 * app.deltaTime);

	if (done && SDL_GetTicks() - startTime > 2500)
	{
		// snipped

		addHudMessage("System reboot complete.", 64, 225, 64);
		addHudMessage("ERROR: File system block corrupt.", 64, 225, 64);
		addHudMessage("ERROR: No memory logs available.", 64, 225, 64);
		addHudMessage("ERROR: No backup available. Manual restore required.", 64, 225, 64);
	}
}

As soon as our world is done generating, we'll add some messages to our HUD. Note that we've also updated our generating screen with a logo and a fancy fade, so we're allowing 2.5 second to elapse before switching to our game, to allow it time to display. If you want to jump straight into the game, you should remove this check (be aware that it could make the Generating screen pop into existence for only a brief moment, perhaps creating a sudden, unpleasant flicker).

Next, over to player.c, where we've added in some interesting sound logic. First, in moveOverworld:


static void moveOverworld(int dx, int dy)
{
	int     t, x, y, s;
	Entity *e;

	x = MIN(MAX(game.map->player->x + dx, 0), WORLD_WIDTH - 1);
	y = MIN(MAX(game.map->player->y + dy, 0), WORLD_HEIGHT - 1);

	// snipped

	if (soundTimer == 0 && game.map->player->x == x && game.map->player->y == y)
	{
		soundTimer = SOUND_DELAY;

		switch (game.map->data[x][y] / TILE_TYPE_RANGE)
		{
			case MT_WATER:
				s = SND_BOAT;
				soundTimer = SOUND_DELAY + 15;
				break;

			case MT_SHALLOWS:
				s = SND_WALK_WATER_1 + rand() % 2;
				break;

			case MT_DESERT:
			case MT_SAVANNAH:
				s = SND_WALK_DIRT_1 + rand() % 2;
				break;

			default:
				s = SND_WALK_GRASS_1 + rand() % 2;
				break;
		}

		playSound(s, CH_WALK);
	}
}

If we're able to play a walking sound while moving in the overworld (soundTimer is 0 and the player has actually moved into the new position), we'll test the type of terrain that the player now occupies. If it's water, we'll play the boat movement sound. If it's shallows, we'll randomly play a water walking sound. For desert and savannah, we'll play a random dirt movement sound, and finally for grass we'll play a grass sound. This add some variety to just hearing the same movement sound all the time; walking through shallows should make a splashing noise, while savannah and desert should sound more like treading on a hard surface. Note how for the water movement, we increase the value of soundTimer from the default, due to that sound effect requiring a bit more time to play before repeating.

We make a similar movement sound check when moving through town, in moveTown:


static void moveTown(int dx, int dy)
{
	int   n, x, y, s;
	Town *t;

	// snipped

	if (n < TT_WALL)
	{
		game.map->player->x = x;
		game.map->player->y = y;

		if (soundTimer == 0)
		{
			soundTimer = SOUND_DELAY;

			switch (game.map->data[x][y] / TILE_TYPE_RANGE)
			{
				case TT_FLOOR:
					s = SND_WALK_FLOOR_1 + rand() % 2;
					break;

				default:
					s = SND_WALK_GRAVEL_1 + rand() % 2;
					break;
			}

			playSound(s, CH_WALK);
		}

		visitBuildings(x, y);
	}
}

If the player has made a legitimate move (not into a wall), we're testing if they are walking on a floor (inside a building). If so, we'll play a random floor walking sound. Otherwise, we'll play a gravel walking sound.

In case you're interested, we'll quickly look at how some other sounds are used. In door.c, we're playing a sound when the player touches a door:


static void touch(Entity *self, Entity *other)
{
	if (other == game.map->player)
	{
		playSound(SND_DOOR, CH_ANY);

		self->dead = 1;
	}
}

In quests.c, we're playing one when the player completes a quest:


static void doCompleteQuest(Quest *q)
{
	q->status = QS_COMPLETE;

	game.completedQuests++;

	addHudMessage(expiringFormattedString("%s - Quest compeleted!", q->title), 255, 220, 0);

	reorderQuestList();

	playSound(SND_QUEST_COMPLETE, CH_QUEST);
}

Sound effects are also applied in the quest log, such as when it is opened:


void openQuestLog(void (*rtn)(void))
{
	pulse = 0;

	// snipped

	playSound(SND_QUEST_LOG, CH_HUD);

	app.delegate.logic = logic;
	app.delegate.draw = draw;
}

And when it changes sections or we close the log:


static void doControls(void)
{
	scrollTimer = MAX(scrollTimer - app.deltaTime, 0);

	if (app.keyboard[SDL_SCANCODE_A])
	{
		// snipped

		playSound(SND_QUEST_LOG_TAB, CH_HUD);

		app.keyboard[SDL_SCANCODE_A] = 0;
	}

	if (app.keyboard[SDL_SCANCODE_D])
	{
		// snipped

		playSound(SND_QUEST_LOG_TAB, CH_HUD);

		app.keyboard[SDL_SCANCODE_D] = 0;
	}

	// snipped

	if (app.keyboard[SDL_SCANCODE_TAB])
	{
		playSound(SND_QUEST_LOG, CH_HUD);

		closeQuestLog();

		app.transitionTimer = FPS / 4;

		app.keyboard[SDL_SCANCODE_TAB] = 0;
	}
}

Finally, the HUD will play a sound while text is being typed out:


void doHud(void)
{
	HudMessage *hudMessage;
	long        l;
	static int  numHudMessages;

	// snipped

	if (game.messageBoxHead.next == NULL)
	{
		// snipped

		while (hudMessage != NULL)
		{
			numHudMessages++;

			l = strlen(hudMessage->text) + 1;

			if (hudMessage->timer < l && soundTimer == 0)
			{
				soundTimer = SOUND_DELAY;

				playSound(SND_HUD_TYPING, CH_HUD);
			}

			// snipped
		}
	}

	// snipped
}

Note the use a of soundTimer here, as with the player movement, to add a delay between the typing sounds.

The same approach is taken when a message box types out a message. You will likely have noticed that the sound effect is random per Resident, but remains the same for that individual. This is setup in addMessageBox:


void addMessageBox(char *speaker, char *message)
{
	MessageBox *msg;
	double      r, g, b;
	int         h, s;

	// snipped

	h = hashcode(speaker);
	s = SND_MESSAGE_BOX_1 + (h % 4);

	hsvToRGB(h, 1, 0.25, &r, &g, &b);

	STRCPY(msg->speaker, speaker);
	STRCPY(msg->message, message);
	msg->color.r = r;
	msg->color.g = g;
	msg->color.b = b;
	msg->sound = s;
}

Both the background and sound effect are determined by the speaker name, with a hashcode being generated from that string. The random sound is set to the MessageBox's `sound` field, to be used during its typing effect.

All done. Our game is more or less finished now. We can explore islands, sail boats, complete quests, and discover various places. There is still a mountain of interesting things we could add to our game, such as real stores, more vehicles, dungeons. New features, such as cave entrances, leading to new maps, could be added to mountain paths. A day and night cycle could be implemented, so that when it is dark, stores are closed, and the Residents return to their homes. The Residents themselves could also move around, like the player, having their own tasks to undertake. Residents or other NPCs could be met in the open world, who might have quests for the player, things to sell, or information to provide. Non-human NPCs could also exist.

And, of course, there could many more different types of quests: Rescue someone, assassinate someone, defeat a monster, build something (crafting), investigate something, protect something or someone, survive in a place for a given length of time. Some of these quests might only be possible at a given time of day, while some others might require the player to attain a certain new skill first (such as negotiating dense forests, something that our robot is unable to do).

Done right, one could have a world that feels truly alive, with many different possibilities, and hundreds of randomly generated quests to complete.

After all, an adventurer's work is never done ...

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