• 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
— Mission-based 2D shoot 'em up —
Every good shooter has a boss fight, and our one will be against a Greeble frigate known as The Gravlax. This is a formiddiable opponent, that the player must be well prepared for. Our boss will be composed of three parts: the main body, a fin, and a wing. All three parts can be targetted and have their own health pools. Destroying the fin and wing will prevent them from being able to attack, while destroying the main body will be required to complete the mission. The misson itself has but one objective - to destroy The Gravlax.
Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter3-29 to run the code. You will see a window open displaying the intermission's planets screen. Select Purr, then enter the comms section to start the mission. Play the game as normal. Once The Gravlax is defeated, the mission will be effectively over. Once again, The Gravlax is a very powerful opponent, so it is recommended that the player edit game.c to increase their fighter's damage, weapons, output, and health to stand a better chance (unless one wishes to play all the other mission to legtitmately earn catnip for upgrades). Once you're finished, close the window to exit.
Inspecting the code
At this point in development, The Gravlax boss mission means that we'll be focusing almost exclusively on the compilation unit we've created to handle it. We'll touch on other parts of the code as the mission itself requires, but otherwise there will be very few updates outside of the boss's code.
You might be wondering what's with the name of the frigate, and why Leo makes the comment about it sounding delicious. A funny story about that - I decided on the name of the boss due to an advertising campaign run by the telecoms company Three, in the UK.
Gravlax is a fish dish. Cats like fish. Therefore, the frigate sounds very tasty indeed. So, now you know!
With that out of the way, how about we look at the code additions and updates. Firstly, we've updated structs.h:
We've created a new struct here called Component. This will represent part of a Boss's body. In the case of the Gravlax, it will be the fin or the wing. Quite a number of fields here, but it looks lot like a fighter. `reload` is the rate of fire. `health` is its health. maxHealth is the maximum health. thinkTime is the think / action time. hitTimer is the hit timer. `offset` is a special one - these are coordinates that we'll use to determine where the component is, relative to the boss's main body. We'll see more on this later. destroyedTexture is the texture to use when the component is killed. `owner` is the boss itself.
Next up, we've got the Boss struct:
Again, this looks quite a lot like Fighter, but with some extras. `dx` and `dy` are the velocity. `reload` is the fire rate. `health` and maxHealth are the health values, while `shield` and maxShield are the shield values. thinkTime, hitTimer, and shieldHitTimer are the action times, hit times, and shield hit times. Yep, just like a Fighter. What's new is the `components` variable. This is an array that we'll allocate, containing the boss's components. Finally, numComponents is the count of these components.
Okay, so we've got our boss data structures all sorted out. Now for the boss implementation itself. We've created gravlax.c to handle all of its logic and rendering functions. There are quite a number of functions here, some with more obvious goings on than others. We'll try not to linger too long on things that should be obvious by now, or else everyone's going to get bored!
Starting with initGravlax:
Okay, a factory function. We're creating a Boss, and assigning it all the relevant details, such as the `health` and `shield`. For the entity (`e`) itself, we're assigning the `side`, `data` value, `texture`, and function pointers that we've seen before. We're then calling createComponents. This is where we'll be creating our fin and wing. We'll get to those in a bit. We're also setting Stage's missionTarget details to those of our boss, just like we did with the SS Goodboy, so we can track its status. Finally, we're loading our shieldHitTexture. Our fighters have this in a common shared function, whereas the Gravlax needs to handle all these itself.
Now for createComponents:
This is where we're creating the fin and wing of the Gravlax. We're setting the Boss's (`b`'s) numComponents to 2, and then mallocing the Boss's `components` to an array of Entity pointers of the same size. Next, we make the components themselves. Each one will have an Entity created for them, with a Component created and added to the Entity's `data` field. Each component has its attributes set (including `health`, `offset`, destroyedTexture, etc). The component's `owner` is set as the Boss entity (`e`).
So, just a basic factory / setup function. Onwards to the main `tick` function now:
This is the `tick` call that is made by the main body. It looks just like the fighterTick function from fighters.c! However, it's doing two things different - it's calling a function named huntPlayer and another called updateComponents.
Let's look at huntPlayer first:
This does as its name implies - it causes the boss to chase down the player. Essentially, this is the Boss's main AI. We first test that the player isn't dead, and then calculate the direction that we need to go to reach the player, via calcSlope. We'll randomly choose the speed we want to move at, and then update our thinkTime and `facing`. Notice how the boss chases down the player relentlessly; we're not going to have it sit around and just get shot..! Finally, we're calling doMainAttack.
The doMainAttack function is as follows:
Once again, this looks very much like the attack function we have in ai.c - we need to test that we're able to fire (`reload` == 0) and that we're facing the right way, before choosing which weapon we wish to use. The boss's main body will either fire dual shots, or unleash the devestating red beam! We'll adjust the `reload` times depending on which weapon is fired. Notice how when the Gravlax fires the red beam, it does so three times, to layer three of the beams on top of one another, so that the damage is much greater than a single shot.
Next up we have updateComponents:
This is simple function to understand. What we're doing here is looping through all our components, and positioning them around the Gravlax's main body (`e`), using their offsets. We're also taking the facing into account when positioning the components along the x axis, so that we can align them properly. Our components don't move on their own, so will never drift off anywhere. This function ensures they remain in place.
Now for componentTick:
This function is used by our components. As with the main body, it updates thinkTime, `reload`, and hitTimer, but it doesn't actually have any AI movement itself. That is left up to the main body. It does have its own attack functions, however. We first check that the component is still alive (`health` > 0), and then call doComponentAttack. Finally, we want the component to die if the main body is destroyed. We do this simply by assigning the component's `dead` flag to be the same as its owner's `dead` flag. So, if we happen to destroy the Gravlax's main body before the component, this will ensure that the component doesn't remain hanging around, attacking us..!
Now for doComponentAttack:
Yep, this looks as expected - we're testing if we're able to attack the player, and then we'll either fire a three way spread or launch a rocket. As with the Gravlax's main body, we'll be adjusting the `reload` times based on which weapon we fire.
That's all for our logic. In summary, our boss has its own AI and attacking routines, separate from the fighters. We want our boss to be aggressive, which is why it doesn't give up the chase on the player, and has some powerful weapons available. The player shouldn't be coming into this fight unprepared!
On to our rendering phase. Starting with `draw`:
This function is called by the boss's main body, and is delegating to a function called drawBossPart. We'll come to it in a moment. Notice that we're passing over the boss's hitTimer and shieldHitTimer values.
Next up, we have componentDraw:
As the name suggests, this is used by our components. As with `draw`, componentDraw is delegating to drawBossPart. Notice here that we're passing over the component's hitTimer, but also the boss's shieldHitTimer! The components themselves don't have shields, but will use the boss's, as we'll see later on.
Now for drawBossPart:
Very much like the routine for rendering a Fighter. We're adjusting the `x` and `y` according to the camera location, and then testing the value of hitTimer, to see if we want to tint the texture red or draw it normally. We then test the value of shieldHitTimer, and overlay the texture we're drawing with the shield texture. Again, much like how we do with our fighter rendering. We're doing this because the Gravlax's shield encompasses the entire ship, and not just the main body. The components can all be damaged individually, however, which is why they have separate hitTimers.
Our rendering is done! Time to consider the remaining functions. The Gravlax has its own takeDamage and componentTakeDamage functions, for handling the main body being damaged and a component being damaged. Let's look at takeDamage first:
No real surprises here - the boss's shield will absorb the damage if it can. Otherwise, we'll apply the damage to the boss's health. hit timers are updated as we do this. When the boss's health is reduced to 0 or less, we'll call updateObjective, passing over "gravlax", and throw out a load of explosions and debris. No goodies to be had from defeating the boss, however. The reward here is victory itself!
componentTakeDamage comes next:
Much like takeDamage, we'll damage the shield or the body. Notice that we're testing the boss's `shield`, not the component's (again, the component doesn't have a shield..!). When the component is destroyed, we're doing two special things. First, we're setting its `side` to SIDE_NONE, its takeDamage function to NULL, and also updating its `texture` to now use its destroyedTexture. Removing the function for taking damage means that our bullets will no longer strike it when we fire, and updating `side` to SIDE_NONE means that secondary weapons such as the homing missiles will no longer consider them to be targets. Firing homing missiles that zero in on dead targets would be annoying!
We have the `die` function next:
We're just setting the `dead` flag to 1, and bumping our enemiesDestroyed stat counter. Yep, as big and powerful as the Gravlax may be, that still only counts as one.
Lastly, let's look at the `destroy` function. Here we'll see why we're using a pointer to a function for our entities:
We're freeing the boss's `components` array (but not actually the entities), as well as its `data` itself. If we were to assume that an entity only has a `data` field set, the Gravlax boss would become a source of memory leaks in our game.
Finally, we have destroyComponent:
This function is free to just remove the data (a Component) we created.
That's it for gravlax.c! There was rather a lot of it, but as you can see, it was all the necessary functions packed into a single file. There is nothing that can be shared, unlike the fighters.
We have just a handful more things to add in, and this part is done. You'll notice that once we defeat the Gravlax, the remaining fighters run away from the player. This is handled by a script command called "RETREAT_ENEMIES". If we head over to script.c, we can see how this is being handled in doScript:
We've added a check for a command named "RETREAT_ENEMIES" and calling retreatEnemies when we encounter it. The retreatEnemies function is found in ai.c:
A very simple function - we're looping through all the entities in the stage, looking for any whose `side` is SIDE_GREEBLE. Once found, we're setting the Fighter's ai `type` to AI_EVADE. We're also setting totalEnemies to 0, so that no more enemies spawn in the stage, in case the player decides to start killing off those who are running away. A word of caution here - we're assuming that the entities are Fighters. This works in our game, since we know we certain they will be at the point we're calling this function. If this game were to be expanded, we'd want to give our entities a type, and test against that, along with the `side`. So, for example, we could set the `type` to ET_FIGHTER (1), and assign this to all our fighters. This will protect us from a bad cast, which could well occur in the logic above.
Before we finish, let's take a look at our final entity list, in entityFactory.c:
All our entities are registered with our factory, so that we can spawn these types during our mission data files.
And that's a wrap for our main game! We've only got a handful of things left to do, and we'll be completely finished. Our next step is to introduce a title screen, and then add in some finishing touches. Making a game such as this is no small task, but hopefully you can see it's not quite as difficult as you might've first expected. Once again, the ground work is there to expand the game out a lot. We could have new star systems that the player moves to, and our script allows us to make some varied and interesting missions.
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: