• 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 —
In many shoot 'em ups, it is possible for the player to collect power-ups, in order to increase their firepower, up their speed, and gain new abilities. In this part of the tutorial, we'll look at how collecting a power-up token will result in the player gaining a pair of sidearms.
Extract the archive, run make, and then use ./shooter2-03 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. A blue power-up token (that looks a bit like the one found in R-Type) will be falling down the screen towards the player. Fly into it to collect the token and gain the sidearm power-up. Firing your guns will now produce bullets from both the fighter and the two sidearms. Note that only one token appears, so if you do not collect it, you will need to restart the game. When you're finished, close the window to exit.
Inspecting the code
Supporting the power-up token and the sidearms in our game is rather easy, and we've not had to make any complex changes. We'll start with defs.h and structs.h. defs.h has had some new enums added:
We've added to ETs (Entity Types) to our enum list, ET_SIDEARM and ET_POWER_UP_POD, which will be used to specify the entity is a sidearm and power-up, respectively. We've also added in an enum to denote our power-up pod type:
PP_SIDEARM will tell us that the power-up pod is for the sidearm power-up. The reason for PP_MAX is because we'll want our power-up pod to change occasionally when it is active, giving the player the option (more like a short window of time) to pick which power-up they want. We'll see this in action when we come to define our Powerup entity.
Speaking of entities, let's look at structs.h, to see the changes there. Starting with a tweak to Fighter:
We've added in a flag called invokeSidearm. This will be used to invoke the sidearms when the player fires their guns. More on this later. Next, we've added in the Sidearm struct itself:
The Sidearm struct contains just one field: `ox`. This variable, short of offset X, will be used to tell the Sidearm where it resides, relative to the player's Fighter. Finally, we've created a struct to represent the power-up itself:
Two fields: the `type` and changeTimer. `type` will right now only be PP_SIDEARM, while changeTimer will be used (at a future time) to cycle between all power-up types.
We'll dive into some actual logic code now. Naturally, we've introduced some new files to support our power-ups and sidearms. We'll start by looking at sidearm.c, which will drive the logic for how our sidearms behave. The file has three functions. We'll start with initSidearm:
This function will create an entity to act as a sidearm. Note how it accepts an `ox` parameter. The first thing we do is malloc and memset a Sidearm struct, then assign the `ox` value to the Sidearm's `ox`. Next, we're grabbing the textures for the Sidearm itself, as well as the bullet it will fire. Note that we need only test the sidearmTexture here, as we're grabbing both required textures at once. With that done, we'll spawn an Entity with the ET_SIDEARM type, assign it the sidearmTexture, and also set the Sidearm as its `data` field. We're then setting the Entity's `tick` to be that of the `tick` function in the file. Finally, we're calling `tick`. To see why this is, we'll look at what `tick` does right now:
`tick` will first extract the Sidearm data from the Entity (`self`), and then align the sidearm entity to the player. We're first centering the sidearm over the player themselves, making use of the player's `x` coordinate, as well as the midpoints of the player and the sidearm, based on their texture widths. Finally, we're adding the Sidearm's `ox` value to the entity's `x`. Ultimately, this will mean that we're centering the sidearm over the player, but then shifting it according to the horizontal offset of `ox`. We're also vertically aligning the sidearm to the base of the Fighter, according to the player's texture's height.
Remember that `tick` is called for each entity on a loop. What all this means is that the sidearm will constantly be aligned to the player as they move around. Our first call to `tick` just helps to ensure that the first draw call of the sidearms makes it appear in the correct place.
Finally, we're extracting the Fighter data from the player, and testing whether the invokeSidearm flag is set. If so, we're firing a bullet. Again, we'll discuss the reason for why this is required when it comes to processing the player. The last function in sidearm.c is fireBullet:
Nothing unusual here, as it's rather the same as the fireBullet function in player.c (which means it could be refactored at some point). We're spawning a bullet, setting the bullet's texture, aligning it to the center-top of the sidearm, and setting its vertical velocity to -15.
Since we're in the context of working with the sidearm, let's look at what we've changed in player.c. We've merely updated the doPlayer function:
Now, as well as decreasing the Fighter's `reload`, we're also resetting the invokeSidearm flag to 0. The other change we've made is when we test if the player has fired. Now, not only do we issue the bullet and reset the Fighter's `reload`, we also set the invokeSidearm flag to 1. The reason we're doing this is because the sidearms need to be told to fire when we do. However, the sidearms can't test the left control key and the Fighter's `reload` state, as when we fire, we reset the Fighter's `reload` to full. This means that the sidearms, due to their position in the entity queue, would never be able to fire, as the conditions for firing a bullet would never hold true. As such, we set a flag to tell them that they may do so, and clear it at the beginng of the player's logic step.
That's our sidearms handled, but what about the PowerUpPod itself, the thing we collect to grant us the additional firepower? That's defined in powerUpPod.c, a file consisting of 4 functions. Well start, as ever, with the init function, known here as addPowerUpPod:
The addPowerUpPod function takes three parameters: the `x` and `y` coordinates that it will appear at, and the type. Creating our PowerUpPod is quite similar to other entities in our game. We malloc a PowerUpPod, and then set its `type` and starting changeTimer value. CHANGE_TIMER is defined as 3 seconds in our header (FPS * 3). Next, we create the base entity and assign the `x` and `y` coordinates, as well as the data field using the PowerUpPod. We also assign the `tick` function and then test if we need to fetch sidearmPodTexture. After that, we'll call updateTexture.
We'll look at the `tick` function next:
The `tick` function will be responsible for rotating the type of power-up, as well as checking if the player has collected it. We start by extracting the PowerUpPod from the entity `data`, and then decrease its changeTimer value. If the changeTimer falls to 0 or less, we'll change the type of power up. We'll do this by incrementing the PowerUpPod's `type` by 1, then calling the modulo of that value using PP_MAX, to ensure it stays within the range of allowed types. With that done, we reset the changeTimer, and then call updateTexture.
The next thing we want to do is test whether the player has collided with the pod. If it has, we'll call activatePowerUp, to make use of whatever ability it wants to grant up. Finally, we'll alway increase the `y` value of the entity, to move the PowerUpPod down the screen. If it moves off the bottom of the screen, we'll set its `health` to 0, to remove it.
The updateTexture function comes next. As we've already seen, it is called by the addPowerUpPod and `tick` functions. What it does is quite simple (and you've likely already guessed, based on the name of the function):
The function takes two arguments - the entity and the PowerUpPod. Although the entity already contains the PowerUpPod in it's data structure, we have already extracted it in both instances that updateTexture is called, so we can just pass it in. The function performs a switch against the PowerUpPod's type, then, depending on what type it is, it will set the appropriate texture for the entity.
Our activatePowerUp is the last function to look at. Like updateTexture, it takes two parameters:
Again, we're passing in the PowerUpPod entity and the PowerUpPod itself. We're then testing the PowerUpPod's `type`, to determine what kind of power up it is. We only have one right now, PP_SIDEARM, so that's the one we'll use. We're calling initSidearm twice, to create two sidearms, the first positioned to the left of the Fighter (offset at -48 from center), and the other to the right (offset at 48 from the center). With that done, we set the PowerUpPod's entity's health to 0, to remove it.
All in all, this means that when the player flies into the PowerUpPod, they will gain two sidearms, on the left and right of the fighter. Note how there's potential here to keep accumulating sidearms and power ups. We'll fix that in a later tutorial. For now, we only have one power up type, and our demo only has the one power up token.
Speaking of the PowerUpPod, we should look to see how it's created. Well, right now we're just hardcoding it into initStage:
We're calling addPowerUpPod, setting it to (roughly) halfway across the screen, -50 pixels from the top of the screen, and with a type of PP_SIDEARM. Again, this means that there will only be one PowerUpPod available for this part of the demo.
We now have our first power-up, so the aliens don't stand a chance! Something you may have now noticed is that it's all too easy to clear the screen of aliens, and that there are an awful lot of PointsPods floating around. This is something we'll be tweaking later on, to make it so that PointsPods will only be dropped if the player completely destroys a wave. We'll obviously be adding in new enemy types and waves, too. Our next part will focus on this, as well as making the enemies fire back and also randomizing the wave forms a bit more.
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.