• 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
— Creating a simple roguelike —
An essential part of roguelikes is combat. We've introduced a monster, a Micro Mouse, one that cannot move just yet. We're therefore going to take the opportunity to introduce combat into our game. Combat in our game will be a simple affair - just walk into the monster to attack it.
Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./rogue03 to run the code. You will see a window open like the one above, showing our main character in a dungeon environment. Use the same controls as before to move around. To attack the Micro Mouse, simply walking into it. The Mouse's HP is random and so it may take a few successful strikes to defeat. Once you're finished, close the window to exit.
Inspecting the code
To support our combat, we've updated structs.h to introduce some new fields and structs. Starting with Entity:
We've added in two new fields: `dead` and `data`. `dead` is a flag to say whether the Entity has been killed (through combat or other means), so that it can be removed from the dungeon. `data` is a pointer to extended data that we've created, such as the Monster struct below:
Our Monster struct holds the basic information that we'll need for combat (note that the player themselves is also a monster). `hp` and maxHP are the monster's hit points and maximum hit points. minAttack is the minimum amount of damage that our attack can do, while maxAttack is the maximum. When fighting, we'll pick a random value between these two numbers. `defence` is the amount of protection our monster has. It will be factored into our combat system that we'll see in a little bit.
We've also made some updates to Dungeon:
attackingEntity is a pointer to the entity that is currently attacking, while attackDir will be used to hold the information about the direction the attack is going. These two variables will be used for animating the attacks when they happen. We've also added animationTimer, that will be used to record when an animation is in progress, such as attacking or the blood splat that is rendered during a hit.
Let's now move straight to how our combat works. We've introduced a new file called combat.c to handle this. We've got just one function in it - doMeleeAttack:
The function takes two arguments, `attacker` and `target`, both of them being Entities. The idea behind this function is that the attacker (a monster) will use their attributes to attack a target (also a monster), which will use its attributes to defend against the attack. Damage will be dealt accordingly.
To being with, we work out the direction the attack is moving. This is simply a case of subtracting `target`'s `x` and `y` from `attacker`'s `x` and `y`, and assigning the results to dungeon's attackDir's `x` and `y`. We also set dungeon's animationTimer to 1/3 of a second. We'll see how this comes into play later on, during our entity rendering.
Next, we assign dungeon's attackingEntity as the attacker. We then extract the Monster data from the attacker and assign it to a variable called atkMonster (attacking monster), a Monster. We also extract the Monster data from the target and assign that to a variable called tarMonster (target monster), also a Monster. We then calculate our attack strength (assigned to a variable named `attack`). We do this by picking a random number between the atkMonster's minAttack and their maxAttack. Notice that we're adding 1 to maxAttack, to account for random being zero indexed. If we didn't do this, we'd always fall 1 point short of our maximum attack value.
With our attack determined, we're going to calculate our damage. We're making use of a common RPG damage calculation that divides the square of our attack value by our attack value plus the target monster's defence. This equation is popular due to how it scales with defence and attack values of the attacker and defender. There are many, many different forumlas available. One nice thing about this forumla is not only how simple it is, but also that attacks can miss and cause 0 damage. Some forumla will, for example, always cause damage, no matter what. We assign the result of our calculation to a variable named `damage`.
We now know our damage, and can respond accordingly. If `damage` isn't 0, we're going to subtract the damage from tarMonster's `hp`. We're then testing the value of tarMonster's `hp`. If it's fallen to 0 or less, the target was killed in the attack (except if `target` is the player - for now, the player will be immortal). We therefore set `target`'s `dead` flag to 1. We finally call a function named setBloodSplat, passing in `target`'s `x` and `y` coordinates, to tell our system to add a blood splat effect. More on this later.
Now, let's head over to entities.c, where we've made a few changes. We've added in a whole new function named doEntities, which will process our entities each frame. It might look like a lot is happening, but it's easier than that (and if you've followed the previous tutorials, it will be even easier to understand).
The idea behind doEntities is to process all our entities and remove the dead ones from the dungeon. We'll also be collecting the entities that we want to draw into an array. We start by setting two variables, updateFOW and `i` to 0. updateFOW is a flag to say whether we should call updateFogOfWar at the end of our entity processing.
Next, we memset an array called entsToDraw, to clear all the existing data. This array will be holding all the entities we want to draw when we come to that phase. We then begin a for-loop, to process our entities. For each one, we check if its `dead` flag is set. If not, we'll check if the map tile it currently occupies is visible and add it to our entsToDraw array (first testing that we have room, by seeing if `i` is less than MAX_ENTS_TO_DRAW - defined as 128).
If the entity's `dead` flag is set, we'll remove it from our linked list and throw it into our dead list. As we do so, we're checking if the entity is solid. If it is, we're setting updateFOW to 1. The reason for this is because a solid entity might be blocking our line of sight while it is in the dungeon. Now removed, our line of sight might no long be obstructed and we need to reveal what was behind it (and we don't want to wait for the player to move to do this).
With our entities processed, we want to sort our entsToDraw list. The reason for this is because we don't want to draw our entities in the order they were added into the dungeon. It would mean that entities such as items could be drawn on top of monsters, for example, hiding them. We therefore want to draw our entities in a particular order. We also want the attacking entity to be drawn last. We'll see more on all this in a little while. The final thing we do in the function is test whether we want to update our fog of war. Therefore, if our updateFOW flag is set (as a result of an entity being removed from the dungeon), we'll call updateFogOfWar.
We've also updated isBlocked:
Before, when encountering an entity of type ET_PLAYER or ET_MONSTER, we were simply returning 1. Now, we're calling our new doMeleeAttack function, passing over the entity moving into the tile (`e`) and `other`, the current occupier. In effect, this means an entity moving into a square with the player or monster present will attack them. That's all that's needed for our melee attacks to take place.
Next, we've made some changes to drawEntities:
As the entities we want to draw now live in an array, we're going to iterate through that, instead of dungeon entity linked list. We set up a for-loop, assigning `i` a value of 0 and `e` (an entity pointer) to the first entity in our array. We're going to loop through the array while `i` is less than MAX_ENTS_TO_DRAW and `e` is not NULL. So, we stop drawing as soon as we hit our first NULL element in our array or we come to the end. For each item in our list, we'll call drawEntity, passing over `e`.
Our drawEntity function is quite simple:
Like the original entity drawing code, we're working out the screen rendering position for the entity and drawing it. However, we're also testing to see whether this entity is the current attacking entity (attackingEntity), as assigned in combat.c. If so, we'll want to displace the entity a little, to make it appear to be moving into the target entity's square. We do this by testing if `e` is the same as dungeon's attackingEntity, then adding dungeon's attackDir's `x` and `y` to our `x` and `y`, multiplied by half MAP_TILE_SIZE. So, whenever an entity attacks another, it will briefly jump forward half a tile.
Finally, we have our drawComparator function, as used by qsort in doEntities:
A standard qsort function, we want to order our entities so that an entity that is attacking is pushed to the end of the list (to be drawn last, so it always renders above the thing it is attacking) and then sort the other entities according to their type. Those with a higher enum type value will be drawn first, meaning that we can ensure the player and monsters are rendered after things such as items, to stop them from being hidden.
As well as combat.c, we've added in a file called hud.c. This file is currently responsible for handling our blood splat. We'll expand our HUD to be more helpful in the next part. hud.c currently consists of a number of short functions. We'll work through them one at a time. Starting with initHud:
initHud does two things - grabs the texture for the blood splat from our texture atlas and assigns it to bloodSplatTexture, and also sets a variable called bloodSplatTimer to 0. bloodSplatTimer is the amount of time the blood splat will be displayed for before vanishing. Both this variables are static within hud.c.
Next, we have doHud:
All we're doing right now is decreasing the value of bloodSplatTimer and limiting it to 0. We're also then assigning dungeon's animationTimer to higher value of what it is currently versus the value of bloodSplatTimer. We'll see later on how dungeon's animationTimer halts all processing while it is greater than 0. We therefore want to ensure that the blood splat has timed out before allowing the game to continue. Checking for the higher value between the two solves this.
Next, we have setBloodSplat:
This function takes two parameter values - `x` and `y`, which are the dungeon coordinates at which we'll display the blood splat. We're also setting bloodSplatTimer to one-fifth of a second. setBloodSplat is basically a convinence function.
drawHud is up next. There's little to it:
We're merely calling drawBloodSplat:
drawBloodSplat itself is also very simple. We're first testing if bloodSplatTimer is greater than 0. If so, we're figuring out the render location of the blood splat by subtracting the dungeon's camera's `x` and `y` from the bloodSplat's `x` and `y`, and assigning the results to two variables named `x` and `y`, before then drawing the blood splat itself, using blitAtlasImage. As always, we're scaling up our `x` and `y` to our dungeon map tile sizes and adding the render offsets (MAP_RENDER_X and MAP_RENDER_Y).
Now we can turn to player.c, to see the changes we've made there. As stated before, the player is techincally a Monster, since they have hit points, defence, and attack attributes. If we look at initPlayer, we can see this being done:
We're now mallocing and memsetting a Monster struct for our player, and assing it to a variable named `m`. We're then setting its `hp` and maxHP to 25, its `defence` to 4, its minAttack to 1, and maxAttack to 4. Our player will therefore have 25 hit points and be able to deal up to 4 points of damage to an enemy. With that done, we assign `m` to our player entity's `data` field.
If we turn to monsters.c, we can see that we've now expanded our createMonster function:
Where before we were just setting the entity's `type` and `solid`, we're now mallocing and memsetting a Monster, as a variable called `m`, and assigning it to the entity's data field. Note also that this function is now returning the Monster that we've created. This is just for convinence, so that we don't need to extract the Monster from the entity's `data` field. You'll notice that we're not setting any of the monster's fields here (`hp`, etc). This is because these values will all change depending on the type of monster we're creating.
initMicroMouse is a good example of this:
We're assigning the result of createMonster (a Monster) to a variable called `m`, and then setting the Monster's attributes. We're setting the Monster's `hp` and maxHP to a random of 1 - 3, its `defence` to 1, it's minAttack to 1, and its maxAttack to 2. Our Micro Mouse is therefore quite weak, as one would expect of a first monster encounter.
We're almost done. The only thing left to do is bring this all together. We'll move over to dungeon.c, where we've made our final updates. Starting with initDungeon:
Alongside initMap, we're now calling initHud. A one line addition. `logic`, on the other hand, has seen quite a few more changes:
First, we're calling doEntities and doHud. Next, we're decreasing dungeon's animationTimer, and limiting it to 0. We're then checking to see if animationTimer is less than a fifth of a second (FPS / 5). If so, we're setting dungeon's attackingEntity to NULL. This will result in any attacking entity that is currently stepping forward to return to their regular position.
We're then testing if the animationTimer is 0. If so, we'll be calling doPlayer or doMonsters as before. What this means is that if dungeon's animationTimer is greater than 0, neither the player nor the monsters will be able to take any action. We'll see how this benefits us in a later part, when we have more monsters and they are able to attack.
The final change we've made is to `draw`:
We're simply calling drawHud here, to draw our blood splat (and any other information that we'll want to show, once we expand hud.c).
And there you have it. The start of a combat system. As you can see, supporting combat isn't too difficult and our code structure has allowed us to expand things out very nicely. What would be good is to show some more on-screen information, such as our hit points, and messages about what is happening during combat. We'll be doing this in the next step, when we update the hud.
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: