• 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 —
Old school 2D shooters often featured levels where the enemies would come towards the player along predefined paths, AKA attack patterns. Examples of this are arcade games such as Galaga and Phoenix, that featured enemy groups moving onto the screen with an intricate dance. In this tutorial, we'll look at how we can have some enemies enter the battlefield using a pattern.
Extract the archive, run make, and then use ./shooter2-02 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. The enemies will enter from the top of the screen, swinging left and right as they move. They can be shot and killed, causing them to drop points pods. However, they cannot harm the player. Close the window to exit.
Inspecting the code
We've made a good number of changes to the code now, in some areas larger than others. We'll look at what's changed in structs.h first, starting with the Entity struct, to which we've added some new fields:
All entities now have a type, which can be ET_PLAYER, ET_ALIEN, or ET_POINTS. This will be used for collision detection and other things, to filter out unwanted entities when processing. Entities now also have `health`, that will default to 1 when we spawn a new entity. Any entity with 0 or less health will be destroyed. We've also introduced two function pointers - `tick` and `die`. `tick` will be called each frame to handle things like movement, timeouts, etc. `die` will be called when an entity is killed (almost exclusively after being struck by a bullet).
Next, we've created a new struct - SwingingAlien.
This struct will be used to hold the information about our small yellow alien, who moves back and forth across the screen, as seen in the game and the screenshot above. We'll see more on how these aliens work in a little while.
Next, we have a struct to handle our points pickup, named PointsPod:
A simple struct, that just holds the PointsPod's movement vectors (`dx` and `dy`), as well as a `timeout` variable. `timeout` will decrease each frame, and once it reaches 0, the pod will disappear.
We've next added a `dead` flag to our bullets:
This will be used to remove the bullets if they either move offscreen or have struck an alien.
Finally, we've updated Stage:
We've added in a variable called `hasAliens`, which we'll use to detect whether all the aliens in the current wave has been defeated, and also two variables to hold the `score` and `highscore`, since it's now possible to earn points.
Quite a few changes already, but essentially ones. The first new piece of code we should look at is that for controlling our little yellow aliens. All the functions for handling their behaviour live in swingingAlien.c, so let's cover that next. There are just three functions in the file. We'll start with initSwingingAlien:
We're passing in a parameter called startDelay, which we'll talk about in a bit. The first thing we're doing is creating a SwingingAlien struct, mallocing it and memsetting it. We're then assigning its startDelay value to that which we passed into the function. Next, we're grabbing the texture for the alien, from our texture atlas. littleYellowAlienTexture is a static variable within the file, set to NULL. As we don't want to look it up each time we create this alien, we'll grab it the first time it is needed (by testing if the value is NULL) and then cache it.
With that done, we'll spawn an entity to represent the SwingingAlien. Note that we're now passing ET_ALIEN to spawnEntity; the utility function will set the type for us when it creates the entity. We're then assigning the entity's `texture` and `data` field, using the SwingingAlien struct we created beforehand. For positioning, we're placing the alien in the centre of the screen, and also vertically offscreen. Doing so means that when the enemy is spawned, it doesn't suddenly pop into existence, which would look a bit odd. Finally, we set our `tick` and `die` function pointers, using those contained in this same file.
As stated earlier, our `tick` function is called each frame. When called, this one will drive the logic for our yellow alien:
We start by extracting the SwingingAlien from the entity's `data` field, then decrease its startDelay variable. Once it reaches 0, the alien is free to start moving. Basically, when we create our SwingingAliens they will all spawn in the same location. Moving them all at once will mean that they are stacked on top of each other. By giving each alien created a staggered start time (the startDelay), the aliens will move one after the other, giving the appearance of them following the leader.
With our startDelay at 0, we know we can move, so we're going to increase the SwingingAlien's swing value by a small amount. Next, we'll adjust the main Entity's `x` value. We'll do this by taking the cos value of `swing` and multiplying it by 8. Taking the cos value of the SwingingAlien's `swing` will give us a number between -1 and 1, which we'll multiply by 8 to get a final value between -8 and 8. In effect, our `x` position will constantly move back and forth, with a swinging effect, based around the start position. We'll then also increase the `y` position, making the alien move down the screen. This pattern, when combined with a load of other aliens, creates a snake-like flow of moving bodies.
We're then checking to see if the alien has moved off the bottom of the screen. If it has, we'll set its `health` to 0, so that it is removed by our entity processing code (done in entities.c). Lastly, we want to flag that there are aliens currently present. We do this by setting stage.hasAliens to 1 (we'll ignore that we might have literally just removed the alien in question).
The final function in the file is `die`. It's quite simple:
When our alien dies, it will spawn a PointsPod, that the player can collect. In future, this will be expanded to include an explosion effect.
Now, let's turn our attention to wave.c. wave.c is where we'll be defining all our enemy attack waves. We're going to be randomly generating our waves, but using a fixed seed. This will mean that each run of the game will be the same. Let's look at initWave to begin with:
We're setting a variable called waveSeed to a fixed value. This is a number that we will feed into srand, that will form the basis of all our waves in the game. The next variable we have is a flag called setupNextWave. This will be used to control the setting up of our attack waves. We'll see how this happens in a bit. Finally, we're calling a function named nextWave. It's defined below:
nextWave will essentially generate the next enemy attack wave. The first thing we're doing is testing if setupNextWave is 0. If so, we'll know that we want to start setting up the next attack wave, so we'll now set setupNextWave to 1. Next, we'll set a variable called waveStartTimer to FPS (60). This will act as a countdown timer until we actually create our next wave. Doing so stops the next enemy wave from appearing too soon after we clear the previous one.
If setupNextWave is 1, then we'll decrease waveStartTimer, limiting it to 0. Once it reaches 0, we'll know we can actually do the work of creating the next alien attack wave. We seed srand using waveSeed, call addSwingingAliens, then set waveSeed to a new random value (that will always be the same, due to the starting seed), and zero setupNextWave. Now, we obviously only have one type of enemy attack pattern right now - the SwingingAliens. When we have more, we'll be expanding the nextWave function to include them. For now, we only have the little yellow aliens, so we'll create them in addSwingingAliens:
What this function is going to do is randomly create between 6 to 12 aliens, with a random start delay (for all aliens). With our number of aliens determined, we simply use a for-loop to call initSwingingAlien, passing in our delay. Our delay will increase with each alien we added, simply by multiplying up the delay value (i * delay). So, if we have 6 aliens and a delay value of 10, our delays will be 0, 10, 20, 30, 40, 50. Ultimately, this means that the first alien will move immediately and alien 6 will start moving last.
That's the main code for the aliens and the waves done, so we can now look at how it's used. Turning to stage.c, we've made just a handful of changes. Starting with initStage:
Alongside calling the init functions for our existing elements, we're calling initWave, to kick off the first alien attack. We've also updated the logic function to handle creating new waves:
At the start of the loop, we'll set stage.hasAliens to 0 (false), to say there are no aliens in the stage. At the end of our logic processing, when we've been through all our entities, we'll check to see if stage.hasAliens still holds true. You'll remember from the SwingingAlien `tick` that it will be set to 1 by the alien, if it's currently active. If there are no longer any active aliens stage.hasAliens will be 0. We'll call nextWave to kick off the next alien wave. As stage.hasAliens will remain at 0 until the wave has been created, nextWave will continue to be called, resulting in the waveStartTimer decreasing.
We should now look at what other changes we've made to the code, to support various other updates. We added some extra fields to our Entity struct, so we should look at how these are used. Turning to entities.c, we've made changes to the doEntities function:
While looping through all our entities, we're testing if the `tick` function has been assigned. If so, we'll call it. We're also checking to see if the `health` of the entity is less than or equal to 0, and removing it if so (not forgetting to also remove the extended data, if it exists, to prevent a memory leak).
Obviously we can now kill the aliens that are attacking us, so we need to do some collision checks for our bullets. We've made some updates to bullets.c for this purpose. To start with, we'll examine doBullets:
In addition to checking if the bullet has gone offscreen, we're now checking if the bullet is owned by the player. If so, we're making a call to doAlienCollisons, to check if the bullet has hit any aliens. It's not a complex function as, we'll see:
Our bullet will loop through all the entities in the stage, looking for any aliens (with a type of ET_ALIEN). If we find a match, we'll perform a collision check, to see if our bullet has made contact with the alien. If so, well reduce the alien's `health` by 1. Should the alien's `health` have been reduce to 0 or less, we'll then call the alien's `die` function. With this done, we'll mark the bullet as `dead`, so it can be removed.
Again, there is an optimisation that could exist here - each bullet is checking each entity currently in the stage. If there were 8 bullets and 21 entities (not all of whom are enemies), that would mean we're performing 168 tests each frame. It's not a huge amount, but if one wanted to reduce the number of checks, we could extract the enemies ahead of time, or even divide the playfield into segments, and only test those entities that live in the same segment as the bullet, thus reducing the number of checks considerably. Our game, however, is small, and in the grand scheme of things we're not doing a lot. Most modern computers will handle this without any trouble whatsoever. In a future tutorial, we might look at how best to handle dividing up the playfield.
We're almost done with this part of the tutorial, so let's take a quick look at our PointsPod, defined in pointsPod.c. There are two main functions in this file, addPointsPod and `tick`. We'll start with addPointsPod:
The function takes `x` and `y` coordinates as parameters, that are used to determine where the PointsPod will be created (usually in the place of the alien). To start with, we're mallocing and memsetting a PointPod, and then assigning its `dx` and `dy` some random values. These values will be used to determine the direction and speed the PointsPod will move along its `x` and `y` axis. Next, we're setting its `timeout` to 5 seconds (FPS * 5). We're then assigning the texture, using the same lazy caching strategy as with the SwingingAliens. After that, we're creating an Entity, with a type of ET_POINTS, and assigning the position, `texture`, `data`, and `tick`.
The `tick` function itself might look a little involved, but it's actually not at all:
We're extracting the PointPod data from the Entity, then adding its `dx` and `dy` to the entity's `x` and `y`. We're then testing to see if the movement has pushed the PointsPod offscreen. If so, we'll reposition the PointsPod at the edge of the screen, and negate the `dx` or `dy` value that caused it to leave the screen. This will, in effect, cause the PointsPod to bounce around the screen. With this done, we then call the collision function, to see if the PointsPod has come into contact with the player. If so, we'll increment the stage's `score` (and test if we've now exceeded the `highscore`), before setting the PointsPod entity's `health` to 0, to remove it. Regardless of what happens, we'll always decrease the PointPod's timeout. If it falls to 0 or less, we'll set its `health` 0, to remove it.
All in all, this will make the PointsPod bounce around the screen until it either times out or is collected by the player, to earn some points.
That's it for our first alien attack sequence. It's only the second part, but already we've got a near-finished game! The only thing that would be missing at this point is the enemies firing back. Well, we'll add that in a later part, and also tweak the SwingingAliens a bit, so their patterns can be wider and tighter, depending on the wave's seed. Our next tutorial, however, will focus on something more exciting: introducing our first powerup!
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.