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


The Attribute of the Strong (Battle for the Solar System, #3)

The Pandoran War is nearing its end... and the Senate's Mistake have all but won. Leaving a galaxy in ruin behind them, they set their sights on Sol and prepare to finish their twelve year Mission. All seems lost. But in the final forty-eight hours, while hunting for the elusive Zackaria, the White Knights make a discovery in the former Mitikas Empire that could herald one last chance at victory.

Click here to learn more and read an extract!

« Back to tutorial listing

— Mission-based 2D shoot 'em up —
Part 28: Mission: SS Goodboy

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

Introduction

The penultimate mission that we're going to add will involve the player having to protect another vessel. The SS Goodboy is having trouble with her engines, and so Leo has come to the rescue. The enemy will be able to both target, attack, and destroy the SS Goodboy, so the player must stay on their toes when it comes to keeping the ship safe. If the SS Goodboy is destroyed, the mission will be failed.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter3-28 to run the code. You will see a window open displaying the intermission's planets screen. Select Pea, then enter the comms section to start the mission. Play the game as normal, defeating the enemies, and ensuring they don't destroy the SS Goodboy. After some time, the SS Goodboy will report their engines are fixed, and the player need only clean up the remaining enemies. 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

As promised, these parts are decreasing in length and complexity, thanks to us having already built the foundations upon which this mission is based. We will need to make a few additions, but they are minor compared to everything else, as we'll see.

Right then, starting with structs.h:


typedef struct
{
	// snipped
	Mission    *mission;
	struct
	{
		double health;
		int    maxHealth;
	} missionTarget;
} Stage;

We've added in an inner struct to Stage called missionTarget, to hold the mission target's health. `health` is the current health, while maxHealth is the maximum health. Note that `health` is a pointer, so that it always reflects the SS Goodboy's current health.

Now for the SS Goodboy itself. We've created ssGoodboy.c to hold all of the Goodboy's functions. Once more, they will be very familiar. So, let's jump in, starting with initSSGoodboy:


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

	f = malloc(sizeof(Fighter));
	memset(f, 0, sizeof(Fighter));
	f->health = f->maxHealth = 150;
	f->ai.weaponType = AI_WPN_NONE;
	f->ai.type = AI_EVADE;
	f->ai.thinkTime = rand() % FPS;

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

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

	stage.missionTarget.health = &f->health;
	stage.missionTarget.maxHealth = f->maxHealth;
}

The SS Goodboy has no weapons, uses the evade AI type, and is also on the side of the cats (SIDE_CATS). At the end of the function, we're setting Stage's missionTaraget's `health` to the SS Goodboy's health (as a pointer), and the maxHealth to the Goodboy's maxHealth.

Now for the `tick` function:


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 - 25 : 25), self->y + 60);
	}
}

Engine position, blah blah blah! Shall we move on?

Again, we're going to skip `draw` and `destroy`, and look at the `die` function:


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, 40);

	addDebris(x, y, 100);

	self->dead = 1;

	updateObjective("ssGoodboyDestroyed", 1);
}

When the SS Goodboy is destroyed, it will call updateObjective "ssGoodboyDestroyed". We have a conditional objective with this as the target name, meaning that the mission will fail if we lose the SS Goodboy.

That's all we need to do for the SS Goodboy..! It's rather like the Greeble POW ships, except that it's on the side of the player. The SS Goodboy will make an effort to run away from the enemies, just like how the POW ships flee the player.

Our mission introduces a few new script commands: COMPLETE_OBJECTIVE and SET_ENEMY_LIMIT. We just need to head over to script.c, and update doScript to support these:


void doScript(void)
{
	// snipped

	for (s = head.next; s != NULL; s = s->next)
	{
		if (s->running)
		{
			stage.scriptRunning = 1;

			s->delay = MAX(s->delay - app.deltaTime, 0);

			if (s->delay == 0)
			{
				// snipped

				else if (strcmp(command, "COMPLETE_OBJECTIVE") == 0)
				{
					sscanf(line, "%*s %s", strParam[0]);

					completeObjective(strParam[0]);
				}
				else if (strcmp(command, "SET_ENEMY_LIMIT") == 0)
				{
					sscanf(line, "%*s %d", &stage.totalEnemies);
				}
				else
				{
					SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_CRITICAL, "ERROR: Unrecognised script command '%s'\n", command);
					exit(1);
				}

				// snipped
			}
		}
	}
}

Starting with COMPLETE_OBJECTIVE. This command will instantly complete an objective, identified by id. We're reading the argument for the command (the `id` of the objective) into a strParam, and then calling a new function named completeObjective, passing over the id. We'll see this function in a moment.

SET_ENEMY_LIMIT is another simple command to handle. As the name implies, this sets the enemy limit in the stage. We're reading the value and setting Stage's totalEnemies to it. In the case of our mission, totalEnemies is -1 (unlimited), and we'll be setting it to 5, so that only 5 more enemies will be spawned on the stage from here on out.

That's all we need to do for our scripting updates. Let's look at the new completeObjective function, in objectives.c:


void completeObjective(char *id)
{
	Objective *o;
	char       text[MAX_LINE_LENGTH];

	for (o = stage.objectiveHead.next; o != NULL; o = o->next)
	{
		if (o->currentValue < o->targetValue && strcmp(o->id, id) == 0)
		{
			o->currentValue = o->targetValue;

			sprintf(text, "%s - Objective Complete!", o->description);

			addHudMessage(text, 0, 255, 0);
		}
	}
}

As expected, we're looping through all our objectives, searching for an objective that isn't yet complete (currentValue less than targetValue), and whose `id` matches the `id` passed into the function. Once found, we'll set the currentValue to the targetValue, and print the Objective Complete message. Simples.

We're very nearly done..! All that remains is to head over to stage.c, to make the updates to display the mission target's health bar.

Starting with drawHUD:


void drawHUD(void)
{
	drawIndicatorArrows();

	drawHealthBar();

	if (game.kite.secondaryWeapon == SW_SHIELD)
	{
		drawShieldBar();
	}
	else if (game.kite.secondaryWeapon != SW_NONE)
	{
		drawAmmoBar();
	}

	drawCatnip();

	drawObjectives();

	drawHudMessages();

	drawMessageBox();

	drawMissionTargetHealthBar();
}

We're calling a new function, drawMissionTargetHealthBar:


static void drawMissionTargetHealthBar(void)
{
	int       i, x;
	double    percent;
	SDL_Color c;

	if (stage.missionTarget.health != NULL)
	{
		x = SCREEN_WIDTH - 20;

		percent = (*stage.missionTarget.health / *stage.missionTarget.maxHealth) * MISSION_TARGET_HEALTH_BAR_SIZE;

		for (i = 0; i < MISSION_TARGET_HEALTH_BAR_SIZE; i++)
		{
			c.r = c.g = c.b = 0;

			if (i < MISSION_TARGET_HEALTH_BAR_SIZE * 0.15)
			{
				c.r = 255;
			}
			else if (i < MISSION_TARGET_HEALTH_BAR_SIZE * 0.5)
			{
				c.r = c.g = 255;
			}
			else
			{
				c.g = 255;
			}

			if (i >= percent)
			{
				c.r *= 0.35;
				c.g *= 0.35;
				c.b *= 0.35;
			}

			drawRect(x, SCREEN_HEIGHT - 20, HEALTH_BLOCK_WIDTH, HEALTH_BLOCK_HEIGHT, c.r, c.g, c.b, 255);

			drawOutlineRect(x, SCREEN_HEIGHT - 20, HEALTH_BLOCK_WIDTH, HEALTH_BLOCK_HEIGHT, 0, 0, 0, 255);

			x -= HEALTH_BLOCK_WIDTH;
		}
	}
}

We're first testing if stage.missionTarget.health is not NULL (remember, it's a pointer!), and then preparing to draw the health bar. Unlike our player's health and shield bars, the mission target's health bar is always the same length (MISSION_TARGET_HEALTH_BAR_SIZE, defined as 75), but the associated value will be displayed as a percentage. In other words, we'll always draw MISSION_TARGET_HEALTH_BAR_SIZE blocks, but adjust the brightness of the blocks if they are within the target's health percentage.

We calculate the percent of the missionTarget's `health`, multiply that by MISSION_TARGET_HEALTH_BAR_SIZE to discover how many bright blocks to draw, then use a for-loop to render the blocks. As with the player's health bar, we'll render the blocks in red, yellow, and green, based on their threshold. You will have observed as the SS Goodboy takes damage that the number of bright blocks decreases.

One thing to keep in mind is that, unlike the player's health bar, we're presenting the target's health bar from right to left. Green blocks on the left, red on the right. So, our x position is decreasing from its starting point, to move left.

Another mission done! We've now only got one mission left to do, and we're nearly finished! The next mission is the big one - the boss fight. The Greebles aren't impressed with Leo's antics in the new KIT-E fighter, and have deployed a frigate to deal with him. The player must now prepare themselves to face off against The Gravlax!

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