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


Firmware

HF-Tech's chips have changed the world. Embedded into the heads of over 90% of the world's population, they have cured autism, dementia, provided intelligence boosts, and helped to ease some of the more mundane tasks in life. Daniel Blair, hacker and Workshop member, however is not convinced that everything is as rosy as it seems. But is he looking in all the wrong places..?

Click here to learn more and read an extract!

« Back to tutorial listing

— A simple turn-based strategy game —
Part 7: Combat #2: Weapons, accuracy, and damage

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

Introduction

We can now attack enemies. However, they are being killed right away and we never miss. That's not much fun! Imagine if they could do that to you! In this part, we'll add in accuracy, damage ranges, and hit point handling.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS07 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 accuracy percentage, along with the damage ranges in the right-top of the hud. Switching to different mages will result in different values. Also note how the accuracy increases and decreases according to the distance from the target. Fire upon the ghost until it is destroyed (there is no hit indication). Once you're finished, close the window to exit.

Inspecting the code

Our units are going to possess weapons, that will determine how accurate they are and how much damage they do. Distance from the target will also play a part in the level of accuracy. Adding all this in is once again quite straightforward, though there is quite a bit to get through.

Starting with defs.h:


enum {
	WT_BLUE_MAGIC,
	WT_RED_MAGIC,
	WT_PURPLE_MAGIC,
	WT_MAX
};

We've added in a new set of enums, to define our weapon types. WT stands for "weapon type", while the rest of the enum should be self-explanatory. We'll be looking up our weapons this way, rather than by name (for no real reason - we could've easily looked them up by name, in the same way as we do with entities).

Next, let's turn to structs.h:


typedef struct {
	int type;
	int minDamage, maxDamage;
	int accuracy;
	AtlasImage *texture;
} Weapon;

We've added in a new struct called Weapon. This will, of course, define our weapon. `type` is the type of weapon, according to the enum values from defs.h. minDamage and maxDamage is the damage range that the weapon can inflict, and be randomly applied. `accuracy` is the base accuracy of the weapon, and will be modified by distance from the target. `texture` is the image of the bullet when it is in-flight.

Next, we've updated Entity:


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);
	void (*takeDamage) (Entity *self, int damage);
	Entity *next;
};

We've added in a new function pointer called takeDamage, that will be used to handle damage to the assigned entity.

Unit has been tweaked:


typedef struct {
	int hp, maxHP;
	int ap, maxAP;
	int moveRange;
	Weapon weapon;
	struct {
		int type;
		SDL_Point goal;
	} ai;
} Unit;

We've added in a `weapon` field, to hold the data about the weapon the Unit is using. Note that this isn't a pointer to the weapon, but a copy of the data. This is so that we can't globally modify the weapon for all users, if we happen to fiddle with its values (basically, this is defensive coding).

Finally, we've updated Bullet:


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

Bullet now has two extra fields: `accuracy` and `damage`, that will hold the accuracy and the damage respectively.

Now let's have a look at weapons.c. This is a new compilation unit, that will hold that data about our weapons. It has just two functions. Starting with initWeapons:


void initWeapons(void)
{
	Weapon *w;

	w = &weapons[WT_BLUE_MAGIC];
	w->type = WT_BLUE_MAGIC;
	w->minDamage = 1;
	w->maxDamage = 7;
	w->accuracy = 60;
	w->texture = getAtlasImage("gfx/bullets/blueMagic.png", 1);

	w = &weapons[WT_RED_MAGIC];
	w->type = WT_RED_MAGIC;
	w->minDamage = 3;
	w->maxDamage = 5;
	w->accuracy = 65;
	w->texture = getAtlasImage("gfx/bullets/redMagic.png", 1);

	w = &weapons[WT_PURPLE_MAGIC];
	w->type = WT_PURPLE_MAGIC;
	w->minDamage = 1;
	w->maxDamage = 12;
	w->accuracy = 35;
	w->texture = getAtlasImage("gfx/bullets/purpleMagic.png", 1);
}

initWeapons basically configures our weapons. Within weapons.c, we have a Weapon array of WT_MAX size. For each type of weapon, we're grabbing a reference to the weapon at an index (WT_BLUE_MAGIC, etc), and setting the `type`, minDamage, maxDamage, `accuracy`, and `texture`. We're working with a pointer called `w` simply to make things a bit more readable, rather than keep writing `weapons[WT_BLUE_MAGIC].type = WT_BLUE_MAGIC`, etc. Adding in more weapons would be a case of expanding our enum in defs.h and adding in the details the same way as above. We'll actually be doing that in a later part, when we come to adding in the ghosts' weapons.

The only other function is getWeapon:


Weapon getWeapon(int type)
{
	return weapons[type];
}

This function takes an argument called `type`, and returns a copy of the weapon at the index of type in the `weapons` array. Note, again, that we are returning a copy of the Weapon and not a pointer to it. This will prevent unwanted or accidental modifications (we could return a const pointer here, but we'll be updating weapon in a later part for handle ammo).

Heading over next to mages.c, we've updated each of our mage init functions to now set a weapon:


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

	u = initMage(e, "Andy", "gfx/units/andy.png");
	u->hp = u->maxHP = 25;
	u->ap = u->maxAP = 2;
	u->moveRange = 10;
	u->weapon = getWeapon(WT_BLUE_MAGIC);
}

In initAndyMage, we're setting the unit's `weapon` with a call to getWeapon, passing over WT_BLUE_MAGIC. We're also setting his `hp` and maxHP to 25.

We're doing the same thing with Danny, and assigning him the WT_RED_MAGIC weapon, as well as 30 `hp`:


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

	u = initMage(e, "Danny", "gfx/units/danny.png");
	u->hp = u->maxHP = 30;
	u->ap = u->maxAP = 2;
	u->moveRange = 9;
	u->weapon = getWeapon(WT_RED_MAGIC);
}

And Izzy, who is getting the WT_PURPLE_MAGIC weapon, and 20 `hp`:


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

	u = initMage(e, "Izzy", "gfx/units/izzy.png");
	u->hp = u->maxHP = 20;
	u->ap = u->maxAP = 2;
	u->moveRange = 12;
	u->weapon = getWeapon(WT_PURPLE_MAGIC);
}

Heading over to ghosts.c, we've also 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->ai.type = AI_PASSIVE;
}

Our white ghost has a total of 10 `hp`. No weapon for now, as ghosts still cannot attack us (and a good thing too, as they'll be chucking slime at our brave little wizards).

With our weapons setup and our units having hit points, we should now look at how we cause damage. We've already seen that entities how have a takeDamage function pointer, so if we head over to units.c, we can see how it is being put to use. In initUnit, we've setting the pointer:


Unit *initUnit(Entity *e)
{
	Unit *u;

	u = malloc(sizeof(Unit));
	memset(u, 0, sizeof(Unit));

	e->data = u;
	e->draw = draw;
	e->takeDamage = takeDamage;

	return u;
}

Each unit's takeDamage function pointer now points to a function called takeDamage (static in units.c):


static void takeDamage(Entity *self, int damage)
{
	Unit *u;

	u = (Unit*) self->data;

	u->hp -= damage;

	if (u->hp <= 0)
	{
		self->dead = 1;
	}
}

The function takes an entity (`self`, the one being hurt) and the `damage` to inflict. We extract the unit data from the entity, then subtract `damage` from the unit's `hp`. If `hp` falls to 0 or less, we're setting the entity's `dead` flag.

Moving next to bullets.c, we've made another few adjustments and additions. Starting with applyDamage:


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

Now, instead of killing the target immediately, we're both testing to see whether the hit landed and also applying some damage. We make a call to a new function called getAttackAccuracy (see below) and testing to see if that value is lower than a random of 100. Our getAttackAccuracy function will return a value between 0 and 100 (a percentage, basically) that will represent the chance of the bullet hitting its target. If it's less than or equal to than the random of 100, we call takeDamage on Stage's targetEntity, passing over the bullet's `damage` to actually inflict the damage.

So, if getAttackAccuray were to return 29, it would mean we have a 30% chance of our attack hitting. Our random of 100 will therefore need to be 29 or less in order for our attack to be successful. 30 or above and our attack misses.

The getAttackAccuracy function itself follows:


int getAttackAccuracy(int baseAccuracy)
{
	int dist, adj;

	adj = 0;

	dist = getDistance(stage.currentEntity->x, stage.currentEntity->y, stage.targetEntity->x, stage.targetEntity->y);

	dist -= NORMAL_ACCURACY_DISTANCE;

	dist = dist * -1;

	adj += (dist * ACCURACY_ADJUST_AMOUNT);

	return MIN(MAX(baseAccuracy + adj, 0), 100);
}

As you will have seen, the accuracy of the attacks relies on both the accuracy of the weapon itself, and the distance of the attacker from the target. We start by setting a variable called `adj` (short for adjustment) to 0. This will be our accuracy adjustment amount that will apply to our base accuracy. Next, we calculate the distance of the current entity (the attacker) from the target.

We then subtract NORMAL_ACCURACY_DISTANCE from dist. NORMAL_ACCURACY_DISTANCE is defined in bullets.h, and defines as a distance at which there will be no adjustments to our attack accuracy. NORMAL_ACCURACY_DISTANCE is defined as 5, so if we are 5 tiles away, dist will become 0. If we're 4, it will become 1. If we're 6 tiles away, 1, etc. With that done, we then multiply the distance by -1, to basically invert the value. We next add `dist` multiplied by ACCURACY_ADJUST_AMOUNT (defined in bullets.h as 6) to `adj`. This means that as we approach the target we'll increase the value of `adj`, whereas as we move away we'll decrease it (once more, we're putting all this on separate lines so that it is easier to read).

Finally, we'll add the value of `adj` to the baseAccuracy passed into the function, ensure it stays within the bounds of 0 and 100, and return the value.

Finally, we've updated fireBullet:


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->damage = u->weapon.minDamage + rand() % ((u->weapon.maxDamage - u->weapon.minDamage) + 1);
	b->accuracy = u->weapon.accuracy;
	b->life = (1.0 * steps) / BULLET_SPEED;
	b->texture = u->weapon.texture;

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

	u->ap--;
}

We're now setting the bullet's `damage` amount to be a random value between the attacking unit's weapon's minDamage and maxDamage values. The bullet's `accuracy` is set to the attacker's weapon's `accuracy`, and finally the bullet's `texture` is set to the weapon's `texture`.

We're almost done! We've just got to make a few little tweaks elsewhere, and our combat is taking another step towards completiton. Moving over to hud.c, we've updated drawTopBar:


static void drawTopBar(void)
{
	Unit *u;
	int x;
	char text[MAX_DESCRIPTION_LENGTH];

	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);

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

	x += 250;
	sprintf(text, "AP: %d / %d", u->ap, u->maxAP);
	drawText(text, x, 0, 200, 200, 200, TEXT_ALIGN_LEFT, 0);

	if (stage.targetEntity != NULL && stage.targetEntity->type == ET_GHOST)
	{
		sprintf(text, "%s (%d%%, %d - %d)", stage.targetEntity->name, getAttackAccuracy(u->weapon.accuracy), u->weapon.minDamage, u->weapon.maxDamage);

		drawText(text, SCREEN_WIDTH - 10, 0, 120, 160, 255, TEXT_ALIGN_RIGHT, 0);
	}
}

We're now rendering the current unit's `hp`, so the player can see how well the current wizard is doing (even though right now they are in no danger). Next, we're testing to see whether the current target is not NULL and also whether they are a ghost (more on this in a later part!), and are drawing their name, the chance of our attack hitting them, and also the damage range that we can inflict.

Finally, we just need to turn to init.c, where we've updated initGameSystem:


void initGameSystem(void)
{
	srand(time(NULL));

	initAtlas();

	initDraw();

	initFonts();

	initSound();

	initAStar();

	initEntityFactory();

	initWeapons();
}

We've added in a call to initWeapons, to setup our weapon data.

Another part down, another step closer to completing our combat. Now, of course you will have noticed a serious short coming with our game in its current state. There is no way of knowing how much damage our attacks did. We don't even know if they've missed or not! In the next part, we'll look into adding in some visual enhancements, to let the player know the outcome of their attack, and overall make things a bit more aesthetically pleasing.

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