• 2D shoot 'em up
SDL2 turn-based strategy tutorial
Water Closet ported to PlayStation Vita
The Legend of Edgar 1.35
SDL2 Rogue tutorial
— A simple turn-based strategy game —
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:
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:
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:
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:
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:
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:
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:
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:
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:
In initMages, we're setting the entity's `die` function pointer to a static function called `die`:
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:
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:
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.
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:
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.