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
SDL 1 tutorials (outdated)

Latest Updates

SDL2 turn-based strategy tutorial
Thu, 14th April 2022

Water Closet ported to PlayStation Vita
Tue, 4th January 2022

The Legend of Edgar 1.35
Sat, 1st January 2022

Achievements tutorial
Thu, 2nd December 2021

SDL2 Rogue tutorial
Thu, 30th September 2021

All Updates »

Tags

android (3)
battle-for-the-solar-system (9)
blob-wars (9)
brexit (1)
code (6)
edgar (7)
games (39)
lasagne-monsters (1)
making-of (5)
match3 (1)
numberblocksonline (1)
orb (2)
site (1)
tanx (4)
three-guys (3)
three-guys-apocalypse (3)
tutorials (10)
water-closet (4)

Books


The Battle for the Solar System (Complete)

The Pandoran war machine ravaged the galaxy, driving the human race to the brink of destruction. Seven men and women stood in its way. This is their story.

Click here to learn more and read an extract!

« Back to tutorial listing

— A simple turn-based strategy game —
Part 17: Lilac Ghost

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

Introduction

It's time to introduce some new ghost types. We're going to start with the least aggressive and work our way up to the most dangerous. In this part, we're going introduce a Lilac Ghost. This ghost will be a coward, and will run away from the mages whenever it sees them.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS17 to run the code. You will see a window open like the one above, showing three wizards in a maze-like map, as well as a number of ghosts. Play the game as normal. Notice how the Lilac Ghosts will aim to keep their distance from the player (note: the ghosts rely on line-of-sight and not a full map scan, to determine where the mages are). The Lilac Ghosts won't attack the mages. Once you're finished, close the window to exit.

Inspecting the code

Adding in the new ghosts is quite straightforward. The behaviour can get a little complicated, but we've got a good base to start from.

Let's start with defs.h:


enum {
	AI_PASSIVE,
	AI_COWARD,
	AI_NORMAL
};

We've added in a new AI enum type: AI_COWARD. This AI type will mean that our ghosts will flee the player, on sight.

Now over to ai.c:


void doAI(void)
{
	Unit *u;

	if (stage.currentEntity != NULL)
	{
		u = (Unit*) stage.currentEntity->data;

		if (u->ap != 0)
		{
			switch (u->ai.type)
			{
				case AI_PASSIVE:
					doPassive();
					break;

				case AI_COWARD:
					doCoward();
					break;

				case AI_NORMAL:
					doNormal();
					break;

				default:
					u->ap = 0;
					break;
			}
		}
		else
		{
			nextUnit();
		}
	}
	else
	{
		endTurn();
	}
}

In our switch statement for our ai types, we've added in AI_COWARD, which is calling a new function named doCoward. This is the function that will drive our Lilac Ghost's behaviour.

doCoward is quite a simple function:


static void doCoward(void)
{
	if (!fallback())
	{
		moveRandom();
	}
}

This function will have the current ghost attempt to flee from the mages, by calling a function named `fallback`. If the result of this function is negative (it didn't need to flee or couldn't), it will call moveRandom. Nothing tricky so far.

The `fallback` function, however, is quite hefty:


static int fallback(void)
{
	Entity *e;
	Unit *u;
	int numMages, x, y, distance, attempts;
	double dx, dy;

	u = (Unit*) stage.currentEntity->data;

	numMages = 0;

	x = y = 0;

	for (e = stage.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->type == ET_MAGE)
		{
			distance = getDistance(stage.currentEntity->x, stage.currentEntity->y, e->x, e->y);

			if (distance <= 35 && hasLOS(stage.currentEntity->x, stage.currentEntity->y, e->x, e->y))
			{
				numMages++;

				x += e->x;
				y += e->y;
			}
		}
	}

	if (numMages > 0)
	{
		x /= numMages;
		y /= numMages;

		calcSlope(stage.currentEntity->x, stage.currentEntity->y, x, y, &dx, &dy);

		dx *= MAP_RENDER_WIDTH;
		dy *= MAP_RENDER_HEIGHT;

		u->ai.goal.x = MIN(MAX(stage.currentEntity->x + dx, 0), MAP_WIDTH - 1);
		u->ai.goal.y = MIN(MAX(stage.currentEntity->y + dy, 0), MAP_HEIGHT - 1);

		attempts = 25;

		do
		{
			x = u->ai.goal.x + rand() % 10 - rand() % 10;
			y = u->ai.goal.y + rand() % 10 - rand() % 10;

			x = MIN(MAX(x, 0), MAP_WIDTH - 1);
			y = MIN(MAX(y, 0), MAP_HEIGHT - 1);

			if (!isWall(x, y))
			{
				createAStarRoute(stage.currentEntity, x, y);
			}

			attempts--;
		}
		while (stage.routeHead.next == NULL && attempts > 0);

		u->ai.goal.x = x;
		u->ai.goal.y = y;

		return stage.routeHead.next != NULL;
	}

	return 0;
}

Quite a lot happening there. Let's begin with a summary of what's going on: the ghost will count up all the mages that it can see nearby, work out where they are standing, and then attempt to flee to a part of the map that lies in the opposite direction to the average location of the mages. Now, let's go through the function one step at a time.

We're setting a variable called numMages to 0. This will be a count of the number of mages the ghost can see. We're also setting two variables called `x` and `y` to 0. These will hold the average location of our mages. We next step through all the entities in the game, looking for mages (`type` of ET_MAGE). For each one we find, we'll calculate the distance from the ghost to the mage. If the distance is 35 or less, we'll call hasLOS, to test whether the ghost has line of sight of the mage; we want to make sure that the ghost can see the mage, and isn't blocked by a wall, etc. If both of these are true, we'll increment numMages, and add the mage's (`e`) `x` and `y` to our function's `x` and `y`.

With our loop done, we're testing if numMages is greater than 0. If so, our ghosts has spotted some mages. We'll now respond. First, we divide our `x` and `y` by numMages. This will give us the average location of where the mages that were spotted are standing. With this known, we'll call calcSlope, passing through the ghost's `x` and `y`, our function's `x` and `y` (the average mage position), and `dx` and `dy` doubles. `dx` and `dy` will now hold normalized deltas for moving in the opposite direction to where the mages stand. We then multiply `dx` by MAP_RENDER_WIDTH and `dy` by MAP_RENDER_HEIGHT, to work out a location to move to, and finally set the ghost's ai's `goal` location. We're doing this by adding `dx` and `dy` to our ghost's current `x` and `y`, and then clamping the values to the bounds of the map (by using the MIN and MAX macros).

Our ghost's ai's `goal` now holds x and y values that are about a screen's distance away from where the mages are.

We then set a variable called `attempts` to 25, and enter a while-loop. The idea here is to now take the location we wish to move to (the ghost's ai's goal) and randomly pick a spot around that. The purpose of this is to jitter about the location, as it is not unlikely that the goal's location is in fact a wall or some other obstacle that the ghost will not be allowed to move to. During each loop, we're assigning `x` and `y` to the values of goal, with a random adjustment of -10 and +10. We then call isWall, passing through `x` and `y`, and if the result is negative (not a wall), we'll call createAStarRoute. Our loop will continue until we either exhaust our attempts to find a place to flee to or we successfully create an A* route.

Finally, the ghost's AI's `goal` is set to the values of `x` and `y`, and we will return whether we were able to create a route (0 or 1).

Wow, quite a lot there. But, as you can see from the game, it's quite effective at making the ghosts run away from the mages. That's all we need to do to make our cowards work.

One other function to look at before moving on, and that's a tweak to addAIUnits:


static void addAIUnits(void)
{
	Entity *e;
	Unit *u;
	int i, x, y, ok;

	for (i = 0 ; i < 3 ; i++)
	{
		e = initEntity("Lilac Ghost");

		// snipped
	}
}

Instead of creating three white ghosts, we're creating three Lilac Ghosts, by calling initEntity and passing over "Lilac Ghost".

Finally, let's jump over to ghosts.c, where we've added a new function - initLilacGhost:


void initLilacGhost(Entity *e)
{
	Unit *u;

	STRCPY(e->name, "Lilac Ghost");

	u = initGhost(e, "gfx/units/lilacGhost.png");
	u->hp = u->maxHP = 7;
	u->ap = u->maxAP = 2;
	u->moveRange = 15;

	u->ai.type = AI_COWARD;
}

This function simply sets up our Lilac Ghost, setting its `name`, `texture`, `ap` and `hp`, moveRange, and also setting its AI `type` to AI_COWARD. Remember that this function is invoked via our entity factory (entityFactory.c), to which we've added in the appropriate key and init function values.

Done! Adding in our Lilac Ghost wasn't too difficult, at all. Since we've now defined so many other functions, our new ghost has a lot to draw upon to define its behaviour.

Next, let's add a ghost that is a bit more aggressive, but only a little so. The Blue Ghost we'll introduce next will keep its distance, but will fight back against the wizards.

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:

Directly

If you do not wish to create an itch.io account, you can also purchase the tutorial bundle using PayPal, and then download the tutorials directly from the main tutorials page.

SDL2_Tutorials.tar.gz 56.76MB 23rd April 2022

Click here to see the list of files in the archive

Mobile site