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


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 13: Start / end screens

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

Introduction

We have our objectives system in place, but it would be nice to be able to view the objectives that need fulfilling. Indeed, it would be best if the objectives could be displayed at the beginning of the mission. In this part, we're going to introduce such a view, as well as allow the player to pause the game and see the objectives at any time.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter3-13 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 Tab key acts as the Pause button. Press this to dismiss the objectives display at the start, and also view it during gameplay. Once you have completed all objectives, the list will be shown again, at which point the game will effectively come to an end. Once you're finished, close the window to exit.

Inspecting the code

Our focus for this part, at least for now, is on stage.c. Later on, we'll be expanding the end game feature to make it more fancy, and also handle the player death. For now, the player remains immortal, and the mission cannot be failed.

Let's start with structs.h:


typedef struct
{
	int         status;
	double      missionCompleteTimer;
	// snipped
} Stage;

We've added one new field: missionCompleteTimer. This will be used to control how soon the mission ends after it has been completed or failed. Although it is only used by stage.c right now, we'll want to be able to access its state later on, and so we're adding it to the Stage struct.

Now, onto stage.c itself. We've started by adding in a pair of enums:


enum
{
	SHOW_STAGE,
	SHOW_PAUSE
};

These enums will control our logic and rendering. When in SHOW_STAGE state, the game will play as normal. When in SHOW_PAUSE state, we'll be rendering our objectives list, and the game will be paused.

Now for the updates to our functions. Starting with initStage:


void initStage(void)
{
	// snipped

	stage.status = MS_INCOMPLETE;

	stage.missionCompleteTimer = FPS;

	addEnemyTimer = 0;

	timePlayed = 0;

	doCamera();

	show = SHOW_PAUSE;

	app.delegate.logic = logic;
	app.delegate.draw = draw;
}

We're setting Stage's missionCompleteTimer to 1 second, a new variable called timePlayed (just used to track how long the stage took to complete) to 0, and also setting `show`, our logic and display control variable, to SHOW_PAUSE. This means that when the mission starts, we'll be greeted by the mission objectives list first.

The updates to `logic` follow:


static void logic(void)
{
	if (show == SHOW_STAGE)
	{
		if (stage.status == MS_INCOMPLETE)
		{
			if (isControl(CONTROL_PAUSE))
			{
				clearControl(CONTROL_PAUSE);

				show = SHOW_PAUSE;
			}
		}

		if (stage.missionCompleteTimer > MISSION_COMPLETE_STATUS_TIME)
		{
			doStage();

			doStatus();
		}
	}
	else if (show == SHOW_PAUSE)
	{
		if (isControl(CONTROL_PAUSE))
		{
			clearControl(CONTROL_PAUSE);

			show = SHOW_STAGE;
		}
	}
}

You'll remeber that before, this function simply called doStage. We've now expanded it, to test the value of `show` and act accordingly. We're first testing if `show` is SHOW_STAGE. If so, we're then checking if Stage's `status` is MS_INCOMPLETE (the mission is currenly in progress) and allowing the use of the pause control. If the pause control is pressed, we're going to update `show` to SHOW_PAUSE, to pause the game. We're next testing if Stage's missionCompleteTimer is greater than MISSION_COMPLETE_STATUS_TIME (negative 1 second). If so, we're calling doStage and a new function called doStatus. In short, if the mission is still in what we consider an on-going state, we'll continue to drive the mission logic.

If `show` is SHOW_PAUSE, however, we're going to do nothing more than test to see if the pause control has been pushed to unpause the game, and resume play.

Next we've made a small update to doStage:


static void doStage(void)
{
	if (stage.status == MS_INCOMPLETE)
	{
		timePlayed += app.deltaTime;
	}

	stage.numActiveEnemies = 0;

	// snipped
}

If the mission is still in progress, we're updating the timePlayed variable. Nothing more.

Now for the new doStatus function:


static void doStatus(void)
{
	switch (stage.status)
	{
		case MS_COMPLETE:
			stage.missionCompleteTimer -= app.deltaTime;
			break;

		default:
			break;
	}
}

Right now, we're just testing the value of Stage's `status`. If it's MS_COMPLETE, we're going to reduce the value of Stage's missionCompleteTimer. Again, this is a function that will see a much larger update in the future.

That's our logic updates done, so now we can move onto the rendering phase. To begin with, we've made tweeks to the `draw` function:


static void draw(void)
{
	// snipped

	if (show == SHOW_STAGE && stage.missionCompleteTimer > 0)
	{
		drawHUD();
	}

	if (show == SHOW_PAUSE)
	{
		drawPause();
	}
	else if (stage.status == MS_COMPLETE && stage.missionCompleteTimer < MISSION_COMPLETE_STATUS_TIME)
	{
		drawMissionComplete();
	}
}

Before, we were always drawing the HUD. Now, we're only drawing it if `show` is SHOW_STAGE, and also if Stage's missionCompleteTimer is greater than 0. We don't want to show our HUD if we've paused the game, as it can make things look cluttered. We also don't want to draw the HUD if the mission is drawing to a close (you'll notice the HUD goes away just before the mission complete screen is displayed). Next, we're checking if the game is paused. If so, we're calling a new function called drawPause. Finally, if the mission is complete and we've fallen below the value of MISSION_COMPLETE_STATUS_TIME, we'll call a new function named drawMissionComplete.

Let's look at drawPause now:


static void drawPause(void)
{
	app.fontScale = 3;

	drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, 0, 128);

	drawText("MISSION IN-PROGRESS", SCREEN_WIDTH / 2, 80, 255, 255, 255, TEXT_ALIGN_CENTER, 0);

	drawObjectiveList();

	drawText("PRESS [PAUSE] TO CONTINUE", SCREEN_WIDTH / 2, SCREEN_HEIGHT - 100, 255, 255, 255, TEXT_ALIGN_CENTER, 0);

	app.fontScale = 1;
}

A simple function - we're drawing various pieces of text on the screen. However, we're also calling a function named drawObjectiveList, which we'll come to in a moment.

The drawMissionComplete function follows:


static void drawMissionComplete(void)
{
	char text[16];

	app.fontScale = 3;

	drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, 0, 128);

	drawText("MISSION COMPLETE!", SCREEN_WIDTH / 2, 80, 255, 255, 255, TEXT_ALIGN_CENTER, 0);

	app.fontScale = 1.5;

	sprintf(text, "Time: %02d:%02d", (int)timePlayed / (FPS * 60), (int)(timePlayed / FPS) % FPS);

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

	drawObjectiveList();

	app.fontScale = 1;
}

Again, we're drawing various pieces of information, including the time it took for the player to complete the mission. For this, we're using the value of timePlayed, formatting it into minutes and seconds (and placing the result int a variable called `text`). Like drawPause, this function is also calling drawObjectiveList.

So, let's look at the drawObjectiveList function now:


static void drawObjectiveList(void)
{
	Objective *o;
	SDL_Color  c;
	int        y;
	char       text[16];

	app.fontScale = 2;

	y = 280;

	for (o = stage.objectiveHead.next; o != NULL; o = o->next)
	{
		c.r = c.g = c.b = 0;

		if (o->currentValue == o->targetValue)
		{
			c.g = 255;
		}
		else
		{
			c.r = c.g = c.b = 255;
		}

		sprintf(text, "%d / %d", o->currentValue, o->targetValue);

		drawText(o->description, 300, y, c.r, c.g, c.b, TEXT_ALIGN_LEFT, 0);

		drawText(text, SCREEN_WIDTH - 300, y, c.r, c.g, c.b, TEXT_ALIGN_RIGHT, 0);

		y += 80;
	}

	app.fontScale = 1;
}

This function is very easy to understand. We're just looping through all our objectives and rendering their descriptions and progress (using their currentValue and targetValue variables) to the screen. Each one will be drawn below the previous. If the objective's currentValue is equals to its targetValue, we'll render the text in green (with help from an SDL_Color named `c`). Otherwise, it will be drawn in white.

We now have our objective being displayed when the mission starts, offer the ability to show them when pausing the game, and finally show them again when the mission is complete. There is more to add, but these can be added in later parts, as they become more relevant. But for the moment, this will do.

For now, our main gameplay is done, and we have a very solid foundation upon which to build our missions. What we should do now is move on to the part of our game that sits between missions. In the next several parts, we'll be looking at the intermission phase, which will involve the star system map, comms system, shop, statistics, options, and our save game management. As you can see, there is still much to do ...

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