• 2D shoot 'em up
SDL2 Santa game tutorial 🎅
SDL2 Shooter 3 tutorial
The Legend of Edgar 1.36
SDL2 map editor tutorial [UPDATED]
TBFTSS: The Pandoran War - Amiga OS4 Port
— A simple turn-based strategy game —
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:
We've added a new enum called ET_ITEM, to represent our items.
Next, let's look at the updates to structs.h:
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:
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:
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:
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:
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`:
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:
A lot like initHealth, though we're setting the touch as ammoTouch:
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:
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:
Like our tombstones, the `tick` function for items does nothing. #bettertohaveitandnotneedit
Finally, we have the addRandomItem function:
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:
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:
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:
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:
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:
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:
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.
The source code for all parts of this tutorial (including assets) is available for purchase:
It is also available as part of the SDL2 tutorial bundle: