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
SDL 1 tutorials (outdated)

Latest Updates

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

The Legend of Edgar 1.36
Sun, 1st January 2023

SDL2 map editor tutorial [UPDATED]
Sat, 10th September 2022

All Updates »

Tags

android (3)
battle-for-the-solar-system (10)
blob-wars (10)
brexit (1)
code (6)
edgar (9)
games (41)
lasagne-monsters (1)
making-of (5)
match3 (1)
numberblocksonline (1)
orb (2)
site (1)
tanx (4)
three-guys (3)
three-guys-apocalypse (3)
tutorials (15)
water-closet (4)

Books


The Attribute of the Strong (Battle for the Solar System, #3)

The Pandoran War is nearing its end... and the Senate's Mistake have all but won. Leaving a galaxy in ruin behind them, they set their sights on Sol and prepare to finish their twelve year Mission. All seems lost. But in the final forty-eight hours, while hunting for the elusive Zackaria, the White Knights make a discovery in the former Mitikas Empire that could herald one last chance at victory.

Click here to learn more and read an extract!

« Back to tutorial listing

— 2D Santa game —
Part 8: Game Over

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

Introduction

It's time to implement the first part of our game loop. In this part and the next, we're going to implement our game over sequence, and our restart logic (including highscore display). Both these parts will be somewhat unfair, since there is no way to earn more gifts and coal, meaning that failure will be swift and inevitable.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./santa08 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. You have limited coal and gifts to make deliveries, with no means of gaining more. This will mean that eventually your Xmas Spirit will run out, due to missed deliveries, and the game will end. Additionally, if Santa collides with either a house or a chimney, the sleigh will explode and the game will end. The game doesn't restart upon a game over (Xmas Cancelled), so when you're finished close the window to exit.

Inspecting the code

There's quite a lot that we need to add for this first part, since we're handling the destruction of the sleigh, the loss of Xmas Spirit, and lots of misc. other pieces. We'll start, as is usual, with defs.h:


enum
{
	SS_PLAYING,
	SS_GAME_OVER
};

We've added in a new enum group. SS is short of Stage State, and will control the state of the Stage itself. SS_PLAYING will indicate that the game is in progress, while SS_GAME_OVER will be used to indicate that the game has ended. These states, as we'll see, will be used to draw the logic and rendering in the stage itself.

Now over to structs.h, where we've made a number of changes:


typedef struct
{
	SDL_Point hit;
	double    shudder;
} Santa;

We've added in a new struct to represent Santa himself. `hit` is a set of x and y coordinates, that will be used to determine where on the player sprite a collision occurred. As of right now, this will alway be the middle of the sleigh, but in later parts this will come in useful when we strike other hazards. `shudder` is a variable that will be used to control the shaking of the sleigh when we lose Xmas Spirit. This will operate much like the shuddering of the Xmas Spirit bar on the HUD.

We've also made some changes to Stage:


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

We've added in the `state` field, that the SS enums will be applied to. We've also added in a variable called pauseTimer. This will be used to control the small break in the action that occurs when Santa's sleigh is destroyed. Without this small break, the sleigh would instantly come apart and our hit sprite would only show for an brief moment. We want to add a little bit of drama to the proceedings, which is why we pause.

Now for a new compilation unit - debris.c. You will have noticed that we scatter sleigh parts, Santa himself, and some gifts and coal when the game ends. This is our debris, which is all defined and created in this file. It will be pretty standard, so you need not expect anything complicated.

Starting with initDebris:


void initDebris(int x, int y, AtlasImage *texture)
{
	Entity *e;

	e = spawnEntity();
	e->x = x;
	e->y = y;
	e->dx = 0.1 * (rand() % 50 - rand() % 50);
	e->dy = -(8 + rand() % 8);
	e->texture = texture;

	e->tick = tick;
	e->draw = draw;
}

This function takes three parameters: `x`, `y`, and `texture`. `x` and `y` are the positions we want to create our debris at, while `texture` is the texture to use when drawing the debris. Our debris is very generic and is just a bunch of entities. We set the entity's (`e`) `x` and `y` values, as well as its `texture`, and assign the `tick` and `draw` function pointers. For its `dx` and `dy` (its velocity), we randomly set values between -5 and 5 on the horizontal, and between -8 and -15 on the vertical.

On to `tick`:


static void tick(Entity *self)
{
	self->dy += 0.5 * app.deltaTime;

	self->x += self->dx * app.deltaTime;
	self->y += self->dy * app.deltaTime;

	self->dead = self->y > SCREEN_HEIGHT;
}

We're applying gravity to our debris, by increasing the `dy`, and then applying the `dx` and `dy` values to the debris's `x` and `y`, to make them move. Our debris is considered to be dead if it falls off the bottom of the screen, the `dead` flag being set to 1. Nice and simple.

The `draw` function is equally straightforward:


static void draw(Entity *self)
{
	blitAtlasImage(self->texture, self->x, self->y, 0, SDL_FLIP_NONE);
}

We're just rendering the debris, using its `texture`.

Now over to player.c, where we've implemented all the changes to handle the reaction to loss of Xmas Spirit, and the destruction of the sleigh. First up is initPlayer:


void initPlayer(void)
{
	Entity *e;
	Santa  *s;

	// snipped

	s = malloc(sizeof(Santa));
	memset(s, 0, sizeof(Santa));

	e = spawnEntity();
	e->type = ET_PLAYER;
	e->x = 400;
	e->y = 200;
	e->dy = -8;
	e->texture = sleighTexture;
	e->data = s;

	// snipped
}

We're creating a new Santa object (`s`) and assigning it to the player entity's `data`.

Next up is the changes to `tick`:


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

	move(self);

	dropGift();

	s = (Santa *)stage.player->data;

	s->shudder = MAX(s->shudder - app.deltaTime, 0);
}

We're decreasing the value of the Santa object's `shudder` here, limiting it to 0. This will stop Santa from constantly shaking when we lose Xmas Spirit.

A tweak to dropGift follows:


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

		app.keyboard[SDL_SCANCODE_J] = 0;

		initGift(ET_GIFT);
	}

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

		app.keyboard[SDL_SCANCODE_L] = 0;

		initGift(ET_COAL);
	}
}

Our supply of gifts and coal is no longer unlimited, and so we've added a check that Stage's numGifts and numCoal must be greater than 0 before we can deploy them.

On to `draw` next:


static void draw(Entity *self)
{
	Santa *s;
	int    x, y;

	s = (Santa *)stage.player->data;

	x = sin(s->shudder) * 5;
	y = -cos(s->shudder) * 3;

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

	if (self->dead)
	{
		blitAtlasImage(explosionTexture, s->hit.x + x, s->hit.y + y, 1, SDL_FLIP_NONE);
	}
}

We're doing two things new here. First, we're adjusting the sleigh's drawing position according to the value of Santa's `shudder`, by way of sine and cosine, applied to variables named `x` and `y`. These two values are applied to the entity (`self`) `x` and `y` when drawn. This will make the sleigh visibly wobble in the air.

Next up, we're testing if the entity's `dead` flag is set, and drawing an explosion image (explosionTexture) over the top. The location of the explosion is based on the values of Santa's `hit`, that we'll be coming to in a little bit.

Now for the `die` function, that we've newly added:


static void die(Entity *self)
{
	int i;

	initDebris(self->x, self->y, sleighPart1Texture);
	initDebris(self->x, self->y, sleighPart2Texture);
	initDebris(self->x, self->y, santaTexture);

	for (i = 0; i < 6; i++)
	{
		if (rand() % 2 == 0)
		{
			initDebris(self->x, self->y, giftTextures[rand() % NUM_GIFT_TEXTURES]);
		}
		else
		{
			initDebris(self->x, self->y, coalTextures[rand() % NUM_COAL_TEXTURES]);
		}
	}

	stage.state = SS_GAME_OVER;

	stage.player = NULL;
}

This function is responsible for scattering the debris when Santa's sleigh is destroyed. We call initDebris three times to begin with, passing over textures that are two halves of our sleigh, as well as Santa himself. Next up, we using a for-loop to randomly throw six other pieces of debris, that have a 50/50 change of being either a gift or piece of coal (as textures). So, overall we'll be issuing 9 pieces of debris whenever Santa loses.

Next up, we set Stage's `state` to SS_GAME_OVER, to mark the game as concluded and set Stage's `player` pointer to NULL, since our doEntities loop will be freeing the associated memory.

Another new function we have added is killPlayer:


void killPlayer(int x, int y)
{
	Santa *s;

	s = (Santa *)stage.player->data;

	if (x > -1 && y > -1)
	{
		s->hit.x = x;
		s->hit.y = y;
	}
	else
	{
		s->hit.x = stage.player->x + (stage.player->texture->rect.w / 2);
		s->hit.y = stage.player->y + (stage.player->texture->rect.h / 2);
	}

	stage.pauseTimer = FPS / 2;

	stage.player->dead = 1;
}

Admittedly, a somewhat violently named function for this type of game, but it makes the intension clear. It takes two arguments: `x` and `y`, which will be the position of Santa's `hit`. If both `x` and `y` are greater than -1, we'll be assigning `hit` the two values. Otherwise, `hit` will be centered on the player entity, taking into account the size of the texture. In this part, we're only ever passing over -1,-1, but in later parts we'll be more specific about where the hit occurred.

Next, we set Stage's pauseTimer to half a second (FPS / 2), to allow for the hit to be seen (as we'll soon see, pauseTimer pauses our logic processing, but continues to render graphics). Finally, we set the player's `dead` flag to 1.

Last, we've updated the loadTexture function:


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

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

	explosionTexture = getAtlasImage("gfx/explosion.png", 1);
	sleighPart1Texture = getAtlasImage("gfx/sleighPart01.png", 1);
	sleighPart2Texture = getAtlasImage("gfx/sleighPart02.png", 1);
	santaTexture = getAtlasImage("gfx/santa.png", 1);

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

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

We're loading some new textures here, including the explosion texture, santa, and the sleigh parts, and our coal and gift textures.

That's all for player.c, which represents the largest update in this part. We'll now look at how these changes are used by other parts of the game. Starting first with chimney.c, where we've updated the `touch` function:


static void touch(Entity *self, Entity *other)
{
	Chimney *c;
	int      score;

	if (other->type == ET_GIFT || other->type == ET_COAL)
	{
		// snipped
	}
	else if (other == stage.player)
	{
		killPlayer(-1, -1);
	}
}

Now, as well as testing for collisions with gifts and coal, the chimney is testing whether the object that struck it (`other`) is the player. If so, we're calling killPlayer.

We've also updated the chimney's `die` function:


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

	c = (Chimney *)self->data;

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

		if (stage.player != NULL)
		{
			((Santa *)stage.player->data)->shudder = FPS / 2;
		}
	}
}

Now, as well as decreasing the level of Stage's xmasSpirit when a chimney passes, we're setting Santa's `shudder` value to half a second, making Santa shake.

Next up, we've made some changes to house.c. Starting with 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))
	{
		// snipped

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

		// snipped
	}
}

When creating a House (`e`) we're assigning the nearly added `touch` function to its function pointer:


static void touch(Entity *self, Entity *other)
{
	if (other == stage.player)
	{
		killPlayer(-1, -1);
	}
}

The `touch` function is simple enough - it just calls killPlayer if the thing that touched the house happens to be the player.

The last updates that we need to make are to stage.c itself. This is where our proper game loop will start to finally show up. First, to doStage:


void doStage(void)
{
	switch (stage.state)
	{
		case SS_GAME_OVER:
			stage.speed *= 1 - (0.01 * app.deltaTime);

			gameOverTimer -= app.deltaTime;
			break;

		default:
			stage.speed = INITIAL_GROUND_SPEED;
			break;
	}

	stage.pauseTimer = MAX(stage.pauseTimer - app.deltaTime, 0);

	if (stage.pauseTimer == 0)
	{
		doGround();

		addHouse();

		doHUD();

		doEntities();

		if (stage.xmasSpirit == 0 && stage.player != NULL)
		{
			killPlayer(-1, -1);
		}
	}
}

We're doing several new things here. First, we're testing the value of Stage's `state`. If we're in the game over state (SS_GAME_OVER), we're going to slowly bring the speed of the stage's scrolling to a halt, and also decrease the value of gameOverTimer (a local double in stage.c). Otherwise, we're going to maintain the speed of the stage to INITIAL_GROUND_SPEED.

Next, we're going to decrease the value of Stage's pauseTimer. If it's 0 (which it will be, unless we've updated it in killPlayer) then we'll call all our usual functions. We're also testing if we've run out of Xmas Spirit, and calling killPlayer if so. The reason we're doing this here and not in the player's `tick` function is due to a race condition - we want to check and call this kill function after all the entity processing (or at least after we've processed the player). Doing so earlier causes the player to instantly break apart with no explosion becoming visible.

Now for the changes to drawStage:


void drawStage(void)
{
	drawGround();

	drawEntities();

	drawHUD();

	if (stage.state == SS_GAME_OVER && stage.speed < 1)
	{
		blit(xmasCancelledTexture, SCREEN_WIDTH / 2, (SCREEN_HEIGHT / 2) - 100, 1, SDL_FLIP_NONE);
	}
}

One new addition here. We're testing whether we're in the game over state, and also if Stage's `speed` has dropped below 1. If so, we're going to draw our large "Xmas Cancelled" texture in (mostly) the middle of the screen. We're actually raising the `y` value up by 100 pixels, so that it doesn't completely obscure the houses on screen.

The very last change is to loadTextures:


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

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

	xmasCancelledTexture = loadTexture("gfx/xmasCancelled.png");
}

We're loading our xmasCancelledTexture.

Phew! That was a large part, definitely the largest so far. But we have completed the first part of our main game loop. We now just need a way to restart our game once we are hit by a game over, and our loop will be complete.

So, in the next part we're going to look into adding in our highscore table, and allowing the player to start the game again.

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