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


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

— 2D Santa game —
Part 15: Finishing touches

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

Introduction

With the previous part, we finished up making our game a bit more pleasing to look at. In this final part, we'll be adding in sound, music, a particle system, and a quadtree. All stuff that isn't essential to our game, but helps to improve it in various ways.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./santa15 to run the code. Press Space to play. Play the game as normal, aiming to earn a highscore. When you're finished, close the window to exit.

Inspecting the code

Other than our sound and music, there are a few little tweaks we could make to our game, to improve it. One such addition is an indication to the player that they have successfully delivered a gift to a house on the Nice list. We'll be illuminating a series of lights on that house, so that the player can be sure they haven't missed it, or have negative points (remember, it's possible to drop a gift down a chimney, and then throw lots of coal down there as well - this won't affect our Xmas Spirit, but will affect our score).

Note: while we've added things such as a particle system, a quadtree, and our sound and music, we won't be going into extensive details on, as they've been covered many, many times in other tutorials, and at this point we don't want to be repeating ourselves.

We go first to structs.h, where we've made an update to House:


typedef struct
{
	double   successLights;
	int      naughty;
	Chimney *chimney;
} House;

We've added in a new variable called successLights. This will track the animation of the successful delivery lights. We're using a variable here for each house, so that all the lights don't run in step with one another, which might look a bit boring. Next, we've added a reference to the chimney itself (as a pointer called `chimney`). This is so that we can interrogate the chimney attached to the House, to find out if it is complete.

Next, it's over to house.c, to make use of that update. First, to initHouse:


void initHouse(void)
{
	Entity *e, *chimney;
	House  *h;
	int     x, y;

	// snipped

	if (canAddEntity(x, y, houseTextures[0]->rect.w, houseTextures[0]->rect.h))
	{
		h = malloc(sizeof(House));
		memset(h, 0, sizeof(House));
		h->naughty = rand() % 2;

		chimney = initChimney(h->naughty);

		h->chimney = (Chimney *)chimney->data;

		// snipped
	}
}

We're assigning the created chimney's data to the House (`h`) `chimney` pointer. We can now track the chimney from the house.

Over then to `tick`:


static void tick(Entity *self)
{
	House *h;

	h = (House *)self->data;

	if (h->chimney->complete)
	{
		h->successLights += 0.1 * app.deltaTime;

		if (h->successLights >= NUM_SUCCESS_LIGHTS_TEXTURES)
		{
			h->successLights = 0;
		}
	}

	self->x -= stage.speed * app.deltaTime;

	self->dead = self->x < -self->texture->rect.w;
}

We're now testing if the house's `chimney` is complete, and if so we're increasing the value of the house's successLights. As we'll see in a moment, we're using this value to choose which texture to draw in our successLights texture array. Therefore, if the value of successLights equals or exceeds NUM_SUCCESS_LIGHTS_TEXTURES (defined in house.c as 3), we're resetting it to 0.

The updates to `draw` follows:


static void draw(Entity *self)
{
	House *h;

	h = (House *)self->data;

	blitAtlasImage(self->texture, self->x, self->y, 0, SDL_FLIP_NONE);

	if (!h->naughty)
	{
		if (!h->chimney->complete || h->chimney->points == 0)
		{
			blitAtlasImage(dullSuccessLightTexture, self->x, self->y, 0, SDL_FLIP_NONE);
		}
		else
		{
			blitAtlasImage(successLightTextures[(int)h->successLights], self->x, self->y, 0, SDL_FLIP_NONE);
		}
	}
	else
	{
		blitAtlasImage(houseLights, self->x, self->y, 0, SDL_FLIP_NONE);
	}
}

Now, when drawing houses on the Nice light (the house's `chimney` is not `naughty`), we're going to additionally render a set of dull lights if the house is incomplete (dullSuccessLightTexture), or one of our success lights if the chimney is complete (successLightTextures). As noted earlier, the house's successLights variable picks a texture from the successLightTextures texture array, and uses that. We also render dull success lights if the house has 0 points, just to cover the case where a player has completed a house before then pushing it into negative points, by dropping coal!

Lastly, we need to load our new textures, so it's over to loadTextures:


static void loadTextures(void)
{
	int  i;
	char filename[MAX_NAME_LENGTH];

	for (i = 0; i < NUM_HOUSE_TEXTURES; i++)
	{
		sprintf(filename, "gfx/house%02d.png", i + 1);
		houseTextures[i] = getAtlasImage(filename, 1);
	}

	houseLights = getAtlasImage("gfx/houseLights.png", 1);

	dullSuccessLightTexture = getAtlasImage("gfx/successLights00.png", 1);

	for (i = 0; i < NUM_SUCCESS_LIGHTS_TEXTURES; i++)
	{
		sprintf(filename, "gfx/successLights%02d.png", i + 1);
		successLightTextures[i] = getAtlasImage(filename, 1);
	}
}

We're loading our dullSuccessLightTexture and our successLightTextures array (as gfx/successLights01.png, gfx/successLights02.png, etc).

That's it for the changes we've made to house.c. Of couse, we've added in music and sound effects. We won't talk about every single place we've add in sounds, but just point to a few instances where it has been done. Using player.c as an example, we've calling playSound in the `die` function:


static void die(Entity *self)
{
	// snipped

	stage.state = SS_GAME_OVER;

	stage.player = NULL;

	playSound(SND_GAME_OVER, CH_SANTA);
}

Our sounds are played using a function called playSound, where we pass over the sound id we want to play (SND_GAME_OVER) and the channel we want to play the sound on (CH_SANTA). These are all defined in defs.h, and loaded via sound.c.

Another example can be found in snowball.c, in the `tick` function:


static void tick(Entity *self)
{
	Snowball *s;

	s = (Snowball *)self->data;

	if (s->thinkTime > 0)
	{
		// snipped
	}
	else
	{
		// snipped

		if (self->y > s->startY)
		{
			self->y = s->startY;

			s->thinkTime = FPS + rand() % (int)FPS;

			playSound(SND_SNOWMAN_CATCH, CH_SNOWMAN);
		}
	}

	// snipped
}

We've added a call to playSound when the snowball returns to its startY value, when the snowman "catches" the snowball. Here, we're caling playSound, passing over SND_SNOWMAN_CATCH on the CH_SNOWMAN channel.

While we're still on the subject of sound and music, an interesting example of sound control can be found in killPlayer (back in player.c):


void killPlayer(int x, int y)
{
	// snipped

	stage.pauseTimer = FPS / 2;

	stage.player->dead = 1;

	playSound(SND_SANTA_HIT, CH_SANTA);

	pauseMusic();
}

As well as playing the sound effect that occurs when Santa's sleigh is struck (or we run out of Xmas Spirit), we're also calling pauseMusic (from sound.c). This is what causes our music to stop playing when we get a game over.

We start playing it again in initTitle (from title.c), which we always return to when the game is concluded:


void initTitle(void)
{
	if (titleTexture == NULL)
	{
		titleTexture = loadTexture("gfx/sdl2Santa.png");
	}

	initStage();

	showTimer = SHOW_TIME;

	showScores = game.latestHighscore != NULL;

	resumeMusic();

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

We've added in a call to resumeMusic (again, in sound.c) which will make our music start playing again.

As previously noted, we've added in a particle system (in particles.c). It's once again done in a very standard way, so we won't go into details. Instead, we'll just show an example of where it is used. Over to carrot.c, we've updated `touch`:


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

	if (other == stage.player)
	{
		killPlayer(self->x, self->y);

		self->dead = 1;

		for (i = 0; i < 25; i++)
		{
			addParticle(self->x, self->y, 255, 128 + rand() % 64, 0);
		}
	}
}

Now, when the carrot hits the player, we're going to spawn 25 particles, via a call to addParticle. We'll give each particle a random orange hue (the final three parameters of addParticle at the RGB values). Other places where we've added in particles include chimney.c, gift.c, and snowball.c.

We've also added in a quadtree. While this isn't strictly required, it does help to reduce the number of collision checks that are going on. In our game, it is quite easy to rocket to 300 collision checks per frame, due to each entity checking for collisions against each other (such as when gifts and coal are deployed by the player). We're therefore using a quadtree to divide the game screen into various sections, and only checking for collisions that are relevant to the current entity.

We can see how this is being put to use in entities.c. Starting with doEntities:


void doEntities(void)
{
	Entity *e, *prev;

	prev = &stage.entityHead;

	for (e = stage.entityHead.next; e != NULL; e = e->next)
	{
		removeFromQuadtree(e, &stage.quadtree);

		e->tick(e);

		if (e->touch != NULL)
		{
			doCollisions(e);
		}

		if (!e->dead)
		{
			addToQuadtree(e, &stage.quadtree);
		}
		else
		{
			// snipped
		}

		prev = e;
	}
}

We're removing the current entity (`e`) from our quadtree, by using a call to removeFromQuadtree, passing over the entity and the quadtree we want to work with (our one is set in Stage). While processing the entity in the usual loop, we test whether it is still alive, and placing it back into the quadtree if so. This means that dead entities are removed from our tree.

When it comes to testing our collisions, we just update doCollisions to tell our code to use the quadtree:


static void doCollisions(Entity *e)
{
	int     i;
	Entity *other, *candidates[MAX_QT_CANDIDATES];

	getAllEntsWithin(e->x, e->y, e->texture->rect.w, e->texture->rect.h, candidates);

	for (i = 0, other = candidates[0]; i < MAX_QT_CANDIDATES && other != NULL; other = candidates[++i])
	{
		if (other != e && collision(e->x, e->y, e->texture->rect.w, e->texture->rect.h, other->x, other->y, other->texture->rect.w, other->texture->rect.h))
		{
			e->touch(e, other);
		}
	}
}

This works much like the collision checks in SDL2 Gunner. We call getAllEntsWithin (defined in quadtree.c), passing over the rectangular area occupied by our current entity (`e`), as well as an array of entity pointers (`candidates`), into which the discovered entities will be added. We then loop through all of the found entities and test just those ones for collisions. The result is that a call to this function that might have performed 300 checks now only performs 20.

We'll start wrapping this part up now, briefly looking at some setup and tear down for our new features. Over to stage.c, where we've updated initStage:


void initStage(void)
{
	if (reset)
	{
		resetStage();
	}

	reset = 1;

	memset(&stage, 0, sizeof(Stage));

	if (groundTextures[0] == NULL)
	{
		loadTextures();
	}

	initQuadtree(&stage.quadtree);

	initEntities();

	initGround();

	initHills();

	initSnow();

	initParticles();

	stage.state = SS_DEMO;

	houseSpawnTimer = FPS;

	objectSpawnTimer = FPS * 5 + ((int)FPS * rand() % 5);

	gameOverTimer = FPS * 5;

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

We're now calling initQuadtree and initParticles, to create our quadtree and setup our particle system, respectively.

We've also updated resetStage:


static void resetStage(void)
{
	clearEntities();

	clearParticles();

	destroyQuadtree(&stage.quadtree);
}

Here, we're calling clearParticles and destroyQuadtree, to remove all our particles and reset our quadtree.

Lastly, over in init.c, we've updated initGameSystem:


void initGameSystem(void)
{
	srand(time(NULL));

	initAtlas();

	initTextures();

	initFonts();

	initHighscores();

	initSound();

	loadMusic("music/Christmas synths.ogg");

	playMusic(-1);

	setMusicVolume(64);
}

We're loading our sounds (initSound) and music (loadMusic), and playing our music on a loop (playMusic, passing over -1). We have no volume control, so we're setting the volume of our music here to a value that won't make it louder than our sound effects (setMusicVolume).

SDL2 Santa is finally complete! We have a full game loop, sound, music, a title screen, and a highscore table. Of course, there are a number of things that one might now wish to add to our game, to expand it. A small list of such things can be found below.

  • Name entry on the highscore table
  • Loading and saving highscores
  • Joypad controls
  • Sound and music configuration
  • Allowing the player to survive more than one hit - perhaps decrease Xmas Spirit instead
  • Difficulty settings
  • Stage goals: must score X points before introducing new hazards, etc.

Otherwise, we have a great starting point for a game like this. Maybe one could change it around, to make it Halloween themed, and have a witch on a broomstick, dropping pumpkins onto zombies below? Perhaps even change the Santa game so that one can fly both left and right, like Defender. The opportunities are nearly endless. Like the scrolling in our game.

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