« Back to tutorial listing

— An old-school isometric game —
Part 12: Finishing touches

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

Introduction

This will be a short part, as we'll simply be adding in some sound and music.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./isometric12 to run the code. You will see a window open like the one above. Play the game as normal, and enjoy the music and sound effects. Note: there is no win screen. Once you're finished, close the window to exit.

Inspecting the code

With our game finished, let's throw in some sound effects and music, to complete the atmosphere. Note: since this is more of a demo for how to create an isometric world and view, there is no win state; the game will continue on even when all items have been collected.

Adding in our sound and music is easy, so we'll just head right in. Starting with defs.h:


enum {
	CH_WALK,
	CH_ITEM,
	CH_SPILL,
	CH_BUTTON
};

We've created a new enum set here. CH is short of channel, and will be used to determine the channel through which a sound will play. In order, we've created separate ones for walking (CH_WALK), collecting items (CH_ITEM), cleaning up spills (CH_SPILL), and activating a button (CH_BUTTON).

We've also created an enum set for the sounds themselves:


enum {
	SND_WALK_1,
	SND_WALK_2,
	SND_WALK_3,
	SND_WALK_4,
	SND_ITEM,
	SND_SPILL,
	SND_BUTTON,
	SND_TRANSITION,
	SND_MAX
};

These enums are used when loading and play sounds, as we'll see.

If we turn to sound.c in system, we can see the loadSound function is loading our effects:


static void loadSounds(void)
{
	sounds[SND_WALK_1] = Mix_LoadWAV("sound/166509__yoyodaman234__concrete-footstep-1.ogg");
	sounds[SND_WALK_2] = Mix_LoadWAV("sound/166508__yoyodaman234__concrete-footstep-2.ogg");
	sounds[SND_WALK_3] = Mix_LoadWAV("sound/166507__yoyodaman234__concrete-footstep-3.ogg");
	sounds[SND_WALK_4] = Mix_LoadWAV("sound/166506__yoyodaman234__concrete-footstep-4.ogg");
	sounds[SND_ITEM] = Mix_LoadWAV("sound/467610__triqystudio__pickupitem.ogg");
	sounds[SND_SPILL] = Mix_LoadWAV("sound/445117__breviceps__cartoon-splat.ogg");
	sounds[SND_BUTTON] = Mix_LoadWAV("sound/476178__unadamlar__correct-choice.ogg");
	sounds[SND_TRANSITION] = Mix_LoadWAV("sound/626136__almitory__air-move.ogg");
}

`sounds` is an array of SDL_mixer Mix_Chunks, static in sound.c. We're loading in a sound for each of our SND enums.

With our sounds loaded and our enums set, playing sounds is simple. We already have a playSound function defined in sound.c, meaning all we need to do is call it from the approrpiate place, using the appropriate sound and channel values.

In button.c, we're calling playSound whenever Purple Guy touches a button, via the `touch` function, passing over SND_BUTTON as the sound effect, and CH_BUTTON as the channel:


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

	if (other == world.player)
	{
		playSound(SND_BUTTON, CH_BUTTON);

		self->texture = getAtlasImage("gfx/entities/buttonOn.png", 1);

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

		self->touch = NULL;
	}
}

In glass.c, we're calling playSound when Purple Guy picks up an item, via `touch`, passing over SND_ITEM and CH_ITEM as the sound and channel, respectively:


static void touch(Entity *self, Entity *other)
{
	if (other == world.player)
	{
		playSound(SND_ITEM, CH_ITEM);

		world.water += 5;

		self->dead = 1;
	}
}

The same occurs in knife.c, in its `touch`:


static void touch(Entity *self, Entity *other)
{
	if (other == world.player)
	{
		playSound(SND_ITEM, CH_ITEM);

		world.utensils++;

		self->dead = 1;
	}
}

And also in redBox.c, in `touch`:


static void touch(Entity *self, Entity *other)
{
	if (other == world.player)
	{
		playSound(SND_ITEM, CH_ITEM);

		world.redBoxes++;

		self->dead = 1;
	}
}

For the `touch` in spill.c, we're calling playSound with SND_SPILL and CH_SPILL, whenever Purple Guy touches a spill, and has water to do so:


static void touch(Entity *self, Entity *other)
{
	if (other == world.player && world.water > 0)
	{
		playSound(SND_SPILL, CH_SPILL);

		world.spills++;

		world.water--;

		self->dead = 1;
	}
}

Done. Nothing taxing, at all. We're just playing sounds to accompany the various interactions.

Now for player.c, where we've updated doPlayer:


void doPlayer(void)
{
	Node *n;
	Entity *other;
	int x, z, dx, dz, facing;

	// snipped

	else if (walkTimer == 0)
	{

		// snipped

		walkTimer = WALK_SPEED;

		x = world.player->x - world.camera.x;
		z = world.player->z - world.camera.z;

		if (x == 0 || z == 0 || x == MAP_RENDER_SIZE - 1 || z == MAP_RENDER_SIZE - 1)
		{
			world.camera.x += dx * MAP_RENDER_SIZE;
			world.camera.z += dz * MAP_RENDER_SIZE;

			world.player->x += (dx * 2);
			world.player->z += (dz * 2);

			initISORender();
		}

		playSound(rand() % SND_WALK_4, CH_WALK);
	}

	// snipped
}

We've snipped a lot here, as the function is rather large, but have kept some code to act as context. Whenever the player moves, we're randomly playing a walk sound effect via the CH_WALK. Notice here that we're using rand() with SND_WALK_4, to pick a value between 0 and 3. This assumes that the walk sound effects we want to use are the first four in our list (that they are). Due to the delay in movement, it sounds like Purple Guy is running along quickly, but with some variation to his steps.

The last sound effect we want to add is in iso.c, in the doISOObjects function:


void doISOObjects(void)
{
	if (drawTimer == 0)
	{
		playSound(SND_TRANSITION, 0);
	}

	drawTimer = MIN(drawTimer + ISO_RENDER_SPEED * app.deltaTime, numISOObjects);

	// uncomment the line below to draw the scene instantly each time
	// drawTimer = numISOObjects;
}

As we know, whenever we enter a new zone, the drawing resets, and scene is recreated. Since this is controlled via a variable (drawTimer), we can test this to see if we want to play a sound. If the value of drawTimer is 0, we know that the scene is fresh, and hasn't drawn anything yet. We can therefore call playSound, passing over SND_TRANSITION. The channel we're using doesn't matter, so we're just passing over 0.

Of course, we could always play this sound effect whenever Purple Guy moves into a new zone, via doPlayer, if we wished. Neither approach is wrong.

The final thing we're going to do is play music. We do this in world.c, during initWorld:


void initWorld(void)
{
	loadMusic("music/264129__shadydave__a-forest-adventrue.mp3");

	playMusic(1);

	initLights();

	// snipped
}

Here, we've added calls to loadMusic, and then playMusic, to get our music playing. Both these functions are defined in sound.c. Passing 1 to playMusic means that it will loop forever.

All done! Our little game is finished. More importantly, we've learned how to make a simple isometric engine, with decent sorting, tile picking, lighting, and other interactions. There is much more that could be added, but if one only wanted a simple adventure game, this is a great basis to work from. As previously stated, one could expand this into a huge game with multiple maps and zones.

Hopefully, you will have found this very helpful.

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