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


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

— A simple turn-based strategy game —
Part 10: Combat #5: AI attacking

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

Introduction

Our game won't be much fun if our mages can just walk around picking off the ghosts without fear of retaliation, so it's time for the ghosts to fight back. Knowing the ghosts can fire at us means we'll need to take care about where we place our mages and when to attack, so we don't lose one during the battle.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS10 to run the code. You will see a window open like the one above, showing three wizards in a small room, surrounded by walls, as well as a white ghost. Play the game as normal. Notice how the ghost now attacks by throwing slime balls at the mages when it sees them. The mages cannot die, so you don't need to worry about keeping them alive. Once you're finished, close the window to exit.

Inspecting the code

Giving our AI the ability to attack the mages isn't as hard as it might seem. We already have a lot in place (LOS checks, etc.), so it is merely a case of making the ghost look for the mages and fire upon them when it can. Note: while our white ghost is supposed to be passive and not attack, we're going to hijack him for the purpose of this part. We'll be temporarily changing his AI type, to make him aggressive.

Let's begin going through things, starting with defs.h:


enum {
	AI_PASSIVE,
	AI_NORMAL
};

We've added in a new enum: AI_NORMAL will be used as the AI profile for a ghost that moves around and attacks the mages. We'll be expanding on all AI profiles in the coming parts.

We've also added a new weapon type:


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

WT_SLIME_BALL is the weapon type used by the ghosts.

Jumping over to weapons.c now, we can see how we've defined the actual weapon, in initWeapons:


void initWeapons(void)
{
	Weapon *w;

	//  snipped

	w = &weapons[WT_SLIME_BALL];
	w->type = WT_SLIME_BALL;
	w->minDamage = 2;
	w->maxDamage = 7;
	w->accuracy = 55;
	w->range = 15;
	w->texture = getAtlasImage("gfx/bullets/slimeBall.png", 1);
}

The slime ball does a fair amount of damage, has decent accuracy, and has a large range! Wow, the ghosts aren't mucking about, eh? Good thing our mages are immortal right now!

With our weapon defined, we can apply it to our ghost. Heading over to ghosts.c, we've updated initWhiteGhost:


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

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

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

	u->ai.type = AI_NORMAL;
}

We're now assigning the unit its `weapon`, with a call to getWeapon and passing over WT_SLIME_BALL. We're also setting the Unit's AI `type` to AI_NORMAL (just for now).

With those done, we can move over to ai.c, where we're actually handling our attack logic. The first thing we want to do is update doAI, to handle our next ai profile:


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_NORMAL:
					doNormal();
					break;

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

As well as testing for AI_PASSIVE, we've added AI_NORMAL to our switch statement. It calls a new function named doNormal. That's the only change we need to make here, everything else remains the same.

The doNormal function itself is simple:


static void doNormal(void)
{
	lookForEnemies();

	if (stage.targetEntity != NULL)
	{
		fireBullet();
	}
	else
	{
		moveRandom();
	}
}

In a nutshell, an AI with the AI_NORMAL profile will look for an enemy, and attack them on sight. Otherwise, it will wander around (like the passive ghost). Our doNormal function first makes a call to lookForEnemies. We'll see this in detail in a bit, but essentially it will attempt to set Stage's targetEntity. Next, we check to see if Stage's targetEntity is not NULL. If it's not (meaning we have a enemy), we call fireBullet. This is exactly the same function call we make when the player attacks; there's nothing more we need to do here, as that function will deal with all the bullet firing logic itself. If we don't have an enemy, we'll call moveRandom, so our ghost wanders the map, looking for an opponent.

The lookForEnemies function comes next:


static void lookForEnemies(void)
{
	Entity *e;
	int distance, closest;Unit *u;

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

	stage.targetEntity = NULL;

	closest = u->weapon.range + 1;

	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 < closest && stage.map[e->x][e->y].inAttackRange)
			{
				closest = distance;

				stage.targetEntity = e;
			}
		}
	}
}

The goal of this function, as the name suggests, is to look for an enemy to attack. We first extract the ghost's Unit data and set Stage's targetEntity to NULL. We then set a variable called `closest` to the unit's weapon range, plus 1. Next, we loop through all the entities in the stage, looking for mages (ET_MAGE). Once we find one, we find out how far it is from our ghost (assigned to a variable called `distance`). If `distance` is lower than `closest` and the mage is standing on a MapTile that is flagged as inAttackRange, we'll set the mage as Stage's targetEntity and set the value of `closest` to `distance`. This is why we first set `closest` to our weapon's range plus 1 - we want to find enemies nearer than `closest`. Setting to range + 1 allows us to catch enemies at the limit of our weapon's range (of course, there are numerous other ways we could've handled this situation).

That's it for our AI! Simple, right? The ghost can now attack the mages. We need now only make a few adjustments elsewhere to the code, to get everything shipshape.

Starting with bullets.c, we've updated applyDamage:


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

		switch (b->type)
		{
			// snipped

			case WT_SLIME_BALL:
				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");
	}
}

We've added WT_SLIME_BALL to our switch statement, and are calling addHitEffect with a bright green RGB value.

Next, we've updated mages.c:


Unit *initMage(Entity *e, char *name, char *filename)
{
	Unit *u;

	STRCPY(e->name, name);
	e->type = ET_MAGE;
	e->solid = 1;
	e->texture = getAtlasImage(filename, 1);
	e->die = die;

	u = initUnit(e);
	u->ap = u->maxAP = 2;

	return u;
}

In initMages, we're setting the entity's `die` function pointer to a static function called `die`:


static void die(Entity *self)
{
}

Right now, `die` does nothing at all, as our mages can't be killed. This function (and the pointer assignment) exists because our Units' takeDamage function expects it to be set. If we don't set it, our game will crash when the mages' hp drops to 0 or less. We'll handle it properly in the next part.

Next up, we've updated hud.c, with a tweak to drawTopBar:


static void drawTopBar(void)
{
	Unit *u;
	int x, r, g, b, accuracy;
	char text[MAX_DESCRIPTION_LENGTH];
	double percent;

	drawRect(0, 0, SCREEN_WIDTH, 45, 0, 0, 0, 192);

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

	x = 10;

	drawText(stage.currentEntity->name, x, 0, 255, 255, 255, TEXT_ALIGN_LEFT, 0);

	r = g = b = 200;

	percent = (1.0 * u->hp) / u->maxHP;

	if (percent < 0.2)
	{
		g = b = 0;
	}
	else if (percent < 0.5)
	{
		b = 0;
	}

	x += 200;
	sprintf(text, "HP: %d / %d", u->hp, u->maxHP);
	drawText(text, x, 0, r, g, b, TEXT_ALIGN_LEFT, 0);

	// snipped
}

We're already rendering our mage's hp, but we're now going a step further and adjusting the colour of the number depending on their level of health. We assign variables called `r`, `g`, and `b` (RGB) a value of 200. We then assign a variable called `percent` the value of the unit's current `hp` divided by their maxHP, to get a value between 0 and 1.0. Finally, we test if `percent` is less than 0.2 (20%), and set `g` and `b` to 0. This will mean `r` remains at 200, so our text will render in red. Otherwise, we'll check if it's less than 0.5 (50%) and set `b` to 0, so our text renders in yellow. With our RGB values known, we use them in the drawText function for our health.

The very last thing we do is update units.c:


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

	stage.currentEntity = stage.targetEntity = NULL;

	// snipped
}

When changing turns, we want to reset Stage's targetEntity to NULL. This prevents one of our mages from being the target upon the commencement of the player turn if it was just attacked by a ghost. Basically, this just stops things looking a bit strange.

Done! Hurrah, our ghosts can attack us. The game is moving closer and closer to becoming a true turn-based strategy affair. The next thing we should do is handle the player's units being killed. As of right now, the ghost still isn't much of a menace.

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