SDL2 Shooter 2 tutorial
SDL2 Widget tutorial
SDL2 Adventure tutorial
Orb source code
— Creating a vertical shoot 'em up —
In many shoot 'em ups, the player will encounter bosses. These enemies are typically more powerful than the minions that the player has met so far, having more health and better firepower. In this part, we'll look at introducing bosses to our game. Every 10 waves, the player will be forced to face off against one of 4 guardians. The bosses become increasingly more deadly as the game goes on.
Extract the archive, run make, and then use ./shooter2-06 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. Fight your way through the enemy waves until you reach the boss. Battle and defeat the boss to continue. If you're having trouble reaching the boss, there are various tricks that can be used, such as updating the player's die function to reset their health to 1, thereby avoiding death. When you're finished, close the window to exit.
Inspecting the code
As mentioned before, we've introduced 4 bosses to the game. Something that's nice is that the way the bosses have been designed means that there is a lot of code in common, and the bosses therefore all live in a single file. Before we get there, let's look at what we've done with structs.h:
We've created a Boss struct to hold all the details of our boss. Again, all four bosses are very similiar, so they share all these common elements. A maxHealth field exists to help us out when displaying the boss health on the hud. thinkTime will be used to determine how long before a boss takes another action (in this case, it will be how often to change direction). The `dx` will determine the boss's x velocity, while `reload` will be used to limit how fast the boss can fire. attackTime will set the interval between the boss attacking, while numShotsToFire will tell the boss how many shots to fire during its attack phase. damageTimer works the same as for the other aliens. The fireBullets function pointer will be used to control how the boss attacks, as each one has a different weapon, which will fire differently. We'll see this in action later on.
We've also updated Stage:
We've added a `boss` pointer, so that we can access the boss's details. This will primarily be used with the hud, so it can show the boss's health pool.
Our bosses are defined in a file called bosses.c. It's home to a good number of functions, including ones specific to each boss. To begin with, we'll focus on the Green Boss, the first one the player encounters. The first function we'll look at is initGreenBoss:
For our GreenBoss, we're mallocing and memsetting a Boss struct, then setting its maxHealth to 100 (he has a lot of energy), and his fireBullets function pointer to fireGreenBossBullets, the function he'll invoke when firing. We then grab the textures he'll use, both his own and the bullet texture when firing. We'll then spawn an Entity, with a type of ET_ALIEN. The entity will be assigned its texture, its position on screen (horizontally aligned, but off the top of the screen), and with a `health` amount equal to the Boss's maxHealth. The data field is also assigned to the Boss struct. That done, we finish up by assigning the required `tick`, `draw`, takeDamage, and `die` function pointers.
Pretty standard stuff. The `tick` function is where things get interesting, though. Note that this `tick` function is shared between all the boss types:
Wow, a lot going on! Let's work our way through it from top to bottom. The first thing we're doing is setting Stage's hasAliens flag to 1. After that, we're testing whether the Boss's `y` is less than 120. If so, we'll increase it and then return. What this does is ensure that the boss has arrived on screen before it does anything else. However, it's important that we set the Stage's hasAliens flag in the first instance. If not, our Stage logic will think that no boss has been created and will make another. In short, we'll end up with this:
An army of Green Bosses! This was a bug that cropped up during development and one that I found so amusing I thought I'd save a screenshot of it, to show here.
Moving on - after we've confirmed that the boss has reached 120 on the y axis, we increases the boss's `x` according to their `dx`, making them move left or right across the screen. We're also limiting the Boss's `x` value, not allowing them to leave the screen. To guard against the boss getting stuck at the edge of the screen, we're checking to see if they're at the edge of either side and attempting to keep moving in that direction. If so, we're reversing the direction of the movement, in effect making them bounce. We're then setting the Boss's `y` to 120, to ensure they stay at that position. Next, we're decreasing the thinkTime, limiting it to 0. Once it reaches 0, we'll prepare to change the Boss's movement pattern. We first 0 the Boss's `dx`, to tell them to stay put. There's then a 4 in 5 chance the Boss's `dx` will change, being randomly set between -0.5 and 0.5. We then randomly set the thinkTime, so that the boss will continue to hold position or move for a limited amount of time, before choosing to do something else.
We're then decreasing the Boss's attackTime, again limiting to 0. Once it reaches 0, our Boss is free to prepare to attack. Our attack preparation is simple: we set the Boss's number of shots to fire (numShotsToFire) to a random of between 3 and 6. With that done, we reset the Boss's attackTime to between 2 and 4 seconds.
As we've seen with the regular aliens, we're decreasing `reload`, the variable that will control when an enemy actually fires a bullet. In the case of the bosses, we're testing if reload is 0 and also if the Boss has shots to fire. If so, we're decrementing the number of shots to fire, resetting our `reload` time (for a pause between firing) and then calling the boss's fireBullets function. This differs from the standard aliens, in that when a boss is ready to fire, it will do so as soon as numShotsToFire is greater than 0.
The remainder of the function is code we've seen before - decreasing the damageTimer and also checking for a collision with the player. In the case of a collision, however, the Boss suffers no damage and only the player dies. The last line in the function assigns the Stage's `boss` field to that of the current boss, allowing us to easily track the boss.
Coming now to the draw function, it's pretty standard for all our enemies:
We're rendering the Boss as expected, and also drawing it again with additive blending, if the Boss's `damageTimer` is greater than 0.
The takeDamage function is also much the same as what we've seen before:
The only difference in the Boss's takeDamage function is that the Boss will not take any damage if they're currently moving into position. Once their `y` position is at least 120, they can be destroyed.
The Boss's `die` function is nothing out of ordinary, either:
100 points are awarded and a series of explosions are created, using a for-loop. Each explosion is initially centered around the Boss's midpoint, before being moved to a random point around the Boss, to give the impression of a larger explosion taking place. addExplosion is called for each one.
Now, let's look at what happens when the Green Boss fires. As we saw, Bosses have a function pointer called fireBullets, which, for the Green Boss, is assigned to fireGreenBossBullets:
You can see right away that we're spawning two bullets, each one being given the downBulletTexture and horizontally assigned in the middle of the boss. They'll start off at the bottom of the Boss and have a `dy` of 10, meaning they'll move down screen. What's different between the two bullets is that the first is horizontally shifted 25 pixels to the left (x -= 25) and the second 25 pixels to the right (x += 25). This means that both bullets will issue from the bottom of the boss, somewhat to the left and right of the middle.
That's it for the Green Boss. Now that we've covered the `tick`, `draw`, takeDamage, and `die` functions, we'll see that the only major differences when it comes to the remaining three are the init and fireBullets functions.
Let's look at how the Yellow Boss is implemented, starting with initYellowBoss:
He's very similar to the Green Boss. In fact, so much so that a factory function could've been used to cut down on some of this repeat code..! As with the Green Boss, we're mallocing the memsetting a Boss struct. However, the Yellow Boss has slightly more health (125 vs 100) and its fireBullets pointer is also assigned to fireYellowBossBullets. The only other change is that the boss is using different textures. We're grabbing his main texture (yellowAlienTexture) but also grabbing a different bullet texture, one we've not see before. gfx/alienBullet.png is a spherical sprite, as the Yellow Boss's bullets don't just move directly down the screen. In this case, a sphere looks better. We're assigning this to omniBulletTexture.
That's the init for the Yellow Boss, so we'll move onto the fireYellowBossBullets:
We're setting up a for-loop to spawn some bullets here, starting at -1 and finishing at 1 (inclusive). This means we'll cover the range of -1, 0, and 1, for 3 bullets. Each bullet will use the omniBulletTexture and be horizontally aligned over the alien, as well as being aligned at its base. The bullet's `dy` is also set to 9, to tell it to move down the screen. The `dx` is interesting, as we're taking the value of x (-1, 0, 1) and multiplying it by 3. This means that the first bullet will have a `dx` of -3, the second 0, and the third 3. This creates a spread of three bullets. They don't move too fast, so they're quite easy to dodge.
Our Blue Boss comes next. He's created using the initBlueBoss function:
Much like the Yellow Boss and Green Boss before, the only differences are the amount of health he is given (150); the fireBullets function, which we'll set to fireBlueBossBullets; and the `texture` he's using (gfx/blueAlien.png). Like our Yellow Boss, the Blue Boss will also use the gfx/alienBullet.png image for its bullets. You may be thinking that we need not grab the texture, since we know that the Blue Boss follows the Yellow, and as such we'll already have the texture. However, should we wish to test the Blue Boss by himself, that would not be the case. It's therefore wise to make sure we always have it.
Let's look at the fireBlueBossBullets function. This one's a bit different:
We're spawning a bullet, setting the texture,and aligning the bullet to the center bottom of the Boss, but after this we're doing something new. This Boss, unlike everything else in the game, fires directly at the player. Other aliens fire blindly, down the screen, while this one will target the player no matter where they are. To do this, we're calling on a function called calcSlope, and feeding in the player's midpoint `x` and `y` coordinates, the bullet's `x` and `y` coordinates, and the bullet's `dx` and `dy` fields as references. We've seen this function in previous tutorials (Shooter 1 and Battle Arena Donk), but we'll quickly refresh our memories. What this function does is calculate the normalized step from one point to another. The `dx` or `dy` will always be 1 (or -1), while the other could be any value between -1 and 1. This will be used to tell the Boss's bullets which way they need to go to reach their target when they're fired. Since the bullet won't be moving very fast (moving at 1 pixel per logic frame) we're multiplying up the `dx` and `dy` by 12, to make it move faster. All in all, our Blue Boss will fire a somewhat fast-moving shot at the player, wherever they are on screen.
The Red Boss comes last and is setup using initRedBoss:
Wow, the Red Boss has a lot of health! 200 points! He's also using a function pointer called fireRedBossBullets and using the omni directional bullet texture. We've seen this all before, so let's skip to the fireRedBossBullets function:
Like the Yellow Boss, the Red Boss is firing a spread of shots, although this spread is much wider. Our `x` is set to move from -5 and 5, meaning 11 shots in total are fired! Each bullet's `dx` is set to 2 * the value of `x`, for (-10, -8, -6, etc). The `dy` is also 7, meaning they move slower down screen. This creates a sort of bullet hell scenario. It might seem excessive, but the player need only concetrate on dodging in order to survive. One last thing the fireRedBossBullets does is randomly choose whether it wants to increase the number of shots its fires (incrementing numShotsToFire). This is a bit of a hack around our shared `tick` function, where the bosses will always fire a minimum and maximum number of times. There are some other approaches we could've taken here, such as creating a `tick` just for the Red Boss, or introducing a preFire function pointer, so that each boss can choose how many shots to fire, etc. Our little hack is a one-off and works fine here, so we'll not bother to make a huge change just for that one case.
The only other function we've not covered in bosses.c is the initBoss function:
What this function does is picks a Boss depending on the Stage's wave number. Our bosses show up every 10th wave, so the first thing we do is divide Stage's waveNum by 10, before reducing it to the range of 4, and assigning the result to `n`. This means that depending on the wave, we'll get a number between 0 and 3. Using this number, we'll pick the Boss. You might expect the Green Boss would be in position 0, but it's actually in position 1. This is because 10 / 10 = 1 and the modulo is also 1. Therefore, the first boss is in position 1, while the final boss is in position 0. With the boss having been setup, we're then incrementing Stage's waveNum.
Our initBoss call is made in the `logic` function in stage.c:
We've snipped some of the function here, as it's rather long. The important part is what happens when there are no aliens left in the stage (hasAliens). We're testing if the Stage's waveNum is divisable by 10 and, if so, we're calling initBoss. Otherwise, we're moving on the next wave as usual. This test for the hasAliens flag was the reason for the Boss army screenshot before, as you can see here that if the flag wasn't set, initBoss would be call constantly (the increment of waveNum was also missing).
The last thing we'll look at is the boss health bar. This is done in hud.c. We've done a bit of refactoring to this function, which we can see by looking at drawHud:
We've moved the original score drawing code into a new function called drawScoreBar, and now we're testing to see whether a boss exists by checking Stage's boss field. If so, we're calling a new function called drawBossBar.
For this, we're extracting the Boss data from Stage's `boss` field. We're then working out the percentage of health the boss has left, by dividing the entity's `health` by the Boss's maxHealth. Both fields are ints, so we first multiply the boss health by 1.0, allowing us to work with decimals, and assigning the result of this to `w`. This will mean that `w` has a value of between 0.0 and 1.0 (a normalized percentage). Next, we're multiplying `w` by BOSS_BAR_LENGTH (defined in hud.h as SCREEN_WIDTH - 140). Ultimatley, this means that `w` will be larger while the boss has more health. We're then drawing the "Boss" text, and two rectangles, using drawRect and drawOutlineRect. drawRect is a red bar, with a width of `w`, while drawOutlineRect is a white outline with a length of BOSS_BAR_LENGTH. What we're left with is a red bar that shrinks in length as the boss losses health, contained in a white bar that shows the maximum. Both bars are drawn in the same location, with the outline containing the red bar.
That's our bosses done! What we need now is more power-ups, in order to battle them. In our next part, we'll update the power-up pod so that it rotates between available power-ups, including offering a shield and a mobile drone. We'll also offer the chance to increase the speed of our fighter and the fire rate of our guns. Mind, we need to be careful that our sidearms and shields aren't destroyed by enemy fire ...
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):