• 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 —
In this final part, we're going to look at introducing sound effects and music, as well as some misc. features.
Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS24 to run the code. There are now a handful of command line options available:
Inspecting the code
As you will have seen (heard?), we've added in music and sound effects to the game in this final bit. We won't linger too long on where we've added all of these, as we'll be here all day..! However, we will mention some of the more interesting places we've handled sound.
Starting with defs.h:
We've created an enum for our sound channels, to play certain sound types through.
Another set of enums has also been created for each sound effect:
Quite a lot like the way we've done in all the previous tutorials, so we'll press on.
Moving over to structs.h now, we've made an update to the Stage struct:
We've added in two new fields: `seed` and isCustomMap. `seed` will be the value of the random seed that was used to generate the stage. isCustomMap is a flag to say whether this seed was supplied by the player. This is mainly used for debugging and information purposes.
Now, let's look at how and where we're using some of our sound effects.
Starting with bullets.c, we've update an update to applyDamage:
In our switch statement, we've added a call to playSound. When it comes to successful attacks, depending on the type of weapon we'll play a different sound. Our magic-type weapons will play SND_MAGIC_HIT, while our slime-based weapons will play SND_SLIME_HIT. If the attack misses, we'll play the SND_ATTACK_MISS sound effect.
We've done something similar in fireBullet:
After the bullet is created, we're testing the type of bullet (weapon) we're using, and playing the appropriate sound effect. Once again, magic and slime attacks will use different sounds.
If we had a lot of different weapons, playing a lot of different sounds, we would want to approach this another way, by adding a field to our Weapon struct to specify the sound effects played when a bullet is fired, and the one played when it strikes a target. We have just limited weapons here, so the current approach is fine.
Units themselves play a sound when they are hit, as we can see in units.c, in the takeDamage function:
Now, when the damage is applied, we're calling playSound to issue a punch-like sound.
We've also done the same thing in player.c, in the worldTargetTakeDamage function:
Now, we're playing a sound effect whenever we destroy a slime pool.
Yet again, we're doing the same in ai.c, when the Slimers (Red Ghosts) create their pools:
After the tile is flipped from a ground tile to a slime tile, we're playing a sound effect.
One final place to look at for our sound and music handling is in stage.c, in the `logic` function:
When our game finishes, whether in a win or lose state, we're playing a sound effect. Something else we're doing is stopping the music from playing, as the battle is now at an end. We only want to play our sound effects once and also stop our music when the ending screen flashes up. To do this, we simply test the value of endTimer. If it's currently greater than 0, but will be falling to 0 or less once the value is deducted (using App's deltaTime), we know we're good to go. We first call stopMusic, to stop our music from playing, then check whether we want to play a victory or lose sound effect. If we still have some mages alive (Stage's stat's numMages), the player has been victorious. We'll therefore play the victory sound. Otherwise, we'll play the defeat sound effect. Since the value is endTimer is adjusted after this check, this piece of logic will only execute once.
That's enough about our sound effects and music. Let's look at the other new feature: the command line handling. Unlike the other tutorials in this series, we're not introducing a title screen or any widgets (although these could easily be introduced if one wanted). To that end, we're going to handle some options on the command line.
Turning first to main.c, we've updated the main function:
Before initStage, we're making a call to a new function named handleCommandLine:
This function is responsible for handling the command line options, and takes the arguments passed to `main` as its own parameters. The first thing we do is set some defaults. We set variables called soundVolume and musicVolume to 100 and 60, respectively. These will act as the default values of our sound and music volumes. Next, we're setting the value of a variable called `seed` to 0, and a variable called isCustomMap to 0. These will be the random seed for the regenerated stage and a flag to specify that we're attempting to create a user-specified map (you'll see that these two variable names align with those new fields in Stage).
With those variables setup, we then start to loop through our command line arguments. For each argument, we're testing the value. If it's "-seed", we'll be using the argument that follows as our `seed` value. We'll also be setting isCustomMap to 1. We do the same with "-sound" and "-music", setting soundVolume and musicVolume, respectively. Note that we're multiplying the passed in value (between 0 and 10) by 12.7. This is because sound and music volumes in SDL2 can range between 0 and 127.
With our arguments handled, we're calling setSoundVolume, passing over soundVolume; setMusicVolume, passing over musicVolume; and finally we're memsetting stage, and setting its isCustomMap and `seed` with the values in this function.
This now allows us to set our sound and music volumes from the command line, as well as the random seed for the map that we want to play.
Finally, let's look at map.c, where we've updated generateMapWorker to make use of this new logic:
Something to keep in mind is that not all seeds will generate valid maps. If a player has specified an invalid seed, we'll print a warning, and then generate a different map. If this was incorporated into a UI, we'd want a better way to inform the player that the map couldn't be generated. Now, onto the changes.
The first thing we're now doing is testing if Stage's isCustomMap flag is set. If not, we're going to set Stage's `seed` with a call to rand(), using MAP_RANDOM_SEED (defined in mapGen.h as 2147483647). We'll then call srand, passing over Stage's `seed`. The map generation will proceed as normal, but we're now assigning the result of verifyCapacity to a variable named `ok`. We'll test this variable next, to see if we were able to generate a map. If not, we'll first check if the player specified a map, by checking if Stage's isCustomMap was set. If so, we'll print a warning that the map seed was invalid, and then clear the isCustomMap flag. We'll then reset Stage's `seed` with a fresh call to rand, using MAP_RANDOM_SEED, and set srand once again.
Once we've successfully generated our map, we'll be logging the seed that was used to generate the stage, via SDL_LogMessage.
So, in summary, if the player has specified a map seed, we'll attempt to use it for our map generation. If it doesn't work, we'll simply create a random map and print the seed value. If the player didn't specify a seed, we'll randomly generate a map from the very start, without any warnings.
And that's a wrap! Our little turn based strategy game is done. Well, sort of. To be fair, this is just the battle portion of the game. If we wanted to expand it into a fully fledged game, what we'd want to do is allow our wizards to level up, visit different zones, and face different monsters and challenges. Sort of like X-COM. However, this tutorial should still give you a window into how one might go about creating such a game. And there is much, much more than could be introduced, including new weapons, items, environmental elements, such as lighting, to make things easier or harder to hit, etc. The list is endless and only limited by your own imagination.
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.