« Back to tutorial listing

— A simple turn-based strategy game —
Part 12: Item handling

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

Introduction

It's not unusual for strategy games to features items on the battlefield - loot, and other objects that the player might discover while fighting enemies. In this part, we're going to look into adding in some items that the player units can interact with, such as ammo and health pickups.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS12 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 every time one of the wizards attacks a ghost that the amount of ammo they have reduces. Walking onto a magic crystal will restore 10 ammo. Additionally, walking onto pancakes will restore a portion of the unit's health. Once you're finished, close the window to exit.

Inspecting the code

Adding in items is straightforward, as is interacting with them. We're going to keep things simple and not mess around with inventories or anything like that. An item will be used as soon as the unit steps on it.

To begin with, let's look at defs.h:


enum {
	ET_WORLD,
	ET_ITEM,
	ET_MAGE,
	ET_GHOST,
	ET_TOMBSTONE
};

We've added a new enum called ET_ITEM, to represent our items.

Next, let's look at the updates to 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 (*tick) (Entity *self);
	void (*draw) (Entity *self);
	void (*takeDamage) (Entity *self, int damage);
	void (*touch) (Entity *self, Entity *other);
	void (*die) (Entity *self);
	Entity *next;
};

We've added in a new function pointer to Entity. The `touch` function pointer will be called whenever one of our units stops on tile containing an item.

We've also updated Weapon:


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

The struct now contains `ammo` and maxAmmo fields. This is why we're returning a copy of the weapon in our getWeapon function, rather than a pointer, since the amount of ammo should be set per weapon instance.

Hopping over to weapon.c, we've updated 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->ammo = w->maxAmmo = 18;
	w->range = 10;
	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->ammo = w->maxAmmo = 16;
	w->range = 9;
	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->ammo = w->maxAmmo = 12;
	w->range = 15;
	w->texture = getAtlasImage("gfx/bullets/purpleMagic.png", 1);

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

All the magic weapons now have their ammo limits set. We're not setting the ammo limit for the slime ball, as it's used by the ghosts and is effectively unlimited. We'll see more on this in a bit.

Now let's head over to items.c, a new compilation unit. This file is where we're going to handle the items in our game. First up is initItem:


static void initItem(Entity *e, char *name, char *filename)
{
	e->type = ET_ITEM;
	STRCPY(e->name, name);
	e->texture = getAtlasImage(filename, 1);

	e->draw = draw;
	e->tick = tick;
}

Just our usual entity creation function. It takes three parameters: the entity itself, a `name`, and a `filename` for the entity texture. We're setting the entity's `type` to ET_ITEM, and its `draw` and `tick` function pointers to the `draw` and `tick` functions in the file.

Next up we have initHealth:


void initHealth(Entity *e)
{
	initItem(e, "Pancakes", "gfx/items/health.png");

	e->touch = healthTouch;
}

Again, nothing taxing. We're calling initItem to set the common properties, and also setting the `touch` function pointer to a function called healthTouch (a static function in items.c). You're bound to be wondering at this stage why the health pickups are a stack of pancakes. Well, it was Pancake Day around the time I was working on this tutorial, and everyone loves pancakes!

If we look at healthTouch next, we can see how we're restoring a unit's `health`:


static void healthTouch(Entity *self, Entity *other)
{
	Unit *u;

	if (other->type == ET_MAGE)
	{
		u = (Unit*) other->data;

		if (u->hp < u->maxHP)
		{
			u->hp = MIN(u->hp + 10, u->maxHP);

			self->dead = 1;
		}
	}
}

This will, of course, look very familiar to those who have followed the previous tutorials, so we'll only discuss it briefly. First up, we want to make sure that our pancakes can only be consumed by the mages; ghosts aren't allowed any! If it is a mage, and their `hp` is below their maxHP, we'll add 10 to their `hp`, ensuring with help from the MIN macro that it doesn't exceed the value of maxHP. Finally, we'll flag the item as `dead`, so it is removed from the world. Simples.

Next up, we have initAmmo:


void initAmmo(Entity *e)
{
	initItem(e, "Magic Crystal", "gfx/items/ammo.png");

	e->touch = ammoTouch;
}

A lot like initHealth, though we're setting the touch as ammoTouch:


static void ammoTouch(Entity *self, Entity *other)
{
	Unit *u;

	if (other->type == ET_MAGE)
	{
		u = (Unit*) other->data;

		if (u->weapon.ammo < u->weapon.maxAmmo)
		{
			u->weapon.ammo = MIN(u->weapon.ammo + 10, u->weapon.maxAmmo);

			self->dead = 1;
		}
	}
}

A lot like healthTouch! But here we're adding 10 to the mage's weapon's `ammo`, and limiting it to the weapon's maxAmmo.

The `draw` function follows:


static void draw(Entity *self)
{
	int x, y;

	x = MAP_TO_SCREEN(self->x);
	y = MAP_TO_SCREEN(self->y);

	blitAtlasImage(self->texture, x, y, 1, SDL_FLIP_NONE);
}

A standard rendering function, where we're converting the entity's `x` and `y` map coordinates to screen coordinates, before calling blitAtlasImage.

`tick` comes next:


static void tick(Entity *self)
{
}

Like our tombstones, the `tick` function for items does nothing. #bettertohaveitandnotneedit

Finally, we have the addRandomItem function:


void addRandomItem(void)
{
	Entity *e;
	int x, y, ok;

	if (rand() % 2 == 0)
	{
		e = initEntity("Ammo");
	}
	else
	{
		e = initEntity("Health");
	}

	do
	{
		x = rand() % MAP_WIDTH;
		y = rand() % MAP_HEIGHT;

		ok = isGround(x, y) && getEntityAt(x, y) == NULL;
	}
	while (!ok);

	e->x = x;
	e->y = y;
}

As the name suggests, this function creates a random item (either an ammo pickup or a health pickup). Once created, the item will be randomly added to the map. Like with adding the mages and the ghosts, we enter into a do-loop, attempting to place it on a ground tile, and where no other entities are currently standing. With the location determined, we set the entity's `x` and `y` to the desired location.

That's our item handling code done with. We can now look at how we interact with them. There's just one place this is done, in units.c. If we look at the `move` function, we can see how items are picked up:


static void move(void)
{
	Node *n;

	moveTimer -= app.deltaTime;

	if (moveTimer <= 0)
	{
		// snipped

		if (stage.routeHead.next == NULL)
		{
			((Unit*) stage.currentEntity->data)->ap--;

			resetAStar();

			collectItems();
		}

		moveTimer = 5;
	}
}

Once our unit has finished moving, we're making an additional call to a function named collectItems. Note that we're now allowing our mages to collect the items as they walk. This is done for balance, as the player is able to restore their health, while the ghosts can't. If the player could simply run over a load of pancakes (shoveling them into their mouths as they went) it would make the battle far too one-sided.

collectItems is a simple function:


static void collectItems(void)
{
	Entity *e;

	for (e = stage.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->type == ET_ITEM && e->x == stage.currentEntity->x && e->y == stage.currentEntity->y)
		{
			e->touch(e, stage.currentEntity);
		}
	}
}

All we're doing here is iterating over the entities in the stage, looking for items that occupy the same square as the unit, and calling their `touch` function (passing over themselves and the current entity). We can't use getEntityAt here, since it will return the current unit (since the function prioritises solid entities) and skip over the items. Doing things this way also allow us to collect more than one item at once - in a future part, we'll look at having the ghosts sometimes drop items upon their death, meaning it is possible for more than one item to occupy a single tile.

Just a few more things left to discuss and we'll be wrapping this part up. Moving over to bullets.c, we've updated the fireBullet function:


void fireBullet(void)
{
	// snipped

	u->ap--;

	u->weapon.ammo--;
}

Now, at the end of the function, we're not only deducting a point of `ap` from the attacker, but also some of the `ammo` from their weapon. Since both the mages and ghosts use the fireBullet function in their attacks, this is the only place we need to do this.

Next, we're going to head over to player.c, where we've updated the attackTarget function:


static void attackTarget(Unit *u)
{
	if (u->ap > 0 && u->weapon.ammo > 0 && stage.map[stage.targetEntity->x][stage.targetEntity->y].inAttackRange)
	{
		fireBullet();
	}
}

Not only does a unit require `ap` in order to attack, but their weapon must also have `ammo`. Right now, it's unlikely one will run out of ammo battling just the one ghost (but you could always change its health to a large amount, to see this logic in action). This ammo check is only performed for the player, which is why we don't need to give the ghost weapons any ammo; they can throw as much slime as they want.

And as we now have ammo for our weapons, we should let the player see how much they have remaining. In hud.c, we've updated drawTopBar:


static void drawTopBar(void)
{
	// snipped

	x += 200;
	sprintf(text, "HP: %d / %d", u->hp, u->maxHP);
	drawText(text, x, 0, r, g, b, 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);

	x += 250;
	sprintf(text, "Ammo: %d / %d", u->weapon.ammo, u->weapon.maxAmmo);
	drawText(text, x, 0, 200, 200, 200, TEXT_ALIGN_LEFT, 0);

	// snipped
}

As well as HP and AP, we're now rendering the current unit's ammo, both the current and maximum amounts.

Finally, we've tweaked stage.c, with a change to initStage:


void initStage(void)
{
	int i;

	memset(&stage, 0, sizeof(Stage));

	initEntities();

	initUnits();

	initHud();

	initMap();

	generateMap();

	initPlayer();

	initAI();

	initEffects();

	for (i = 0 ; i < 10 ; i++)
	{
		addRandomItem();
	}

	app.delegate.logic = logic;

	app.delegate.draw = draw;
}

After setting everything up, we're setting up a for-loop to call addRandomItem 10 items, to scatter a bunch of items around the stage for the player to collect.

Items, done! Wasn't difficult, at all. We now have two major additional feature: ammo for the player's weapons, and the ability to collect items.

Now, I don't know about you, but this tiny little map is starting to get a bit claustrophobic. How about we expand it a bit? Well, in the next part, we'll increase the size of the map, add in camera controls, and also increase the number of ghosts to be found.

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:

Desktop site