• 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 —
In our next mission, Leo has been charged with rescuing some fellow resistance fighters from Greeble POW ships. The player will need to chase down the POW ships (that will run away from them), destroy them to release the POWs themselves, and then collect the POW pods. In all, the player will need to break open at least 5 POW ships to finish the mission (and also not lose any POWs in the process).
Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter3-27 to run the code. You will see a window open displaying the intermission's planets screen. Select Pea, then enter the comms section to start the mission. Play the game as normal, chasing down the POW ships, destroying them, and collecting the POW pods that are dropped. Failure to collect any POW pods will result in the mission being failed. You may repeat the mission as often as you like. Remember that you can change the fighter's damage, weapons, output, and health by editing game.c, if you wish to get ahead of things early. Once you're finished, close the window to exit.
Inspecting the code
When we first began this tutorial series, we implemented the player's main gun in such a way that the bullets couldn't harm anything that wasn't on screen. As you may now be able to see, this was a good move, as it prevents us from shattering the POW ships and spilling the contents when we're unaware of it happening. This could cause us to constantly fail the mission simply due to stray fire!
We've made a number of updates here to support our new AI type and objective processing, but it's no major undertaking, as we'll see. So, let's jump into the code.
Starting with defs.h:
We've added in a new AI_ enum. AI_EVADE will specify that our AI will use the evade strategy, to keep away from enemies.
Next up, we've added a new CT_ enum:
CT_POW will identify our POWs amongst the collectables. Like its siblings, the enum will be used to handle how we react to the player picking it up (or not picking it up, as we'll see later!).
Finally, we've added a new objective type:
OT_CONDITIONAL will be used to describe an objective where the target value must NOT be met. If we do so, the mission will fail.
Now for our new enemy type. greeblePOWShip.c contains all the functions for handling this new enemy, and will be very familiar to you. Starting with initGreeblePOWShip:
Again, very standard. Of note here is that the weaponType is set to AI_WPN_NONE, as our POW ships don't have any weapons. The AI `type` is to AI_EVADE.
The `tick` function follows:
Once more, the only major difference between this craft and its siblings is the engine position (call to the addEngineEffect function). Once more, we see a case for refactoring, to have a common tick function.
As with the bombers, we're going to skip in the `draw` and `destroy` functions, as they are identical to the other fighters, and finish with the `die` function:
Here, we're calling a new function named dropPOWs. This, as we will see, will spawn between 2 and 5 POW collectables that the player must pick up, to avoid failing the mission. Our POW craft don't drop catnip, ammo, or health..!
Now, over to collectables.c, where we've made a bunch of updates to support our new POW collectable. Starting with initCollectables:
We're loading the texture used for the POW collectable (powTexture). Next, we've updated doCollectables:
Here, we've added in all the major logic for handling our new POW collectables. First, after reducing the collectable's `health`, we're testing if it's 0 or less. If so, and it's `type` is CT_POW, we're going to call updateObjective, passing over "powLost". Our mission has a conditional objective tied to the target name "powLost", meaning that if this collectable's `health` hits 0 before we have a chance to collect it, we'll fail the mission..!
For the remainder of the function, we're testing for having collected the POW, and are adding a HUD message, and playing a sound. As you can see, it's quite easy to add in new collectables, and tie them to our objectives system. It's extremely flexible!
Lastly, we've added in a new function called dropPOWs:
This function is merely responsible for creating a bunch of collectables, of CT_POW `type`, and using the powTexture. Our POWs will existing for between 7 and 8 seconds.
Now, let's turn to ai.c, where we've added in the new functions to handle our evade AI type. Starting with doFighterAI:
We've added in the AI_EVADE case to our ai `type` switch statement, to call doEvadeAI. This a new function that we'll detail below:
This function is pretty simple, despite how it might appear. To begin with, we're setting the fighter's `dx` and `dy` to 0, to bring them to the stop, and then setting a flag called moveToAlly to 1. This, as we'll see shortly, will be used to control whether our AI should seek out a buddy to move towards. We next test if our AI has a target. If so, and the target is less than a screen's width away, we'll perform the calculations to move the fighter away from the target. We'll then update the moveToAlly flag to 0, so it doesn't attempt to move towards a buddy with enemies nearby. Next, we'll test the value of moveToAlly. If it's set, we'll call a new function named findNearestAlly. If we find one, we'll move towards them. With all that done, we'll set our speeds (`dx` and `dy`), `facing`, and update our thinkTime.
So, in summary - if near an enemy, run away from them. If not, move towards a buddy (safety in numbers!). Due to the nature of our game, you might not see this buddy thing happening too often.
The findNearestAlly function follows:
Very much like the code we use for finding an enemy target (in fact, it's basically the inverse). We're simply looking through our list of entities, to find an entity that is on the same side as us, take note of the closest, and return it (we're assuming that all our entities are fighters).
That's our AI sorted out. Now for another important update - the changes to our objectives. Of course, it's possible now to fail the mission by scoring points in an OT_CONDITIONAL type. Let's turn to objectives.c, and see how this is done.
First up, we have changes to doObjectives:
First, when looping through all our objectives to test how many are not yet complete (numIncomplete) we're ignoring any of `type` OT_CONDITIONAL. If we were to include these in our incomplete count, we'd never be able to finish the mission. Remember: we want to avoid "completing" these objectives..!
Next up, when testing the allComplete flag, we're looping through all our objectives and setting their currentValue to their targetValue. This is just so that when we finish the mission, things don't look weird on our objectives list summary. Seeing the conditional listed as incomplete is basically incorrect.
Now for the changes to updateObjective:
We've updated the response to our objective's currentValue meeting its targetValue. We're now testing if the `type` is OT_NORMAL, and handling it in the standard way if so. If not, we're testing if this is a conditional objective. If so, we're going to inform the player that they have failed the objective, via a HUD message. We'll set Stage's `status` to MS_FAILED, execute a script function named "MISSION_FAILED", stop the music, and play our ominous failure sound effect, as we do when the player is killed..! Yep, we're not wasting any time here - if we "complete" an OT_CONDITIONAL we'll end the game swiftly!
That's almost it. One more change we're going to make is to stage.c, where we display our objectives list via drawObjectiveList:
As we've seen before, this function draws all our objectives to the screen. We're now testing if an objective is of `type` OT_CONDITIONAL and if the mission has been failed, and rendering that objective in red, rather than green. This will further emphasise to the player how and when they failed the mission, so they will know for next time.
Another mission done! And at this point we've more or less added in all the gameplay features that we need. We have our core AI behaviours, and we're able to process three different types of objectives.
We have one more main mission to add now, and it's of a type that every gamer enjoys - protecting an ally from harm! The SS Goodboy is in danger, and needs protecting while her crew fixes the engines.
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: