• 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 —
Our combat system continues to evolve, but we're missing feedback on our attacks. In this part, we're going to add in some effects, damage indication, and other visual indications as to what's happened.
Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS08 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 when you attack a ghost, a particle trail now follows the magic attack. The colour will change depending on the wizard using it. When attacks hit, there is a puff of magic (in the same colour as the attack), the ghost will shudder, and a number will appear, showing the damage dealed. If the attack misses, the word "Miss" is shown. When the ghost is destroyed, it vanishes in a puff of white smoke. Once you're finished, close the window to exit.
Inspecting the code
Adding in effects is a simple tasks, although there is a lot to do. The shudder and damage text both add a feeling of impact, and also inform the player of what just happened.
Just a heads-up - since this tutorial involves adding in particle effects in a way that has been covered in several other tutorials (all the way as far back as the original SDL Shooter..!), we're going to run through them all very fast.
We'll start with structs.h:
We've added in a standard Effects structs, to represent a magic trail, strike, or other explosion.
Next, we have DamageText:
DamageText is used for any time we want to display the result of an attack, whether that be a hit or a miss. `text` is the text to display, `x` and `y` are the screen coordinates, and `life` is how long the DamageText will display for.
We've also updated some existing structs:
Bullet now has a `type` field, as well as a trailTimer field. `type` will determine the type of bullet this is (derived from the weapon), while trailTimer will control how often it produces particle effects when the bullet is in-flight.
Entity has gained two new function pointers: `tick` and `die`. `tick` will be called each frame, and will be used to process data specific to the entity. `die` is called when the entity is killed.
The Unit struct has gained a field called `shudder`. This is used to control the shake effect you see whenever the ghost is injured.
Finally, we've added in a new linked list to Stage, controlled by effectHead and effectTail, as well as an instance of DamageText (as damageText).
Let's now move over to damageText.c, where we actually handle all of our DamageText functions. Starting with addDamageText:
This is a standard function to set our DamageText's data. It takes three parameters: the `x` and `y` screen coordinates, and a variable length char array, identified by `text`. The first thing we're doing in this function is formatting the text, using vsprintf. The formatted text is placed into a char array called `buffer`. Next, we're memsetting Stage's damageText to reset it, before grabbing a pointer to it (`d`). Finally, we're setting all of damageText's fields with that data passed in to the function. For the DamageText's `life`, we're setting a value of FPS / 2, which means it will be visible for half a second.
The next function is doDamageText:
A simple function. We're testing if Stage's damageText's `life` is greater than 0, and if so we're reducing its `life` and also decreasing its `y` value. Decreasing its `y` value will make it move up the screen while it's alive.
drawDamageText is equally simple:
Once again, we're first testing that Stage's damageText's `life` is greater than 0, then rendering the text of the DamageText itself. Notice how we're drawing the text twice. The first time we're drawing it, we're rendering it completely black, as well as at a 2 pixel offset from its normal `x` and `y`. This is to give the text is a shadow, and help to make it a bit more readable on screen. With the shadow drawn, we're rendering the text in white at the normal x and y position.
That's our DamageText done. We're now going to briefly look at effects.c, where are effects are handled.
Starting with initEffects:
We're just setting up a linked list to handle our effects and loading the texture for use.
We're simply processing our effects here, decreasing each one's `life` and `alpha` values, as well as moving them according to their `dx` and `dy` values. Once their `life` or `alpha` values fall to 0 or less, we're removing the effect.
Onto drawEffects now:
We're looping through all our active effects here, setting the colour and alpha values for each before drawing.
addMagicTrailEffect comes next:
This function will create an effect at the `x` and `y` values passed in, as well as apply the RGB values. This function is used by the wizard's magic attacks.
We're creating a number of effects in a for-loop, at the `x` and `y` location passed into the function, using the RGB values of the colour. The effects will also have random `dx` and `dy` values, to make them expand out from the point of origin.
Much like addHitEffect, except with more effects added and in pure white.
With our effects and damageText functions now defined, we can look into incorporating them into the code. We'll start with bullets.c and the doBullet function:
We're now decreasing the value of bullet's trailTimer (and capping it at 0). Once trailTimer hits 0, we're going to reset it to 0.75 so that it doesn't produce another effect too fast, and then test the `type` of bullet this is (derived from the weapon that created it), in a switch statement. Depending on the type, we're going to call addMagicTrailEffect with different RGB parameters. WT_BLUE_MAGIC will use a blue value, WT_RED_MAGIC a red value, and WT_PURPLE_MAGIC a purple value.
The applyDamage function has been updated with similar logic:
Once we've determined that an attack has landed, we're applying the damage to the target, and also calling addHitEffect with RGB values that match the type of weapon that matches the bullet. If the attack misses, we're calling addDamageText, passing over the screen coordinates of the entity we targetted, and the text "Miss".
That's all we need to do for our bullets. Further effects are added in units.c. If we first turn to initUnits:
We've added in a static variable called shudderAmount, that will control how the units shake when they take damage. We're updating this value in doUnits:
Each time doUnits is called, we're increasing the value of shudderAmount.
initUnit has seen an update:
As we saw earlier, we added in a `tick` function pointer to our Unit struct. We're assigning it to all Units here. `tick` itself is a static function:
Each time `tick` is called, we're updating the Unit's `shudder` value. We'll decrease upon each call, limiting it to 0. As we'll see in the `draw` function, the higher the value of `shudder`, the more the Unit will shake from side to side. Decreasing it here will reduce the shudder until the unit doesn't move.
We make use of `shudder` and shudderAmount in `draw`:
Before drawing the Unit, we're testing if its `shudder` value is greater than 0. If so, we're adjusting the `x` position that we're drawing the entity at, by making use of the sin function. Again, the entity will move rapidly from left to right while `shudder` is greater than 0. As it decreases, the movement will become less pronounced, and the unit will appear to stop shaking.
Finally, we've updated the takeDamage function:
We're setting the Unit's `shudder` to 10, to make them start to shake, are making a call to addDamageText, to display the amount of damage done, and finally calling the entity's `die` function if the unit's `hp` falls to 0 or less. The `die` function is handled separately for mages and ghosts, so let's take a look at that now.
Heading to ghosts.c, we've updated initGhost:
We're now assigning the entity's `die` function pointer to the new `die` function in ghosts.c:
The `die` function simply sets the entity's `dead` flag, and also calls addDeathEffect, to generate the white puffs when the ghost is destroyed. Doing things this way means, for example, that we can have ghosts drop items or do something else upon death.
Let's nip over to entities.c, where we can see tick in action, in the doEntites function:
As you can see, during each iteration of our loop we're calling the entity's `tick` function. That's all we need to do to handle our shuddering!
Phew! Almost done! Let's finally head over to stage.c, and put the final pieces together. Starting with initStage:
We're calling initEffects, to prepare our effects. `logic` has been changed, too:
We're calling doEffects and doDamageText, to process those. We've also updated Stage's animating logic. The flag will be set if we have active effects on screen (Stage's effectHead's `next` is not NULL) or our DamageText is showing (Stage's damageText's `life` is greater than 0).
Finally, we've tweaked `draw`:
We've added in calls to drawEffects and drawDamageText, so that they are displayed on screen.
Done! We've now got some great visual feedback on our combat, so we can tell what is happening when we attack enemies.
But ... something's very off. You will noticed that we can attack enemies from anywhere on screen, and that our attacks pass through walls. That's just not cricket. So, in our next part we will add in line of sight checks, as well as distance checks, to ensure that we cannot attack from just anywhere.
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: