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

Android Games

DDDDD
Number Blocks
Match 3 Warriors

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


The Third Side (Battle for the Solar System, #2)

The White Knights have had their wings clipped. Shot down and stranded on a planet in independent space, the five pilots find themselves sitting directly in the path of the Pandoran war machine as it prepares to advance The Mission. But if they can somehow survive and find a way home, they might just discover something far more worrisome than that which destroyed an empire.

Click here to learn more and read an extract!

« Back to tutorial listing

— 2D Santa game —
Part 9: Highscores

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

Introduction

We have our game over phase, but we're unable to play the game again without closing the window and restarting fully. What we want to do is be able to play the game, earn a highscore, and then retry with ease. In this part, we're going to introduce just that, while also including a small title screen.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./santa09 to run the code. You will see a title screen with the prompt to press Space to play. Every 5 seconds, the screen will cycle between the title image and a list of highscores. Press Space to play. Use the same controls as before. When you fail (which is unavoidable right now), you will be either be taken to the title screen again or shown your highscore entry, with your score being highlighted in yellow if you earned enough points to qualify. Press Space to play again. When you're finished, close the window to exit.

Inspecting the code

This second part of our game loop implementation sees us adding in the title screen and highscore table. Once again, most will find there isn't much here that will cause them any trouble understanding, but there is a lot of it; we need to add new files, as well as tweak the existing one to get the desired behaviour. Something to keep in mind is that there is no name entry available. All entries will be "Santa". Supporting name entry is left as an exercise for the reader.

Starting with defs.h:


#define NUM_HIGHSCORES 10

We've added a define to hold the number of highscores in our highscore table.

Next, we've tweaked our SS enum:


enum
{
	SS_DEMO,
	SS_PLAYING,
	SS_GAME_OVER
};

We've added a new enum entry called SS_DEMO. This will be the default value for Stage's `state`, and will be used on the title screen and the highscore display. Our stage logic and rendering will be taking place while displaying these two, and so we don't want certain aspects of the game to occur while this is happening. We'll see this a little later on.

Now over to structs.h, where we've added some new object definitions.


typedef struct
{
	char name[MAX_NAME_LENGTH];
	int  score;
	long time;
} Highscore;

The Highscore struct will hold the data about a highscore entry. `name` is the name of the individual who scored the points, `score` the score they earned, and `time` is a value to record when the score was earned. While this isn't displayed on the highscore table itself, it is used in sorting the scores and highlighting the most recent entry by the player.

We've also added the Game struct:


typedef struct
{
	Highscore  highscores[NUM_HIGHSCORES];
	Highscore *latestHighscore;
} Game;

The Game struct will hold our highscores (as an array of length NUM_HIGHSCORES), as well as a pointer to the most recent highscore, as latestHighscore.

Let's now look at highscores.c, the new compilation unit that will handle the highscores. This will be very much like the highscore processing in SDL2 Shooter and SDL2 Shooter 2, except that there is no name entry offered.

Starting with initHighscores:


void initHighscores(void)
{
	char      *defaultNames[] = {"Dasher", "Dancer", "Prancer", "Vixen", "Comet", "Cupid", "Donner", "Blitzen", "Rudolph", "Sven"};
	int        i;
	Highscore *h;

	memset(&game.highscores, 0, sizeof(Highscore) * NUM_HIGHSCORES);

	for (i = 0; i < NUM_HIGHSCORES; i++)
	{
		h = &game.highscores[i];

		STRCPY(h->name, defaultNames[i]);
		h->score = 250 + (100 * (rand() % 25));
		h->time = time(NULL);
	}

	qsort(game.highscores, NUM_HIGHSCORES, sizeof(Highscore), highscoreComparator);

	game.latestHighscore = NULL;
}

This function sets up our highscore table. We're clearing Game's `highscores`, using memset, then using a for-loop to randomize `score`. For each one, we're copying in the name of a reindeer (in order) from our defaultNames char array. We're also setting `time` to the current time. Finally, we're using qsort to sort our scores in order of highest to lowest, and setting Game's latestHighscore to NULL, so none of the entries are highlighted.

Nothing difficult to understand there. Now for drawHighscores:


void drawHighScores(void)
{
	int        i, y, r, g, b;
	Highscore *h;
	char       text[16];

	app.fontScale = 1.25;

	drawShadowText("Highscores", SCREEN_WIDTH / 2, 50, 255, 255, 255, TEXT_ALIGN_CENTER);

	y = 130;

	for (i = 0; i < NUM_HIGHSCORES; i++)
	{
		h = &game.highscores[i];

		r = g = b = 255;

		if (h == game.latestHighscore)
		{
			b = 0;
		}

		drawShadowText(h->name, 500, y, r, g, b, TEXT_ALIGN_LEFT);

		sprintf(text, "%06d", h->score);

		drawShadowText(text, SCREEN_WIDTH - 500, y, r, g, b, TEXT_ALIGN_RIGHT);

		y += 55;
	}

	app.fontScale = 1;
}

For the most part, this is a standard for-loop that is rendering each of our highscore entries on the screen. We draw the name of the entrant on the left and the score on the right. The one thing of note is that most of the scores will be drawn in white. However, if the current entry (`h`) is Game's latestHighscore, we'll render the text in yellow. Again, just a standard highscore listing.

Over now to addHighscore:


void addHighscore(void)
{
	Highscore *h;
	int        i;
	long       t;

	game.latestHighscore = NULL;

	if (stage.score > game.highscores[NUM_HIGHSCORES - 1].score)
	{
		h = &game.highscores[NUM_HIGHSCORES - 1];

		t = time(NULL);

		STRCPY(h->name, "Santa");
		h->score = stage.score;
		h->time = t;

		qsort(game.highscores, NUM_HIGHSCORES, sizeof(Highscore), highscoreComparator);

		for (i = 0; i < NUM_HIGHSCORES; i++)
		{
			if (game.highscores[i].time == t)
			{
				game.latestHighscore = &game.highscores[i];
			}
		}
	}
}

When it comes to adding a highscore, all our entries will take the name "Santa". We start by setting Game's latestHighscore to NULL, so that by default we don't highlight any scores in the table, then check to see if Stage's `score` is greater than the final entry in the array. If so, we'll set that entry's `time` to now (using time(NULL)), set the `name` as "Santa", and the `score` as Stage's score. We'll then sort the scores from highest the lowest. Next, we need to locate the score we just added. We do this by looping through all the scores and searching for the time the entry was made (stored in a variable called `t`). We can't simply point to the entry prior to calling qsort, since it will continue to point to the same position (i.e., last place), which is not what we want.

Again, no real surprises to be had. Finally, we have the highscoreComparator function:


static int highscoreComparator(const void *a, const void *b)
{
	Highscore *h1, *h2;
	int        result;

	h1 = ((Highscore *)a);
	h2 = ((Highscore *)b);

	result = h2->score - h1->score;

	if (result == 0)
	{
		return h1->time - h2->time;
	}

	return result;
}

This function, used by qsort, simply compares one highscore to another, sorting from high to low. If two highscores have the same score (`result` is 0 when subtracting one score from the other), we compare by `time` instead, from lowest to highest. If two scores are tied, we want the entry that earned the score first to be displayed higher.

That's it for highscores.c. We now have another new compilation unit called title.c. This file is responsible for displaying our title screen, as well as the highscores. It partly uses the logic and rendering from stage.c (doStage and drawStage), which is why we needed to add in the new SS_DEMO state entry. We'll come to all that shortly. For now, let's start with initTitle:


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

	initStage();

	showTimer = SHOW_TIME;

	showScores = game.latestHighscore != NULL;

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

We're starting by loading our title texture (titleTexture), and then calling initStage, to setup the Stage we'll be calling the logic and rendering for. This is why we split the logic for actually starting the game into a new function called startStage, to separate the two aspects. Next, we're setting a variable called showTimer to SHOW_TIME (defined in title.c as 5 seconds). This is a control variable that is used to control how long we display our titles, before moving on to listing our highscores.

Next, we're testing whether we want to display our highscores immediately, by testing if Game's latestHighscore is not NULL, and setting the value of showScores.

Over now to `logic`:


static void logic(void)
{
	doStage();

	showTimer -= app.deltaTime;

	if (showTimer <= 0)
	{
		showTimer += SHOW_TIME;

		showScores = !showScores;
	}

	if (app.keyboard[SDL_SCANCODE_SPACE])
	{
		app.keyboard[SDL_SCANCODE_SPACE] = 0;

		initStage();

		startStage();
	}
}

The first thing that we're doing here is calling doStage, to handle the Stage logic. We'll see how this has changed in a bit. Next, we're decreasing the value of showTimer. If it falls to 0 or less, we're resetting it, and inverting the value of showScores, so it flips between 0 and 1. This will mean that every 5 seconds we'll be displaying either the title image or our list of highscores.

Next, we're testing if Space has been pressed. If so, we're clearing the key before starting the game, via calls to initStage and startStage.

Now for `draw`:


static void draw(void)
{
	drawStage();

	if (!showScores)
	{
		blit(titleTexture, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 100, 1, SDL_FLIP_NONE);
	}
	else
	{
		drawHighScores();
	}

	app.fontScale = 1.25;

	drawShadowText("Press SPACE to Play!", SCREEN_WIDTH / 2, SCREEN_HEIGHT - 150, 128, 255, 128, TEXT_ALIGN_CENTER);

	app.fontScale = 1;
}

Another simple function. We're making a call to drawStage, before then testing the value of showScores. If it's 0, we'll be drawing our title image. Otherwise we'll be rendering our highscore list, with a call to drawHighScores. At the bottom of the screen, we'll print some text to prompt the player to press Space to begin the game.

That's it for title.c. We'll move on to stage.c next, where we've made a bunch of tweaks and changes to support the title screen's calls. As already stated, we want to lean on certain aspects of Stage's logic and rendering, but don't want the game to be playing out in full while we're displaying the titles.

First, to initStage:


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

	reset = 1;

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

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

	initEntities();

	initGround();

	stage.state = SS_DEMO;

	houseSpawnTimer = FPS;

	gameOverTimer = FPS * 5;

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

The first thing we're doing is testing whether a variable called `reset` is set, and calling resetStage if so. This is a control variable to determine whether we want to clear down all of Stage's data, emptying linked lists and the like, before proceeding. We don't want to make this call the very first time we call initStage, since some of our linked lists (and other upcoming aspects) will not have been initialized just yet. Doing so could lead to crashes. With this done, we set `reset` to 1, to ensure resetStage is called the very next time.

The only other change is that we're setting Stage's `state` to be SS_DEMO, so that the game doesn't begin by default.

Next up is the changes to startStage:


void startStage(void)
{
	stage.speed = INITIAL_GROUND_SPEED;
	stage.xmasSpirit = MAX_XMAS_SPIRIT;
	stage.numGifts = 12;
	stage.numCoal = 12;

	initPlayer();

	initHUD();

	stage.state = SS_PLAYING;
}

Just one change here - we're setting Stage's `state` to SS_PLAYING, so that the game begins properly.

doStage has seen far more tweaks:


void doStage(void)
{
	switch (stage.state)
	{
		case SS_PLAYING:
			stage.speed = MIN(stage.speed + (0.00025 * app.deltaTime), MAX_GROUND_SPEED);
			break;

		case SS_GAME_OVER:
			stage.speed *= 1 - (0.01 * app.deltaTime);

			gameOverTimer -= app.deltaTime;

			if (gameOverTimer <= 0)
			{
				addHighscore();

				initTitle();
			}
			break;

		default:
			stage.speed = INITIAL_GROUND_SPEED;
			break;
	}

	// snipped
}

We're now testing for Stage's `state` for SS_PLAYING. If `state` is set to this value, we're going to gradually increase the value of Stage's `speed`, so that the game becomes progressly faster (but limiting the speed to MAX_GROUND_SPEED, defined in stage.c as 6.5).

Our SS_GAME_OVER case has also seen the addition of a test for gameOverTimer falling to 0 or less. At this point, we'll attempt to add the player's score to the highscore table, via addHighscore, and then call initTitle to return to the title screen.

drawStage has also seen an extra check added against Stage's `state`:


void drawStage(void)
{
	// snipped

	if (stage.state != SS_DEMO)
	{
		drawHUD();
	}

	// snipped
}

Now, we'll only draw the HUD if the state is not SS_DEMO. In other words, when not on the title screen or displaying the highscores.

The final update to stage.c is the resetStage function:


static void resetStage(void)
{
	clearEntities();
}

We're calling clearEntities here, to remove all the entities from the game. This function lives in entities.c, and is as simple as the name implies:


void clearEntities(void)
{
	Entity *e;

	while (stage.entityHead.next)
	{
		e = stage.entityHead.next;

		stage.entityHead.next = e->next;

		if (e->data != NULL)
		{
			free(e->data);
		}

		free(e);
	}
}

A mere loop to delete all the entities in the Stage, including their data. We've seen this a number of times before, in past tutorials.

Before we finish up, we'll quickly look at the updates to init.c and main.c. Starting with init.c, we've updated initGameSystem:


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

	initAtlas();

	initTextures();

	initFonts();

	initHighscores();
}

We're calling initHighscores here. We want to do this when the game starts, as part of the initial setup. We've also made a minor modification to `main` in main.c:


int main(int argc, char *argv[])
{
	long then;

	memset(&app, 0, sizeof(App));

	initSDL();

	initGameSystem();

	initTitle();

	atexit(cleanup);

	// snipped

	return 0;
}

Now, instead of calling initStage, we're calling initTitle, so that the title screen is the first thing the player sees.

And there we have it, our game loop is complete! We can start a game, score points, earn a highscore, and replay the game as much as we want.

Only, there's a problem - we have limited supplies of gifts and coal, with no means of obtaining more. This means that the best possible score we can get is 2,400 (12 gifts, 12 coal, with one correct drop per house). This need to be fixed, as it's not exactly fair. Therefore, in our next part we're going to implement magic sacks for Santa to collect, teleported in by his hardworking elves, that will replenish his stock of gifts and coal.

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