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


Alysha

When her village is attacked and her friends and family are taken away to be sold as slaves, Alysha Tanner sets out on a quest across the world to track them down and return them home. Along the way, she is aided by the most unlikely of allies - the world's last remaining dragon.

Click here to learn more and read an extract!

« Back to tutorial listing

— Mission-based 2D shoot 'em up —
Part 22: Main game loop

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

Introduction

We're ready to begin our main game loop. Our game will start at the intermission screen, progress into a mission, and then back to the intermissions screen. The player will carry over an catnip they have earned from their mission, as well as damage received, and ammunition. Entering the shop, the player will be able to repair the KIT-E, and buy upgrades. Saving will be possible, as well as the option to automatically save.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter3-22 to run the code. You will see a window open like the one above. Use the mouse to control the cursor. Select the planet Radish, then click the comms icon, and finally Start Mission to play the game as before, using the same controls. The player is immortal. Once the mission is complete, the game will return to the intermission screen. The Radish mission may be repeated as much as the player wants. Use the catnip earned to buy upgrades for the KIT-E. Once you're finished, close the window to exit.

Inspecting the code

It sounds like our game is basically finished. Not true, as we still have a bunch of missions to add in, and further tweaks to the code to support them. We'll get to them in due course. First up, let's look at the changes we've made to support the game loop.

Starting with structs.h:


struct Entity
{
	// snipped

	void (*die)(Entity *self);
	void (*destroy)(Entity *self);
	Entity *next;
};

We've added a new function pointer to Entity - `destroy`. This will be called when we clear all our mission data, once the stage is finished. We might have something special we want to do other than just free the entity's `data` field if it is set, so using a function pointer gives us the means to do so.

Next, let's head over to comms.c, where we've filled in the startMission function:


static void startMission(void)
{
	initStage();
}

We're simply calling initStage. That's it, that's all we need to do..! Our [still hard-coded] mission will begin. In future, we'll be adding a bit more to do, but for now it's all we need.

Now to stage.c itself. Here, we've made various updates to make sure our game loop works as expected. To begin with, we've updated initStage:


void initStage(void)
{
	startTransition();

	stopMusic();

	// snipped

	if (app.config.autosave)
	{
		saveGame();
	}

	loadMusic("music/Venus.ogg");

	addEnemyTimer = 0;

	timePlayed = 0;

	doCamera();

	show = SHOW_PAUSE;

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

	endTransition();

	playMusic(1);
}

As with initIntermission, we're starting with startTransition and finishing with endTransition. In between, we're stopping any music that might be playing, saving our game (if autosave is enabled), loading some other music to play (Venus.ogg), and finally playing the music, after the transition period has ended.

Simple enough. Now for the changes to `logic`:


static void logic(void)
{
	if (show == SHOW_STAGE)
	{
		// snipped

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

			doStatus();
		}

		if (stage.status == MS_COMPLETE && stage.missionCompleteTimer <= MISSION_COMPLETE_STATUS_TIME && isControl(CONTROL_PAUSE))
		{
			clearControl(CONTROL_PAUSE);

			completeStage();
		}
	}
	else if (show == SHOW_PAUSE)
	{
		// snipped
	}
}

If we're currently playing the game (`show` is SHOW_STAGE), we're now going to check if our mission is complete and missionCompleteTimer is less than MISSION_COMPLETE_STATUS_TIME. This is when we will be displaying the mission complete screen. At this point, we'll be testing the Pause control has been pressed (the game prompts us for this now). If so, we'll clear the control, and call a new funtion named completeStage.

So, basically we're testing if the mission has been completed and the continue prompt is showing, before we move on. completeStage is a simple function:


static void completeStage(void)
{
	game.kite.health = ((Fighter *)stage.player->data)->health;

	clearStage();

	initIntermission();
}

We're setting Game's kite's `health` to the value of the player's current `health`, to carry it over between missions, calling a new function named clearStage, and then calling initIntermission. clearStage is a function that does as its name suggests suggests - it wipes all the data associated with the mission we're playing, to prevent memory leaks, and resource and status bleeds. It's defined thusly:


static void clearStage(void)
{
	clearHud();

	clearEntities();

	clearCollectables();

	clearDebris();

	clearObjectives();

	clearBullets();

	clearEffects();

	memset(&stage, 0, sizeof(Stage));
}

As we can see, it mostly does nothing except call several other clearXXX functions, before memsetting the Stage object itself, to reset it. Our enemyTypes array was built when we loaded the mission, so we need to loop through the array and free all the associated data, before freeing enemyTypes itself. The clearXXX functions all do the same thing, but we'll look at a few briefly now, since that's all for stage.c.

If we head over to entities.c, we've added in the clearEntities function:


void clearEntities(void)
{
	Entity *e;

	while (stage.entityHead.next != NULL)
	{
		e = stage.entityHead.next;

		stage.entityHead.next = e->next;

		if (e->destroy != NULL)
		{
			e->destroy(e);
		}

		free(e);
	}

	while (stage.deadEntityHead.next != NULL)
	{
		e = stage.deadEntityHead.next;

		stage.deadEntityHead.next = e->next;

		if (e->destroy != NULL)
		{
			e->destroy(e);
		}

		free(e);
	}
}

As you can see, it simply loops through both the entity list and the dead entity list, and removes all the entities we created, calling their `destroy` function if it is set.

As another example, let's look at clearCollectables in collectables.c:


void clearCollectables(void)
{
	Collectable *c;

	while (stage.collectableHead.next != NULL)
	{
		c = stage.collectableHead.next;

		stage.collectableHead.next = c->next;

		free(c);
	}
}

Again, just a simple function that loops through all the objects in the list, and frees their data.

You may be wondering what the `destroy` function is calling for our entities. The only implementation right now is in fighters.c, where we've created a function called destroyFighter:


void destroyFighter(Entity *self)
{
	free(self->data);
}

For our fighters, this just clears the fighter data. More complex data structures would require a bit more work here, and using a function pointer makes that possible. We'll see something more complicated in a later part.

As for how it is used, if we look at greebleLightFighter.c, we can see it set to the entity's `destroy` function in initGreebleLightFighter:


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

	//snipped

	e->takeDamage = fighterTakeDamage;
	e->die = die;
	e->destroy = destroyFighter;
}

We're setting the destroyFighter function pointer here. We're doing likewise with the player, in initPlayer:


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

	// snipped

	e->takeDamage = fighterTakeDamage;
	e->die = die;
	e->destroy = destroyFighter;

	stage.player = e;
}

Here, we're setting destroyFighter to the player's entity.

That's it for our game loop! We can play our test mission as many times as we desire, earning catnip to spend on upgrades for the KIT-E, making it more powerful. Some real missions would be nice, though ...

... but before we get there, there is just one more little feature to add, that will truly bring out game alive - a scripting system. Once this is in place, our game will be transformed, with missions able to offer much more than they ordinarily would.

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