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

— Creating a vertical shoot 'em up —
Part 3: Power-ups

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

Introduction

In many shoot 'em ups, it is possible for the player to collect power-ups, in order to increase their firepower, up their speed, and gain new abilities. In this part of the tutorial, we'll look at how collecting a power-up token will result in the player gaining a pair of sidearms.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter2-03 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. A blue power-up token (that looks a bit like the one found in R-Type) will be falling down the screen towards the player. Fly into it to collect the token and gain the sidearm power-up. Firing your guns will now produce bullets from both the fighter and the two sidearms. Note that only one token appears, so if you do not collect it, you will need to restart the game. When you're finished, close the window to exit.

Inspecting the code

Supporting the power-up token and the sidearms in our game is rather easy, and we've not had to make any complex changes. We'll start with defs.h and structs.h. defs.h has had some new enums added:


enum {
	ET_PLAYER,
	ET_ALIEN,
	ET_POINTS,
	ET_SIDEARM,
	ET_POWER_UP_POD
};

We've added to ETs (Entity Types) to our enum list, ET_SIDEARM and ET_POWER_UP_POD, which will be used to specify the entity is a sidearm and power-up, respectively. We've also added in an enum to denote our power-up pod type:


enum {
	PP_SIDEARM,
	PP_MAX
};

PP_SIDEARM will tell us that the power-up pod is for the sidearm power-up. The reason for PP_MAX is because we'll want our power-up pod to change occasionally when it is active, giving the player the option (more like a short window of time) to pick which power-up they want. We'll see this in action when we come to define our Powerup entity.

Speaking of entities, let's look at structs.h, to see the changes there. Starting with a tweak to Fighter:


typedef struct {
	double reload;
	int reloadRate;
	double speed;
	int invokeSidearm;
} Fighter;

We've added in a flag called invokeSidearm. This will be used to invoke the sidearms when the player fires their guns. More on this later. Next, we've added in the Sidearm struct itself:


typedef struct {
	int ox;
} Sidearm;

The Sidearm struct contains just one field: `ox`. This variable, short of offset X, will be used to tell the Sidearm where it resides, relative to the player's Fighter. Finally, we've created a struct to represent the power-up itself:


typedef struct {
	int type;
	double changeTimer;
} PowerUpPod;

Two fields: the `type` and changeTimer. `type` will right now only be PP_SIDEARM, while changeTimer will be used (at a future time) to cycle between all power-up types.

We'll dive into some actual logic code now. Naturally, we've introduced some new files to support our power-ups and sidearms. We'll start by looking at sidearm.c, which will drive the logic for how our sidearms behave. The file has three functions. We'll start with initSidearm:


void initSidearm(int ox)
{
	Sidearm *s;
	Entity *e;

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

	s->ox = ox;

	if (sidearmTexture == NULL)
	{
		sidearmTexture = getAtlasImage("gfx/sidearm.png", 1);
		bulletTexture = getAtlasImage("gfx/playerBullet.png", 1);
	}

	e = spawnEntity(ET_SIDEARM);
	e->texture = sidearmTexture;
	e->data = s;

	e->tick = tick;

	tick(e);
}

This function will create an entity to act as a sidearm. Note how it accepts an `ox` parameter. The first thing we do is malloc and memset a Sidearm struct, then assign the `ox` value to the Sidearm's `ox`. Next, we're grabbing the textures for the Sidearm itself, as well as the bullet it will fire. Note that we need only test the sidearmTexture here, as we're grabbing both required textures at once. With that done, we'll spawn an Entity with the ET_SIDEARM type, assign it the sidearmTexture, and also set the Sidearm as its `data` field. We're then setting the Entity's `tick` to be that of the `tick` function in the file. Finally, we're calling `tick`. To see why this is, we'll look at what `tick` does right now:


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

	s = (Sidearm*) self->data;

	self->x = player->x;
	self->x += (player->texture->rect.w / 2);
	self->x -= (self->texture->rect.w / 2);
	self->x += s->ox;

	self->y = player->y + player->texture->rect.h;
	self->y -= self->texture->rect.h;

	if (((Fighter *)player->data)->invokeSidearm)
	{
		fireBullet(self);
	}
}

`tick` will first extract the Sidearm data from the Entity (`self`), and then align the sidearm entity to the player. We're first centering the sidearm over the player themselves, making use of the player's `x` coordinate, as well as the midpoints of the player and the sidearm, based on their texture widths. Finally, we're adding the Sidearm's `ox` value to the entity's `x`. Ultimately, this will mean that we're centering the sidearm over the player, but then shifting it according to the horizontal offset of `ox`. We're also vertically aligning the sidearm to the base of the Fighter, according to the player's texture's height.

Remember that `tick` is called for each entity on a loop. What all this means is that the sidearm will constantly be aligned to the player as they move around. Our first call to `tick` just helps to ensure that the first draw call of the sidearms makes it appear in the correct place.

Finally, we're extracting the Fighter data from the player, and testing whether the invokeSidearm flag is set. If so, we're firing a bullet. Again, we'll discuss the reason for why this is required when it comes to processing the player. The last function in sidearm.c is fireBullet:


static void fireBullet(Entity *self)
{
	Bullet *b;

	b = spawnBullet(player);
	b->texture = bulletTexture;
	b->x = self->x + (self->texture->rect.w / 2) - (bulletTexture->rect.w / 2);
	b->y = self->y - bulletTexture->rect.h;
	b->dy = -15;
}

Nothing unusual here, as it's rather the same as the fireBullet function in player.c (which means it could be refactored at some point). We're spawning a bullet, setting the bullet's texture, aligning it to the center-top of the sidearm, and setting its vertical velocity to -15.

Since we're in the context of working with the sidearm, let's look at what we've changed in player.c. We've merely updated the doPlayer function:


static void doPlayer(Entity *self)
{
	Fighter *f;

	f = (Fighter*) player->data;

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

	if (app.keyboard[SDL_SCANCODE_LEFT])
	{
		player->x -= f->speed * app.deltaTime;
	}

	if (app.keyboard[SDL_SCANCODE_RIGHT])
	{
		player->x += f->speed * app.deltaTime;
	}

	if (app.keyboard[SDL_SCANCODE_UP])
	{
		player->y -= f->speed * app.deltaTime;
	}

	if (app.keyboard[SDL_SCANCODE_DOWN])
	{
		player->y += f->speed * app.deltaTime;
	}

	if (app.keyboard[SDL_SCANCODE_LCTRL] && f->reload == 0)
	{
		fireBullet();

		f->reload = f->reloadRate;
		f->invokeSidearm = 1;
	}

	player->x = MIN(MAX(player->x, 0), SCREEN_WIDTH - player->texture->rect.w);
	player->y = MIN(MAX(player->y, 0), SCREEN_HEIGHT - player->texture->rect.h);
}

Now, as well as decreasing the Fighter's `reload`, we're also resetting the invokeSidearm flag to 0. The other change we've made is when we test if the player has fired. Now, not only do we issue the bullet and reset the Fighter's `reload`, we also set the invokeSidearm flag to 1. The reason we're doing this is because the sidearms need to be told to fire when we do. However, the sidearms can't test the left control key and the Fighter's `reload` state, as when we fire, we reset the Fighter's `reload` to full. This means that the sidearms, due to their position in the entity queue, would never be able to fire, as the conditions for firing a bullet would never hold true. As such, we set a flag to tell them that they may do so, and clear it at the beginng of the player's logic step.

That's our sidearms handled, but what about the PowerUpPod itself, the thing we collect to grant us the additional firepower? That's defined in powerUpPod.c, a file consisting of 4 functions. Well start, as ever, with the init function, known here as addPowerUpPod:


void addPowerUpPod(int x, int y, int type)
{
	PowerUpPod *p;
	Entity *e;

	p = malloc(sizeof(PowerUpPod));
	memset(p, 0, sizeof(PowerUpPod));

	p->type = type;
	p->changeTimer = CHANGE_TIMER;

	e = spawnEntity(ET_POWER_UP_POD);
	e->x = x;
	e->y = y;
	e->data = p;

	e->tick = tick;

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

	updateTexture(e, p);
}

The addPowerUpPod function takes three parameters: the `x` and `y` coordinates that it will appear at, and the type. Creating our PowerUpPod is quite similar to other entities in our game. We malloc a PowerUpPod, and then set its `type` and starting changeTimer value. CHANGE_TIMER is defined as 3 seconds in our header (FPS * 3). Next, we create the base entity and assign the `x` and `y` coordinates, as well as the data field using the PowerUpPod. We also assign the `tick` function and then test if we need to fetch sidearmPodTexture. After that, we'll call updateTexture.

We'll look at the `tick` function next:


static void tick(Entity *self)
{
	PowerUpPod *p;

	p = (PowerUpPod*) self->data;

	p->changeTimer -= app.deltaTime;

	if (p->changeTimer <= 0)
	{
		p->type = (p->type + 1) % PP_MAX;

		p->changeTimer = CHANGE_TIMER;

		updateTexture(self, p);
	}

	if (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))
	{
		activatePowerUp(self, p);
	}

	self->y += 3 * app.deltaTime;

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

The `tick` function will be responsible for rotating the type of power-up, as well as checking if the player has collected it. We start by extracting the PowerUpPod from the entity `data`, and then decrease its changeTimer value. If the changeTimer falls to 0 or less, we'll change the type of power up. We'll do this by incrementing the PowerUpPod's `type` by 1, then calling the modulo of that value using PP_MAX, to ensure it stays within the range of allowed types. With that done, we reset the changeTimer, and then call updateTexture.

The next thing we want to do is test whether the player has collided with the pod. If it has, we'll call activatePowerUp, to make use of whatever ability it wants to grant up. Finally, we'll alway increase the `y` value of the entity, to move the PowerUpPod down the screen. If it moves off the bottom of the screen, we'll set its `health` to 0, to remove it.

The updateTexture function comes next. As we've already seen, it is called by the addPowerUpPod and `tick` functions. What it does is quite simple (and you've likely already guessed, based on the name of the function):


static void updateTexture(Entity *e, PowerUpPod *p)
{
	switch (p->type)
	{
		case PP_SIDEARM:
			e->texture = sidearmPodTexture;
			break;

		default:
			break;
	}
}

The function takes two arguments - the entity and the PowerUpPod. Although the entity already contains the PowerUpPod in it's data structure, we have already extracted it in both instances that updateTexture is called, so we can just pass it in. The function performs a switch against the PowerUpPod's type, then, depending on what type it is, it will set the appropriate texture for the entity.

Our activatePowerUp is the last function to look at. Like updateTexture, it takes two parameters:


static void activatePowerUp(Entity *self, PowerUpPod *p)
{
	switch (p->type)
	{
		case PP_SIDEARM:
			initSidearm(-48);
			initSidearm(48);
			break;

		default:
			break;
	}

	self->health = 0;
}

Again, we're passing in the PowerUpPod entity and the PowerUpPod itself. We're then testing the PowerUpPod's `type`, to determine what kind of power up it is. We only have one right now, PP_SIDEARM, so that's the one we'll use. We're calling initSidearm twice, to create two sidearms, the first positioned to the left of the Fighter (offset at -48 from center), and the other to the right (offset at 48 from the center). With that done, we set the PowerUpPod's entity's health to 0, to remove it.

All in all, this means that when the player flies into the PowerUpPod, they will gain two sidearms, on the left and right of the fighter. Note how there's potential here to keep accumulating sidearms and power ups. We'll fix that in a later tutorial. For now, we only have one power up type, and our demo only has the one power up token.

Speaking of the PowerUpPod, we should look to see how it's created. Well, right now we're just hardcoding it into initStage:


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

	initEntities();

	initPlayer();

	initStars();

	initBullets();

	initWave();

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

	backgroundY = -SCREEN_HEIGHT;

	addPowerUpPod(SCREEN_WIDTH / 2, -50, PP_SIDEARM);

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

We're calling addPowerUpPod, setting it to (roughly) halfway across the screen, -50 pixels from the top of the screen, and with a type of PP_SIDEARM. Again, this means that there will only be one PowerUpPod available for this part of the demo.

We now have our first power-up, so the aliens don't stand a chance! Something you may have now noticed is that it's all too easy to clear the screen of aliens, and that there are an awful lot of PointsPods floating around. This is something we'll be tweaking later on, to make it so that PointsPods will only be dropped if the player completely destroys a wave. We'll obviously be adding in new enemy types and waves, too. Our next part will focus on this, as well as making the enemies fire back and also randomizing the wave forms a bit more.

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