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

Android Games

DDDDD
Number Blocks
Match 3 Warriors

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


A North-South Divide

For over a hundred years, messenger Duncan has wandered the world, searching for the missing pieces of an amulet that will rid him of his curse; a curse that has burdened him with an extreme intolerance of the cold, an unnaturally long life, and the despair of watching all he knew and loved become lost to the ravages of time. But now, Duncan is close to the end of his long quest.

Click here to learn more and read an extract!

« Back to tutorial listing

— Mission-based 2D shoot 'em up —
Part 12: Objectives

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

Introduction

It's time to introduce our objectives system into our game. Each of our missions will feature one of more objectives that need to be completed, and so we need a system to track them. Our objectives system will be simple to understand and implement. No major complexity will be found here!

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter3-12 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. Play the game as normal. The goal is to destroy 10 enemies fighters and collect 150 catnip (total value) that is dropped by them. Take note of the objective update messages. When all objectives are met, the Mission Complete message will be displayed in green (the game will continue playing, however). Once you're finished, close the window to exit.

Inspecting the code

Adding in our objectives isn't a difficult task, at all, and the system we're going to build will come with a lot of flexability. At the end of the day, our objectives system is just a list of ids and values. Let's get into the code.

We'll start with defs.h, where we've added a new enum:


enum
{
	MS_INCOMPLETE,
	MS_COMPLETE
};

This enum will hold the state of the current mission. MS is short for Mission Status. MS_INCOMPLETE will mean our mission is in progress, while MS_COMPLETE will mean we've completed all our objectives.

Now on to structs.h, where we've made some additions and updates. Starting with the Objective struct itself:


struct Objective
{
	char       description[MAX_DESCRIPTION_LENGTH];
	char       targetName[MAX_NAME_LENGTH];
	int        currentValue;
	int        targetValue;
	Objective *next;
};

As expected, this struct will hold all the data for an objective in our game. `description` is the description of the objective, targetName is the name of the target item that is tied to this objective, currentValue is the progress of this objective, while targetValue is the goal value.

We've also updated Stage:


typedef struct
{
	int         status;
	double      ssx, ssy;
	Effect      effectHead, *effectTail;
	Entity      entityHead, *entityTail;
	Entity      deadEntityHead, *deadEntityTail;
	Bullet      bulletHead, *bulletTail;
	Debris      debrisHead, *debrisTail;
	Collectable collectableHead, *collectableTail;
	HudMessage  hudMessageHead, *hudMessageTail;
	Objective   objectiveHead, *objectiveTail;
	double      engineEffectTimer, rocketEffectTimer;
	int         numActiveEnemies;
	Entity     *player;
	PointF      camera;
} Stage;

We've added in two new fields: `status`, and objectiveHead and objectiveTail, the linked list that will hold our objectives.

With those all setup, we can now look at the new compilation unit that will handle our objectives logic. objectives.c features a handful of functions right now, but will fill out in future. We'll start with initObjectives:


void initObjectives(void)
{
	Objective *o;

	stage.objectiveTail = &stage.objectiveHead;

	objectiveTimer = FPS;

	o = malloc(sizeof(Objective));
	memset(o, 0, sizeof(Objective));
	stage.objectiveTail->next = o;
	stage.objectiveTail = o;
	STRCPY(o->targetName, "ENEMY");
	STRCPY(o->description, "Defeat 10 enemies");
	o->targetValue = 10;

	o = malloc(sizeof(Objective));
	memset(o, 0, sizeof(Objective));
	stage.objectiveTail->next = o;
	stage.objectiveTail = o;
	STRCPY(o->targetName, "catnip");
	STRCPY(o->description, "Collect 150 catnip");
	o->targetValue = 150;
}

We're doing a number of things here. First, we're setting up our objectives linked list in Stage. Next, we're setting a variable called objectivesTimer to FPS. The purpose of objectivesTimer is to test whether all our objectives have been completed, and our mission is over. The reason for this is because we won't always be able to check that all the objectives are done when updating one. We'll see this in a future part. This is basically a pre-emptive measure.

Next, we're setting up some objectives. Note that this is only temporary; our objectives will come from our mission files in future. We're creating two objectives here. The first requires us to defeat 10 enemies. Note how the targetName of the objective is "ENEMY" and the targetValue is 10. Next, we're setting up another objective to collect 150 catnip. Again, note how the targetName is "catnip" and the targetValue is 150.

Now, let's checkout the doObjectives function:


void doObjectives(void)
{
	Objective *o;
	int        allComplete;

	if (stage.status == MS_INCOMPLETE)
	{
		objectiveTimer -= app.deltaTime;

		if (objectiveTimer <= 0)
		{
			allComplete = 1;

			objectiveTimer = FPS;

			for (o = stage.objectiveHead.next; o != NULL; o = o->next)
			{
				if (o->currentValue < o->targetValue)
				{
					allComplete = 0;
				}
			}

			if (allComplete)
			{
				addHudMessage("Mission Complete!", 0, 255, 0);

				stage.status = MS_COMPLETE;
			}
		}
	}
}

This function is responsible for checking if all our objectives have been completed. We're first checking the status of the mission (Stage's `status`). If `status` is MS_INCOMPLETE, we'll reduce the value of objectiveTimer, and begin checking our objectives if it hits 0 or less. We'll then set a flag called allComplete to 1, to assume all our objectives are complete. Next, we'll loop through all the objectives and check if any are below their targetValue. If so, we'll set allComplete to 0. With that done, we'll finally check the value of allComplete. If it's 1, we'll display a HUD message saying the mission is complete, and then set Stage's `status` to MS_COMPLETE.

The doObjectives function will only run once a second. It adds a nice little pause to the proceeding when we finish a mission, and there's a slight delay between completing the last objective and seeing the "Mission Complete!" message (yes, this is basically artistic license).

Now let's look at updateObjective. This function is the tent pole of our system:


void updateObjective(char *targetName, int value)
{
	Objective *o;
	char       text[MAX_LINE_LENGTH];
	double     oldVal, newVal;

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

			o->currentValue = MIN(o->currentValue + value, o->targetValue);

			if (o->currentValue == o->targetValue)
			{
				sprintf(text, "%s - Objective Complete!", o->description);

				addHudMessage(text, 0, 255, 0);

				objectiveTimer = FPS;
			}
			else
			{
				oldVal /= o->targetValue;
				oldVal = (int)(oldVal * 10);

				newVal = o->currentValue;
				newVal /= o->targetValue;
				newVal = (int)(newVal * 10);

				if (newVal > oldVal)
				{
					sprintf(text, "%s - %d / %d", o->description, o->currentValue, o->targetValue);

					addHudMessage(text, 128, 200, 255);
				}
			}
		}
	}
}

This function takes the targetName of the objective and a value as its arguments. We start by looping through all our objectives, searching for an objective with a matching targetName. It is also expected that the currentValue is below the targetValue (it's not yet complete). With one found, we'll keep a track of the currentValue before updating (assigned to oldVal), and then add `value` to the objective's currentValue (ensuring it doesn't exceed targetValue). Next, we test if our currentValue is now at our targetValue, and if so we'll display a HUD message to say that the objective is complete. We'll also reset objectivesTimer to FPS (again, artistic license..!).

If we've not hit our targetValue, we might output a progress message instead. We take both the previous value (oldVal) and the current value of the objective (currentValue), divide them by the target value, and then multiply them by 10, giving us a value between 0 and 10 each. We then check if the new value (newVal) is higher than the old value (oldVal) and display a HUD message to show our progress. The calculation means we get to see the objective progressing every 10%, meaning we don't clutter the HUD with messages for every single progress update. An objective with a large targetValue would create a flood.

And that's it for objectives.c! As you can see, our objectives are merely key-value pairs, with goals. Now, let's look at how we integrate with the rest of the code.

Starting with collectables.c:


static void doCollectable(Collectable *c)
{
	Fighter *f;
	char     text[MAX_DESCRIPTION_LENGTH];

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

	c->health -= app.deltaTime;

	if (collision(c->x, c->y, c->texture->rect.w, c->texture->rect.h, stage.player->x, stage.player->y, stage.player->texture->rect.w, stage.player->texture->rect.h))
	{
		switch (c->type)
		{
			case CT_CATNIP:
				game.catnip += c->value;
				sprintf(text, "Got %d CN", c->value);
				addHudMessage(text, 255, 255, 255);
				updateObjective("catnip", c->value);
				break;

			// snipped
		}

		c->health = 0;
	}
}

We've added a call to updateObjective in our switch statement, when we pickup a CT_CATNIP collectable. We're passing over "catnip", plus the value of the collectable itself. Thus, we get to push up the value of our catnip objectives. Naturally, if there was no objective with a targetName of "catnip" nothing would happen here.

We've also updated greebleLightFighter.c with a similar call:


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

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

	// snipped

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

We're calling updateObjective here, passing over "ENEMY" and 1. We're also passing over "greebleLightFighter" and 1. While our objective's targetName is "ENEMY", we could actually become more specific with our targets. Later, we'll introduce more enemies, who will also trigger the "ENEMY" objective update. But what if we wanted to update an objective for defeating a particular enemy type, and not just any enemy? This is what this code would be. Again, there is no objective for "greebleLightFighter" in this example, so this call will do nothing.

Let's now look at the updates to hud.c. You will have noticed that our objective number is displayed at the top of the screen. Starting with drawHUD:


void drawHUD(void)
{
	// snipped

	drawCatnip();

	drawObjectives();

	drawHudMessages();
}

We're calling a new function drawObjectives:


static void drawObjectives(void)
{
	char       text[32];
	int        completed, total;
	Objective *o;

	completed = total = 0;

	for (o = stage.objectiveHead.next; o != NULL; o = o->next)
	{
		if (o->currentValue == o->targetValue)
		{
			completed++;
		}

		total++;
	}

	sprintf(text, "Objectives: %d / %d", completed, total);

	drawText(text, SCREEN_WIDTH / 2, 0, 255, 255, 255, TEXT_ALIGN_CENTER, 0);
}

This function basically loops through all our objectives, counting how many there are (`total`) and how many are complete (`completed`). We'll then draw that text centered at the top of the screen.

Only one thing left to do now, and that's to update stage.c. We start with initStage:


void initStage(void)
{
	// snipped

	stage.status = MS_INCOMPLETE;

	// snipped

	initObjectives();

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

	// snipped
}

We're setting Stage's `status` to MS_INCOMPLETE, and also calling initObjectives. Finally, we update doStage:


static void doStage(void)
{
	// snipped

	doDebris();

	doCollectables();

	doObjectives();

	// snipped
}

Here, we're calling the doObjectives function.

Finished! A simple, yet highly capable, objectives system. As you can see, it's quite easy to implement other objectives and triggers. If we wanted a objective for picking up ammo, for example, we just create an objective with that target name, and add the updateObjectives call to our collectables code. We can also add in other types of objectives, such as ones that will cause our mission to fail if they are met. We'll actually see this later on.

What we need now are screens to display our objectives and their progress, so that the player can see at a glance what needs to be done. So, in the next part, we'll look into implementing these.

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