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

— Mission-based 2D shoot 'em up —
Part 26: Mission: Bombers

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

Introduction

Our third mission is going to do something special - we're going to be charged with chasing down 5 bombers, and destroying them. This mission will make use of destroying named targets, and not just enemies. Defeating enemies won't count towards our total, only destrying the bombers will. Once the quota of bombers is met, the player will need to defeat all the remaining enemies, and the mission will be complete.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter3-26 to run the code. You will see a window open displaying the intermission's planets screen. Select Bluey, then enter the comms section to start the mission. Play the game as normal. You may repeat the mission as often as you like. Remember that you can change the fighter's damage, weapons, output, and health by editing game.c, if you wish to get ahead of things early. Once you're finished, close the window to exit.

Inspecting the code

Let's take a quick look at the mission file (data/missions/bluey.json):


{
	"musicFilename": "music/Venus.ogg",
	"entities" : [
	],
	"enemies" : {
		"types" : "greebleLightFighter,greebleLightFighter,greebleDualFighter,greebleBomber",
		"total" : -1,
		"limit" : 4
	},
	"objectives" : [
		{
			"id": "",
			"type" : "OT_NORMAL",
			"description" : "Destroy Greeble Bombers",
			"targetValue" : 5,
			"targetName" : "greebleBomber"
		},
		{
			"id": "",
			"type" : "OT_DEFEAT_ALL_ENEMIES",
			"description" : "Defeat all remaining enemies",
			"targetValue" : 1,
			"targetName" : ""
		}
	],
	"script" : [
		...
	]
}

Our enemy types is a comma separated list of enemies. Notice how we've got greebleLightFighter listed twice. This means they are twice as likely to appear compared to the others. Our objectives list contains, as expected, a requirement to defeat 5 greebleBombers (as the targetName). That's all we need to do to make the bombers the active target..!

Other than our new mission file and the update the planets.json, there isn't a great deal that we need to do here! This a short part indeed.

To support our new enemy, we've updated defs.h with some new enums:


enum
{
	AI_NORMAL,
	AI_DEFENSIVE
};

AI_DEFENSIVE is an enum that will be used to specify that the enemy uses a defensive AI pattern, and will tend to stay away from enemies.

We've also added a new AI_WPN:


enum
{
	AI_WPN_NONE,
	AI_WPN_SINGLE,
	AI_WPN_DUAL,
	AI_WPN_ROCKET
};

AI_WPN_ROCKET will give this AI access to rockets as a weapon, just what the bombers need.

Now for the bombers themselves. We've created a new compilation unit called greebleBomber.c, that contains all the functions required. Starting with initGreebleBomber:


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

	f = malloc(sizeof(Fighter));
	memset(f, 0, sizeof(Fighter));
	f->health = f->maxHealth = 12;
	f->ai.weaponType = AI_WPN_ROCKET;
	f->ai.weaponReload = FPS;
	f->ai.type = AI_DEFENSIVE;
	f->ai.thinkTime = rand() % FPS;

	e->side = SIDE_GREEBLE;
	e->facing = FACING_RIGHT;
	e->data = f;
	e->texture = getAtlasImage("gfx/fighters/greebleBomber.png", 1);

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

Okay, a very standard factory function here. Of note is that the weaponType is AI_WPN_ROCKET, and the ai type itself is AI_DEFENSIVE. The bombers have a fair bit of health, but no shielding.

On to `tick`:


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

	f = (Fighter *)self->data;

	f->dx = MIN(MAX(f->dx, -MAX_SPEED), MAX_SPEED);
	f->dy = MIN(MAX(f->dy, -MAX_SPEED), MAX_SPEED);

	fighterTick(self, f);

	doFighterAI(self, f);

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

	stage.numActiveEnemies++;
}

Very similar to the other fighters. The only difference here is where we're positioning the engine (self->y + 25). If we were going to make a generic bunch of functions for our fighters, we'd want to either pass the engine position over as a parameter, or build it into the Fighter struct.

We're going to skip over the `draw` and `destroy` functions, as they are the same as greebleLightFighter and greebleDualFighter, and only consider the `die` function:


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

	playSound(SND_EXPLODE_1 + rand() % 4, CH_ANY);

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

	addExplosions(x, y, 25);

	addDebris(x, y, 20);

	dropCatnip(self->x, self->y, 100);

	if (rand() % 5 == 0)
	{
		dropHealth(self->x, self->y);
	}

	n = 1 + rand() % 3;

	for (i = 0; i < n; i++)
	{
		dropAmmo(self->x, self->y);
	}

	game.catnip += 25;

	self->dead = 1;

	game.stats.enemiesDestroyed++;

	updateObjective("ENEMY", 1);
	updateObjective("greebleBomber", 1);
}

Mostly the same as the others. However, our bombers don't drop any catnip when they are destroyed! Instead, there is a 100% chance of dropping ammo, with them releasing up to 3 of the powerups. Also, note that we're passing over "greebleBomber" to updateObjective. This is essential if we want our objective count (Destroy 5 Bombers) to be reachable.

That's all for the bomber, so let's head over to ai.c, where we've put in the new defensive AI code. Starting with doFighterAI:


void doFighterAI(Entity *self, Fighter *f)
{
	if (!stage.player->dead)
	{
		if (f->ai.thinkTime <= 0)
		{
			// snipped

			switch (f->ai.type)
			{
				case AI_NORMAL:
					doNormalAI(self, f);
					break;

				case AI_DEFENSIVE:
					doDefensiveAI(self, f);
					break;

				default:
					break;
			}
		}

		// snipped
	}
}

We've added AI_DEFENSIVE to our switch statement, and calling doDefensiveAI when we meet it. The doDefensiveAI function is up next:


static void doDefensiveAI(Entity *self, Fighter *f)
{
	int      x, y, speed;

	speed = 0;

	if (f->ai.target != NULL && rand() % 10 <= 7)
	{
		if (rand() % 3 == 0)
		{
			calcSlope(self->x, self->y, f->ai.target->x, f->ai.target->y, &f->dx, &f->dy);
		}
		else
		{
			calcSlope(f->ai.target->x, f->ai.target->y, self->x, self->y, &f->dx, &f->dy);
		}

		speed = 5 + rand() % 8;
	}
	else
	{
		x = self->x + rand() % SCREEN_WIDTH - rand() % SCREEN_WIDTH;
		y = self->y + rand() % SCREEN_HEIGHT - rand() % SCREEN_HEIGHT;

		calcSlope(self->x + x, self->y + y, self->x, self->y, &f->dx, &f->dy);

		speed = 2 + rand() % 8;
	}

	f->dx *= speed;
	f->dy *= speed;

	self->facing = f->dx < 0 ? FACING_LEFT : FACING_RIGHT;

	f->ai.thinkTime = FPS + (FPS * rand() % 3);
}

So, what's going on here? Well, we're first testing if we have a `target` and also if a random of 10 is 7 or less. If so, there's a 1 in 3 chance that our AI will head towards its `target`. Otherwise, it will fly in the opposite direction! Basically, it's more likely to retreat from the player than attack them. If the first if-statement was false, our bomber will choose a random point a screen's width and height away from it, and head for that. This is a more extreme form of the random darting movement the red and blue fighters use when battling the player. With that decided, we sort out the bomber's speed (`dx` and `dy`), `facing`, and update its thinkTime.

Yes, our defensive AI is very simple. Again, it's more likely to keep away from us than to engage us. But given how deadly the bombers are, that's no bad thing.

The last function we've updated is attackTarget:


static void attackTarget(Entity *self, Fighter *f)
{
	int distX, distY;

	if (f->reload == 0 && ((self->facing == FACING_LEFT && f->ai.target->x < self->x) || (self->facing == FACING_RIGHT && f->ai.target->x > self->x)))
	{
		distX = abs(self->x - f->ai.target->x);
		distY = abs(self->y - f->ai.target->y);

		if (distX < SCREEN_WIDTH / 2 && distY < 25)
		{
			switch (f->ai.weaponType)
			{
				// snipped

				case AI_WPN_ROCKET:
					fireRocket(self, self->facing);
					break;

				default:
					break;
			}

			f->reload = f->ai.weaponReload;
		}
	}
}

As we now have a new weapon type for the AI, we've added AI_WPN_ROCKET to our switch statement, calling fireRocket when the condition is met. That's all we need to do..! When we first created the fireRocket function, we didn't tie it to the player, allowing it to be used by any fighter. And so, all we need to do is pass over the enemy fighter's details (self and its facing) and we're all good.

Before we wrap up, just a reminder that when adding in fighters and other entities, we need to register them with our entityFactory. The initEntityFactory function contains a mapping of the entities we want to spawn, against their init functions:


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

	addInitFunc("player", initPlayer);
	addInitFunc("greebleLightFighter", initGreebleLightFighter);
	addInitFunc("greebleDualFighter", initGreebleDualFighter);
	addInitFunc("greebleBomber", initGreebleBomber);
}

As we add more entities, we'll be updating this function with the relevant details.

And that's a wrap! Simple, wasn't it? Again, since we already spent time putting together our objectives, mission loading, AI, etc., all we need do now is slot the missing pieces in. We need to make adjustments here and there, but nothing major.

Right, so we've now seen offensive and defensive enemies. How about ones that flee us completely? Our next mission will see the player having to chase down enemies that will try to evade them, and don't possess any weapons. But not only that, we're about to introduce the possibility of failing a mission without being killed..!

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