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 Honour of the Knights (First Edition) (The Battle for the Solar System)

When starfighter pilot Simon Dodds is enrolled in a top secret military project, he and his wingmates begin to suspect that there is a lot more to the theft of a legendary battleship and an Imperial nation's civil war than either the Confederation Stellar Navy or the government are willing to let on.

Click here to learn more and read an extract!

« Back to tutorial listing

— 2D Santa game —
Part 7: HUD

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

Introduction

It's time to give the player some more information as to what's going on. We need to be able to see our score, gift and coal supplies, and other data. A HUD is a stable of many games, and this one is no exception.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./santa07 to run the code. You will see a window open like the one above, with the scene moving from right to left. Use the same controls as before. As your earn points, your score on the HUD will increase. Additionally, when you deploy coal and gifts, those levels will decrease (but remain unlimited). Pay attention, too, to the Xmas Spirit bar on the right. If you fail to deliver a gift to a Nice house, the bar will decrease and visibly shudder. When you're finished, close the window to exit.

Inspecting the code

Once again, our HUD is quite trivial to implement, and other than some aesthetics there won't be too many surprises here; it should be quite simple for all to understand, so let's get straight to it.

First to defs.h:


#define MAX_XMAS_SPIRIT 5

We've added MAX_XMAS_SPIRIT, that will define the maximum value of Xmas spirit.

Now over to structs.h, where we've updated Stage:


typedef struct
{
	double  speed;
	int     score;
	int     xmasSpirit;
	int     numGifts;
	int     numCoal;
	Entity  entityHead, *entityTail;
	Entity *player;
} Stage;

We've added three new fields - xmasSpirit, that will hold the current level of our Xmas Spirit; numGifts, that will be the number of gifts the player currently has; and numCoal, which is the amount of coal the player has.

Now onto hud.c, the compilation unit we've created to handle all our HUD functions. Once again, there won't be too many surprises to come here; it's mostly just rendering strings at the top of the screen.

Starting with initHUD:


void initHUD(void)
{
	score = 0;

	xmasSpirit = stage.xmasSpirit;

	xmaxSpiritBarShudder = 0;

	xmaxSpiritBarWarningTimer = 0;
}

We're setting a variable called `score` here. This score is defined as a double, and is used to smoothly animate the counting up of our score display when we earn points. We'll see this in a moment. Next up, we're setting xmasSpirit to the value of Stage's xmasSpirit. Once again, this variable is a double, and will be used to smoothly animate the decrease of Xmas Spirit display. xmasSpiritBarShudder is a control variable to handle the Xmas Spirit bar shaking when it drains. xmaxSpiritBarWarningTimer will be used to flash the Xmas Spirit bar when it is low. All these variables are declare static in hud.c.

With our variables all setup, we can move on to doHUD, where we're processing our HUD's logic:


void doHUD(void)
{
	if (score < stage.score)
	{
		score = MIN(score + 2 * app.deltaTime, stage.score);
	}
	else
	{
		score = stage.score;
	}

	if (xmasSpirit > stage.xmasSpirit)
	{
		xmasSpirit = MAX(xmasSpirit - (0.05 * app.deltaTime), stage.xmasSpirit);

		xmaxSpiritBarShudder += app.deltaTime;
	}
	else
	{
		xmaxSpiritBarShudder = 0;
	}

	xmaxSpiritBarWarningTimer += app.deltaTime;
}

This is where we're adding up the scores and handling the Xmas Spirit bar shudder. To begin with, we're testing whether our local `score` variable is lower than Stage's score, and if so we're increasing its value (ensuring that we don't go over). Otherwise, we're setting `score` to be the same as Stage's `score`. Note that we're not counting down if we lose points. It's something that would be added easily enough if one desired, as we'll see next.

Our Xmas Spirit is doing much the same thing as our score display, except that we're decreasing the value of our local xmasSpirit if it's higher than Stage's. We're also increasing the value of xmaxSpiritBarShudder at the same time, to make the bar shake. If both xmasSpirit values are equal, we stop the bar from shaking.

Finally, we're always increasing the value of xmaxSpiritBarWarningTimer, to control how often we flash the bar, if needed.

Now for the rendering routine, so over to drawHUD:


void drawHUD(void)
{
	char text[64];
	int  w, y, r, g, b;

	sprintf(text, "Score: %06d", (int)score);
	drawText(text, 10, 0, 255, 255, 255, TEXT_ALIGN_LEFT, 0);

	sprintf(text, "Gifts: %d", stage.numGifts);
	drawText(text, 500, 0, 255, 255, 255, TEXT_ALIGN_LEFT, 0);

	sprintf(text, "Coal: %d", stage.numCoal);
	drawText(text, 850, 0, 255, 255, 255, TEXT_ALIGN_LEFT, 0);

	sprintf(text, "Xmas Spirit:");
	drawText(text, SCREEN_WIDTH - (XMAS_SPIRIT_BAR_WIDTH + 20), 0, 255, 255, 255, TEXT_ALIGN_RIGHT, 0);

	y = 11 + (sin(xmaxSpiritBarShudder) * 5);

	drawOutlineRect(SCREEN_WIDTH - (XMAS_SPIRIT_BAR_WIDTH + 10), y, XMAS_SPIRIT_BAR_WIDTH, 24, 255, 255, 255, 255);

	w = XMAS_SPIRIT_BAR_WIDTH - 4;
	w = MAX(w * (xmasSpirit / MAX_XMAS_SPIRIT), 0);

	r = 160;
	g = 192;
	b = 255;

	if (xmasSpirit <= 1 && (int)xmaxSpiritBarWarningTimer % (int)FPS < FPS / 2)
	{
		r = g = b = 255;
	}

	drawRect(SCREEN_WIDTH - (XMAS_SPIRIT_BAR_WIDTH + 10) + 2, y + 2, w, 20, r, g, b, 255);
}

For the most part, this is rather simple - we're just rendering the text of our score, gifts, and coal at various points across the top of the screen. This happens to be the reason why we're limiting Santa to 50 pixels on the vertical, since we don't want him to enter the HUD area.

When it comes to our Xmas Spirit, we're drawing a white rectangle of XMAS_SPIRIT_BAR_WIDTH length, and then drawing another rectangle that will have a width based on the percentage of Xmas Spirit we have remaining (using our local xmasSpirit, divided by MAX_XMAS_SPIRIT - for a value between 0 and 1 - and multiplying our width by the result). Using the local xmasSpirit, which is a double, this will cause the bar to smoothly decrease in length, rather than immediately jump to the new value. To make the bar shudder, we're drawing the bar at a vertical position (`y`) that will be adjusted by the sine of xmaxSpiritBarShudder, multiplied by 5. When it comes to choosing our bar's colour (`r`, `g`, `b`), we're setting a light blue colour, then testing if our xmasSpirit is low ( <= 1) and whether the modulo of our xmaxSpiritBarWarningTimer is within half a second, and altering the colour to white, before rendering the bar.

All pretty simple at the end of the day. Nothing we've not seen before, and just some added glitz.

Our HUD is done, so let's look at the other little tweaks we've made. First to player.c, where we've updated dropGift:


static void dropGift(void)
{
	if (app.keyboard[SDL_SCANCODE_J])
	{
		stage.numGifts--;

		app.keyboard[SDL_SCANCODE_J] = 0;

		initGift(ET_GIFT);
	}

	if (app.keyboard[SDL_SCANCODE_L])
	{
		stage.numCoal--;

		app.keyboard[SDL_SCANCODE_L] = 0;

		initGift(ET_COAL);
	}
}

As we're now displaying the number of gifts and coal we have remaining, we're going to decrease the values of each in Stage when we drop them. Note how there's no limit set on these just yet, allowing them to go negative. We'll soon prevent drops if the player is out of stocks.

Next to chimney.c, where we've made a couple of other changes. First, to initChimney:


Entity *initChimney(int naughty)
{
	// snipped

	e->tick = tick;
	e->draw = draw;
	e->touch = touch;
	e->die = die;

	e->data = c;

	return e;
}

We're assigning the `die` function pointer to the new `die` function we've added:


static void die(Entity *self)
{
	Chimney *c;

	c = (Chimney *)self->data;

	if (!c->complete && !c->naughty)
	{
		stage.xmasSpirit = MAX(stage.xmasSpirit - 1, 0);
	}
}

This `die` function will be called by the doEntities loop in entities.c, before an entity is removed. Here, we're testing if this is a Nice chimney and whether it is incomplete (we never dropped a gift into this house, as we were supposed to). If so, we're going to decrease Stage's xmasSpirit. This means that whenever a chimney moves off the left side of the screen and its `dead` flag is set, we'll lose xmasSpirit if we missed a gift delivery.

We could, of course, have put this code into our `logic` function. The advantage here is that we could extend the game in future to cause other things to destroy the chimney, triggering the `die` function, and centralizing our response code.

We're nearly done, so it's over to stage.c to implement the final changes. First, we've added a new function called startStage:


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

	initPlayer();

	initHUD();
}

Here, we're initializing various aspects of our stage, including the starting speed, Xmas Spirit, the amount of coal and gifts we have, setting up the player, and calling initHUD. We've moved these things into a new function as later we'll want to call initStage without setting these up (for example, when viewing the title screen / highscores).

Next up, we've updated doStage:


void doStage(void)
{
	doGround();

	addHouse();

	doEntities();

	doHUD();
}

We've added in a call to doHUD here.

On to drawStage:


void drawStage(void)
{
	drawGround();

	drawEntities();

	drawHUD();
}

Here, we've added in a call to drawHUD.

Lastly, we've got to make one change to main.c, to account for our new startStage function:


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

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

	initSDL();

	initGameSystem();

	initStage();

	startStage();

	atexit(cleanup);

	// snipped
}

We've added in the call to startStage just after initStage.

And that's it for our HUD and information display. A small update, but an important one. We can now see how many points we've earned, and how many gifts and coal we have remaining, and whether we're in danger of losing too much Xmas Spirit.

But what about the endgame? Our game is an endless scroller, but at some point we're going to lose (there is no "win" condition). It's about time we added in the endgame, in the form of a Game Over screen. So, in the next part we're going to implement the losing phase. It will be a bit unfair, as Santa won't have enough gifts to satisfy all the Nice houses, so will eventually run out of Xmas Spirit. But only if he doesn't crash into a house first!

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