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 Honour of the Knights (Second Edition) (Battle for the Solar System, #1)

When starfighter pilot Simon Dodds is enrolled in a top secret military project, he and his wingmates begin to suspect that there is a lot more to the theft of a legendary battleship and the Mitikas Empire's civil war than the Helios Confederation is willing to let on. Somewhere out there the Pandoran army is gathering, preparing to bring ruin to all the galaxy...

Click here to learn more and read an extract!

« Back to tutorial listing

— A simple turn-based strategy game —
Part 6: Combat #1: Player attacking

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

Introduction

Another aspect of turn-based strategy games is combat. You don't need to have combat in a game, of course. You could make a zen-like experience where everyone and everything is passive. However, in our game our wizards need to deal with an outbreak of ghosts, so they will be using magic to dispatch them. In this part, we'll look at how we're going to handle attacking. Note that our game only uses ranged attacks. No melee combat happens.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS06 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. Move the mages around as normal. Clicking once on the ghost will target it. Notice the red pulsing square that appears. Clicking again will result in the current wizard attacking (so long as they have AP to do so). Attacks will always hit and will always kill the target. Once you're finished, close the window to exit.

Inspecting the code

Adding in combat is quite a process, so we're going to do this over the next several parts of this tutorial. First, we're going to add in our targetting, firing, and target elimination.

To begin with, let's update structs.h:


struct Entity {
	unsigned int id;
	int type;
	char name[MAX_NAME_LENGTH];
	int x;
	int y;
	int side;
	int solid;
	int facing;
	int dead;
	AtlasImage *texture;
	void (*data);
	void (*draw) (Entity *self);
	Entity *next;
};

We've updated the Entity struct and added in a field called `dead`. This is a flag to allow us to remove the entity from the game once it is no longer alive.

Next, we've added in a Bullet struct, to represent our bullet.


typedef struct {
	double x;
	double y;
	double dx;
	double dy;
	double life;
	double angle;
	AtlasImage *texture;
} Bullet;

You'll be aware that we only fire one bullet at a time in our game. However, having this as a separate struct makes it easier to handle, as we'll see later. The bullet struct has several fields: `x` and `y` are the location of the bullet on screen (these are not map coordinates!). `dx` and `dy` are the movement deltas value, which will be used to determine the direction of travel. `life` is how long the bullet will live for before removing removed. `angle` is the animation angle (our bullets spin), while `texture` is the texture to display.

Next, we've updated Stage:


typedef struct {
	unsigned int entityId;
	MapTile map[MAP_WIDTH][MAP_HEIGHT];
	Entity entityHead, *entityTail;
	Entity deadEntityHead, *deadEntityTail;
	Entity *currentEntity, *targetEntity;
	Node routeHead;
	int turn;
	int animating;
	int showRange;
	Bullet bullet;
	struct {
		int x;
		int y;
	} selectedTile;
} Stage;

We've added a few new fields: targetEntity, which will be the entity that is targetted (such as the ghost); deadEntityHead and deadEntityTail, which will act as a linked list into which our dead entities will be placed; and `bullet`, which is our bullet.

Now let's move over to bullets.c. This is a new compilation unit, where all the code for our bullet handling lives. We'll work through this file one function at a time. Starting with doBullet:


void doBullet(void)
{
	Bullet *b;

	b = &stage.bullet;

	if (b->life > 0)
	{
		b->life -= app.deltaTime;

		b->x += (b->dx * app.deltaTime);
		b->y += (b->dy * app.deltaTime);

		b->angle += app.deltaTime * 10;

		if (b->angle >= 360)
		{
			b->angle -= 360;
		}

		if (b->life <= 0)
		{
			applyDamage(b);
		}
	}
}

This function is where we drive our bullet. We grab a reference to Stage's `bullet`, assigning it to a variable named `b` (to make the code a bit more readable, rather than constantly write stage.bullet ...). We then test if the bullet is alive, by checking that its `life` is greater than 0. If so, we'll decrease the value of `life`, and then move the bullet by adding its `dx` and `dy` to its `x` and `y`, respectively. Next, we'll update the bullet's `angle` so that it spins when we draw it (and also loop the value back around when it passes 360). Finally, we test if the bullet's `life` has reached 0 or less. If so, we'll call applyDamage.

Note how that we don't test for collisions, etc. In our game, we'll always assume the bullet has made contact with the target or has reached its destination once its `life` hits 0. We'll then check for damage, etc. after that.

The applyDamage function follows:


static void applyDamage(Bullet *b)
{
	stage.targetEntity->dead = 1;
}

Not much to this function. We're always assuming the bullet hits its target and that it killed them. We therefore set the entity's `dead` flag to 1 (we'll see this being used in doEntities).

Next up, we have drawBullet:


void drawBullet(void)
{
	Bullet *b;

	b = &stage.bullet;

	if (b->life > 0)
	{
		blitRotated(b->texture, b->x, b->y, b->angle);
	}
}

Not a lot to explain here. We're testing to see if the bullet's `life` is greater than 0, before calling blitRotated and passing through the bullet's `texture`, its `x` and `y`, and its `angle`, so that it spins in place. blitRotated always draws with the image centered around the x and y.

Finally, we come to fireBullet. This is the function that is invoked whenever a unit attacks:


void fireBullet(void)
{
	Bullet *b;
	int x1, x2, y1, y2, steps;
	Unit *u;

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

	memset(&stage.bullet, 0, sizeof(Bullet));

	b = &stage.bullet;

	x1 = MAP_TO_SCREEN(stage.currentEntity->x);
	y1 = MAP_TO_SCREEN(stage.currentEntity->y);
	x2 = MAP_TO_SCREEN(stage.targetEntity->x);
	y2 = MAP_TO_SCREEN(stage.targetEntity->y);

	calcSlope(x2, y2, x1, y1, &b->dx, &b->dy);

	steps = MAX(abs(x1 - x2), abs(y1 - y2));

	b->x = x1;
	b->y = y1;
	b->dx *= BULLET_SPEED;
	b->dy *= BULLET_SPEED;
	b->life = (1.0 * steps) / BULLET_SPEED;
	b->texture = getAtlasImage("gfx/bullets/blueMagic.png", 1);

	if (x1 > x2)
	{
		stage.currentEntity->facing = FACING_LEFT;
	}
	else if (x1 < x2)
	{
		stage.currentEntity->facing = FACING_RIGHT;
	}

	u->ap--;
}

Quite a few things here to discuss. First up, we're extracting the current entity's unit data, and also memsetting Stage's `bullet` (another reason to use a separate struct, as it makes clearing all the bullet's data quite straightforward). As usual, we also grab a reference to the bullet and assign it to `b`.

Next, we work out the screen coordinates of both the attacking (the current) entity and the target entity, and assign these to `x1`, `y1` and `x2`, `y2`. With that done, we call a function named calcSlope. This function will calculate a normalized 2D vector from the attacker to the target, and set the results into two doubles. In this case, we're passing over bullet's `dx` and `dy`, to be populated.

Next, we want to work out how many "steps" it will take for our bullet to reach its target. We do this by taking the absolute values of `x1` - `x2` and `y1` - `y2`, and choosing the greater value (via the MAX macro). So, if the distance between `x1` and `x2` is greater than `y1` and `y2`, the value of steps will be the former. Otherwise, it will be the latter.

We're now ready to set all our bullet's values. We first the bullet's `x` and `y` as `x1` and `y1`, the attacking entity's screen coordinates. We also multiply the bullet's `dx` and `dy` by BULLET_SPEED (defined in bullet.h). When it comes to the bullet's `life`, we set this as the number of steps (as a decimal, hence multiplying by 1.0), divided by BULLET_SPEED. This will mean that the bullet lives only as long as it takes to cross the screen from the attacker to the target.

Finally, we set the bullet's `texture` and also ensure the attacker is facing the correct direction, by testing where `x1` lies relative to `x2`, and also deduct one AP from the attacking unit. Again, we do this here to centralize the AP deduction logic.

With the core bullet logic in place, we now just need to incorporate it into the rest of the game. Turning first to player.c, we've updated doSelectUnit:


static void doSelectUnit(void)
{
	Entity *e;
	Unit *u;

	if (app.mouse.buttons[SDL_BUTTON_LEFT])
	{
		u = (Unit*) stage.currentEntity->data;

		e = getEntityAt(stage.selectedTile.x, stage.selectedTile.y);

		if (e != NULL)
		{
			app.mouse.buttons[SDL_BUTTON_LEFT] = 0;

			if (e->side == SIDE_PLAYER)
			{
				if (e == stage.currentEntity)
				{
					if (++stage.showRange >= SHOW_RANGE_MAX)
					{
						stage.showRange = SHOW_RANGE_NONE;
					}
				}
				else
				{
					stage.currentEntity = e;

					updateUnitRanges();
				}
			}
			else if (e->side == SIDE_AI)
			{
				if (stage.targetEntity != e)
				{
					stage.targetEntity = e;
				}
				else
				{
					attackTarget(u);
				}
			}
		}
	}

	if (app.mouse.buttons[SDL_BUTTON_X1])
	{
		app.mouse.buttons[SDL_BUTTON_X1] = 0;

		cyclePlayerUnits(-1);
	}

	if (app.mouse.buttons[SDL_BUTTON_X2])
	{
		app.mouse.buttons[SDL_BUTTON_X2] = 0;

		cyclePlayerUnits(1);
	}
}

We're now testing the side of the entity that we've clicked on. If it's SIDE_PLAYER, we're handling the wizards as normal. If it's SIDE_AI, we will test to see if it is Stage's targetEntity. If not, we'll assign it as such. Otherwise, we'll call attackTarget. This means that clicking on an enemy twice will cause us to attack it.

The attackTarget function itself is quite basic right now:


static void attackTarget(Unit *u)
{
	if (u->ap > 0)
	{
		fireBullet();
	}
}

The function takes a single argument: `u`, the Unit that is attacking. It first checks that the attacking unit has AP available, and will call fireBullet.

Almost done! We just need to make a few more updates and this part is finished. If we turn to entities.c, we've made a few tweaks. Starting with initEntities:


void initEntities(void)
{
	memset(&stage.entityHead, 0, sizeof(Entity));
	stage.entityTail = &stage.entityHead;

	memset(&stage.deadEntityHead, 0, sizeof(Entity));
	stage.deadEntityTail = &stage.deadEntityHead;

	selectedUnitTexture = getAtlasImage("gfx/hud/selectedUnit.png", 1);
	selectedTargetTexture = getAtlasImage("gfx/hud/selectedTarget.png", 1);

	selectedUnitPulse = 0;
}

We've setting up our dead entity linked list, and also loading a new texture called selectedTargetTexture. This will be used to highlight the currently targetted entity.

Next, we've updated doEntities:


void doEntities(void)
{
	Entity *e, *prev;

	prev = &stage.entityHead;

	for (e = stage.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->dead)
		{
			prev->next = e->next;

			if (e == stage.entityTail)
			{
				stage.entityTail = prev;
			}

			if (e == stage.targetEntity)
			{
				stage.targetEntity = NULL;
			}

			stage.deadEntityTail->next = e;
			stage.deadEntityTail = e;

			e = prev;
		}

		prev = e;
	}

	selectedUnitPulse += app.deltaTime * 0.1;
}

Since our entities now have a `dead` flag, we're going to test to see if it's set (such as when a bullet hits a unit) we're going to remove the entity from our main linked list, and add it to our dead list. We're also testing to see if the dead entity was Stage's target entity, and set it to NULL if so.

We've also made changes to drawEntities:


void drawEntities(void)
{
	Entity *e;
	int x, y, size;

	for (e = stage.entityHead.next ; e != NULL ; e = e->next)
	{
		e->draw(e);
	}

	if (!stage.animating && stage.turn == TURN_PLAYER)
	{
		size = 48 + sin(selectedUnitPulse) * 8;

		x = MAP_TO_SCREEN(stage.currentEntity->x);
		y = MAP_TO_SCREEN(stage.currentEntity->y);

		blitScaled(selectedUnitTexture, x, y, size, size, 1);

		if (stage.targetEntity != NULL)
		{
			x = MAP_TO_SCREEN(stage.targetEntity->x);
			y = MAP_TO_SCREEN(stage.targetEntity->y);

			blitScaled(selectedTargetTexture, x, y, size, size, 1);
		}
	}
}

As well as highlighting the current entity, we're testing to see if a target entity has been set, and rendering selectedTargetTexture in a similar way.

Moving over to stage.c next, we've updated the `logic` function:


static void logic(void)
{
	int wasAnimating;

	wasAnimating = stage.animating;

	if (!stage.animating)
	{
		if (stage.turn == TURN_PLAYER)
		{
			doHud();

			doPlayer();
		}
		else
		{
			doAI();
		}
	}

	doEntities();

	doUnits();

	doBullet();

	stage.animating = stage.routeHead.next != NULL || stage.bullet.life > 0;

	app.mouse.visible = !stage.animating && stage.turn == TURN_PLAYER;

	if (wasAnimating && !stage.animating)
	{
		updateUnitRanges();
	}
}

We're now calling doBullet. Also, we will consider Stage's `animating` flag to be set if our bullet's `life` is greater than 0 (meaning it's active). So now, if a unit is moving or the bullet is inflight, our game will be in animating state.

The `draw` function has also been tweaked:


static void draw(void)
{
	drawMap();

	drawAStarPath();

	drawEntities();

	drawBullet();

	if (stage.turn == TURN_PLAYER)
	{
		drawHud();
	}
}

Naturally, we now need to call drawBullet, so that our bullet is rendered.

Finished! We can now target and fire on our ghost, destroying it! So, while it can run, it cannot hide.

We should now expand our combat a bit, since attacks that always hit and destroy enemies immediately won't make for a very fun game. So, in our next part we'll add in hit points, accuracy, and weapons that do variable amounts of damage.

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