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

Latest Updates

SDL2 Gunner tutorial
Thu, 26th August 2021

SDL2 Shooter 2 tutorial
Tue, 13th July 2021

SDL2 Widget tutorial
Fri, 18th June 2021

SDL2 Adventure tutorial
Tue, 8th June 2021

New tutorials
Tue, 11th May 2021

All Updates »

Tags

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

Books


Alysha

When her village is attacked and her friends and family are taken away to be sold as slaves, Alysha Tanner sets out on a quest across the world to track them down and return them home. Along the way, she is aided by the most unlikely of allies - the world's last remaining dragon.

Click here to learn more and read an extract!

« Back to tutorial listing

— Creating a Run and Gun game —
Part 8: Player health + power-ups

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

Introduction

The next step to making our game is to introduce some power-ups for the player to collect. We're going to start with adding in some health packs, to restore the player's health, as well as a gun power-up. While these two additions seem simple on the surface, we've had to add in a good bit of new code in order to support them.

Extract the archive, run make, and then use ./gunner08 to run the code. You will see a window open like the one above. The main player character starts off at the top left-hand corner. The same controls from past tutorials apply. The player's health is now displayed in the top left-hand corner of the screen, as a series of 5 green blocks. The blocks will empty as the player takes damage and will be restored when the player collects health. A spread gun power-up can be found in an alcove just below where the player starts, guarded by an enemy. Once you're finished, close the window to exit.

Inspecting the code

There's a lot of new code to get through, so we'll jump straight in. Starting with defs.h:


#define MAX_PLAYER_LIFE        5

// snipped

enum {
	WPN_NORMAL,
	WPN_SPREAD
};

First, we've added in a define called MAX_PLAYER_LIFE, to determine the maximum health the player can have (5 units). We've also dropped in a new enum, to specify the types of weapons available, each prefixed with WPN_.

Next, we've updated structs.h, with a change to the Gunner struct:


typedef struct {
	int frame;
	int ducking;
	int aimingUp;
	int life;
	int weaponType;
	double animTimer;
	double reload;
	double immuneTimer;
} Gunner;

We've added in two new fields: `life` and weaponType. `life`, as you may have already guessed, is the amount of health the player has remaining. weaponType will determine the type of weapon that the player currently has and what will happen when we fire.

We'll look at the changes we've made to the rest of the code now. To keep things simple, we'll first tackle what we've done to support the player's health, then the new weapon, so as we don't get things jumbled up and keep hopping back and forth constantly. Turning first to player.c, we've updated initPlayer:


void initPlayer(Entity *e)
{
	// snipped

	g = malloc(sizeof(Gunner));
	memset(g, 0, sizeof(Gunner));

	g->animTimer = ANIM_TIME;
	g->life = MAX_PLAYER_LIFE;
	g->weaponType = WPN_NORMAL;

	// snipped
}

Now, when creating the Gunner object for the player, we're setting the `life` and weaponType defaults to MAX_PLAYER_LIFE (5) and WPN_NORMAL, respectively.

As the player now has health, we should next make changes to takeDamge:


static void takeDamage(Entity *self, int amount, Entity *attacker)
{
	Gunner *g;

	g = (Gunner*) self->data;

	if (g->immuneTimer == 0)
	{
		g->life -= amount;

		g->immuneTimer = FPS * 1.5;
	}
}

Before, takeDamage used to just set the Gunner's immuneTimer. We're now actually applying the damage that was passed into the function. We're first extracting the Gunner data from the entity, then checking to see if the immuneTimer is 0. If so, we're deducting `amount` from the Gunner's `life`, and then setting the immuneTimer to one and a half seconds. The idea behind this is that when the player is hit, they become immune to damage for a short period. We're testing to see if the player is immune before appling the damage. If they are immune (immuneTimer being a non-zero value) we're ignoring the damage (but also not resetting immuneTimer).

That's all that's needed in order to handle the player's health. We can now look at how we're displaying it. We've created a new file called hud.c, to hold all our display information. There are a number of functions in the file, so we'll work our way through from top to bottom. Starting with initHud:


void initHud(void)
{
	if (playerLifeBlock == NULL)
	{
		playerLifeBlock = getAtlasImage("gfx/hud/playerLifeBlockFull.png", 1);

		playerLifeBlockEmpty = getAtlasImage("gfx/hud/playerLifeBlockEmpty.png", 1);

		playerLifeBlockWarning = getAtlasImage("gfx/hud/playerLifeBlockWarning.png", 1);
	}

	playerLifeWarningTimer = 0;
}

Nothing complicated happening here. We're testing if a texture called playerLifeBlock is NULL and then loading three required textures if so. playerLifeBlock will be the full green rectangular life block seen on screen. playerLifeBlockEmpty is the texture that will be drawn when a player's health is below the maximum, while playerLifeBlockWarning is a texture that will show intermittently when the player's health is dangerously low (i.e., 1 life point remaining). With our textures loaded, we also set a variable called playerLifeWarningTimer to 0. This variable will be used to control how often we show the warning block.

The next function is doHud. This function is used to drive any logic that the hud needs to process:


void doHud(void)
{
	playerLifeWarningTimer += app.deltaTime;

	if (playerLifeWarningTimer >= FPS)
	{
		playerLifeWarningTimer = 0;
	}
}

There's not a lot to it. We're increasing the value of playerLifeWarningTimer and the resetting it to 0 if it is equal to or greater than FPS (60).

drawHud is the next function to look at. It is just as simple:


void drawHud(void)
{
	drawPlayerLife();

	if (0)
	{
		drawDebugInfo();
	}
}

We're calling out to another function here: drawPlayerLife. We can also optionally draw some debugging information here, with a call to drawDebugInfo. Note that the reason this is wrapped in an if-statement rather than being commented out is due to the compiler options in the make file. Warnings are converted into errors, preventing compiling from finishing. If we comment out drawDebugInfo here, the compiler will tell us the function is unused and halt. So, this is just a way around it.

Moving on, we can look at drawPlayerLife. This is more interesting:


void drawPlayerLife(void)
{
	int x, i;
	Gunner *g;
	AtlasImage *blockTexture;

	x = 20;

	g = (Gunner*) stage.player->data;

	for (i = 0 ; i < MAX_PLAYER_LIFE ; i++)
	{
		blockTexture = playerLifeBlock;

		if (i >= g->life)
		{
			blockTexture = playerLifeBlockEmpty;

		}
		else if (g->life == 1 && playerLifeWarningTimer < (int)FPS / 2)
		{
			blockTexture = playerLifeBlockWarning;
		}

		blitAtlasImage(blockTexture, x, 20, 0, SDL_FLIP_NONE);

		x += blockTexture->rect.w + 15;
	}
}

The idea behind this function is that we always want to draw 5 health blocks, the texture in use depending on the amount of health the player has. We're first setting up a variable called `x` and assigning it a value of 20. `x` will be the horizontal position from which we'll start drawing our life blocks. Next, we're extracting the Gunner from the player's `data`, then setting up a for-loop, from 0 to MAX_PLAYER_LIFE. We're then assigning an AtlasImage reference to a variable called blockTexture. playerLifeBlock will be the default texture that we'll draw at the start of the loop. We're then testing to see if the value of `i` (our for-loop index) is greater than or equal to the player's current `life`. If it is, we want to draw an empty block texture. We therefore assign playerLifeBlockEmpty to blockTexture. Otherwise, we'll perform another if test. This time we'll check to see if the player's `life` is 1. We'll additionally check to see if the value of playerLifeWarningTimer is less than half of FPS. If both these conditions hold true, we'll assign playerLifeBlockWarning to blockTexture. In other words, if the player has 1 life point remaining and our warning timer is under 30, we'll show a red health block. This will flash one and off every 1/2 a second.

With the texture we want to draw decided, we then call blitAltasImage, passing in blockTexture and `x`. We finally increment the value of `x` by the width of blockTexture, plus 15 pixel to act as padding.

That's all that's need for our health display. We can now look at how the hud is handled in the main game. Turning to stage.c, we've added in a few calls. Starting with initStage:


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

	skyTexture = getAtlasImage("gfx/stage/sky.png", 1);

	moonTexture = getAtlasImage("gfx/stage/moon.png", 1);

	initMap();

	initEntities();

	initBullets();

	initHud();

	loadStage("data/stages/1.json");

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

We're calling initHud here, to setup the hud. We've also added to the `logic` function:


static void logic(void)
{
	doEntities();

	doBullets();

	doHud();

	updateCamera();

	if (app.keyboard[SDL_SCANCODE_F1])
	{
		app.dev.showHitboxes = !app.dev.showHitboxes;

		app.keyboard[SDL_SCANCODE_F1] = 0;
	}
}

The call to doHud was added here. The `draw` function has seen a similar one line change:


static void draw(void)
{
	drawSky();

	drawMap();

	drawEntities();

	drawBullets();

	drawHud();
}

We're now calling drawHud in this function. It probably goes without saying, but keep in mind that drawHud needs to go last, so that the sprites and map don't cover the information display.

That's our health handled. Now we can look at the first of the two power-ups that have been added. The first is a health power-up, the green box with the white cross on it, to denote a medical kit.

In order to handle power-ups, we've added in a new function pointer to Entity:


struct Entity {
	// snipped
	void (*tick)(Entity *self);
	void (*draw)(Entity *self);
	void (*touch)(Entity *self, Entity *other);
	void (*takeDamage)(Entity *self, int amount, Entity *attacker);
	Entity *next;
};

The `touch` function pointer takes two parameters: the entity itself and the thing that has touched it. We'll see this in action when we come to handling two entities touching one another (such as the player coming into contact with a health kit).

All our power-ups live in a single file called powerUps.c, since there is very little to each and they share some common functions. We'll look first at the health power-up. Starting with initHealth:


void initHealth(Entity *e)
{
	if (healthTexture == NULL)
	{
		healthTexture = getAtlasImage("gfx/sprites/health.png", 1);
	}

	e->texture = healthTexture;

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

The function is quite standard. We're testing if we need to load a texture (healthTexture), doing so as required, and then assigning entity's `texture`, and function pointers. The `tick` and `draw` functions are shared in this file, while the touch function assigned (healthTouch) is custom for the health power-up.

The shared `tick` function is simple enough:


static void tick(Entity *self)
{
	self->hitbox.x = self->x;
	self->hitbox.y = self->y;
	self->hitbox.w = self->texture->rect.w;
	self->hitbox.h = self->texture->rect.h;
}

We're just setting the entity's `hitbox` to be the same as it's `x` and `y` coordinates, with the width and height being taken from the entity's `texture`.

`draw` is also simple:


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

Just a call to blitAtlasImage, passing through the entity's `texture` and position, while taking into account the camera location. We're not bothered about the direction the entity is facing.

Now, let's look at healthTouch. This is where the power-up's main logic resides:


static void healthTouch(Entity *self, Entity *other)
{
	Gunner *g;

	if (other == stage.player)
	{
		g = (Gunner*) other->data;

		g->life = MIN(g->life + 1, MAX_PLAYER_LIFE);

		self->dead = 1;
	}
}

The idea behind the function is that when the player touches the health power-up, they will recover a point of health. We first test to see if the entity that has touched the health power-up is the player, by checking if `other` is `stage.player`. If so, we're extracting the Gunner data from `other` (the player), and then increasing the Gunner's `life` by 1. We're using the MIN macro here to ensure it doesn't exceed MAX_PLAYER_LIFE. With that done, we're setting the power-up's `dead` flag to 1, so that it can be removed from the world.

We can now look at how we're processing the entity collisions. We've made some changes to entities.c to handle this. Starting with doEntities:


void doEntities(void)
{
	Entity *e, *prev;

	prev = &stage.entityHead;

	for (e = stage.entityHead.next ; e != NULL ; e = e->next)
	{
		// snipped

		e->tick(e);

		if (e->touch != NULL)
		{
			touchOthers(e);
		}

		// snipped
	}
}

While looping through each entity, we're checking to see if the current entity has a `touch` function set (it's not NULL), and if so we're calling a function named touchOthers, passing over the entity:


static void touchOthers(Entity *e)
{
	Entity *other;

	for (other = stage.entityHead.next ; other != NULL ; other = other->next)
	{
		if (e != other && collisionRects(&e->hitbox, &other->hitbox))
		{
			e->touch(e, other);
		}
	}
}

touchOthers is quite straightforward. We're setting up a for-loop to move through all the entities in the stage, referring to each as `other`. We're then checking to see if the entity we're checking (`other`) is not the same as the entity passed into the function (`e`). If they're not the same, we're calling collisionRects, passing over each entity's hitboxes, to see if a collision has occurred. Finally, if the two entities have collided, we're calling `e`'s `touch` function, passing over `e` and `other`.

So, in the case of a health power-up, the healthTouch function will be invoked when the player walks into it (or if another entity, such as an enemy were to do so). Should the player being the thing to touch the health power-up, one point of life is restored.

Now, something you'll realise here is that there is room for an O(n) problem to arise. Each entity with a `touch` function set will check each other entity in the world, to see if they are colliding. This could result in hundreds of unnecessary collision checks, due to entities being many screens away from one another. For example, just 32 entities on a stage, each with a touch function, could mean close to 1,000 checks being performed per frame. This is something we'll fix in a future tutorial. For now, we'll ignore the less than optimal performance.

We've done quite a lot already: we can display the player's health, reduce their health when they take damage, and also restore their health when they grab a health power-up. What we'll look at now is getting a better weapon. We'll return to powerUps.c for this, and look at a function called initSpreadGun:


void initSpreadGun(Entity *e)
{
	if (spreadGunTexture == NULL)
	{
		spreadGunTexture = getAtlasImage("gfx/sprites/spreadGun.png", 1);
	}

	e->texture = spreadGunTexture;

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

Much like the health power-up, our spread gun's init is simple - we're grabbing the texture as needed, and setting the `tick`, `draw`, and `touch` function pointers. Also like the health power-up, the spread gun's `touch` uses a custom function, named spreadGunTouch.


static void spreadGunTouch(Entity *self, Entity *other)
{
	Gunner *g;

	if (other == stage.player)
	{
		g = (Gunner*) other->data;

		g->weaponType = WPN_SPREAD;

		self->dead = 1;
	}
}

A very basic function. We're checking if the entity (`other`) that has touched the spread gun (`self`) is the player. If so, we're extracting the Gunner data from the player, and then setting the Gunner's weaponType to WPN_SPREAD. That's all we need to do to change the weapon; the weapon logic handling for what happens when we fire is done in player.c. With our new weapon type set, we're setting the spread gun's `dead` flag to 1, to remove it from the world.

Now that our weapon can be changed, we should look at how this affects our player code. player.c has seen a handful of update to the shooting code. We'll start with handleShoot:


static void handleShoot(Entity *self, Gunner *g)
{
	g->reload = MAX(g->reload - app.deltaTime, 0);

	if (app.keyboard[SDL_SCANCODE_J] && g->reload == 0)
	{
		switch (g->weaponType)
		{
			case WPN_SPREAD:
				fireBullet(self, g, 10);
				fireBullet(self, g, 0);
				fireBullet(self, g, -10);
				break;

			default:
				fireBullet(self, g, 0);
				break;
		}

		g->reload = RELOAD_SPEED;
	}
}

We're still decreasing `reload`, as well as testing to see if J has been pushed and that `reload` is 0 before firing a bullet. However, after that we're testing the Gunner's weaponType, using a switch statement, to see how we wish to handle things. We've introduced a new function to do the actual bullet firing, named fireBullet. It takes three arguments: the player entity, the gunner data, and a number that represent an angle. We'll detail what's going on here.

When our Gunner weaponType is WPN_SPREAD, we'll be firing three bullets, as this is what our spread gun does. The first bullet will issue at an angle of 10 degree, the second at 0 degrees, and the third at -10 degrees. This will mean, as expected, that our three bullets will be issued as a spread, separated by 10 degrees each. If the Gunner's weaponType is not WPN_SPREAD, we'll be falling into the default statement, which will call fireBullet with an angle of 0 degrees. Not matter what, the gunner's `reload` is then reset to RELOAD_SPEED, as before.

We'll look now at fireBullet:


static void fireBullet(Entity *self, Gunner *g, int angle)
{
	Bullet *b;
	int baseAngle;

	b = spawnBullet(self);
	b->texture = bulletTexture;
	b->hitbox.w = bulletTexture->rect.w;
	b->hitbox.h = bulletTexture->rect.h;
	b->damage = 1;
	b->life = FPS * 2;

	baseAngle = self->facing == FACING_RIGHT ? 90 : -90;

	if (g->ducking)
	{
		b->y = self->y + self->texture->rect.h - 22;
		b->x = self->facing == FACING_RIGHT ? self->x + self->texture->rect.w : self->x - b->texture->rect.w;
	}
	else if (g->aimingUp)
	{
		b->y = self->y - b->texture->rect.h;
		b->dy = -BULLET_SPEED;

		if (self->dx == 0)
		{
			b->x = self->x + (self->texture->rect.w / 2) - (b->texture->rect.w / 2);
			b->x += self->facing == FACING_RIGHT ? 12 : -12;

			baseAngle = 0;
		}
		else
		{
			b->x = self->facing == FACING_RIGHT ? self->x + self->texture->rect.w : self->x;
			b->dx = self->facing == FACING_RIGHT ? BULLET_SPEED : -BULLET_SPEED;

			baseAngle = self->facing == FACING_RIGHT ? 45 : -45;
		}
	}
	else
	{
		b->x = self->facing == FACING_RIGHT ? self->x + self->texture->rect.w : self->x;
		b->y = self->y + 19;
	}

	angle += baseAngle;

	b->dx = sin(TO_RAIDANS(angle)) * BULLET_SPEED;
	b->dy = -cos(TO_RAIDANS(angle)) * BULLET_SPEED;
}

You might recognise this as the code that was originally in handleShoot, albeit with some tweaks. Since we've already covered the code before, we'll just discuss the changes we've made.

Previously, we were manually setting the bullet's `dx` and `dy` values, based on the direction the player was facing, and whether they were aiming up, etc. We've now introduced a new variable called baseAngle, which we'll use to determine the direction the bullet should move. A baseAngle of 0 means the bullet will move up the screen, 45 diagonally up-right, 90 will mean directly to the right, etc. The first thing we're doing is setting baseAngle to 90 or -90 depending on the player's facing direction. This is our default, and will suffice for most situations, since we'll be shooting straight ahead. If we're aiming up (Gunner's aimingUp flag is 1) and we're standing still (`dx` is 0), we'll set baseAngle to 0. If our `dx` isn't 0, however, we'll set baseAngle to 45 or -45, depending on if we're facing right or left. This will mean we're shooting in diagonal.

We now know the angle at which we want to fire the bullet, based on the player's state. We now want to actually set the bullet's `dx` and `dy`. Before that, we add the value of baseAngle to `angle`, the parameter we passed into the function. Remember that our spread gun fires three shots, at angles 10, 0, and -10. Adjusting the baseAngle value will mean that, when facing right, we'll now issue bullets at angle of 100, 0, and 80 (10 + 90, 0 + 90, -10 + 90). We now have the angle at which we want to fire our bullet. The final step to calculating out our `dx` and `dy` values is to use some trigonometry, with sin and cos for the `dx` and `dy` respectively, the result of which we'll multiply by BULLET_SPEED. Notice that we're using a macro named TO_RAIDANS here. This is just a macro that converts degrees to raidans, so that our calculations with sin and cos are correct:


#define TO_RAIDANS(angleDegrees) (angleDegrees * PI / 180.0)

And that's our bullet firing and weapons updated. We're more or less done now. The only thing remaining is to add the power-ups to our entity factory, so that they can be created when we load a stage. We do this in initEntityFactory:


void initEntityFactory(void)
{
	memset(&head, 0, sizeof(InitFunc));
	tail = &head;

	addInitFunc("player", initPlayer);
	addInitFunc("target", initTarget);
	addInitFunc("greenSoldier", initGreenSoldier);
	addInitFunc("blueSoldier", initBlueSoldier);
	addInitFunc("health", initHealth);
	addInitFunc("spreadGun", initSpreadGun);
}

Just a couple of lines added here to allow for the power-ups to be created, using their names and init functions.

Our definitions in our stage data are like the other entities:

[
	{
		"type" : "player",
		"x" : 35,
		"y" : 65
	},
	{
		"type" : "health",
		"x" : 545,
		"y" : 350
	}
	...
]

We need only add a JSON object with the type of power-up we wish to add, along with the x and y positions in the map.

That's it for our power-ups. We've now got a very extensible system that can be easily built upon. What we should add in next is some more interactive elements, such as solid entities that can act as door and barriers. We'll see how this is done in our next part.

Purchase

The source code for all parts of this tutorial (including assets) is available here:

It is also available as part of the SDL2 tutorial bundle (with on-going updates):

If you do not wish to create an itch.io account, you can also purchase the tutorial bundle using PayPal. This method will be slower, however, as it will require manual verification of the transaction.

Comments

Mobile site