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


The Red Road

For Joe Crosthwaite, surviving school was about to become more than just a case of passing his exams ...

Click here to learn more and read an extract!

« Back to tutorial listing

— Making a 2D split screen game —
Part 15: Zones (again)

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

Introduction

Up until now, we've not had a proper game loop. Either it is impossible to win a match or the game stops once someone wins. That's no good, at all. What we want is for the game to continue on once someone wins, changing zones if requested. This part finally establishes our main game loop, so that the game can continue on after a player wins one of the matches. Each time the match is finished, we'll move to next zone, randomly. Ultimately, this behaviour can be configured by the players themselves. Here, however, we've hardcoded the conditions (1 life each, random zone switching).

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./versus15 to run the code. You will see a window open like the one above, with each player on either side of our zone. Use the default controls (or, at your option, copy the config.json file from a previous tutorial, to use that - remember to exit the game before copying the replacement file). Play the game as normal. To speed things up, each player only has one life. Each time a player wins a match, the game will randomly move onto a new one. Once you're finished, close the window to exit.

Inspecting the code

Our code changes are simple, and mostly involve cleaning things up once the match is over, to prevent memory leaks.

Let's start with defs.h:


#define NUM_ZONES 5

We've added a new define called NUM_ZONES. This is the set number of Zones in our game, which will be used when we randomly choose a Zone to move to.

Now for zone.c, where the first of our major changes lie. First up, we've updated initZone:


void initZone(int n)
{
	memset(&zone, 0, sizeof(Zone));

	zoneNum = n;

	game.livesLimit = 1;
	game.scoreLimit = ZONE_UNLIMITED_TARGET;
	game.timeLimit = ZONE_UNLIMITED_TARGET;
	game.zoneNum = -1;

	// snipped

	initWorld();

	loadWorld(zoneNum);

	loadEntities(zoneNum);

	// snipped

	app.transitionTimer = FPS / 2;
	app.delegate.logic = logic;
	app.delegate.draw = draw;
}

The function now accepts an int parameter, called `n`. This is the Zone number. We set a new static variable called zoneNum to the value of `n`, and then use that in the calls to loadWorld, and the new function loadEntities.

(for the purpose of this part, we're setting our goals to 1 life per player, and unlimited targets for everything else - included in this code above, for the sake of context and completeness).

Over next to `logic`:


static void logic(void)
{
	// snipped

	if (gameOverTimer > -FPS * 2)
	{
		// snipped
	}
	else if (gameOverTimer < -FPS * 5)
	{
		nextZone();
	}

	app.deltaTime = oldDeltaTime;

	// snipped
}

One simple change here. We're now checking if gameOverTimer has fallen to -5 seconds, and calling a function named nextZone if so. This gives us a small delay after the WINNER / LOSER! text is displayed on screen before we move to the next match. The call to nextZone will set this up.

nextZone is an easy function to understand:


static void nextZone(void)
{
	clearZone();

	if (game.zoneNum != -1)
	{
		initZone(game.zoneNum);
	}
	else
	{
		initZone(1 + (rand() % NUM_ZONES));
	}
}

We start by calling clearZone, a function that will tear down all our data (deleting entities, bullets, particles, etc), and getting everything back into a ready state. Next, we test if Game's zoneNum is not -1. If so, we'll use that number as the zone to load. This is will be the case if the players have opted to play the same zone in the options. Otherwise, we'll randomly pick a zone to play. Note that our Zones start from 1. -1 will mean random (there's a reason why we're not using 0 here, and it's to do with our widgets and configuration..!).

Now for clearZone:


void clearZone(void)
{
	clearEntities();

	clearBullets();

	clearParticles();

	clearWorld();

	destroySpatialGrid();
}

This function is simply responsible for deleting all the entities, bullets, etc. that currently exist. It delegates to the appropriate "clear" functions. We'll cover these briefly later.

That's all the changes needed to zone.c. Let's now take a look at the new loadEntities function we've created in entities.c:


void loadEntities(int zoneNum)
{
	int    x, y;
	char   filename[MAX_FILENAME_LENGTH], *data, *type;
	cJSON *root, *node;

	sprintf(filename, "data/zones/%02d.json", zoneNum);

	data = readFile(filename);

	root = cJSON_Parse(data);

	for (node = root->child; node != NULL; node = node->next)
	{
		type = cJSON_GetObjectItem(node, "type")->valuestring;
		x = cJSON_GetObjectItem(node, "x")->valueint;
		y = cJSON_GetObjectItem(node, "y")->valueint;

		if (strcmp(type, "player1") == 0)
		{
			initPlayer(0, x, y);
		}
		else if (strcmp(type, "player2") == 0)
		{
			initPlayer(1, x, y);
		}

		if (game.aliens)
		{
			if (strcmp(type, "orangeAlien") == 0)
			{
				initOrangeAlien(x, y);
			}
			else if (strcmp(type, "purpleAlien") == 0)
			{
				initPurpleAlien(x, y);
			}
			else if (strcmp(type, "redAlien") == 0)
			{
				initRedAlien(x, y);
			}
		}
	}

	cJSON_Delete(root);

	free(data);
}

This function takes the zone number (zoneNum) as a parameter. It then attempts to load a corresponding JSON file. So, zoneNum of 2 will load "data/zones/02.json", for example. The JSON file simply contains an array of JSON objects that detail our entities. There are just three attributes per object: `type`, `x`, and `y`. `type` is the type of entity, while `x` and `y` is the location of the entity in the zone. We loop through all these entries, extract this data, and then test the `type`. We'll create the appropriate player for "player1" and "player2". Our aliens will be created for types of "orangeAlien", "purpleAlien", or "redAlien". Note that we first test if our game permits aliens to be added before testing these values. If aliens are not allowed, no aliens will be created.

So, we're just loading a JSON file, and creating our entities based on the available data. In previous tutorials, we've used an entity factory for this. We're skipping that here, as our game is much similar than those others.

What follows now is general clearing up function that we'll use when resetting a zone. We'll have seen these all before, so we'll gloss over them quickly.

Starting first with clearEntities:


void clearEntities(void)
{
	Entity *e;

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

		zone.entityHead.next = e->next;

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

		free(e);
	}

	while (respawnHead.next != NULL)
	{
		e = respawnHead.next;

		respawnHead.next = e->next;

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

		free(e);
	}
}

We want to ensure we delete all our entities here, both those alive and those waiting to respawn.

bullets.c has its own clearing function:


void clearBullets(void)
{
	Bullet *b;

	while (head.next != NULL)
	{
		b = head.next;

		head.next = b->next;

		free(b);
	}
}

As does particles.c:


void clearParticles(void)
{
	Particle *p;

	while (head.next != NULL)
	{
		p = head.next;

		head.next = p->next;

		free(p);
	}
}

As for the environment itself, we want to delete all the Triangles we created in world.c, when loading that data:


void clearWorld(void)
{
	Triangle *t;

	while (head.next != NULL)
	{
		t = head.next;

		head.next = t->next;

		free(t);
	}
}

For out spatial grid, things are equally as simple:


void destroySpatialGrid(void)
{
	SpatialGridCell *c;
	int              x, y;

	for (x = 0; x < GRID_SIZE; x++)
	{
		for (y = 0; y < GRID_SIZE; y++)
		{
			c = &grid[x][y];

			if (c->triangles != NULL)
			{
				free(c->triangles);
			}

			if (c->entities != NULL)
			{
				free(c->entities);
			}
		}
	}
}

We need only loop through each of the cells in our grid, and free the pointers arrays if they are non-null.

That's everything cleared. When initZone is called to create a new zone, the init functions for everything will be called again (initEntities, initBullets, etc), to set things up for our game; we're effectively creating a clean slate to work with.

The last little thing we need to do is update main.c, to make initZone start with a random:


int main(int argc, char *argv[])
{
	long then;

	memset(&app, 0, sizeof(App));

	initSDL();

	initGameSystem();

	atexit(cleanup);

	initZone(1 + (rand() % NUM_ZONES));

	while (1)
	{
		// snipped
	}

	return 0;
}

Our game loop is done! We can play as many matches as we like, and the winner count will be retained and incremented as we go.

But our game conditions are still hardcoded! We need to fix that, so that the player can choose how they want to play. So, in Part 16, we'll look at allowing the players to configure the matches the way they like. This can easily be done, using our widgets.

Purchase

The source code for all parts of this tutorial (including assets) is available for purchase, as part of the SDL2 tutorials bundle:

From itch.io

Mobile site