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 Attribute of the Strong (Battle for the Solar System, #3)

The Pandoran War is nearing its end... and the Senate's Mistake have all but won. Leaving a galaxy in ruin behind them, they set their sights on Sol and prepare to finish their twelve year Mission. All seems lost. But in the final forty-eight hours, while hunting for the elusive Zackaria, the White Knights make a discovery in the former Mitikas Empire that could herald one last chance at victory.

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:

Mobile site