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


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

— Mission-based 2D shoot 'em up —
Part 3: Adding enemies

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

Introduction

It's time to move onto shooting things. In this part, we're going to spawn some enemies for the player to destroy. These will be static fighters that won't move or attack the player. That will come later. For now, enjoy battling an unlimited number of enemies (take note not to wander too far away from the enemies, or they will be near-impossible to find again, as there is no radar or direction indicators available).

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter3-03 to run the code. You will see a window open like the one above. Use the WASD control scheme to move the fighter around. Hold J to fire your main guns. Shoot the red enemy fighters that appear to destroy them. Each enemy requires 3 shots each to be destroyed (when the KIT-E has a damage rating of 1 - you can modify this in game.c). When the enemy is destroyed, a new one will appear randomly around the player, for a maximum of 5 at any one time. When you're finished, closed the window to exit.

Inspecting the code

Adding in enemies is a very simple task, since it's just a case of using the entity factory and placing the resulting enemy near to the player. Of course, we've made a bunch of other changes to the code to support destroying them, so let's take a look.

Starting, as always, with structs.h:


struct Entity
{
	// snipped
	void (*takeDamage)(Entity *self, double amount);
	void (*die)(Entity *self);
	Entity *next;
};

We've added two function pointers: takeDamage and `die`, that will handle an entity taking damage (from a bullet impact) or also for what happens when it is destroyed.

Next, we've updated Fighter:


typedef struct
{
	// snipped
	double hitTimer;
} Fighter;

We've added a field called hitTimer, that will be used to control the flash that occurs when a fighter (either the player or an enemy) is hit.

A new struct has been introduced, called Debris:


struct Debris
{
	double  x;
	double  y;
	double  dx;
	double  dy;
	double  health;
	double  thinkTime;
	Debris *next;
};

Our Debris struct will represent the small trails of explosions that randomly move around after a fighter has been destroyed. We'll touch on how these work a little later on.

Finally, we've updated Stage.


typedef struct
{
	double  ssx, ssy;
	Effect  effectHead, *effectTail;
	Entity  entityHead, *entityTail;
	Entity  deadEntityHead, *deadEntityTail;
	Bullet  bulletHead, *bulletTail;
	Debris  debrisHead, *debrisTail;
	double  engineEffectTimer;
	int     numActiveEnemies;
	Entity *player;
	PointF  camera;
} Stage;

We've added in two new linked lists: one for dead entities, and another for our debris. Another variable that we've added is numActiveEnemies. This will hold the value of the number of enemies that are currently active on the stage (i.e., the ones that the player is battling). This will come in very useful later on when we're working with objectives, etc.

Now, let's move onto our enemy definition. We've added in a compilation unit called greebleLightFighter. It contains a number of functions, so we'll work through them one at a time, starting with initGreebleLightFighter:


void initGreebleLightFighter(Entity *e)
{
	Fighter *f;

	if (fighterTexture == NULL)
	{
		fighterTexture = getAtlasImage("gfx/fighters/greebleLightFighter.png", 1);
	}

	f = malloc(sizeof(Fighter));
	memset(f, 0, sizeof(Fighter));
	f->health = f->maxHealth = 3;

	e->side = SIDE_GREEBLE;
	e->facing = FACING_RIGHT;
	e->data = f;
	e->texture = fighterTexture;

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

A very standard entity init function. Much like the player, we're creating a fighter and assigning all the various attributes to it. Notice how we're assigning the takeDamage and `die` function pointers.

The `tick` function comes next:


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

	f = (Fighter *)self->data;

	fighterTick(self, f);

	if (stage.engineEffectTimer <= 0)
	{
		addEngineEffect(self->x + (self->facing == FACING_LEFT ? self->texture->rect.w : 0), self->y + 16);
	}

	stage.numActiveEnemies++;
}

Again, very much like the tick function for the player. One thing that's different is that we're incrementing Stage's numActiveEnemies.

The `draw` function is also near-identical to that of the player:


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

	f = (Fighter *)self->data;

	fighterDraw(self, f);
}

The `die` function is the final one to look at. It's quite simple:


static void die(Entity *self)
{
	int x, y;

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

	addExplosions(x, y, 25);

	addDebris(x, y, 12);

	self->dead = 1;
}

This function is called when the enemy is killed (in its takeDamage function, that'll we'll look at in a bit). We're doing three major things here - adding in a load of explosions around its centre, via a call to addExplosions (25 is the number of explosions we wish to generate), we're throwing out some debris (12 being the amount of pieces), and we're finally setting the entity's (`self`) `dead` flag to 1, so that it can be removed from our main entity linked list.

If we now move over to fighters.c, we can see that we've made some updates and additions. To begin with, we've updated fighterDraw:


void fighterDraw(Entity *e, Fighter *f)
{
	int x, y;

	x = e->x - stage.camera.x;
	y = e->y - stage.camera.y;

	if (f->hitTimer == 0)
	{
		blitAtlasImage(e->texture, x, y, 0, e->facing == FACING_LEFT ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE);
	}
	else
	{
		SDL_SetTextureColorMod(e->texture->texture, 255, 0, 0);
		blitAtlasImage(e->texture, x, y, 0, e->facing == FACING_LEFT ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE);
		SDL_SetTextureColorMod(e->texture->texture, 255, 255, 255);
	}
}

Before drawing we're testing the value of the fighter's new hitTimer variable. If it's 0, we're going to draw the fighter as normal. Otherwise, we're going to render the fighter with a red tint applied (via a called to SDL_SetTextureColorMod). This is a similar technique to that which we used in SDL2 Gunner and SDL2 Shooter 2.

Now for fighterTakeDamage, a new function:


void fighterTakeDamage(Entity *self, double damage)
{
	Fighter *f;

	f = (Fighter *)self->data;

	f->health -= damage;

	f->hitTimer = FPS / 8;

	if (f->health <= 0 && !self->dead)
	{
		self->die(self);
	}
}

Pretty straightforward. We're subtracting from the fighter's `health` the value of `damage` we passed into the function, setting the hitTimer, and finally checking if the fighter has been killed (`health` is 0 or less). If so, and the fighter is not already dead, we're going to call the die function (for example, the one in greebleLightFighter.c).

Now for bullets.c, where we've made an update and an addition. Before now, our bullets weren't checking for collisions; they had nothing to collide against. But now we have enemies, so we need to tell them how respond to that.

standardTick has seen a very simple change:


static void standardTick(Bullet *b)
{
	int x, y;

	if (b->owner == stage.player)
	{
		x = b->x - stage.camera.x;
		y = b->y - stage.camera.y;

		if (x <= -b->texture->rect.w || y <= -b->texture->rect.h || x >= SCREEN_WIDTH + b->texture->rect.w || y >= SCREEN_HEIGHT + b->texture->rect.h)
		{
			b->health = 0;
		}
	}

	checkCollisions(b);
}

We're now calling a function named checkCollisions. This, as we'll see next, is a function used to test bullet-entity collisions:


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

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

			e->takeDamage(e, b->damage);
			b->health = 0;

			return;
		}
	}
}

Another straightforward function. We're looping through all the entities in the stage, checking if their `side` is different to the bullet's `owner`'s `side` (i.e., this isn't an enemy-enemy collision or a player-player collision). Next, we're checking if the entity has the takeDamage function set, meaning it can be hurt. Finally, we're checking for a collision based on the bullet's rectangle and the entity's. If all of this is true, we'll add an effect to represent a hit (addHitEffect, defined in effects.s), and also call the entity's takeDamage function. We'll then set the bullet's `health` to 0 to remove it, and return out of the function, since we don't want to keep checking for collisions.

All simply enough. Before bringing all of this together, let's quickly look at debris.c, to see how our debris works. Our debris is really nothing more than some effects, so we'll take a high level view of the code.

First, initDebris:


void initDebris(void)
{
	stage.debrisTail = &stage.debrisHead;

	fireTimer = 0;
}

We're setting up our linked list and setting a variable called fireTimer to 0. fireTimer, as we'll see, is used to control how often a piece of debris will release a fire effect.

On to addDebris:


void addDebris(int x, int y, int amount)
{
	Debris *d;
	int     i;

	for (i = 0; i < amount; i++)
	{
		d = malloc(sizeof(Debris));
		memset(d, 0, sizeof(Debris));
		stage.debrisTail->next = d;
		stage.debrisTail = d;

		d->x = x;
		d->y = y;
		d->dx = rand() % 200 - rand() % 200;
		d->dy = rand() % 200 - rand() % 200;
		d->dx *= 0.05;
		d->dy *= 0.05;
		d->thinkTime = 1 + rand() % 10;
		d->health = FPS + (rand() % FPS * 4);
	}
}

As we saw earlier, this function produces a set amount (`amount`) of debris, at a given position (`x`, `y`). In a loop, we're creating Debris and adding it to our linked list. We're then giving it a random velocity (`dx` and `dy`), a random thinkTime, and random `health`. The thinkTime of the debris is used to control how often it changes direction.

Let's take a quick look at the doDebris function:


void doDebris(void)
{
	Debris *d, *prev;

	fireTimer -= app.deltaTime;

	prev = &stage.debrisHead;

	for (d = stage.debrisHead.next; d != NULL; d = d->next)
	{
		if (fireTimer <= 0)
		{
			addDebrisEffect(d->x, d->y);
		}

		d->health -= app.deltaTime;

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

		d->thinkTime -= app.deltaTime;

		if (d->thinkTime <= 0)
		{
			d->dx += (1 * rand() % 25 - rand() % 25) * 0.1;
			d->dy += (1 * rand() % 25 - rand() % 25) * 0.1;
			d->thinkTime = 1 + rand() % 10;
		}

		if (d->health <= 0)
		{
			prev->next = d->next;

			if (d == stage.debrisTail)
			{
				stage.debrisTail = prev;
			}

			free(d);

			d = prev;
		}

		prev = d;
	}

	if (fireTimer <= 0)
	{
		fireTimer = rand() % 200;
		fireTimer *= 0.01;
	}
}

In this, decreasing the value of fireTimer, and then looping through all our debris. Each debris will call addDebrisEffect (in effects.c) if fireTimer is 0 or less. We're also decreasing the `health` of each debris, and moving it according to its velocity (`dx` and `dy`). We're then decreasing its thinkTime. If it's 0 or less, we're changing the `dx` and `dy`, and assigning a new thinkTime. This basically causes our debris to move about chaotically, changing direction every now and again, while streaming fire in its wake.

That's all the main changes and updates done. We can now head over to stage.c, where we've put everything together (and added some new functions). Starting with initStage:


void initStage(void)
{
	initStarfield();

	initBullets();

	initEntities();

	initEffects();

	initDebris();

	initEntity("player");

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

	addEnemyTimer = 0;

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

We're calling initDebris, and also setting a new variable called addEnemyTimer (static in stage.c) to 0. This variable will control the frequency at which enemies are added to the stage.

Now for doStage:


static void doStage(void)
{
	stage.numActiveEnemies = 0;

	stage.engineEffectTimer -= app.deltaTime;

	doEntities();

	doBullets();

	doEffects();

	doDebris();

	doStarfield(stage.ssx * 0.75, stage.ssy * 0.75);

	doCamera();

	doBackground(stage.ssx * 0.125 * app.deltaTime, stage.ssy * 0.125 * app.deltaTime);

	addEnemyTimer -= app.deltaTime;

	if (addEnemyTimer <= 0)
	{
		addEnemy();

		addEnemyTimer = FPS;
	}

	if (stage.engineEffectTimer <= 0)
	{
		stage.engineEffectTimer = 1;
	}
}

We're doing several new things here. To begin with, we're setting Stage's numActiveEnemies to 0. As we've seen, when the enemy fighter we created calls its tick function, it will increment the value. What this means is that we'll always have an exact count of the number of enemies in the stage by the end of the doStage function. Next, we're reducing the value of addEnemyTimer. Once this hits 0, we're calling addEnemy, and resetting it to FPS (1 second). This means that once a second we will attempt to add a new enemy.

Finally, let's look at addEnemy:


static void addEnemy(void)
{
	Entity *e;

	if (stage.numActiveEnemies < 5)
	{
		e = initEntity("greebleLightFighter");

		e->x = stage.player->x + (rand() % 500) - (rand() % 500);
		e->y = stage.player->y + (rand() % 500) - (rand() % 500);
	}
}

We're first testing how many enemies are active in the stage, and if it's fewer than 5, we're creating a new one, via a call to initEntity. We're then placing the newly created enemy within range of the player (it may appear on top of them, due to the nature of our random chance). It's important that we call this function after doEntities, so that we know how many enemies exist, otherwise this function will always add an enemy regardless of how many already exist, which isn't what we want.

Excellent, part 3 is done! We're making steady progress in our game, and one can already see we've established a great framework for going forward. It's time to allow the enemies to chase after us and attack us (although we'll not allow them to destroy the player just yet). So, in our next part we'll implement some simple AI.

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