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


Project Starfighter

In his fight back against the ruthless Wade-Ellen Asset Protection Corporation, pilot Chris Bainfield finds himself teaming up with the most unlikely of allies - a sentient starfighter known as Athena.

Click here to learn more and read an extract!

« Back to tutorial listing

— 2D Top-down shooter tutorial —
Part 4: Battling enemies

Introduction

Note: this tutorial series builds upon the ones that came before it. If you aren't familiar with the previous tutorials, or the prior ones of this series, you should read those first.

This first tutorial will explain how to read the mouse in SDL2. Extract the archive, run cmake CMakeLists.txt, followed by make to build. Once compiling is finished type ./bad04 to run the code.

A 1280 x 720 window will open, with a dark grey background over which a light purple grid is shown. A targetter will be shown that will track the mouse movements. The main character, Donk, will also display. Donk can be moved with the WSAD control scheme and will always face the targetter. Use the left mouse button to fire. The mouse wheel will cycle through the available weapons. The right mouse button will reload the pistol when it is out of ammo. Enemies will enter the screen from all angle and may drop powerups when defeated. Close the window by clicking on the window's close button.

Inspecting the code

We're going to add some enemies to the arena, for Donk to shoot. Right now, our enemies won't attack Donk, so the player is free to shoot them. We'll start by making some additions to defs.h and structs.h:


enum
{
	SIDE_NONE,
	SIDE_PLAYER,
	SIDE_ENEMY
};

In defs.h we've added an enum to help us determine the side the entity is on. This is mainly to filter bullets and collision detection, so that enemies don't shoot one another and items don't get hit by bullets. Next, we've added some extra fields to Entity in structs.h:


struct Entity {
	int side;
	...
	int radius;
	...
	void (*tick)(void);
	void (*touch)(Entity *other);
	void (*die)(void);
};

We've added a field to hold the Entity's side, and also one to hold its radius value. This radius value will be used for collision detection: circle to circle. This is just the way we're going to handle collisions in this game, rather than use rectangular collisions which can look wrong due to rotation of entities. We've also got tick, touch, and die function pointers. These function pointers will be used to determine how the entity will respond in certain situations. We'll see more on this later. We've also added a new function to util.c:


int getDistance(int x1, int y1, int x2, int y2)
{
	int x, y;

	x = x2 - x1;
	y = y2 - y1;

	return sqrt(x * x + y *y);
}

The getDistance function will simply return a distance between two points. Nothing special.

Now for something bigger - a new file called enemies.c, which will be used to handle all our enemy logic. This file contains three functions: addEnemy, tick, and die. We'll go through them in order, starting with addEnemy.


void addEnemy(int x, int y)
{
	Entity *e;

	e = malloc(sizeof(Entity));
	memset(e, 0, sizeof(Entity));
	stage.entityTail->next = e;
	stage.entityTail = e;

	e->side = SIDE_ENEMY;
	e->texture = loadTexture("gfx/enemy01.png");
	e->health = 5;
	e->x = x;
	e->y = y;
	SDL_QueryTexture(e->texture, NULL, NULL, &e->w, &e->h);
	e->radius = 32;

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

This function creates an Entity and places it at the x and y parameters that are passed to it. The side, texture, and health are set, as well as the radius. The radius is much smaller than the size of the texture, meaning that collisions need to be more on target; shots can breeze by the outer portions of the enemy texture without making contact, which looks much better given its overall shape. We then assign the tick and die functions to two local functions of the same name:


static void tick(void)
{
	self->angle = getAngle(self->x, self->y, player->x, player->y);

	calcSlope(player->x, player->y, self->x, self->y, &self->dx, &self->dy);
}

The tick function calls getAngle to set the angle of the current entity to always face the player (passing in its own coordinates and those of the player). It also calls calcSlope to set its dx and dy to the player's position. In effect, this will mean that the enemy will always face and move forwards the player. Note that we're using a variable called self. This variable is a global Entity pointer that is assigned to an entity at various points during our logic processing. We'll see this happening when we come to the doEntities update later on. Finally, the die function:


static void die(void)
{
	if (rand() % 2 == 0)
	{
		addRandomPowerup(self->x, self->y);
	}

	stage.score += 10;
}

When this function is called, there is a 50/50 chance that a random powerup will be created. The player's score will also be incremented by 10 points. We'll look at the addRandomPowerup function when we come to consider item creation and handling. Continuing with our enemy updates, we've made some changes to bullets.c:


void doBullets(void)
{
	...
	bulletHitEntity(b);

doBullets now calls a new function called bulletHitEntity shown below:


static void bulletHitEntity(Entity *b)
{
	Entity *e;
	int distance;

	for (e = stage.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->side != SIDE_NONE && e->side != b->side)
		{
			distance = getDistance(e->x, e->y, b->x, b->y);

			if (distance < e->radius + b->radius)
			{
				b->health = 0;
				e->health--;
				return;
			}
		}
	}
}

This function simply steps over each Entity in the stage, checking to see if a collision is valid (the bullet's side is not SIDE_NONE and not the same side as the bullet itself, meaning enemies and the player won't kill themselves or allies). The distance between the bullet and the entity is then calculated and the tested against the radii of the two (sphere to sphere collision check). The two spheres will overlap if the distance is less than the sum of the two radii. If so, we kill the bullet and decrement the entity's health. This will allow our bullets to collide with entities, so that Donk can shoot targets.

Onto the changes in doEntities:


void doEntities(void)
{
	for (e = stage.entityHead.next ; e != NULL ; e = e->next)
	{
		self = e;

		if (e->tick)
		{
			e->tick();
		}

		...

		if (e->touch)
		{
			touchOthers();
		}

		...

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

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

			prev->next = e->next;
			free(e);
			e = prev;
		}

		prev = e;
	}
}

At the start of the loop, we're assigning our global self pointer to the current entity. We're then testing to see if the entity has the tick function assigned and calling it if so. We're doing the same with touch, calling a new function called touchOthers as needed. Finally, we're testing to see if the Entity's health is <= 0 and removing it from our linked list, calling the die function if needed. Our touchOthers function is quite simple:


static void touchOthers(void)
{
	Entity *e;
	int distance;

	for (e = stage.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e != self)
		{
			distance = getDistance(self->x, self->y, e->x, e->y);

			if (distance < e->radius + self->radius)
			{
				self->touch(e);
			}
		}
	}
}

As we can see, it simply loops through our entity list, checking that the entity is not itself, testing distances, and then calling touch as needed. Note that the current entity is known as self. In our case, the only entities that are using this function are items.

In order to add enemies to our game, a new function called spawnEnemy has been added to the logic function of stage.c:


static void logic(void)
{
	...
	spawnEnemy();

Being in the logic function, spawnEnemy will be called every frame. The function itself is quite straightforward:


static void spawnEnemy(void)
{
	int x, y;

	if (--enemySpawnTimer <= 0)
	{
		switch (rand() % 4)
		{
			case 0:
				x = -100;
				y = rand() % SCREEN_HEIGHT;
				break;

			case 1:
				x = SCREEN_WIDTH + 100;
				y = rand() % SCREEN_HEIGHT;
				break;

			case 2:
				x = rand() % SCREEN_WIDTH;
				y = -100;
				break;

			case 3:
				x = rand() % SCREEN_WIDTH;
				y = SCREEN_HEIGHT + 100;
				break;
		}

		addEnemy(x, y);

		enemySpawnTimer = FPS + (rand() % FPS);
	}
}

First, we decrement a variable called enemySpawnTimer (a static int within stage.c). When this hits <= 0, we'll place an enemy somewhere outside of the screen (left, right, top, bottom), done by a switch against a random of 4. We'll then call addEnemy, passing over the x and y we've assigned earlier and reset our spawn timer to between 1 and 2 seconds.

One of the final things we want to look at is items.c. Right now, powerups like ammo and health will randomly appear when an enemy is killed. The file contains quite a few functions. Starting with initItems:


void initItems(void)
{
	uziTexture = loadTexture("gfx/uzi.png");
	shotgunTexture = loadTexture("gfx/shotgun.png");
	healthTexture = loadTexture("gfx/health.png");
}

initItems merely loads textures for use later. addRandomPowerup is the next function to consider:


void addRandomPowerup(int x, int y)
{
	int r;

	r = rand() % 5;

	if (r == 0)
	{
		addHealthPowerup(x, y);
	}
	else if (r < 3)
	{
		addUziPowerup(x, y);
	}
	else
	{
		addShotgunPowerup(x, y);
	}
}

We saw this being called when an enemy is killed. What this will do is randomly create a health, uzi, or shotgun powerup. Ammo is twice as likley to apper as health, being one chance in 5. We'll then either call addHealthPowerup, addUziPowerup, addShotgunPowerup. Each of these functions shares some common setup functionality, which we've put into a function called createPowerup:


static Entity *createPowerup(int x, int y)
{
	Entity *e;

	e = malloc(sizeof(Entity));
	memset(e, 0, sizeof(Entity));
	stage.entityTail->next = e;
	stage.entityTail = e;

	e->x = x;
	e->y = y;
	e->health = FPS * 5;
	e->tick = tick;
	e->radius = 16;

	e->dx = -200 + (rand() % 400);
	e->dy = -200 + (rand() % 400);

	e->dx /= 100;
	e->dy /= 100;

	return e;
}

The function basically sets up an Entity for use, setting the x and y, tick, and radius variables. It also sets the dx and dy, to make the item move in a random direction when it is created. The item won't move forever, however, as we can see in the tick function below:


static void tick(void)
{
	self->health--;

	self->dx *= 0.98;
	self->dy *= 0.98;
}

The item's health is decremented with each call to tick, so that it will eventually vanish. The Entity's dx and dy are also multiplied by 0.98, causing the movement to slow. In effect, the item will slide for a brief moment before slowing to a stop.

With the general setup done, we can look at the creation of the individual powerups:


static void addHealthPowerup(int x, int y)
{
	Entity *e;

	e = createPowerup(x, y);

	e->texture = healthTexture;
	e->touch = healthTouch;
}

static void addUziPowerup(int x, int y)
{
	Entity *e;

	e = createPowerup(x, y);

	e->texture = uziTexture;
	e->touch = uziTouch;
}

static void addShotgunPowerup(int x, int y)
{
	Entity *e;

	e = createPowerup(x, y);

	e->texture = shotgunTexture;
	e->touch = shotgunTouch;
}

addHealthPowerup, addUziPowerup, and addShotgunPowerup all call createPowerup before then assigning their own textures and touch functions. This should all be very clear, so let's look at the touch functions next:


static void uziTouch(Entity *other)
{
	if (other == player)
	{
		self->health = 0;

		stage.ammo[WPN_UZI] += 25;
	}
}

static void shotgunTouch(Entity *other)
{
	if (other == player)
	{
		self->health = 0;

		stage.ammo[WPN_SHOTGUN] += 4;
	}
}

static void healthTouch(Entity *other)
{
	if (other == player)
	{
		self->health = 0;

		player->health++;
	}
}

Notice that each of the touch functions takes an Entity as an argument. This matches with our function pointer signature in the Entity struct. Each of the touch functions performs very similar action. They each test to see if the Entity that has touched them is the player and, if so, set their own health to 0. After that, they do something specialized. uziTouch will increase the player's uzi ammo by 25, shotgunTouch will increase the shotgun ammo by 4, and healthTouch will give the player an extra point of life.

There is now plenty to do in our game, but still some things that could be done to make it more fun. For a start, the arena is too cramped. In the next tutorial we'll make it so that Donk can move around in a wider space. We'll also make the enemies fire at Donk, and introduce some new ones. Health and ammo might also be capped at starting values, so that it's no longer possible to hoard hundreds of life points and bullets.

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