• 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 vertical shoot 'em up —
So far, we only have one style of enemy attack pattern - a snake-like spiral that starts from the top of the screen. In this tutorial, we'll add in some new enemy attack patterns and also restore the absent power-up.
Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter2-05 to run the code. You will see a window open like the one above. Use the arrow keys to move the fighter around, and the left control key to fire. Dodge the enemy fire and shoot the aliens. Every now and again, a special enemy will appear at the top of the screen (a supply ship). Shoot it to gain a power-up pod. When you're finished, close the window to exit.
Inspecting the code
This is a long part, so buckle up! Again, we've added in some new enemy types, and also made several tweaks and updates to the existing code. You will have noticed, for example, that some of the enemies now require more than one hit to kill them, and that they also flash when they are hit. We'll look at structs.h first, to see what we've added in:
We've added two new function pointers to our Entity: `draw` and takeDamage. `draw` will be used for special draw functions for certain entities, while takeDamage will be used to figure out what should happen when a bullet collides with an alien.
We've also created two new structs for our aliens. The first is SwoopingAlien:
This is the little blue alien that starts at the top of the screen and moves up and down in a U shape. The other one is StraightAlien:
StraightAliens are the little grey aliens that move from left-to-right and right-to-left across the screen. We also have a struct defined for our supply ship:
It's quite a lot simplier than the other aliens, as it just moves either left or right across the top of the screen. Finally, we've updated Stage:
Yes, the hasAliens flag is back. This is due to a bug in the previous tutorial that could result in the PointsPod being awarded for destroying the last alien in a wave, but not the wave entirely. We'll see how this is fixed later on. For now, let's look at the first of our new alien types.
Everything for handling our SwoopingAlien is done in swoopingAlien.c. There are several functions in this file, so we'll tackle them one at a time, starting with initSwoopingAlien:
What's happening here should look quite familiar to you, as it's quite similar to SwingingAlien. We're passing over parameters such as the startDelay, the `x` position, `dx` (the speed at which it moves along the x-axis), and a variable called swoopAmount, to govern the depth of the swoop. We're mallocing a SwoopingAlien and setting all the appropriate variables, as well as grabbing the alien's `texture` and the bullet texture. With that done, we create our Entity as usual. Note two new things going on here, compared to before. First, our SwoopingAlien has its `health` set to 2. It will take two shots to kill, as our bullets cause 1 health damage per hit. The other thing that's new is that we're setting two additional function pointers - `draw` and takeDamage. We'll discuss these in a bit.
For now, let's look at the `tick` function:
We won't go into full details on this one, as, again, it's similar to SwingingAlien. After our startDelay timer has expired, the SwoopingAlien's `x` is moved along by its `dx`, while its `y` is adjusted by the sin of its `swoop` value (which inturn was updated by the swoopAmount). If our SwoopingAlien was moving right, its health will be set to 0 if it leaves the right-hand side of the screen. If it was moving left, it will be removed after it fully leaves the left-hand side. It will also fire randomly, when it is given the chance, and kill the player if it comes into contact with them. As mentioned earlier, our hasAliens field in Stage has also returned, which we're setting to 1, to tell the main loop that aliens are present (this was an oversight in the previous change - it happens and is all part of the development process).
Something new is the damageTimer field. We're decreasing this each call to `tick` and limiting it to 0. You will have noticed that the SwoopingAliens flash white for a moment when they are hit, to indicate damage. What the damageTimer field does is tell us that the alien was just hit and allows us to draw things a little different. This is why we now have a `draw` function pointer. To illustrate the damageTimer, let's look at it now:
The `draw` function is rendering the SwoopingAlien as normal, but then doing something more. It's checking to see if the damageTimer of the SwoopingAlien is greater than 0. If it is, we're drawing the alien again, but this time with additive blending. The result looks something like the image below (mocked up in GIMP):
What this means is that when we draw the image, all the colours of the pixels below it will be added to, increasing their RGB values (and shifting towards pure white). For us, it means that our alien's image will brighten while damageTimer is greater than 0. This gives us pleasing visual feedback on a hit being scored.
The damageTimer value is set in the new takeDamage function:
This function takes two parameters - the entity that has received the damage, and the amount of damage (as `amount`). We're subtracting the `amount` from the entity's `health` and calling the entity's `die` function if its `health` has fallen to 0 or less. This was once done in bullets.c whenever an alien was hit by a bullet. Doing it here gives us more flexibilty on how we want to handle such things.
Finally, we come to the `die` function, that we've tweaked since last time:
A single point is now awarded for an alien being killed. We're still generating an explosion, but we've updated the conditions upon which a PointsPod is released. Now, we're decrementing a variable called numWaveAliens in Stage. If this hits zero, we're releasing a PointsPod. This ultimately means that bonus points will be made available for eliminating the entire wave of aliens. Our previous attempt at this featured a bug where this wasn't necessary, so this new approach fixes that issue.
That's our SwoopingAlien done, so now we can look at the StraightAlien, the ones that move (mostly) in a straight line across the screen. Our StraightAlien is handled by straightAlien.c. Like SwoopingAlien, it contains a good deal of functions and is quite similar in behaviour. We won't cover all the functions, as they're near identical, other than a few tweaks. Instead, we'll focus on the specifics, starting with initStraightAlien:
No surprises here. The function takes `x` and `y` coordinates, as well as `dx` and `dy` variables, to tell the alien how it will move. We're creating our StraightAlien and setting all its variables, as well as grabbing the required textures. Again, notice how the health is set to 2, to make the alien require two hits to kill. We're also setting the `draw` and takeDamage function pointers.
Our `tick` function won't appear all that foreign, either:
We're moving the alien once its startDelay has expired. Notice how we're adjusting the entity's `x` and `y` variables using `dx` and `dy`. Our alien doesn't move entirely in a straight line and might creep up and down slightly. This is merely to add a bit of variety to the pattern. Like the SwoopingAlien, our StraightAliens will be removed if they move off the edge of the screen, depending on the direction they are going. Everything else remains the same as the other aliens (including the `draw` and takeDamage functions).
As can be seen, adding in new alien types is largely a case of creating a struct to hold custom data, and creating init and `tick` functions to deal with them. It should be noted that SwingingAlien now also features `draw` and takeDamage functions, as its behaves in the same manner as these two new updates. So as not to get bogged down, we'll not look at those changes.
With our new aliens defined, we can look at how they're implemented into the attack waves system. Looking at wave.c, there are a few changes having been made. Starting with nextWave:
As we now have three different alien types, we've added in a switch statement. Based on the result of a random of 3 (0-2), we'll either create SwingingAliens, SwoopingAliens, or StraightAliens. Remember that we are using a random seed for our waves, so the waves will always be the same each time. Our current game starts with a wave of SwingingAliens, then two waves of StraightAliens, then a wave of SwoopingAliens.
Let's look at what addSwoopingAliens does:
You'll notice it's rather like addSwingingAlien. We set a variable called `n` to determine how many aliens we want to create (between 5 and 12), space the aliens with a `delay` of 25, set a random x speed (`dx`), a random swoopAmount, and an `x` position of -75, so the aliens start on the left-hand side of the screen (though they will appear offscreen). With all those variables set, we're then testing a random of 2. If the value is 0 (50% chance), we're negating the value of `dx` and also setting `x` to the width of the screen, plus 75. What this means is that there's a 50% chance that our SwoopingAliens will move from right to left, starting at the right-hand side of the stage.
We then create all our SwoopingAliens, using a for-loop up to the value of `n`, passing in all the parameters we defined earlier. Finally, we're setting numWaveAliens to the value of `n`. This will let us track how many aliens there are in the wave, and allow us to later test whether we've destroyed all of them to earn some bonus points.
Our addStraightAliens function is much the same:
Once again, we're defining a random number of aliens (`n` - between 10 and 15), setting a random delay, random `dx` and `dy` values, a fixed horizontal starting position of -75, and a random `y` value of between 1/10th and 1/4 of the way down the screen. With that done, we're randomly flipping the starting position to the right-hand side of the screen, and also randomly negating the `dy` value. The aliens are created in the for-loop, and Stage's numWaveAliens are set to the same value as `n`.
All this means that, like our SwingingAliens, there is a bit of randomization to the StraightAliens and SwoopingAliens, so that they do not always start from the same place and don't move in exactly the same way. The variations will be somewhat minor, but it does stop things from becoming too predictable.
Something that we've not looked at yet is the SupplyShip. It appears at a fixed interval and moves from left-to-right (or right-to-left) across the top of the screen. Its functions are defined in supplyShip.c. The file looks rather similar to the aliens we've seen already, but with a few minor differences. Let's look at initSupplyShip:
Unlike the aliens, we're not passing any parameters into this function; we don't need to, as the SupplyShip is a single alien that doesn't need to consider its position in a group. To start with, we're creating a SupplyShip, mallocing and memsetting it. We're then grabbing its texture, if needed. We're then creating the actual Entity, with a type of ET_ALIEN. The SupplyShip always appears at the top of the screen, so we can fix the `y` position. Our SupplyShip also have 5 health! Yes, it's well defended. Better than the aliens themselves. We then set the Entity's `texture` and `data` fields.
We then want to choose where the SupplyShip starts from, and so there's a 50-50 chance it will start on the left and move right or start on the right and move left. With that decided, we then set our `tick`, `draw`, takeDamage, and `die` function pointers. The draw and takeDamage functions are basically the same as for the other aliens, so we need only consider the `tick` and `die` functions. The `tick` function won't come as a shock:
The SupplyShip is moving along the x axis according to its `dx` value. Once it reaches the other side of the screen, we're removing it. damageTimer is being decremented and we're testing to see if the player has collided with it. The `die` function is different to the other aliens, however:
We're generating an explosion, as expected. However, no score increase occurs. Instead, we're calling addPowerUpPod, feeding in the midpoint location of the SupplyShip as parameters. In other words, once the SupplyShip is destroyed, it will drop a power-up for us.
Our SupplyShips don't form part of any waves; they're independent and arrive at a mostly fixed interval. If we look at stage.c, we can see where this is happening in initStage:
We have a new variable called nextSupplyShipTimer, set here to half the value of SUPPLY_SHIP_INTERVAL. SUPPLY_SHIP_INTERVAL is set at FPS * 30 in stage.h, which means that it will turn up after 30 seconds. To begin with, we're going to halve that to 15 seconds, so that the first SupplyShip shows up sooner.
If we now look at the `logic` function, we can see where the nextSupplyShipTimer is used:
We're decreasing the value of nextSupplyShipTimer at each `logic` call. If the value falls to 0 or less, we're going to call initSupplyShip, to create a new SupplyShip and have it crawl across the top of the screen. We'll also update nextSupplyShipTimer to the value of SUPPLY_SHIP_INTERVAL. This means that a SupplyShip will show up every 30 seconds.
That's it for the new aliens and attack waves. Before finishing, we'll have a look over the other things that have been added to the code. entities.c has seen the drawEntities function updated:
Since entities can have their own `draw` function, we're now testing to see if its been set, before calling it. If not, we'll render the entity using a standard blitAtlasImage call. As we saw earlier, the custom `draw` call is being used by the aliens and supply ship to make them flash when taking damage.
doAlienCollisions in bullets.c has also been updated:
Now, we're calling the entity's takeDamage function, passing over the entity itself and an amount of 1.
The last thing we should look at, briefly, is the highscores. Up until now, our highscore was being reset each time the stage restarted, which wasn't ideal. There is now a highscore.c file in the game directory. It will be used to manage all our highscore functions. We've not got a fully working highscore table just yet, but we will cover the basics nonetheless. We've created a Highscore struct to structs.h to hold our highscores:
It's simple, consisting of just a name and a score (MAX_NAME_LENGTH is defined in defs.h as 32). We also added a Game struct, to hold our highscores:
`highscores` is an array of Highscores (NUM_HIGHSCORES is defined as 11 in defs.h). The Game struct will be expanded in future, to include things such as sound configuration options, etc. For now, it just holds our highscores. Returning to highscores.c, we'll start with initHighScores:
We're looping through all our highscores, memsetting each one, setting the name to Anonymous, and then setting the score to 500 minus i * 50. So, the scores will be 500, 450, 400, 350, etc. Next, we have a function called updateHighscores. This is called in stage.c whenever the player is killed and the game resets:
We're setting the value of the final score in the array list to the score that was achieved by the player. Now, here comes the fun bit - our highscores array is 11 items in length. However, we're only going to recognise the first 10 scores. This means that the score in position 11 will never be displayed. We're therefore always overwriting it with the score the player earned. We're then sorting all the scores in the array, using `qsort`. This will result in the highscores array being reordered so that the highest score is in position 0 and the lowest score in position 10. Effectively, this means that the lowest score will never been displayed (when it comes to displaying our highscore table that is).
Our sorting logic is defined in highscoreComparator:
A standard number sorting function. Returning the difference between the two scores will mean that scores that are higher will be pushed up the list.
Finally, the drawHud function in hud.c has been updated to make use of the highscore list:
We're now testing to see if the player's score (stage.score) is higher than the best (game.highscore). If so, we're drawing the highscore value as the player's score in a pale green, to show it's overtaken first place. Otherwise, we're rendering the value of the game.highscore in white. Due to our highscore init and update functions, we can be confident that the first entry in the highscores array will be the highest value.
Phew! That was a lot to get through. But we now have many new features - new alien waves, a supply ship carrying power-ups, the ability to extend our entities with new function pointers, and the start of a highscore table. We also got our power-up back, giving us extra guns. It would be nice to be able to turn that firepower against something more meaty, don't you think? Well, in the next part, we'll look at introducing bosses to the mix, who will show up after several waves of enemies.
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: