• 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 —
It's time to introduce our objectives system into our game. Each of our missions will feature one of more objectives that need to be completed, and so we need a system to track them. Our objectives system will be simple to understand and implement. No major complexity will be found here!
Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter3-12 to run the code. You will see a window open like the one above. Use the WASD control scheme to move the fighter around. Hold J to fire your main guns. Play the game as normal. The goal is to destroy 10 enemies fighters and collect 150 catnip (total value) that is dropped by them. Take note of the objective update messages. When all objectives are met, the Mission Complete message will be displayed in green (the game will continue playing, however). Once you're finished, close the window to exit.
Inspecting the code
Adding in our objectives isn't a difficult task, at all, and the system we're going to build will come with a lot of flexability. At the end of the day, our objectives system is just a list of ids and values. Let's get into the code.
We'll start with defs.h, where we've added a new enum:
This enum will hold the state of the current mission. MS is short for Mission Status. MS_INCOMPLETE will mean our mission is in progress, while MS_COMPLETE will mean we've completed all our objectives.
Now on to structs.h, where we've made some additions and updates. Starting with the Objective struct itself:
As expected, this struct will hold all the data for an objective in our game. `description` is the description of the objective, targetName is the name of the target item that is tied to this objective, currentValue is the progress of this objective, while targetValue is the goal value.
We've also updated Stage:
We've added in two new fields: `status`, and objectiveHead and objectiveTail, the linked list that will hold our objectives.
With those all setup, we can now look at the new compilation unit that will handle our objectives logic. objectives.c features a handful of functions right now, but will fill out in future. We'll start with initObjectives:
We're doing a number of things here. First, we're setting up our objectives linked list in Stage. Next, we're setting a variable called objectivesTimer to FPS. The purpose of objectivesTimer is to test whether all our objectives have been completed, and our mission is over. The reason for this is because we won't always be able to check that all the objectives are done when updating one. We'll see this in a future part. This is basically a pre-emptive measure.
Next, we're setting up some objectives. Note that this is only temporary; our objectives will come from our mission files in future. We're creating two objectives here. The first requires us to defeat 10 enemies. Note how the targetName of the objective is "ENEMY" and the targetValue is 10. Next, we're setting up another objective to collect 150 catnip. Again, note how the targetName is "catnip" and the targetValue is 150.
Now, let's checkout the doObjectives function:
This function is responsible for checking if all our objectives have been completed. We're first checking the status of the mission (Stage's `status`). If `status` is MS_INCOMPLETE, we'll reduce the value of objectiveTimer, and begin checking our objectives if it hits 0 or less. We'll then set a flag called allComplete to 1, to assume all our objectives are complete. Next, we'll loop through all the objectives and check if any are below their targetValue. If so, we'll set allComplete to 0. With that done, we'll finally check the value of allComplete. If it's 1, we'll display a HUD message saying the mission is complete, and then set Stage's `status` to MS_COMPLETE.
The doObjectives function will only run once a second. It adds a nice little pause to the proceeding when we finish a mission, and there's a slight delay between completing the last objective and seeing the "Mission Complete!" message (yes, this is basically artistic license).
Now let's look at updateObjective. This function is the tent pole of our system:
This function takes the targetName of the objective and a value as its arguments. We start by looping through all our objectives, searching for an objective with a matching targetName. It is also expected that the currentValue is below the targetValue (it's not yet complete). With one found, we'll keep a track of the currentValue before updating (assigned to oldVal), and then add `value` to the objective's currentValue (ensuring it doesn't exceed targetValue). Next, we test if our currentValue is now at our targetValue, and if so we'll display a HUD message to say that the objective is complete. We'll also reset objectivesTimer to FPS (again, artistic license..!).
If we've not hit our targetValue, we might output a progress message instead. We take both the previous value (oldVal) and the current value of the objective (currentValue), divide them by the target value, and then multiply them by 10, giving us a value between 0 and 10 each. We then check if the new value (newVal) is higher than the old value (oldVal) and display a HUD message to show our progress. The calculation means we get to see the objective progressing every 10%, meaning we don't clutter the HUD with messages for every single progress update. An objective with a large targetValue would create a flood.
And that's it for objectives.c! As you can see, our objectives are merely key-value pairs, with goals. Now, let's look at how we integrate with the rest of the code.
Starting with collectables.c:
We've added a call to updateObjective in our switch statement, when we pickup a CT_CATNIP collectable. We're passing over "catnip", plus the value of the collectable itself. Thus, we get to push up the value of our catnip objectives. Naturally, if there was no objective with a targetName of "catnip" nothing would happen here.
We've also updated greebleLightFighter.c with a similar call:
We're calling updateObjective here, passing over "ENEMY" and 1. We're also passing over "greebleLightFighter" and 1. While our objective's targetName is "ENEMY", we could actually become more specific with our targets. Later, we'll introduce more enemies, who will also trigger the "ENEMY" objective update. But what if we wanted to update an objective for defeating a particular enemy type, and not just any enemy? This is what this code would be. Again, there is no objective for "greebleLightFighter" in this example, so this call will do nothing.
Let's now look at the updates to hud.c. You will have noticed that our objective number is displayed at the top of the screen. Starting with drawHUD:
We're calling a new function drawObjectives:
This function basically loops through all our objectives, counting how many there are (`total`) and how many are complete (`completed`). We'll then draw that text centered at the top of the screen.
Only one thing left to do now, and that's to update stage.c. We start with initStage:
We're setting Stage's `status` to MS_INCOMPLETE, and also calling initObjectives. Finally, we update doStage:
Here, we're calling the doObjectives function.
Finished! A simple, yet highly capable, objectives system. As you can see, it's quite easy to implement other objectives and triggers. If we wanted a objective for picking up ammo, for example, we just create an objective with that target name, and add the updateObjectives call to our collectables code. We can also add in other types of objectives, such as ones that will cause our mission to fail if they are met. We'll actually see this later on.
What we need now are screens to display our objectives and their progress, so that the player can see at a glance what needs to be done. So, in the next part, we'll look into implementing these.
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: