« Back to tutorial listing

— Creating a Run and Gun game —
Part 12: Player death + effects

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

Introduction

One major thing we're missing from our game is player death. Right now, nothing happens when the player loses all their health. We've changed that now so that when the player is killed, they will explode, and be reset. We've also thrown in a load of explosions and other little effects for good measure.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./gunner12 to run the code. You will see a window open like the one above. The same controls from past tutorials apply. Play the game as normal. Notice how when you are killed, the game pauses for a brief moment before resetting the player to where they were a few moments earlier, while making them invulnerable for a short time. When you're finished, close the window to exit.

Inspecting the code

We've made updates to defs.h and structs.h, to add in our new effects and handle the player death. Starting with defs.h:


#define EF_BOUNCES             (2 << 4)

EF_BOUNCES is a new flag, to say that an entity bounces when making contact with the map. It's only used by the debris that is thrown when the player, an enemy, a door, or oil drum are destroyed.

Next, let's look at the changes in structs.h:


typedef struct {
	int frame;
	int ducking;
	int aimingUp;
	int life;
	int weaponType;
	double animTimer;
	double reload;
	double immuneTimer;
	int hasKeycard[KEYCARD_MAX];
	SDL_Point checkpoint;
	double checkpointTimer;
} Gunner;

To handle the player's resetting, we've added in two new fields: `checkpoint` and checkpointTimer. `checkpoint` will act as the location (x and y) of the player when they are reset. checkpointTimer is a counter to say how often the checkpoint should be set.

We've also added in a few struct called Debris:


typedef struct {
	double life;
	double smokeTime;
} Debris;

Debris, as you've no doubt seen, are the pieces of scrap metal that are thrown around when something is destroyed. It only has two fields: `life` and smokeTime. `life` is how long the piece of debris will live for, while smokeTime will be used to handle how often the debris produces smoke.

We've also added in a struct to hold our effects:


struct Effect {
	double x;
	double y;
	double dx;
	double dy;
	int size;
	double life;
	double alpha;
	SDL_Color color;
	Effect *next;
};

Effect has several fields: `x` and `y` are its position, while `dx` and `dy` are its movement values along the x and y axis. `size` is how big it is, while `life` is how long it will live for. `alpha` is used to control the texture's alpha value. We're declaring this here as a double, so we can control it more accurately. `color` is the colour of the Effect. Our Effects form a linked list, hence the `next` field.

Finally, we've added the Effects linked list to our Stage:


typedef struct {
	Entity entityHead, *entityTail;
	Bullet bulletHead, *bulletTail;
	Effect effectHead, *effectTail;
	Entity *player;
	int map[MAP_WIDTH][MAP_HEIGHT];
	SDL_Point camera;
	Quadtree quadtree;
} Stage;

effectHead and effectTail form the beginning and end of our linked list for the effects.

We'll look at our effects themselves now. We've seen effects in both SDL2 Shooter and SDL2 Shooter 2, and the code and approach here is much the same, so we won't linger too long on these.

Our effects live in effects.c. Starting with initEffects:


void initEffects(void)
{
	explosionTexture = getAtlasImage("gfx/effects/explosion.png", 1);

	memset(&stage.effectHead, 0, sizeof(Effect));
	stage.effectTail = &stage.effectHead;
}

We're loading a texture called gfx/effects/explosion.png, and assigning it to explosionTexture. We're only using one texture for all our effects, so we only change the size, colour, and alpha of, as needed. We're also setting up our effects linked list.

doEffects is where we process the effects themselves:


void doEffects(void)
{
	Effect *e, *prev;

	prev = &stage.effectHead;

	for (e = stage.effectHead.next ; e != NULL ; e = e->next)
	{
		e->life -= app.deltaTime;
		e->alpha -= app.deltaTime;

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

		if (e->life <= 0 || e->alpha <= 0)
		{
			prev->next = e->next;

			if (e == stage.effectTail)
			{
				stage.effectTail = prev;
			}

			free(e);

			e = prev;
		}

		prev = e;
	}
}

For each effect in our linked list, we're decreasing both its `life` and `alpha`. We're then moving the effect by adding on their `dx` and `dy` to their `x` and `y`, respectively. If either the effect's life or alpha falls to below 0, we're removing it (an effect with no life is obviously dead, while one with 0 alpha will be invisible).

We're rendering our effects in drawEffects:


void drawEffects(void)
{
	Effect *e;
	int x, y;

	SDL_SetRenderDrawBlendMode(app.renderer, SDL_BLENDMODE_ADD);
	SDL_SetTextureBlendMode(explosionTexture->texture, SDL_BLENDMODE_ADD);

	for (e = stage.effectHead.next ; e != NULL ; e = e->next)
	{
		SDL_SetTextureAlphaMod(explosionTexture->texture, MIN(e->alpha, 255));
		SDL_SetTextureColorMod(explosionTexture->texture, e->color.r, e->color.g, e->color.b);

		x = e->x - stage.camera.x - (e->size / 2);
		y = e->y - stage.camera.y - (e->size / 2);

		blitAtlasImageScaled(explosionTexture, x, y, e->size, e->size, 0);
	}

	SDL_SetTextureColorMod(explosionTexture->texture, 255, 255, 255);
	SDL_SetTextureAlphaMod(explosionTexture->texture, 255);

	SDL_SetRenderDrawBlendMode(app.renderer, SDL_BLENDMODE_BLEND);
	SDL_SetTextureBlendMode(explosionTexture->texture, SDL_BLENDMODE_BLEND);
}

When it comes to rendering our effects, we want to do so using additive blending. This will result in colours in our effects being combined, becoming more intense, and shifting towards a brilliant white. Perfect for explosions. We're calling SDL_SetRenderDrawBlendMode and setting SDL_BLENDMODE_ADD, to tell our renderer how to handling the subsequent blitting operations, while also setting the explosion texture's blend mode (actually the texture atlas's blend mode) to SDL_BLENDMODE_ADD, using SDL_SetTextureBlendMode.

We then loop through all our effects in our linked list, calling SDL_SetTextureAlphaMod to set the alpha value of the explosion texture using the `alpha` of the Effect. We're also setting the colour of the texture using SDL_SetTextureColorMod, and feeding in the effect's `color` RGB values.

Next, we're preparing to draw the actual effect. Our effects are drawn centered, so we first assign variables named `x` and `y` the position of the effect, adjusted for the position of our camera, plus half the effect's `size`. We're then calling a new function named blitAtlasImageScaled, to draw the texture using its `size` (note: this function was covered in the Sprite Atlas and SDL2 Adventure tutorials).

When we're done drawing all our effects, we're resetting the texture's colours and alpha to their normal states, and changing the texture and render's blending modes back to SDL_BLENDMODE_BLEND.

That's our processing and rendering of our effects. We can now look at how we add in effects. We've created several functions to help with generating them. Starting with addExplosionEffect:


void addExplosionEffect(int x, int y, int size)
{
	Effect *e;

	e = malloc(sizeof(Effect));
	memset(e, 0, sizeof(Effect));
	stage.effectTail->next = e;
	stage.effectTail = e;

	e->x = x;
	e->y = y;
	e->size = size;
	e->life = e->alpha = FPS + (FPS / 4);

	switch (rand() % 5)
	{
		case 0:
			e->color.r = 255;
			break;

		case 1:
			e->color.r = 255;
			e->color.g = 128;
			break;

		case 2:
			e->color.r = 255;
			e->color.g = 255;
			break;

		case 3:
			e->color.r = 255;
			e->color.g = 255;
			e->color.b = 255;
			break;

		default:
			break;
	}
}

This function creates an explosion effect, with a random colour of red, orange, yellow, white, or black. The function takes three parameters: `x`, `y`, and `size`. We start by mallocing and memsetting an Effect and adding it to our stage's effect linked list. We then set our Effect's `x` and `y` to the values of the `x` and `y` we passed into the function. We also set the explosion's `size` to the value of `size` we passed in. We want our explosion to last for a little while before vanishing, so we set both its `life` and `alpha` to FPS + (FPS / 4). This will mean the explosion will live for one and a quarter seconds. Finally, we select a random colour for our explosion by performing a switch against a random of 5.

Our next function is addSmokeEffect, that creates a smoke-like effect:


void addSmokeEffect(int x, int y)
{
	Effect *e;

	e = malloc(sizeof(Effect));
	memset(e, 0, sizeof(Effect));
	stage.effectTail->next = e;
	stage.effectTail = e;

	e->x = x;
	e->y = y;
	e->dy = (1.0 * (-rand() % 300)) * 0.01;
	e->size = 32;
	e->life = e->alpha = 64;

	e->color.r = e->color.g = e->color.b = rand() % 255;
}

addSmokeEffect takes two parameters, the `x` and `y` position of the effect. Again, we're creating our Effect and adding it to our linked list. We're then assigning the `x` and `y` values. We want our smoke to rise, so we therefore give it a random `dy` value of between 0 and -2.99. Our smoke effects will always be the same size: 32 pixels, and they will also live for just over one second, with `life` and `alpha` values of 64. Finally, we want our smoke effects to be a random grey. We can achieve this by setting the RGB values of its color to the same random of 255.

The last function to look at is addBulletImpactEffect. As the name implies, this function creates an effect for when bullets strike the world or entities:


void addBulletImpactEffect(Bullet *b)
{
	Effect *e;
	int i;

	for (i = 0 ; i < 8 ; i++)
	{
		e = malloc(sizeof(Effect));
		memset(e, 0, sizeof(Effect));
		stage.effectTail->next = e;
		stage.effectTail = e;

		e->x = b->x + (b->texture->rect.w / 2);
		e->y = b->y + (b->texture->rect.h / 2);
		e->dx = (1.0 * (rand() % 100) - (rand() % 100)) * 0.01;
		e->dy = (1.0 * (rand() % 100) - (rand() % 100)) * 0.01;
		e->size = 8;
		e->life = FPS / 2;
		e->alpha = 255;

		if (b->owner == stage.player)
		{
			e->color.r = 128;
			e->color.g = 128;
			e->color.b = 255;
		}
		else
		{
			e->color.r = 255;
			e->color.g = 128;
			e->color.b = 128;
		}
	}
}

The function takes the bullet (`b`) the effect is for as a parameter. We're setting up a for-loop to create 8 effects. Each one is centered around the bullet (taking the bullet's `x` and `y` and subtracting half the explosion texture's width and height). We're also setting each effect's `dx` and `dy` to a random value between 0 and 1, to make it slowly drift out from where it was created. We're setting each effect `size` to 8 (so they're small), and setting their `life` to half a second. We want the impacts to be bright, however, so we're setting the `alpha` to 255. This means they'll be quite bright before vanishing. With the base effect setup, we're then checking who owns the bullet, by checking `b->owner`. If it's the player, we'll set the effect to be a light blue. If it belongs to an enemy, we'll set it to be a light red.

Our effects are done. Let's turn now to the new Debris entity, that makes use of some of these effects. All the code for our Debris lives in debris.c. We'll start with the first function, addDebris:


void addDebris(int x, int y)
{
	Debris *d;
	Entity *e;

	d = malloc(sizeof(Debris));
	memset(d, 0, sizeof(Debris));
	d->life = FPS * 2 + (rand() % (int)FPS * 11);

	if (textures[0] == NULL)
	{
		textures[0] = getAtlasImage("gfx/effects/debris1.png", 1);
		textures[1] = getAtlasImage("gfx/effects/debris2.png", 1);
		textures[2] = getAtlasImage("gfx/effects/debris3.png", 1);
		textures[3] = getAtlasImage("gfx/effects/debris4.png", 1);
	}

	e = spawnEntity();
	e->x = x;
	e->y = y;
	e->dx = (1.0 * (rand() % 800 - rand() % 800)) * 0.01;
	e->dy = -(1000.0 + (rand() % 1000)) * 0.01;
	e->texture = textures[rand() % NUM_TEXTURES];
	e->data = d;
	e->flags = EF_BOUNCES;

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

Unlike some of the other entity code, this function takes two parameters - `x` and `y` coordinates, rather than an entity to use. This is because we'll create the entity ourselves in the function.

We start by mallocing and memsetting a Debris struct, then setting its `life` to a random of 2 to 12 seconds. After that, we fetch our textures, if needed. There are four different debris textures to load, which we assign to the indexes of an AtlasImage array. We then create our entity (`e`) by calling spawnEntity. We assign its `x` and `y` fields as the values of the `x` and `y` we passed into the function. We then set its `dx` and `dy` to some random values, with the `dy` being given a negative random value. This will result in the debris being "thrown" when it is created. The entity's texture is set as a random image from the textures array (using NUM_TEXTURES, declared as 4). The Debris struct we created earlier is set as the entity's `data` field, and we set the entity's `flags` as EF_BOUNCES, to make it bounce upon contact with the world. Finally, the `tick` and `draw` functions are assigned.

We'll look at the `tick` function next. It's quite simple:


static void tick(Entity *self)
{
	Debris *d;

	d = (Debris*) self->data;

	d->life -= app.deltaTime;

	d->smokeTime -= app.deltaTime;

	if (d->life > FPS && d->smokeTime <= 0)
	{
		addSmokeEffect(self->x + (self->texture->rect.w / 2), self->y + (self->texture->rect.h / 2));

		d->smokeTime = 1 + rand() % 5;
	}

	if (self->onGround)
	{
		self->dx *= 1.0 - (0.015 * app.deltaTime);
	}

	if (d->life <= 0)
	{
		self->dead = 1;
	}
}

We're extracting the Debris from the entity's `data` field, then decreasing its `life` and smokeTime values. We're then checking if its `life` is greater than one second (FPS) and its smokeTime is 0 or less. If so, we're going to add a smoke effect. We do this by calling addSmokeEffect and passing in the debris's coordinates, plus half its texture's width and height, so that the smoke is created about the center. We're then setting smokeTime to between 1 and 5. The reason for this is because we don't want to be adding smoke all the time, and so setting smokeTime to a random value creates some variance.

Next, we're testing to see if the debris is on the ground by testing the onGround flag. You'll have noticed that when the debris is on the ground, it slides for a bit before coming to a halt. When the entity's onGround flag is set, we're reducing the entity's `dx` by multiplying it by 0.985. This will cause the debris to act as though friction is being applied to it.

Finally, we're checking if the debris's `life` has fallen to 0 or less, and setting its `dead` flag to 1 if so.

Our draw function is next. There's not much to it:


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

We're simply drawing the debris's texture, nothing more.

That's our effects and debris code all setup. We can now look at where and how we're using them. There are a number of places in the code where an explosion occurs, such as when an enemy soldier or an oil drum is destroyed. We'll look at the code for how we handle Green Soldiers being destroyed. We do this in greenSoldier.c, in the takeDamage function:


static void takeDamage(Entity *self, int amount, Entity *attacker)
{
	EnemySoldier *s;
	int i, x, y;

	if (attacker == stage.player)
	{
		s = (EnemySoldier*) self->data;

		s->life -= amount;

		s->damageTimer = 4;

		self->facing = stage.player->x < self->x ? FACING_LEFT : FACING_RIGHT;

		s->thinkTime = FPS / 4;

		if (s->life <= 0)
		{
			for (i = 0 ; i < 32 ; i++)
			{
				x = self->x + rand() % self->texture->rect.w;
				y = self->y + rand() % self->texture->rect.h;

				addExplosionEffect(x, y, 96);
			}

			for (i = 0 ; i < 8 ; i++)
			{
				x = self->x + (self->texture->rect.w / 2);
				y = self->y + (self->texture->rect.h / 2);

				addDebris(x, y);
			}

			self->dead = 1;
		}
	}
}

Originally, once the EnemySoldier's `life` fell to 0 or less, we would set their `dead` flag to 1. Now, we're creating two for-loops. The first calls addExplosionEffect 32 times, passing in `x` and `y` values based on a random point about the soldier (using their `x` and `y` coordinates, plus a random of their texture's width and height). We're also telling the explosion effects to be 96 pixels in size, making them quite sizeable. The second for-loop calls addDebris 8 times, passing over the midpoint `x` and `y` of the soldier, so that the debris spawns from its center.

This death code is largely the same for the other soldier type, as well as the weak door, player, and oil drum.

Our bullet effects are handled in bullets.c, whenever a bullet strikes the world or an entity. Each requires just a single line change. Starting with checkWorldCollisions:


static void checkWorldCollisions(Bullet *b)
{
	int mx, my;

	mx = b->x + (b->texture->rect.w / 2);
	mx /= MAP_TILE_SIZE;

	my = b->y + (b->texture->rect.h / 2);
	my /= MAP_TILE_SIZE;

	if (!isInsideMap(mx, my))
	{
		b->life = 0;
	}
	else if (stage.map[mx][my] != 0)
	{
		b->life = 0;

		addBulletImpactEffect(b);
	}
}

When a bullet hits the map (a non-zero tile), we're setting its `life` to 0. In addition, we're calling addBulletImpactEffect and passing over the bullet. This is all that's needed to create our effect.

A similar update is done in checkEntityCollisions:


static void checkEntityCollisions(Bullet *b)
{
	// snipped

	for (i = 0, e = candidates[0] ; i < MAX_QT_CANDIDATES && e != NULL ; e = candidates[++i])
	{
		app.dev.collisionChecks++;

		if (((e->flags & EF_SOLID) || e->takeDamage != NULL) && collisionRects(&r, &e->hitbox))
		{
			if (e->takeDamage)
			{
				e->takeDamage(e, b->damage, b->owner);
			}

			addBulletImpactEffect(b);

			b->life = 0;

			return;
		}
	}
}

Once the bullet has hit something solid or something that takes damage, we're calling addBulletImpactEffect before setting the bullet's `life` to 0. Again, just a one line addition.

You will remember that our debris bounces when it hits the world. This is, again, a very simple change to apply. Turning to entities.c, we've updated both moveToWorldX and moveToWorldY. Starting with moveToWorldX:


static void moveToWorldX(Entity *e)
{
	int mx, my, adj;

	mx = e->dx > 0 ? (e->x + e->texture->rect.w) : e->x;
	mx /= MAP_TILE_SIZE;

	for (my = (e->y / MAP_TILE_SIZE) ; my <= (e->y + e->texture->rect.h - 1) / MAP_TILE_SIZE ; my++)
	{
		if (isInsideMap(mx, my) && stage.map[mx][my] != 0)
		{
			adj = e->dx > 0 ? -(e->texture->rect.w) : MAP_TILE_SIZE;

			e->x = (mx * MAP_TILE_SIZE) + adj;

			if (e->flags & EF_BOUNCES)
			{
				e->dx = -(e->dx * 0.75);
			}
			else
			{
				e->dx = 0;
			}

			return;
		}
	}
}

Before, we were immediately setting the entity's `dx` to 0 once it hit a solid map tile. Now, we're testing to see if it has the EF_BONUCES flag set. If so, we're going to invert the entity's `dx` and multiply the result by 0.75. This will mean that if move left, the entity will instead begin moving right. Multiplying the result by 0.75 will make it move slower than it was before, making it appear as though it has lost energy in the impact.

We do a similar thing in moveToWorldY:


static void moveToWorldY(Entity *e)
{
	int mx, my, adj;

	my = e->dy > 0 ? (e->y + e->texture->rect.h) : e->y;
	my /= MAP_TILE_SIZE;

	for (mx = e->x / MAP_TILE_SIZE ; mx <= (e->x + e->texture->rect.w - 1) / MAP_TILE_SIZE ; mx++)
	{
		if (isInsideMap(mx, my)&& stage.map[mx][my] != 0)
		{
			adj = e->dy > 0 ? -(e->texture->rect.h) : MAP_TILE_SIZE;

			e->y = (my * MAP_TILE_SIZE) + adj;

			e->onGround = e->dy > 0;

			if (e->flags & EF_BOUNCES)
			{
				e->dy = -(e->dy * 0.75);
			}
			else
			{
				e->dy = 0;
			}

			return;
		}
	}
}

Instead of changing the entity's `dx`, however, we're changing the entity's `dy`.

Now, let's look at what happens when the player is killed. Again, the player used to be impervious to death. Now they actually explode. Turning to player.c, we've made some changes to `tick`:


void tick(Entity *self)
{
	Gunner *g;

	g = (Gunner*) self->data;

	g->immuneTimer = MAX(g->immuneTimer - app.deltaTime, 0);

	g->checkpointTimer = MAX(g->checkpointTimer - app.deltaTime, 0);

	if (g->checkpointTimer == 0 && self->onGround)
	{
		g->checkpoint.x = self->x;
		g->checkpoint.y = self->y;

		g->checkpointTimer = CHECKPOINT_TIMER_TIME;
	}

	handleMovement(self, g);

	handleShoot(self, g);

	updateHitbox(self, g);
}

We're now decreasing the Gunner's checkpointTimer, limiting it to 0. Once it hits 0, we're checking if the player is also on the ground, and then setting the Gunner's checkpoint's `x` and `y` to the player's own `x` and `y`. We're then setting checkpointTimer to CHECKPOINT_TIMER_TIME (defined as 10 seconds in player.c). In effect, we're recording the player's position every 10 seconds, so that we can reset them if they die. Something that's important to do is ensure the player is on the ground before we do so. If we don't, the player could be in midair and this could lead to all kinds of problems (and could also be disorientating for the user).

As already mentioned, the takeDamage function was also updated:


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

	g = (Gunner*) self->data;

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

		g->immuneTimer = FPS * 1.5;

		if (g->life <= 0)
		{
			for (i = 0 ; i < 32 ; i++)
			{
				x = self->x + rand() % self->texture->rect.w;
				y = self->y + rand() % self->texture->rect.h;

				addExplosionEffect(x, y, 96);
			}

			for (i = 0 ; i < 8 ; i++)
			{
				x = self->x + (self->texture->rect.w / 2);
				y = self->y + (self->texture->rect.h / 2);

				addDebris(x, y);
			}

			self->dead = 1;
		}
	}
}

Before, only the immune timer would be set. Now, we're adding in a similar explosion effect seen for the soldier. We've also added in a new function to player.c, to help with resetting the player, named resetPlayer:


void resetPlayer(void)
{
	Gunner *g;

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

	stage.player->x = g->checkpoint.x;
	stage.player->y = g->checkpoint.y;
	stage.player->dead = 0;
	stage.player->onGround = 1;
	stage.player->texture = standTexture;

	stage.entityTail->next = stage.player;
	stage.entityTail = stage.player;

	g->life = MAX_PLAYER_LIFE;
	g->weaponType = WPN_NORMAL;
	g->immuneTimer = FPS * 3;
}

The function basically returns the player to their last checkpoint and makes them immune for a number of seconds. We start by extracting the Gunner from the player. We're then setting the player's `x` and `y` to the Gunner's checkpoint `x` and `y`, resetting their `dead` flag, setting their onGround flag to 1, and changing their texture back to the standTexture. We're then adding the player back into the stage's entity linked list. Like other entities, the player is removed from the stage once they are killed. We'll see more on this in a bit. With the player restored, we're resetting their life back to MAX_PLAYER_LIFE, resetting their weapon back to their default gun, and also making them immune for 3 seconds.

We've also made some changes to doEntities in entities.c to handle the player death:


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

	prev = &stage.entityHead;

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

		if (!e->dead)
		{
			addToQuadtree(e, &stage.quadtree);
		}
		else
		{
			prev->next = e->next;

			e->next = NULL;

			if (e == stage.entityTail)
			{
				stage.entityTail = prev;
			}

			if (e != stage.player)
			{
				deadTail->next = e;
				deadTail = e;
			}

			e = prev;
		}

		prev = e;
	}
}

Most entities are moved to the dead list when they are killed. In the case of the player, we're not doing this; we've already got a reference to the player in Stage, so we can access them that way. We also don't want to add the player to the dead list more than once, as this will break our list and make our game lock up. Therefore, we're testing to see if the entity to remove (`e`) is not the player (`stage.player`) before removing them.

We're almost done! Let's finally look at stage.c, where we're processing everything. 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);

	initQuadtree(&stage.quadtree);

	initMap();

	initEntities();

	initBullets();

	initHud();

	initEffects();

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

	playerRespawnTimer = RESPAWN_TIMER_TIME;

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

We're now calling initEffects, along with all our other init functions. We're also setting a variable named playerRespawnTimer to RESPAWN_TIMER_TIME (defined as 3 seconds). This variable is used to control the time between the player being killed and them being reset. We don't want the player to be reset instantly upon death, so we pause for a brief period.

Turning now to logic, we can see a few new additions:


static void logic(void)
{
	app.dev.collisionChecks = 0;

	doEntities();

	doBullets();

	doEffects();

	doHud();

	updateCamera();

	if (stage.player->dead)
	{
		playerRespawnTimer -= app.deltaTime;

		if (playerRespawnTimer <= 0)
		{
			resetPlayer();

			playerRespawnTimer = RESPAWN_TIMER_TIME;
		}
	}

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

		app.keyboard[SDL_SCANCODE_F1] = 0;
	}
}

As expected, we're calling doEffects, to process our effects. We're now also testing to see if the player is dead. If they are, we're decreasing playerRespawnTimer. Once this hits 0 or less, we calling resetPlayer (in player.c) and resetting playerRespawnTimer to RESPAWN_TIMER_TIME. This is how we prevent the player from being returned to the game immediately upon death. Remember that the player entity has been removed from the game at this point, so attempting to fire, move, etc. will do nothing (no `tick` function is called for the player).

The `draw` function is the last thing we need to look at:


static void draw(void)
{
	if (playerRespawnTimer > FPS / 2)
	{
		drawSky();

		drawEntities(LAYER_BACKGROUND);

		drawMap();

		drawEntities(LAYER_FOREGROUND);

		drawBullets();

		drawEffects();

		drawHud();
	}
}

Our `draw` function has now wrapped all the other function calls in an if-statement, testing whether playerRespawnTimer is greater than half a second. If so, we're drawing everything as normal. What this means is that there will be half a second when the screen goes blank before the player is returned to game. This is help with respawning the player, in case they are a long way from where they died. It would be jarring for the screen to suddenly flip. Making it go blank for a moment helps to overcome this.

Additionally, we're calling drawEffects. We're doing this after drawing everything else and before the hud. We want our smoke and explosions to be drawn on top of our entities, bullets, and map, so they need to be rendered last.

Those were the last major things we needed to add in, meaning we're very nearly done with our game! We'll be popping in some enemies in the next part, as well as making a much larger map to explore.

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:

Desktop site