• 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 —
Much of our game is done. Now what we need is to handle the post-game situation. Right now, one of three things can happen: the player wins, and keeps walking around the map; the ghosts win, and do likewise; or the mages and ghosts are both defeated at the same time, at which point the game will exit on it own. In this part, we're going to introduce a post-game screen, that will show the player a bunch of stats before they then exit the game.
Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS21 to run the code. You will see a window open like the one above, showing three wizards in a maze-like map, as well as a number of ghosts. Play the game as normal. Upon all the ghosts being defeated or all the mages being killed, a game over panel will display, showing the stats from the game. At this point, clicking the left mouse button will exit the game. Otherwise, once you're finished, close the window to exit.
Inspecting the code
Adding in our win/lose screen is simple enough. There are a lot of things to touch on, however, as we add in various stat counting and other checks.
Let's go first to structs.h:
To the Stage struct, we've added in an anonymous struct called `stats`. This struct contains a number of fields: numMages is the number of mages still active in the game, while totalMages is the number starting number. numGhosts is the number of ghosts remaining, while totalGhost is the starting number. `rounds` is the number of rounds played by each team. bulletsFired is the number of shots the player has fired, while bulletsHit is the number of shots that hit their target (excluding attacking the world). numMoves is the number of squares moved by the mages. numPancakes is the number of pancakes consumed by the player, while totalPancakes is the total available at the start of the game. numAmmo is the number of magic crystals the player collected, while totalAmmo is the total number of magic crystals on the stage. Finally, timePlayed is the amount of time the game was played for, before it ended.
That's our stats done. We'll be making use of them throughout the remainder of this part. Let's now head over to stage.c, where the bulk of the update has happened. First, let's deal with initStage:
We're first setting the `rounds` stat to 1, since this is the first round of the game. Next, we're setting a variable called endTimer to 2 seconds (FPS * 2). This is a countdown timer that will begin once either all the ghosts have been killed or all the mages have. Once it reaches 0, it will display the stats page. We're using this so that the end screen doesn't flash up immediately.
Now for the changes to `logic`. There have been a few tweaks here:
The first change is that we're now testing that there are mages and ghosts remaining in play (Stage's stat's numMages and numGhosts). If so, we'll increase the value of the timePlayed stat. Otherwise, we'll decrease the value of endTimer (and limit it to 0), and also set Stage's `animating` flag to 1. Doing this prevents any interactions from happening, either by the player or the AI.
Further down, we're setting the values of Stage's stat's numMages and numGhosts to 0. We're doing so here as we'll be taking a count of how many are active later on. In effect, we're reseting the count at the start of our gameplay loop and getting a finished count at the end of it.
Another new addition is the call to doEndStage. We'll look at this function now:
Not a lot happening here. We're testing whether endTimer is 0 and if the left mouse button has been pressed. If so, we're calling exit, to exit the game. If we wanted to do some extra stuff, like saving the game or the stats, we could do so in this function, to keep things tidy.
Moving onto `draw` now:
Before drawing the HUD, we're now testing whether endTimer is greater than 0 and also if there are mages still left alive in the game. This helps to ensure the HUD is not rendered along with the post-game screen, as this can look a bit messy and distracting.
We're also testing whether endTimer is 0, and calling drawEndStage if so.
The drawEndStage function is where we render our post-game screen:
To begin with, we're setting up an SDL_Rect called `r` to hold the dimensions of our stats panel, and are then darkening the screen with a call to drawRect, passing over the entire dimensions of the screen. Another call to rectRect follows, this time using the `x`, `y`, `w`, and `h` values of `r`. Finally, we're calling drawOutlineRect, to draw a grey outline rectangle. We now have a dark screen, with a darkened panel with a white grey outline.
Next, we're testing the value of stat's numMages. If it's greater than 0, we were victorious. We're therefore rendering some text with the word "VICTORY" in green. Otherwise, we're rendering "DEFEAT" in red.
With our header setup, we're then rendering each stat one at a time. We're first setting a variable called `y` to 110, which we'll increase by 50 before drawing each subsequent stat, it they appear on a different line. In order, we're then rendering the data for our mages, ghosts, rounds, moves made, shots fire and hit, pancakes eaten, and ammo collected. For our mages and ghosts, we're substracting the current count (numMages, numGhosts) from the total count (totalNumMages, totalNumGhosts) to get the number defeated in battle.
Lastly, we're rendering our time stat. We're taking the value of stat's timePlayed and dividing it by FPS, to get the total number of seconds elapsed, and assigning this to `secs`. Next, we're dividing `secs` by 60, to get the total number of minutes played, and assigning this to a variable called `mins`. With that done, we're taking the modulo of 60 against `secs`, to get the number of seconds according to our time calculation. We're then using `mins` and `secs` in our Time Played stat display.
Phew! That was a lot. But we're done with the majority of the work now. We just need to update one more function - endTurn:
Now, when we flip the turn and it is once again the player's go (TURN_PLAYER), we're incrementing our `rounds` stat. We consider it a new round upon the start of the player's go.
That's stage.c done with. We can now turn our attention to the rest of the code. What follows will mostly be little updates and tweaks, as we incorporate the stat tracking.
Turning to bullets.c, we've updated two functions. First, applyDamage:
When a bullet hits, we're testing if it's the player's turn (we're assuming the player cannot attack during the AI's turn) and also if the target is not the world (ET_WORLD), and then incrementing our bulletsHit stat.
The fireBullet function has seen a similar update:
Once again, if it's the player's turn, we're incrementing the bulletsFired stat.
Now onto items.c. We've updated all the `init` and `touch` functions for our two item types:
In initHealth, we're incrementing totalPancakes, to get a count of how many stacks of tasty, tasty pancakes exist on the stage.
We've also updated healthTouch:
Whenever those lovely pancakes are eaten, we're incrementing the value of numPancakes.
Similarly, initAmmo and ammoTouch has been changed:
We're increasing the value of totalAmmo upon creating some ammo.
In ammoTouch, we're increasing the value of numAmmo whenever ammo is picked up.
Over in player.c, we've made a small update to addPlayerUnits:
We're setting our totalMages stat to NUM_PLAYER_UNITS (found in player.h).
ai.c has seen a small update of its own:
We're now randomly filling the stage with ghosts!
We're setting up a char array called ghostTypes, into which we're adding all the names of our ghost types. Next, we're setting our totalGhosts to a random between 3 and 12. This will be the total number of ghosts found on the stage. We're then using a for-loop to create that many ghosts. For each iteration of the loop, we're generating a random number between 0 and 4, and assigning it to a variable called `r`. We're then using this as the index in our ghostTypes array, that we're passing over to initEntity.
So, each time we play a level, it will have a unique layout, and a random selection of ghosts to go along with it.
Heading to units.c now, we've updated `move`:
Now, whenver a unit moves, we're testing to see if it's the player's turn. If so, we're incrementing the numMoves stat. There are actually two different ways we could do this - incrementing the stat upon each square moved or updating it when the movement phase has finished. I opted here to update it for each square moved, just to make the stats screen look a bit more interesting; the shots fired and hit stats might look very similar to it otherwise.
And finally, we come to the most important update - the `tick` function:
Upon each call, we're testing the type of entity that is calling `tick`. If it's a mage (ET_MAGE), we're increasing the numMages stat. If it's ET_GHOST, we're increasing the numGhosts stat. As you can now see, we're resetting both these stats to 0 at the start of our gameplay loop, but then incrementing them in this `tick` function. This constant recounting allows us to keep our stats perfectly in sync, without the hassle of any micromanagement. In essence, this tick function is the most important one in the game, as it governs whether the game has been won or lost..!
Hurrah! Another part down, three to go.
How are you finding the camera? It's good, right? But it could be better. The flicking back and forth when the ghosts move can be a little bit disorientating. It would be nice if we could see everything in context. In the next part, we're going to investigate making the camera move smoothly around the battlefield, so we can clearly see where everything lies, relative to each other.
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.