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


H1NZ

Arriving on the back of a meteorite, an alien pathogen has spread rapidly around the world, infecting all living humans and animals, and killing off all insect life. Only a handful are immune, and these survivors cling desperately to life, searching for food, fresh water, and a means of escape, find rescue, and discover a way to rebuild.

Click here to learn more and read an extract!

« Back to tutorial listing

— 2D Shoot 'Em Up Tutorial —
Part 8: Enemies fighting back!

Introduction

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

Our game wouldn't be much fun if it didn't present a challenge. Now the enemies can fire back and kill the player! Extract the archive, run cmake CMakeLists.txt, followed by make to build. Once compiling is finished type ./shooter08 to run the code.

A 1280 x 720 window will open, with a near-black background. A spaceship sprite will also be shown, as in the screenshot above. The ship can now be moved using the arrow keys. Up, down, left, and right will move the ship in the respective directions. You can also fire by holding down the left control key. Enemies (basically red versions of the player's ship) will spawn from the right and move to the left. Shoot enemies to destroy them. Enemies can fire back so you should avoid their fire. Close the window by clicking on the window's close button.

Inspecting the code

To allow the enemies to fight back and destroy the player, a number of tweaks are needed to the code, not only to make the enemies fire, but also to handle the response to the player being killed. We'll start with some updates to defs.h:


#define FPS 60

#define ALIEN_BULLET_SPEED    8

We've added a constant here called ALIEN_BULLET_SPEED to control the speed at which the bullets fired by the enemies will move. We've also added another called FPS that will be used in various places for timing calculations. It is basically our 60fps cap that we've chosen to name rather than sprinkle 60 all over the code, since it could get confusing.

Next, we've updated util.c, adding a new function called calcSlope:


void calcSlope(int x1, int y1, int x2, int y2, float *dx, float *dy)
{
	int steps = MAX(abs(x1 - x2), abs(y1 - y2));

	if (steps == 0)
	{
		*dx = *dy = 0;
		return;
	}

	*dx = (x1 - x2);
	*dx /= steps;

	*dy = (y1 - y2);
	*dy /= steps;
}

This function takes six arguments: the x and y of a src coordinate, the x and y of a destination coordinate, and two float references. What this function does is calculate the normalized step from one point to another. The dx or dy will always be 1 (or -1), while the other could be any value between -1 and 1. This will be used to tell the alien's bullets which way they need to go to reach their target when they're fired.

Moving on we see that once again it is stage.c that has seen the bulk of the changes. Beginning with initStage:


void initStage(void)
{
	...
	alienBulletTexture = loadTexture("gfx/alienBullet.png");
	playerTexture = loadTexture("gfx/player.png");

	resetStage();

Since the enemies can fire back, we need a bullet texture for them to use. We're going to use a different one from the player's, in order to keep things distinctive. We're also now caching the player's ship texture into playerTexture. This is so that whenever we create (and recreate) the player, we don't waste time and resources by loading the texture again. Another change is that we've moved some of the initialization code for the stage into a new function called resetStage.


static void resetStage(void)
{
	Entity *e;

	while (stage.fighterHead.next)
	{
		e = stage.fighterHead.next;
		stage.fighterHead.next = e->next;
		free(e);
	}

	while (stage.bulletHead.next)
	{
		e = stage.bulletHead.next;
		stage.bulletHead.next = e->next;
		free(e);
	}

	memset(&stage, 0, sizeof(Stage));
	stage.fighterTail = &stage.fighterHead;
	stage.bulletTail = &stage.bulletHead;

	initPlayer();

	enemySpawnTimer = 0;

	stageResetTimer = FPS * 2;
}

This function will do a number of things. It deletes any existing fighters and bullets, and then clears down the stage object (as well as restoring the linked list tails). It calls the initPlayer function and resets the enemySpawnTimer to 0. It also sets the value of a new variable called stageResetTimer. This is a variable that we will make use of when the player is killed. More on this below. Note that we use the FPS define here, to tell the counter to start at two seconds.

The logic function is the next one to see changes. This function has gained a few new lines:


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

	if (player == NULL && --stageResetTimer <= 0)
	{
		resetStage();
	}

The code now tests to see if player is NULL (which will happen in the case of them being killed by an alien bullet). If so, stageResetTimer will be decremented. Once it reaches 0 or less, the resetStage function will be called. We do this so that the stage is not reset instantly upon the player being killed, as this would be confusing and also look bad. Since player can now be NULL, we need to add a check to doPlayer to avoid a crash due to the NULL reference:


static void doPlayer(void)
{
	if (player != NULL)
	{
		player->dx = player->dy = 0;
		...

This is straightforward: we simply add a check for NULL on player to the whole of doPlayer. Nothing will be executed if player is NULL. Now for a new function: doEnemies:


static void doEnemies(void)
{
	Entity *e;

	for (e = stage.fighterHead.next ; e != NULL ; e = e->next)
	{
		if (e != player && player != NULL && --e->reload <= 0)
		{
			fireAlienBullet(e);
		}
	}
}

doEnemies could be considered the AI call for the enemies. What this code does is step through each fighter, first testing to see if the fighter is not the player, whether the player is alive, and whether the fighter's decremented reload variable is 0 or less. If all these are true, the enemy can fire, and will call the fireAlienBullet function, passing itself over as an argument:


static void fireAlienBullet(Entity *e)
{
	Entity *bullet;

	bullet = malloc(sizeof(Entity));
	memset(bullet, 0, sizeof(Entity));
	stage.bulletTail->next = bullet;
	stage.bulletTail = bullet;

	bullet->x = e->x;
	bullet->y = e->y;
	bullet->health = 1;
	bullet->texture = alienBulletTexture;
	bullet->side = SIDE_ALIEN;
	SDL_QueryTexture(bullet->texture, NULL, NULL, &bullet->w, &bullet->h);

	bullet->x += (e->w / 2) - (bullet->w / 2);
	bullet->y += (e->h / 2) - (bullet->h / 2);

	calcSlope(player->x + (player->w / 2), player->y + (player->h / 2), e->x, e->y, &bullet->dx, &bullet->dy);

	bullet->dx *= ALIEN_BULLET_SPEED;
	bullet->dy *= ALIEN_BULLET_SPEED;

	e->reload = (rand() % FPS * 2);
}

There are a number of similarities to the fireBullet function that is used by the player, but also some differences. We malloc an Entity to use as a bullet as expected, setting the texture to be the alienBulletTexture we cached earlier, and position the bullet in the centre of the attacker. We then calculate the direction the bullet will need to travel in order to hit the player with a call to calcSlope. We pass over the player coordinates and the attacker's, as well as references to the bullet's dx and dy so that the values can be set. We then multiply the dx and dy by ALIEN_BULLET_SPEED. Remember that either the dx or dy will be 1, meaning that the bullet will move at a constant speed of ALIEN_BULLET_SPEED along one axis. We next set the side of the bullet to be SIDE_ALIEN. This means that the bullet will only hit the player, passing through both the fighter that issued it, as well as any other ships that it happens to touch. Finally, we tell the attacker that it may fire again anytime within the next 2 seconds.

All this ultimately means that the enemies have multidirectional fire, in contrast to the player's straight shots. Now we need to make some changes to doFighters to accommodate the ability for the player to be killed:


static void doFighters(void)
{
	Entity *e, *prev;

	prev = &stage.fighterHead;

	for (e = stage.fighterHead.next ; e != NULL ; e = e->next)
	{
		e->x += e->dx;
		e->y += e->dy;

		if (e != player && e->x < -e->w)
		{
			e->health = 0;
		}

		if (e->health == 0)
		{
			if (e == player)
			{
				player = NULL;
			}

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

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

		prev = e;
	}
}

We retain the test for the enemies moving off the left-hand side of the screen, setting their health to 0 if they do so. But now when we test if the fighter's health is 0 we also check to see if this is the player. If so, we NULL player so that we don't have a dangling pointer when we free it. Not doing so and not checking for NULL as we have done earlier would either lead to a crash or undefined behaviour (for example, it's possible the player might briefly turn into one of the enemies).

Another update we need to make is to tell the game to kill a bullet if it leaves the screen at any point and not just the right-hand side.


static void doBullets(void)
{
	...
	if (bulletHitFighter(b) || b->x < -b->w || b->y < -b->h || b->x > SCREEN_WIDTH || b->y > SCREEN_HEIGHT)
	{
	...

This is as simple as testing to see if the bullet's x or y coordinate (plus w and h) is outside of the screen and setting its health to 0.

We're almost done. Just one more tweak to make to spawnEnemies:


static void spawnEnemies(void)
{
	...
	enemy->reload = FPS * (1 + (rand() % 3));


We want to make sure that the enemies don't open fire the moment they are created, but to wait a few seconds before doing so. We achieve this by setting their reload to 1 to 2 seconds, giving the player a chance to destroy them and not have to survive a hail of fire from the very start.

Finally, we don't want the player to be able to run off-screen, so let's clip player to the bounds of the playfield:


static void clipPlayer(void)
{
	if (player != NULL)
	{
		if (player->x < 0)
		{
			player->x = 0;
		}

		if (player->y < 0)
		{
			player->y = 0;
		}

		if (player->x > SCREEN_WIDTH / 2)
		{
			player->x = SCREEN_WIDTH / 2;
		}

		if (player->y > SCREEN_HEIGHT - player->h)
		{
			player->y = SCREEN_HEIGHT - player->h;
		}
	}
}

The code here should be obvious: it stops the player from leave the screen and also prevents them from moving forward any further than about the midway point.

And there you have it: the bones of a 2D shooter! There is still much that we can add to the game, such as effects, sound and music, scoring, and other bits, but all the pieces are falling nicely into place.

Exercises

  • Allow the player or enemies to survive more than one shot.
  • Vary the speed at which the enemy's shots move.
  • Add a new firing type for the enemies, so that they sometimes fire straight shots. For this, you'll need to create new graphics and make changes to structs.h to specify how the enemy should attack.

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