• 2D shoot 'em up
SDL2 Rogue tutorial
SDL2 Gunner tutorial
SDL2 Shooter 2 tutorial
SDL2 Widget tutorial
SDL2 Adventure tutorial
— Creating a vertical shoot 'em up —
We've already got one power-up type: the sidearm. We also made promises in our code that we would offer more; the power-up pod can rotate between the offerings on a timer. In this part of the tutorial, we're going to add in those missing power-ups, including the ability for the player to increase their movement speed and rate of fire.
Extract the archive, run make, and then use ./shooter2-07 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. Destroy the supply ships that appear at the top of the screen, to spawn a power-up pod. The pods can be shot to push them back up the screen, to allow time for them to change to something more desirable. When you're finished, close the window to exit.
Inspecting the code
To start with, we've expanded our defs.h enums to specify the new types:
We now have entity types (ET) for the sidearms (ET_SIDEARM) and drones (ET_DRONE). Note that these two enums replace the original ET_POWER_UP enum. We've done this so that we can more easily manage our power-ups, as we'll see later on. We've also added to our power-up pod types:
Again, we used to only have a power-up pod (PP_POWER_UP_POD) to represent a sidearm. Now, we support shield, drone, speed, and reload rate power-up pods.
structs.h has also been updated with the Drone struct:
Our Drone is an entity that moves randomly around the screen and attacks nearby enemies. Its fields will likely remind you of the bosses we created in the previous part. `dx` and `dy` will control the direction the Drone is moving. thinkTime will govern how long before the drone changes direction. `target` will be used to track the entity's current target, while attackTime, numShotsToFire, and `reload` will control its attacking phase. Note that damageTimer also exists. This is because our drone is not immortal or invulnerable, and can be hurt (and destroyed) by enemy fire. We also extended the same weakness to the regular sidearms:
Sidearms can now be hurt and destroyed by enemy fire, so the player will need to collect another power-up pod to replace them.
We'll now look at powerUp.c, where we've made a bunch of updates to support our new power-ups. Pretty much every function has been changed, so let's start with addPowerUpPod:
The first change is that we're now assigning a takeDamage function to the power-up pod's entity. Next, we're loading a total of 5 textures, to represent our power-up pods. sidearmPodTexture, shieldPodTexture, dronePodTexture, speedPodTexture, and reloadRatePodTexture all represent their given types (should be clear what they represent..!). One thing you might have noticed is that the blue power-up pod now provides the shield, and not the sidearms, as before. I felt that since the shield is blue, it would make more sense for the blue power-up pod to do this. Red is also seen to represent danger, and so having this for the sidearms works out nicely.
Next, we have the takeDamage function. It's very simple:
When the PowerUpPod is hit by a bullet, we will substract the value of its texture's height from it's `y` coordinate, effectively pushing it back up the screen. This is something that can be used by the player to wait for the pod to change into something more desirable before collecting it.
We've also updated updateTexture to accomodate the new textures:
There's not much to say about the updates to this function, as it should be quite clear what's going on - we're just assigning the entity's texture according to the PowerUpPod's type.
Now we come to the activatePowerUp function, which is where the logic for handling all our new power-ups comes into play:
We're checking what type of PowerUpPod we've interacted with and responding accordingly. If we've touched a sidearm power-up (PP_SIDEARM), we're granting the player some sidearms. Note, however, that we're first removing the existing sidearms. This is something we didn't do in the previous tutorials, meaning our firepower could increase hugely. We therefore want to limit this. We'll see what this function does in a bit. Next, we're handling the shield power-up. All this power-up does is set the player's health to 5, allowing them to take 5 hits before they are killed. For a PP_DRONE type power-up, we're testing to see if we can create a drone before calling initDrone. This is to limit the number of Drones the player can have assisting them, so that we don't reach some absurd number.
Like the PP_SHIELD type, the PP_SPEED and PP_RELOAD_RATE power-up pods affect the player directly, or rather the Fighter. You might remember that we added fields to the player's Fighter for their speed and rate of fire, rather than use constants. We finally get to see these being put to more effective use. When touching a PP_SPEED power-up, we extract the Fighter from the player's `data` field and increase its speed by 2. We limit the speed to MAX_FIGHTER_SPEED (defined in defs.h as 12). For PP_RELOAD_RATE, we're decreasing the Fighter's reloadRate by 4, again limiting it to MAX_FIGHTER_RELOAD_RATE (defined as 4). These allow the player to move very fast and fire very quickly.
We'll look next at two functions that are called by activatePowerUp, starting with removeSideArms:
All this function does is loop through all the entities active in the Stage, looking for any ET_SIDEARM types. If we find one, we'll set its `health` to 0, thereby removing it. Now, this might seem a bit heavy-handed, but the alternative to dealing with updating the sidearms would involve a bit of micromanagement, to find how many sidearms need to be added and in which position. Removing and re-adding them is the best solution here.
The other function, canSpawnDrone, isn't too different:
We're looping through all the active entities and counting how many Drones there are. The function will then return 1 or 0 (true or false) depending on whether there are fewer than MAX_DRONES (defined as 3). In effect, if we already have 3 Drones active, the power-up will do nothing.
Let's now look at how our Drone works. All his functions are defined in drone.c, and there's a good number of them. We'll start with initDrone:
We're mallocing and memsetting a Drone, then fetching the required textures. Our drone will be using a different bullet from the player, since it can move in any direction. Like some of the bosses, the Drone's bullet is a sphere (but blue in colour). With that done, we spawn our Entity with a type of ET_DRONE. Note that the drone has 3 health points, allowing it to be shot three times before it is killed. The drone's `texture` and `data` fields are set, and then we're positioning the drone to the middle bottom of the player. Finally, the `tick`, `draw`, and takeDamage functions are assigned.
Just like the bosses, the `tick` function for the Drone is quite meaty, as it's where its behaviour is performed:
As I said, there's a lot! Starting from the top: the first thing we're doing is extracting the Drone from the entity `data` field. We're then moving the Drone according to its `dx` and `dy` values, by adding them to the entity's `x` and `y`. We're also constraining the Drone to the screen, via some MAX and MIN macros. Much like the player, the Drone's `x` and `y` values won't be allowed to go lower than 0, and not higher SCREEN_WIDTH and SCREEN_HEIGHT on the x and y.
We're then decreasing the thinkTime and limiting it to 0. If we hit 0, we'll want to update the Drone's movement. We're first telling it to hold position, by setting its `dx` and `dy` to 0. There's then a 2 in 3 chance that we'll update the `dx` and `dy` values to something random, making the drone move about. With that done, we're resetting the thinkTime to something random, to let the drone continue on its path for a while, much like how we do with the bosses.
attackTime comes next. Once again, we're decreasing the value and limiting it to 0. Once it reaches 0, the Drone will be ready to attack. Here's where things get interesting, as the Drone doesn't just fire straight up the screen, it actively looks for a target. We're first testing if the Drone has a target or if its current target's health is 0 or less. If so, we're calling a function called selectTarget (which we'll look at in a bit). Should that call result in the Drone finding a target to attack, it will choose to fire between 1 and 3 shots, before resetting its attackTime to between 2 and 5 seconds.
Following this, we're going to check if the Drone is able to fire at its target. We're decreasing the Drone's `reload` and limiting it to 0, then testing if the `reload` is 0 and that numShotsToFire is greater than 0. If so, we're decreasing the numShotsToFire, resetting the `reload`, and calling fireBullets (we'll see this in a bit).
Finally, we're testing if the player has been killed. If so, the Drone itself will die, adding an explosion at its position and setting its own `health` to 0. We're also decreasing the Drone's damageTimer, and limiting it to 0.
All in all, we can see that the Drone move around the screen randomly, firing at enemies. It doesn't do so too often, but keep in mind the player can have up to 3 Drones assisting, meaning that they have the advantage of numbers.
Now, let's look at what selectTarget does:
After extracting the Drone data from our entity, we're setting the Drone's `target` to NULL, to force it to not have a target. Next, we're looping through all the entities in the Stage, looking for aliens. If we encounter one, we're calculating the distance between it and the Drone, by calling a function called getDistance, and feeding in the Drone's position and the alien's position. We're assigning the value of this calculation to a variable called `dist`. The alien will become the target of the Drone if the Drone doesn't already have a target or if alien is closer to the Drone than the best distance (`bestDist`). If the alien does become the target of the Drone, the value of `bestDist` will be updated to the value of `dist`, meaning that only an alien that is now closer to the Drone than the current target will become the new one.
In summary, the Drone will select the nearest alien to it as it's target.
In case you're wondering what the getDistance function looks like, it can be found in util.c:
Just a standard distance function. Simple, but highly useful (and quite portable).
We should cover the remaining functions now, though they won't come as a shock. Starting with `draw`:
We're testing the Drone's damageTimer to see how we want to draw the drone. If it's 0, we'll draw the Drone as normal. However, if it's not we know that the Drone has taken damage. We'll draw the Drone with a red tint applied (using SDL_SetTextureColorMod and feeding in the entire texture atlas). Using the blending modifier as we do with the aliens doesn't work so well for the Drone, due to its colours, so drawing it in red helps. We're then changing the texture atlas's colours back to white (255, 255, 255) after we're done, so everything else doesn't render in red..!
As stated earlier, the Drone is vulnerable to attacks from aliens bullets, so it has its own takeDamage function:
Nothing unexpected. We reduce the Drone's `health` by the value of `amount`. If it falls to 0 or less, we'll call the `die` function. We'll also set the Drone's damageTimer to 8, to show it has taken a hit.
There's very little to the `die` function:
Just an explosion created with addExplosion, nothing more. The fireBullets function is a little more exciting:
Just like the Blue Boss, the Drone fires a shot directed at its `target`. Worth noting is that when we spawn the bullet, it belongs to the player, not the Drone, just like with the sidearms. We set the bullet's texture and align it in the middle of the Drone. After that, we'll call calcSlope and feed in the midpoints of the target's `x` and `y` coordinates, the bullet's `x` and `y`, and also references to its `dx` and `dy`. With our velocity calculated, we just multiply up the `dx` and `dy` by 12, to make the bullet move fast towards the target.
That's our Drone concluded. As you can see, some of its behaviour is quite similar to that of the bosses. It's not the smartest companion, but as a mobile gun, it works well enough. Since our power-ups can now be damaged, we should see how we've updated our bullets to conform to this:
While looping through our bullets, we've remove the functions that specifically handle the aliens or player, and now just have one function called doCollisons, which takes a bullet as a parameter:
For each bullet, the function will loop through all the entities in the stage. We'll set a flag called checkHit to 0 (false) and then test the bullet owner's `type`. If it's ET_PLAYER and the entity we're currently checking is an alien (ET_ALIEN) or a power-up pod (ET_POWER_UP_POD), we'll set our checkHit flag to 1. Otherwise it will become false. If the owner type is an alien (ET_ALIEN) and the current entity is the player (ET_PLAYER), a sidearm (ET_SIDEARM), or a drone (ET_DRONE), we'll set checkHit to 1.
We then test the state of the checkHit flag. If it's 1 and a collision has taken place, we'll call the entity's takeDamage function. This means that we're now expecting a takeDamage function to have been set for an entity that a bullet can collide with. Be aware that we're not testing if it's NULL, which means if we don't set the function pointer, the game will crash when it tries to invoke the function pointer.
Again, it's somewhat wasteful to check every entity for each bullet. If our game was larger, we'd want to break things down a bit, to make them more manageable. With what we have right now, it's not too much of a concern. In a later tutorial, we'll look at dividing up the playfield, to reduce the number of collision checks.
As our sidearms are now vulnerable to enemy damage, we should look at what changes we made to enable this. We'll turn to sidearms.c and start with initSidearm:
The two changes here we've made is to give the sidearm's `health` a value of 3, allowing it to survive 3 hits before it is destroyed. We've also assigned `draw`, takeDamage, and `die` function pointers. Our `tick` has had a one line update, too:
As the SideArm struct now has a damageTimer, we're decrementing it in the `tick` function, limiting it to 0. As expected, the damageTimer field comes into play in the draw function:
You'll recognise what's going on here as being the same as the `draw` function for the Drone. As with the Drone, we're drawing the sidearm in red when it is damaged, due to it being easier to recognise.
The sidearm's takeDamage function is also very familiar:
A standard takeDamage function, that reduces the entity's `health` by `amount` and kills the sidearm as needed.
The last thing we've tweaked is the player. With the aid of a power-up, the player can now survive more than 1 hit, so we've made some tweaks to initPlayer:
We're assigning two new function pointers to the player: `draw` and takeDamage. We're also grabbing a new texture for the shield effect, and assigning it to a variable called shieldTexture.
The takeDamage function we've created for the player is nothing special:
We're just doing as we always do, which is to reduce the player's `health` by the amount specified, and kill them if `health` falls to 0 or less. This is, however, different from when we used to kill the player immediately in bullets.c.
Lastly, let's look at the `draw` function:
There's a little more going on here than just drawing the player. You'll have noticed if you've grabbed the shield power-up that a blue sphere surrounds the player. We're handling this in the draw function. To begin with, we're testing if the player's `health` is greater than 1. If so, the player has a shield. We're first calculating the position of the shield by taking the center point of the player and the shield texture. We're then setting the shield texture's alpha (really the texture atlas's alpha) based on the amount of `health` the player has, multipled by 50. If the player has 5 `health`, the alpha will be 250. If the player has 4 health, it will be 200, and so on. This means that as the player takes damage and their `health` decreases, the alpha will also decrease, causing the shield to fade until it vanishes altogether.
We now have all our power-ups! There's just a few things left to finish off our game. We'll be introducing a couple more alien attack patterns, then layering on all the finishing touches, including a title screen, highscore table, sound effects, and music!
The source code for all parts of this tutorial (including assets) is available here:
It is also available as part of the SDL2 tutorial bundle (with on-going updates):
If you do not wish to create an itch.io account, you can also purchase the tutorial bundle using PayPal. This method will be slower, however, as it will require manual verification of the transaction.
Share your comments and thoughts below. All comments are anonymous and cannot be edited.