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


A North-South Divide

For over a hundred years, messenger Duncan has wandered the world, searching for the missing pieces of an amulet that will rid him of his curse; a curse that has burdened him with an extreme intolerance of the cold, an unnaturally long life, and the despair of watching all he knew and loved become lost to the ravages of time. But now, Duncan is close to the end of his long quest.

Click here to learn more and read an extract!

« Back to tutorial listing

— A simple turn-based strategy game —
Part 19: Red Ghost

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

Introduction

It's time to introduce an enemy with some different capabilities. Our Red Ghost will be able to create slime pools, by targetting the map itself with its weapon. These pools will be just like the ones that are placed when we create the map, although we won't be growing them any larger than the single square they occupy.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS19 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. The Red Ghosts will attack and retreat from the player, much like the Blue Ghosts, but will also target the map around the mages, to create slime pools. Once you're finished, close the window to exit.

Inspecting the code

Once again, adding in our Red Ghosts won't be too difficult, as we have a lot of things already in place to support them. Their behaviour will also be quite a lot like the Blue Ghosts, except they will be a little more aggressive.

Starting with defs.h:


enum {
	AI_PASSIVE,
	AI_COWARD,
	AI_NORMAL,
	AI_SLIMER
};

We've added a new AI enum: AI_SLIMER will be used to tell our ai system that this ghost is a slimer type.

We've also added in a new weapon:


enum {
	WT_BLUE_MAGIC,
	WT_RED_MAGIC,
	WT_PURPLE_MAGIC,
	WT_SLIME_BALL,
	WT_SLIME_POOL,
	WT_MAX
};

WT_SLIME_POOL is the weapon that the Red Ghosts will use to create slime pools. Just like regular weapons, it can also hurt the mages.

Let's define this weapon now. We just need to head over to weapons.c and update initWeapons:


void initWeapons(void)
{
	Weapon *w;

	// snipped

	w = &weapons[WT_SLIME_POOL];
	w->type = WT_SLIME_POOL;
	w->ammo = w->maxAmmo = 9999999;
	w->minDamage = 3;
	w->maxDamage = 10;
	w->accuracy = 35;
	w->range = 20;
	w->texture = getAtlasImage("gfx/bullets/slimeBall.png", 1);
}

Like everything else, we're just defining the weapon's stats. It uses the same texture image as the slime ball, but has a much larger range and can cause a little more damage. It's less accurate, though, as is the case for most powerful weapons in games!

Moving over to ai.c now, we'll add in our new AI profile. Starting with doAI:


void doAI(void)
{
	Unit *u;

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

		if (u->ap != 0)
		{
			switch (u->ai.type)
			{
				// snipped

				case AI_SLIMER:
					doSlimer(u);
					break;

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

We've added AI_SLIMER to our switch statement, where we'll call a new function named doSlimer, passing over our Unit data for the current ghost.

The doSlimer function looks quite a bit like the doNormal function we updated for the Blue Ghost:


static void doSlimer(Unit *u)
{
	int doFallback, doAttack;

	lookForEnemies(u, &doFallback, &doAttack);

	if (doFallback || doAttack)
	{
		if (doFallback && doAttack && rand() % 2 == 0)
		{
			if (rand() % 3 > 0)
			{
				fireBullet();
			}
			else
			{
				throwSlimePool();
			}
		}
		else if (doAttack && rand() % 3 > 0)
		{
			if (rand() % 3 > 0)
			{
				fireBullet();
			}
			else
			{
				throwSlimePool();
			}
		}
		else
		{
			fallback();
		}
	}
	else
	{
		moveRandom();
	}
}

In much the same way as doNormal, we're making a call to lookForEnemies, and deciding how to respond, based on whether the doFallback and doAttack flags are set. However, when it comes to attacking, there is now a 1 in 3 chance that instead of calling fireBullet, we'll call throwSlimePool. Overall, there is also a much higher chance that the Slimer (Red Ghost) will attack a target, rather than falling back, compared to doNormal.

We'll move onto the throwSlimePool function now:


static void throwSlimePool(void)
{
	int ok, attempts, x, y;

	attempts = 12;

	do
	{
		x = stage.targetEntity->x + rand() % 3 - rand() % 3;
		y = stage.targetEntity->y + rand() % 3 - rand() % 3;

		ok = isGround(x, y) && stage.map[x][y].inAttackRange && getEntityAt(x, y) == NULL;

		if (ok)
		{
			stage.worldTarget.x = x;
			stage.worldTarget.y = y;
			stage.worldTarget.takeDamage = worldTargetTakeDamage;

			stage.targetEntity = &stage.worldTarget;
		}

		attempts--;
	}
	while (!ok && attempts > 0);

	fireBullet();
}

This function is responsible for creating a slime pool. It will randomly select a location on the map near to its selected target, and attempt to fire at it.

We start by setting a variable called `attempts` to 12. Again, this is a control variable, to ensure our function gives up trying to target a square within a certain time and not get stuck in an endless loop. Next, we'll enter a while-loop, and take the `x` and `y` coordinates of our currently targeted entity and adjust them by between -3 and 3. These `x` and `y` values will be where we'll attempt to create our slime pool. We test that this new location (x / y) is a ground tile, is within the ghost's attack range, and that there is nothing already occupying the tile, with a call to getEntityAt. We'll assign the result to a variable called `ok`.

If `ok` is 1, we'll prepare to attack the tile. We'll be making use of Stage's worldTarget entity, just as we did with the mages. We set worldTarget's `x` and `y` to the `x` and `y` values we picked, and also assign worldTarget's takeDamage function pointer to a function called worldTargetTakeDamage (we'll see this in a moment). Next, we'll update Stage's targetEntity to be the worldTarget. Ultimately, this will mean that we will be attacking the map, at the specified x and y coordinates.

If we exhaust all our `attempts` (or `ok` is set), we'll exit the loop. Regardless of whether we were able to target the world, we'll call fireBullet, meaning we'll either attack the ghost's original target or throw a slime pool.

That's all we need to do for our slimer logic. We now just need to make a few adjustments elsewhere, to support the action further. First, let's finish up ai.c, by tweaking addAIUnits:


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

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

		// snipped
	}
}

We'll be creating Red Ghosts for this part, so we want to call initEntity and pass over "Red Ghost" (once again, with the appropriate additionals to entityFactory.c).

The last function we want to investigate is worldTargetTakeDamage:


static void worldTargetTakeDamage(Entity *self, int amount)
{
	if (isGround(self->x, self->y))
	{
		stage.map[self->x][self->y].tile = TILE_SLIME;
	}
}

A very simple function - it simply converts a ground tile into a slime tile. We're calling isGround here, just to sanity check Stage's worldTarget's `x` and `y` values, in case we attempt to set memory outside of the bounds of our map array.

Now, let's move over to bullets.c, where we've made one small change to applyDamage:


static void applyDamage(Bullet *b)
{
	if (stage.targetEntity->type == ET_WORLD || rand() % 100 <= getAttackAccuracy(b->accuracy))
	{
		stage.targetEntity->takeDamage(stage.targetEntity, b->damage);

		switch (b->type)
		{
			// snipped

			case WT_SLIME_BALL:
			case WT_SLIME_POOL:
				addHitEffect(b->x, b->y, 0, 255, 0);
				break;

			default:
				break;
		}
	}
	else
	{
		addDamageText(MAP_TO_SCREEN(stage.targetEntity->x), MAP_TO_SCREEN(stage.targetEntity->y) - (MAP_TILE_SIZE / 2), "Miss");
	}
}

WT_SLIME_POOL will cause the same hit effect as WT_SLIME_BALL, so we simply add that case to our bullet type switch statement.

Almost done! Just one very important thing left to do, and that's to define the ghost itself. Moving over to ghosts.c, we've added a new function named initRedGhost:


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

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

	u = initGhost(e, "gfx/units/redGhost.png");
	u->hp = u->maxHP = 10;
	u->ap = u->maxAP = 2;
	u->moveRange = 10;
	u->weapon = getWeapon(WT_SLIME_POOL);

	u->ai.type = AI_SLIMER;
}

We're defining our Red Ghost, giving it its `name`, `texture`, and all other attributes. For the `weapon`, we're calling getWeapon with WT_SLIME_POOL, and for its AI type we're setting AI_SLIMER.

Finished! We now have a ghost that attacks with a little more aggression and can create slime pools, to hinder our mages. 4 ghosts down, 1 more to go. Our final ghost will be a very special type and will have some gameplay implications - the Green Ghost will explode when it is killed, splattering the surrounding area with slime pools, and also injurying anything standing nearby..! We'll see how we create this guy next.

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