• 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 —
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:
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:
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:
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:
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:
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:
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:
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:
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`:
And Izzy, who is getting the WT_PURPLE_MAGIC weapon, and 20 `hp`:
Heading over to ghosts.c, we've also updated initWhiteGhost:
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:
Each unit's takeDamage function pointer now points to a function called takeDamage (static in units.c):
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:
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:
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:
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:
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:
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.
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.