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


A North-South Divide

For over a hundred years, messenger Duncan has wandered the world, searching for the missing pieces of an amulet that will rid him of his curse; a curse that has burdened him with an extreme intolerance of the cold, an unnaturally long life, and the despair of watching all he knew and loved become lost to the ravages of time. But now, Duncan is close to the end of his long quest.

Click here to learn more and read an extract!

« Back to tutorial listing

— Creating a vertical shoot 'em up —
Part 5: Enemy attack patterns (full sequence)

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

Introduction

So far, we only have one style of enemy attack pattern - a snake-like spiral that starts from the top of the screen. In this tutorial, we'll add in some new enemy attack patterns and also restore the absent power-up.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter2-05 to run the code. You will see a window open like the one above. Use the arrow keys to move the fighter around, and the left control key to fire. Dodge the enemy fire and shoot the aliens. Every now and again, a special enemy will appear at the top of the screen (a supply ship). Shoot it to gain a power-up pod. When you're finished, close the window to exit.

Inspecting the code

This is a long part, so buckle up! Again, we've added in some new enemy types, and also made several tweaks and updates to the existing code. You will have noticed, for example, that some of the enemies now require more than one hit to kill them, and that they also flash when they are hit. We'll look at structs.h first, to see what we've added in:


struct Entity {
	int type;
	double x;
	double y;
	AtlasImage *texture;
	int health;
	void (*data);
	void (*tick)(Entity *self);
	void (*draw)(Entity *self);
	void (*takeDamage)(Entity *self, int amount);
	void (*die)(Entity *self);
	Entity *next;
};

We've added two new function pointers to our Entity: `draw` and takeDamage. `draw` will be used for special draw functions for certain entities, while takeDamage will be used to figure out what should happen when a bullet collides with an alien.

We've also created two new structs for our aliens. The first is SwoopingAlien:


typedef struct {
	double swoop;
	double swoopAmount;
	double startDelay;
	double reload;
	double dx;
	double damageTimer;
} SwoopingAlien;

This is the little blue alien that starts at the top of the screen and moves up and down in a U shape. The other one is StraightAlien:


typedef struct {
	double startDelay;
	double reload;
	double dx;
	double dy;
	double damageTimer;
} StraightAlien;

StraightAliens are the little grey aliens that move from left-to-right and right-to-left across the screen. We also have a struct defined for our supply ship:


typedef struct {
	double dx;
	double damageTimer;
} SupplyShip;

It's quite a lot simplier than the other aliens, as it just moves either left or right across the top of the screen. Finally, we've updated Stage:


typedef struct {
	Entity entityHead, *entityTail;
	Bullet bulletHead, *bulletTail;
	int hasAliens;
	int numWaveAliens;
	int score;
} Stage;

Yes, the hasAliens flag is back. This is due to a bug in the previous tutorial that could result in the PointsPod being awarded for destroying the last alien in a wave, but not the wave entirely. We'll see how this is fixed later on. For now, let's look at the first of our new alien types.

Everything for handling our SwoopingAlien is done in swoopingAlien.c. There are several functions in this file, so we'll tackle them one at a time, starting with initSwoopingAlien:


void initSwoopingAlien(int startDelay, int x, double dx, double swoopAmount)
{
	Entity *e;
	SwoopingAlien *s;

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

	s->startDelay = startDelay;
	s->swoopAmount = swoopAmount;
	s->dx = dx;

	if (littleBlueAlienTexture == NULL)
	{
		littleBlueAlienTexture = getAtlasImage("gfx/littleBlueAlien.png", 1);
		bulletTexture = getAtlasImage("gfx/alienDownBullet.png", 1);
	}

	e = spawnEntity(ET_ALIEN);
	e->texture = littleBlueAlienTexture;
	e->data = s;

	e->x = x - (e->texture->rect.w / 2);
	e->y = 0;
	e->health = 2;

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

What's happening here should look quite familiar to you, as it's quite similar to SwingingAlien. We're passing over parameters such as the startDelay, the `x` position, `dx` (the speed at which it moves along the x-axis), and a variable called swoopAmount, to govern the depth of the swoop. We're mallocing a SwoopingAlien and setting all the appropriate variables, as well as grabbing the alien's `texture` and the bullet texture. With that done, we create our Entity as usual. Note two new things going on here, compared to before. First, our SwoopingAlien has its `health` set to 2. It will take two shots to kill, as our bullets cause 1 health damage per hit. The other thing that's new is that we're setting two additional function pointers - `draw` and takeDamage. We'll discuss these in a bit.

For now, let's look at the `tick` function:


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

	s = (SwoopingAlien*) self->data;

	s->startDelay -= app.deltaTime;

	if (s->startDelay <= 0)
	{
		s->swoop += s->swoopAmount * app.deltaTime;

		self->y += sin(s->swoop);
		self->x += s->dx * app.deltaTime;
	}

	if ((s->dx > 0 && self->x >= SCREEN_WIDTH) || (s->dx < 0 && self->x <= -self->texture->rect.w))
	{
		self->health = 0;
	}

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

	if (s->reload == 0)
	{
		if (rand() % 5 == 0)
		{
			fireBullet(self);
		}

		s->reload = FPS;
	}

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

	if (player->health > 0 && collision(self->x, self->y, self->texture->rect.w, self->texture->rect.h, player->x, player->y, player->texture->rect.w, player->texture->rect.h))
	{
		self->health = 0;
		self->die(self);

		player->health = 0;
		player->die(player);
	}

	stage.hasAliens = 1;
}

We won't go into full details on this one, as, again, it's similar to SwingingAlien. After our startDelay timer has expired, the SwoopingAlien's `x` is moved along by its `dx`, while its `y` is adjusted by the sin of its `swoop` value (which inturn was updated by the swoopAmount). If our SwoopingAlien was moving right, its health will be set to 0 if it leaves the right-hand side of the screen. If it was moving left, it will be removed after it fully leaves the left-hand side. It will also fire randomly, when it is given the chance, and kill the player if it comes into contact with them. As mentioned earlier, our hasAliens field in Stage has also returned, which we're setting to 1, to tell the main loop that aliens are present (this was an oversight in the previous change - it happens and is all part of the development process).

Something new is the damageTimer field. We're decreasing this each call to `tick` and limiting it to 0. You will have noticed that the SwoopingAliens flash white for a moment when they are hit, to indicate damage. What the damageTimer field does is tell us that the alien was just hit and allows us to draw things a little different. This is why we now have a `draw` function pointer. To illustrate the damageTimer, let's look at it now:


static void draw(Entity *self)
{
	SwoopingAlien *s;

	s = (SwoopingAlien*) self->data;

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

	if (s->damageTimer > 0)
	{
		SDL_SetTextureBlendMode(self->texture->texture, SDL_BLENDMODE_ADD);
		blitAtlasImage(self->texture, self->x, self->y, 0, SDL_FLIP_NONE);
		SDL_SetTextureBlendMode(self->texture->texture, SDL_BLENDMODE_BLEND);
	}
}

The `draw` function is rendering the SwoopingAlien as normal, but then doing something more. It's checking to see if the damageTimer of the SwoopingAlien is greater than 0. If it is, we're drawing the alien again, but this time with additive blending. The result looks something like the image below (mocked up in GIMP):

What this means is that when we draw the image, all the colours of the pixels below it will be added to, increasing their RGB values (and shifting towards pure white). For us, it means that our alien's image will brighten while damageTimer is greater than 0. This gives us pleasing visual feedback on a hit being scored.

The damageTimer value is set in the new takeDamage function:


static void takeDamage(Entity *self, int amount)
{
	self->health -= amount;

	if (self->health == 0)
	{
		self->die(self);
	}

	((SwoopingAlien*) self->data)->damageTimer = 8;
}

This function takes two parameters - the entity that has received the damage, and the amount of damage (as `amount`). We're subtracting the `amount` from the entity's `health` and calling the entity's `die` function if its `health` has fallen to 0 or less. This was once done in bullets.c whenever an alien was hit by a bullet. Doing it here gives us more flexibilty on how we want to handle such things.

Finally, we come to the `die` function, that we've tweaked since last time:


static void die(Entity *self)
{
	stage.score++;

	addExplosion(self->x + (self->texture->rect.w / 2), self->y + (self->texture->rect.h / 2));

	if (--stage.numWaveAliens == 0)
	{
		addPointsPod(self->x, self->y);
	}
}

A single point is now awarded for an alien being killed. We're still generating an explosion, but we've updated the conditions upon which a PointsPod is released. Now, we're decrementing a variable called numWaveAliens in Stage. If this hits zero, we're releasing a PointsPod. This ultimately means that bonus points will be made available for eliminating the entire wave of aliens. Our previous attempt at this featured a bug where this wasn't necessary, so this new approach fixes that issue.

That's our SwoopingAlien done, so now we can look at the StraightAlien, the ones that move (mostly) in a straight line across the screen. Our StraightAlien is handled by straightAlien.c. Like SwoopingAlien, it contains a good deal of functions and is quite similar in behaviour. We won't cover all the functions, as they're near identical, other than a few tweaks. Instead, we'll focus on the specifics, starting with initStraightAlien:


void initStraightAlien(int startDelay, int x, int y, double dx, double dy)
{
	Entity *e;
	StraightAlien *s;

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

	s->startDelay = startDelay;
	s->dx = dx;
	s->dy = dy;

	if (littleGreyAlienTexture == NULL)
	{
		littleGreyAlienTexture = getAtlasImage("gfx/littleGreyAlien.png", 1);
		bulletTexture = getAtlasImage("gfx/alienDownBullet.png", 1);
	}

	e = spawnEntity(ET_ALIEN);
	e->texture = littleGreyAlienTexture;
	e->data = s;

	e->x = x - (e->texture->rect.w / 2);
	e->y = y;
	e->health = 2;

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

No surprises here. The function takes `x` and `y` coordinates, as well as `dx` and `dy` variables, to tell the alien how it will move. We're creating our StraightAlien and setting all its variables, as well as grabbing the required textures. Again, notice how the health is set to 2, to make the alien require two hits to kill. We're also setting the `draw` and takeDamage function pointers.

Our `tick` function won't appear all that foreign, either:


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

	s = (StraightAlien*) self->data;

	s->startDelay -= app.deltaTime;

	if (s->startDelay <= 0)
	{
		self->x += s->dx * app.deltaTime;
		self->y += s->dy * app.deltaTime;
	}

	if ((s->dx < 0 && self->x < -75) || (s->dx > 0 && self->x > SCREEN_WIDTH))
	{
		self->health = 0;
	}

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

	if (s->reload == 0)
	{
		if (rand() % 5 == 0)
		{
			fireBullet(self);
		}

		s->reload = FPS;
	}

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

	if (player->health > 0 && collision(self->x, self->y, self->texture->rect.w, self->texture->rect.h, player->x, player->y, player->texture->rect.w, player->texture->rect.h))
	{
		self->health = 0;
		self->die(self);

		player->health = 0;
		player->die(player);
	}

	stage.hasAliens = 1;
}

We're moving the alien once its startDelay has expired. Notice how we're adjusting the entity's `x` and `y` variables using `dx` and `dy`. Our alien doesn't move entirely in a straight line and might creep up and down slightly. This is merely to add a bit of variety to the pattern. Like the SwoopingAlien, our StraightAliens will be removed if they move off the edge of the screen, depending on the direction they are going. Everything else remains the same as the other aliens (including the `draw` and takeDamage functions).

As can be seen, adding in new alien types is largely a case of creating a struct to hold custom data, and creating init and `tick` functions to deal with them. It should be noted that SwingingAlien now also features `draw` and takeDamage functions, as its behaves in the same manner as these two new updates. So as not to get bogged down, we'll not look at those changes.

With our new aliens defined, we can look at how they're implemented into the attack waves system. Looking at wave.c, there are a few changes having been made. Starting with nextWave:


void nextWave(void)
{
	if (!setupNextWave)
	{
		setupNextWave = 1;

		waveStartTimer = FPS;
	}
	else
	{
		waveStartTimer = MAX(waveStartTimer - app.deltaTime, 0);

		if (waveStartTimer <= 0)
		{
			srand(waveSeed);

			switch (rand() % 3)
			{
				case 0:
					addSwingingAliens();
					break;

				case 1:
					addSwoopingAliens();
					break;

				case 2:
					addStraightAliens();
					break;

				default:
					break;
			}

			waveSeed = rand() % 99999;

			setupNextWave = 0;
		}
	}
}

As we now have three different alien types, we've added in a switch statement. Based on the result of a random of 3 (0-2), we'll either create SwingingAliens, SwoopingAliens, or StraightAliens. Remember that we are using a random seed for our waves, so the waves will always be the same each time. Our current game starts with a wave of SwingingAliens, then two waves of StraightAliens, then a wave of SwoopingAliens.

Let's look at what addSwoopingAliens does:


static void addSwoopingAliens(void)
{
	int i, n, delay, x;
	double dx, swoopAmount;

	n = 5 + rand() % 8;
	delay = 25;
	dx = (1.0 * (50 + rand() % 150)) * 0.01;
	swoopAmount = (1.0 * (100 + rand() % 100)) * 0.0001;
	x = -75;

	if (rand() % 2 == 0)
	{
		dx = -dx;
		x = SCREEN_WIDTH + 75;
	}

	for (i = 0 ; i < n ; i++)
	{
		initSwoopingAlien(i * delay, x, dx, swoopAmount);
	}

	stage.numWaveAliens = n;
}

You'll notice it's rather like addSwingingAlien. We set a variable called `n` to determine how many aliens we want to create (between 5 and 12), space the aliens with a `delay` of 25, set a random x speed (`dx`), a random swoopAmount, and an `x` position of -75, so the aliens start on the left-hand side of the screen (though they will appear offscreen). With all those variables set, we're then testing a random of 2. If the value is 0 (50% chance), we're negating the value of `dx` and also setting `x` to the width of the screen, plus 75. What this means is that there's a 50% chance that our SwoopingAliens will move from right to left, starting at the right-hand side of the stage.

We then create all our SwoopingAliens, using a for-loop up to the value of `n`, passing in all the parameters we defined earlier. Finally, we're setting numWaveAliens to the value of `n`. This will let us track how many aliens there are in the wave, and allow us to later test whether we've destroyed all of them to earn some bonus points.

Our addStraightAliens function is much the same:


static void addStraightAliens(void)
{
	int n, i, x, y, delay;
	double dx, dy;

	n = 10 + rand() % 6;
	delay = 10 + rand() % 20;
	dx = (1.0 * (100 + rand() % 300)) * 0.02;
	dy = (1.0 * (rand() % 30)) * 0.01;
	x = -75;
	y = (SCREEN_HEIGHT * 0.1) + (SCREEN_HEIGHT * 0.25);

	if (rand() % 2 == 0)
	{
		x = SCREEN_WIDTH + 75;
		dx = -dx;
	}

	if (rand() % 2 == 0)
	{
		dy = -dy;
	}

	for (i = 0 ; i < n ; i++)
	{
		initStraightAlien(i * delay, x, y, dx, dy);
	}

	stage.numWaveAliens = n;
}

Once again, we're defining a random number of aliens (`n` - between 10 and 15), setting a random delay, random `dx` and `dy` values, a fixed horizontal starting position of -75, and a random `y` value of between 1/10th and 1/4 of the way down the screen. With that done, we're randomly flipping the starting position to the right-hand side of the screen, and also randomly negating the `dy` value. The aliens are created in the for-loop, and Stage's numWaveAliens are set to the same value as `n`.

All this means that, like our SwingingAliens, there is a bit of randomization to the StraightAliens and SwoopingAliens, so that they do not always start from the same place and don't move in exactly the same way. The variations will be somewhat minor, but it does stop things from becoming too predictable.

Something that we've not looked at yet is the SupplyShip. It appears at a fixed interval and moves from left-to-right (or right-to-left) across the top of the screen. Its functions are defined in supplyShip.c. The file looks rather similar to the aliens we've seen already, but with a few minor differences. Let's look at initSupplyShip:


void initSupplyShip(void)
{
	Entity *e;
	SupplyShip *s;

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

	if (supplyShipTexture == NULL)
	{
		supplyShipTexture = getAtlasImage("gfx/supplyShip.png", 1);
	}

	e = spawnEntity(ET_ALIEN);
	e->y = 50;
	e->health = 5;
	e->texture = supplyShipTexture;
	e->data = s;

	if (rand() % 2 == 0)
	{
		e->x = -supplyShipTexture->rect.w;
		s->dx = 3;
	}
	else
	{
		e->x = SCREEN_WIDTH;
		s->dx = -3;
	}

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

Unlike the aliens, we're not passing any parameters into this function; we don't need to, as the SupplyShip is a single alien that doesn't need to consider its position in a group. To start with, we're creating a SupplyShip, mallocing and memsetting it. We're then grabbing its texture, if needed. We're then creating the actual Entity, with a type of ET_ALIEN. The SupplyShip always appears at the top of the screen, so we can fix the `y` position. Our SupplyShip also have 5 health! Yes, it's well defended. Better than the aliens themselves. We then set the Entity's `texture` and `data` fields.

We then want to choose where the SupplyShip starts from, and so there's a 50-50 chance it will start on the left and move right or start on the right and move left. With that decided, we then set our `tick`, `draw`, takeDamage, and `die` function pointers. The draw and takeDamage functions are basically the same as for the other aliens, so we need only consider the `tick` and `die` functions. The `tick` function won't come as a shock:


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

	s = (SupplyShip*) self->data;

	self->x += (s->dx * app.deltaTime);

	if ((s->dx < 0 && self->x < -75) || (s->dx > 0 && self->x > SCREEN_WIDTH))
	{
		self->health = 0;
	}

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

	if (player->health > 0 && collision(self->x, self->y, self->texture->rect.w, self->texture->rect.h, player->x, player->y, player->texture->rect.w, player->texture->rect.h))
	{
		self->health = 0;
		self->die(self);

		player->health = 0;
		player->die(player);
	}
}

The SupplyShip is moving along the x axis according to its `dx` value. Once it reaches the other side of the screen, we're removing it. damageTimer is being decremented and we're testing to see if the player has collided with it. The `die` function is different to the other aliens, however:


static void die(Entity *self)
{
	addExplosion(self->x + (self->texture->rect.w / 2), self->y + (self->texture->rect.h / 2));

	addPowerUpPod(self->x + (self->texture->rect.w / 2), self->y + (self->texture->rect.h / 2));
}

We're generating an explosion, as expected. However, no score increase occurs. Instead, we're calling addPowerUpPod, feeding in the midpoint location of the SupplyShip as parameters. In other words, once the SupplyShip is destroyed, it will drop a power-up for us.

Our SupplyShips don't form part of any waves; they're independent and arrive at a mostly fixed interval. If we look at stage.c, we can see where this is happening in initStage:


void initStage(void)
{
	memset(&stage, 0, sizeof(Stage));

	initEntities();

	initPlayer();

	initStars();

	initBullets();

	initEffects();

	initWave();

	background = loadTexture("gfx/background.jpg");

	backgroundY = -SCREEN_HEIGHT;

	gameOverTimer = FPS * 2;

	nextSupplyShipTimer = SUPPLY_SHIP_INTERVAL / 2;

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

We have a new variable called nextSupplyShipTimer, set here to half the value of SUPPLY_SHIP_INTERVAL. SUPPLY_SHIP_INTERVAL is set at FPS * 30 in stage.h, which means that it will turn up after 30 seconds. To begin with, we're going to halve that to 15 seconds, so that the first SupplyShip shows up sooner.

If we now look at the `logic` function, we can see where the nextSupplyShipTimer is used:


static void logic(void)
{
	// snipped

	nextSupplyShipTimer -= app.deltaTime;

	if (nextSupplyShipTimer <= 0)
	{
		initSupplyShip();

		nextSupplyShipTimer = SUPPLY_SHIP_INTERVAL;
	}

	// snipped
}

We're decreasing the value of nextSupplyShipTimer at each `logic` call. If the value falls to 0 or less, we're going to call initSupplyShip, to create a new SupplyShip and have it crawl across the top of the screen. We'll also update nextSupplyShipTimer to the value of SUPPLY_SHIP_INTERVAL. This means that a SupplyShip will show up every 30 seconds.

That's it for the new aliens and attack waves. Before finishing, we'll have a look over the other things that have been added to the code. entities.c has seen the drawEntities function updated:


void drawEntities(void)
{
	Entity *e;

	for (e = stage.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->draw != NULL)
		{
			e->draw(e);
		}
		else
		{
			blitAtlasImage(e->texture, e->x, e->y, 0, SDL_FLIP_NONE);
		}
	}
}

Since entities can have their own `draw` function, we're now testing to see if its been set, before calling it. If not, we'll render the entity using a standard blitAtlasImage call. As we saw earlier, the custom `draw` call is being used by the aliens and supply ship to make them flash when taking damage.

doAlienCollisions in bullets.c has also been updated:


static void doAlienCollisions(Bullet *b)
{
	Entity *e;

	for (e = stage.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->type == ET_ALIEN && collision(e->x, e->y, e->texture->rect.w, e->texture->rect.h, b->x, b->y, b->texture->rect.w, b->texture->rect.h))
		{
			e->takeDamage(e, 1);

			b->dead = 1;

			addSmallExplosion(b->x + (b->texture->rect.w / 2), b->y + (b->texture->rect.h / 2));
		}
	}
}

Now, we're calling the entity's takeDamage function, passing over the entity itself and an amount of 1.

The last thing we should look at, briefly, is the highscores. Up until now, our highscore was being reset each time the stage restarted, which wasn't ideal. There is now a highscore.c file in the game directory. It will be used to manage all our highscore functions. We've not got a fully working highscore table just yet, but we will cover the basics nonetheless. We've created a Highscore struct to structs.h to hold our highscores:


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

It's simple, consisting of just a name and a score (MAX_NAME_LENGTH is defined in defs.h as 32). We also added a Game struct, to hold our highscores:


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

`highscores` is an array of Highscores (NUM_HIGHSCORES is defined as 11 in defs.h). The Game struct will be expanded in future, to include things such as sound configuration options, etc. For now, it just holds our highscores. Returning to highscores.c, we'll start with initHighScores:


void initHighscores(void)
{
	int i;

	for (i = 0 ; i < NUM_HIGHSCORES ; i++)
	{
		memset(&game.highscores[i], 0, sizeof(Highscore));

		STRCPY(game.highscores[i].name, "Anonymous");
		game.highscores[i].score = 550 - (i * 50);
	}
}

We're looping through all our highscores, memsetting each one, setting the name to Anonymous, and then setting the score to 500 minus i * 50. So, the scores will be 500, 450, 400, 350, etc. Next, we have a function called updateHighscores. This is called in stage.c whenever the player is killed and the game resets:


void updateHighscores(void)
{
	game.highscores[NUM_HIGHSCORES - 1].score = stage.score;

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

We're setting the value of the final score in the array list to the score that was achieved by the player. Now, here comes the fun bit - our highscores array is 11 items in length. However, we're only going to recognise the first 10 scores. This means that the score in position 11 will never be displayed. We're therefore always overwriting it with the score the player earned. We're then sorting all the scores in the array, using `qsort`. This will result in the highscores array being reordered so that the highest score is in position 0 and the lowest score in position 10. Effectively, this means that the lowest score will never been displayed (when it comes to displaying our highscore table that is).

Our sorting logic is defined in highscoreComparator:


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

	return h2->score - h1->score;
}

A standard number sorting function. Returning the difference between the two scores will mean that scores that are higher will be pushed up the list.

Finally, the drawHud function in hud.c has been updated to make use of the highscore list:


void drawHud(void)
{
	char text[16];

	sprintf(text, "Score: %03d", stage.score);
	drawText(text, 10, 0, 255, 255, 255, TEXT_ALIGN_LEFT, 0);

	if (stage.score > game.highscores[0].score)
	{
		sprintf(text, "Highscore: %03d", stage.score);
		drawText(text, SCREEN_WIDTH - 10, 0, 128, 255, 128, TEXT_ALIGN_RIGHT, 0);
	}
	else
	{
		sprintf(text, "Highscore: %03d", game.highscores[0].score);
		drawText(text, SCREEN_WIDTH - 10, 0, 255, 255, 255, TEXT_ALIGN_RIGHT, 0);
	}
}

We're now testing to see if the player's score (stage.score) is higher than the best (game.highscore[0]). If so, we're drawing the highscore value as the player's score in a pale green, to show it's overtaken first place. Otherwise, we're rendering the value of the game.highscore[0] in white. Due to our highscore init and update functions, we can be confident that the first entry in the highscores array will be the highest value.

Phew! That was a lot to get through. But we now have many new features - new alien waves, a supply ship carrying power-ups, the ability to extend our entities with new function pointers, and the start of a highscore table. We also got our power-up back, giving us extra guns. It would be nice to be able to turn that firepower against something more meaty, don't you think? Well, in the next part, we'll look at introducing bosses to the mix, who will show up after several waves of enemies.

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:

Mobile site