![]() | |
PC Games
• Orb Tutorials
• 2D shoot 'em up Latest Updates
SDL2 Quest game tutorial
SDL2 Versus game tutorial
Download keys for SDL2 tutorials on itch.io
The Legend of Edgar 1.37
SDL2 Santa game tutorial 🎅
Tags • android (3) • battle-for-the-solar-system (10) • blob-wars (10) • brexit (1) • code (6) • edgar (9) • games (44) • lasagne-monsters (1) • making-of (5) • match3 (1) • numberblocksonline (1) • orb (2) • site (1) • tanx (4) • three-guys (3) • three-guys-apocalypse (3) • tutorials (18) • water-closet (4) Books ![]() The Red Road For Joe Crosthwaite, surviving school was about to become more than just a case of passing his exams ... |
— Simple 2D quest game — Note: this tutorial assumes knowledge of C, as well as prior tutorials.
Introduction The final missing piece of our game is the ability to undertake quests. In this part, we're going to put together the ground work for supporting them. This will involve setting up a basic quest, that simply involves interacting with one Resident at the request of another (the quest is simply to wish another Resident a happy birthday). Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./quest13 to run the code. As before, you can move around with the WASD control scheme. Play the game as normal. Every now again, a Resident will request that you undertake a "quest" to wish another Resident a happy birthday. They will inform you of whom you need to meet, and where. The Quest Log can help you to locate the town where the Resident resides (so long as you've discovered it). In the town itself, the Resident will be swaying left and right on the screen, as a small indication that they are the one you need to talk to to complete the quest. When you're finished, close the window to exit. Inspecting the code This part is, again, a little involved, as there is a lot to add in; we have to add in the quests themselves, update the Resident logic, and also update the Quest Log. Before we get into the code, we'll first look at overview of how our quest system will work. If you've ever played a game like Skyrim or Fallout, you will be aware that the quests there can be seen to be composed of several steps - talk to person, go to location, find object, return to requester. Our quest system will follow a similar design, with a quest object that contains several steps. These steps will be defined as script commands, as we saw in SDL2 Shooter 3. An example of a fully complete script from this part might look like this: INTERACT 8 START_DEMO_QUEST PULSE_ENTITY 15 1 INTERACT 15 PULSE_ENTITY 15 0 MSG_BOX Daniel Larusso;Wait, it's my birthday today? I'd completely forgotten. COMPLETE_QUEST Translated, line by line, this script would mean: - Wait for the player to interact with entity #8. - Setup the "demo quest". - Make entity #15 start to pulse (they will sway back and forth, to draw our attention to them). - Wait for the player to interact with entity #15. - Make entity #15 stop pulsing. - Show a message box, with the speaker as "Daniel Larusso" and the text "Wait, it's my birthday today? I'd completely forgotten." - Mark the quest as complete. We can have as many script lines as we wish, with as many different commands as we want to support. As you can see already, there is huge scope for massive, complex quests, with plenty of options. We'll see more as we dive into the code. So, let's get started. First, we've updated structs.h:
We've added in QuestStep. This will represent a single command instruction (`line`) in our main quest object. The Quest struct itself follows:
Quite a few fields here. Let's go through them one by one. `title` and `description` are the title and description of the quest; `requester` is the Resident who has requested the quest; `status` is the current status of the quest, such as incomplete, in progress, complete; `island` is the target island of the quest, while `town` is the target town of the quest (either of these could be null, depending on our script). stepHead and stepTail is the linked list for our quest steps, while currentStep is a pointer to the current step. This will required for when the script execution is halted due to waiting on an interaction. A tweak to Resident follows:
We've added a pointer to a `quest`. This will be used during our interactions (`touch`), to test if a Resident has a quest for the player to undertake. Finally, we've updated Game:
We've added in a linked list (questHead, questTail) to support our quests. Now on to the main event. We've added a file called quests.c, where all the logic for handling our quests will live. There's quite a few functions in here to get through, though none are actually very complicated, at all. Starting with initQuests:
We're first setting up our quests linked list, then calling a function named collectTownsResidents. We'll see this later, but know that it is responsible for collecting all the towns and residents in our world into an array, so that we can randomly access them. Next, we have generateQuest:
This function generates a new quest, and assigns it to a Resident (`requester`). To begin with, the quest has two steps: INTERACT and START_DEMO_QUEST. This means that our quest will be activated (and randomly setup) when we first interact with the resident in a town. We ensure that the quest's currentStep points at the INTERACT step, and also increment Game's totalQuests. Next, we have doQuestInteraction:
This function is called whenever a Resident is interacted with (we'll see this later). `e` in this case is the Resident, but it could be any entity. We first create a string containing the expected INTERACT command, with the id of the entity passed into the function (meaning the result might look like INTERACT 18). We then loop through all our quests, to see if the current step matches the expected command. In other words, if a quest is currently blocked on the INTERACT step that matches the command, we will proceed to continue its execution. The matching quest will call executeQuestSteps, and we'll set `rtn` to 1 (to be returned from this function), to indicate that a quest execution occurred. With that done, let's look at what executeQuestSteps does:
Put simply, we're stepping through each quest step, and executing its command. We halt execution if we run out of quest steps or if we hit an INTERACT command. This should be quite clear (and it's also very much like SDL2 Shooter 3), so we'll move on to looking at what each of our commands do: First up, we have doStartDemoQuest:
Here, we're setting up our demo quest (`q`), which involves wishing another resident a happy birthday. We first make a call to getRandomResident, which will return a random Resident in the world (not just the current town). We then set the `title` and `description` of the quest, and also set the quest's `town` field as the target Resident's `town`. This will mean that when we view the quest information in the quest log, we know which town we need to head to, in order to complete the quest. Next, we set up a number of the quest steps that will be required for our script, make a call to doActivateQuest (we'll see this next), and finally display some dialog boxes, so that the requester can tell the player all about the task they wish them to undertake. As an adventurer, we can't refuse! Next up, we have doActivateQuest:
Simply put, this swaps the Quest's (`q`) status to in progress, adds a message to that effect on the HUD, and makes a call to reorderQuestList. As we'll see in a moment, that function pushes all the in progress quests to the top of the list, with complete, and unknown quests pushed further down. doCompleteQuest is quite similar:
The Quest's `status` is set to complete, and Game's completeQuests is incremented. doAddMessageBox comes next:
Nothing special here - we're tokenizing the command line (semi-colon delimited), and using the values to add a message box. doPulseEntity is similarly simple:
Here, we're setting the `pulse` flag of an entity. We look up the entity via getEntityById (this function will search the entire world!), and set `pulse` to the value in the command string. This value will either be 0 or 1, to true or false. That's all our commands handled. As you can see, they're all very simple, but can be combined any way we like, making for some interesting script behaviour. We'll now look at the remainder of the functions in quests.c, to see what they do. None of these will be especially challenging. First, addQuestStep:
This function merely creates a QuestStep, with the command (`line`) passed into the function, and adds it to a Quest (`q`). Next, we have getRandomResident:
This function returns a random Resident from our array of residents, that do not already have a quest associated with them; we don't want to be assigning Residents as quest targets if they already have a quest of their own, as this will conflict with our Resident's `touch` logic. The collectTownsResidents function comes next:
In other places in the code, we're relying on linked lists and counters to iterate through our residents and towns (see the town list in the Quest Log, for example). While we can get away with it there, in the case of quests, we need to be able to quickly access a random Resident or Town. Therefore, we're collecting all of them into arrays called `towns` and `residents` (with length variables called numTowns and numResidents). We can have a maximum of MAX_TOWNS and MAX_RESIDENTS (defined as 32 and 128, respectively). Next up we have reorderQuestList:
As stated previously, this function will reorder our quest list, so that in progress quests will be at the top of the list, completed quests next, and finally incomplete / unknown quests last. We do this by creating an array to hold all our quests, sort the array, and then set our linked list back up again. That's all there is for quests.c for now. We should now turn our attention to how the quests are activated and processed. In our game, this is an event driven system, which is triggered right now by interactions with Residents. So, if we head over to resident.c, we can see that we've updated the `touch` function:
Before, whenever we touched a Resident, we'd make a call to the `chat` function. Now, we're testing first if the Resident is associated with a Quest. We don't want our Resident to start yipping on about the weather when we have important tasks to complete! We therefore make a call to doQuestInteraction. If this returns false, it means that we didn't process a quest script. We then check if the resident has a quest associated with them. If so, we'll prompt the player about the quest progress itself (such as telling them they've yet to finsih it, or thanking them for undertaking it). Otherwise, we'll invoke the `chat` function as normal. What this means is that if a Resident has a quest for the player, they will talk to the player about it, even when it's complete. They won't launch into their normal greetings. If we wanted them to return to their normal level of conversation, we could null their quest pointer in the QS_COMPLETE case.
With that done, we can move over to the Quest Log, where we've made some updates to support our quests. Before, we only have our Islands and Towns sections setup. Now, we're going to extend this functionality to Quests. Starting with `logic`:
Now, if we're on the Quest section, we're calling a new function named doQuests:
Much like the Islands and Towns section handling, we're determining how many items to display in our list, and which item is currently selected. On top of that, we're checking if the selected quest (selectedQuest) is currently in progress. If so, we're going to check if it has a town assigned to it, and also if the town has been discovered, and setting the quest logs highlightedTown to that. Similarly, we're checking if the quest's island has been discovered, and setting highlightedIsland to it. This means that as we scroll through our quest list, if the quest has a town or island assigned to it, these will be highlighted on the map. This makes undertaking our quests much easier, as we know where we need to go to fulfil them. The `draw` function has next been updated:
When in the Quests section, we're calling drawQuests:
This function is very much like drawTowns and drawIslands, except we're printing a lot more detail about the Quest. If the quest isn't known to us, we're printing a series of question marks. The colour of the text will also change, depending on the quest status (unknown will be grey, incomplete a light grey, and complete a pale green).
With our Quest Log updated, we're more or less done. We just need to look at how we're actually generating the quests. This is done in overworldGen.c, after we've finished creating our Residents, Towns, and other entities.
In generateOverworldWorker, we're calling addQuests:
The addQuests function basically loops through all the Residents in our game (via the Towns on the overworld), and will randomly create a quest for them, based on a 1 in 10 chance. If you feel you want more quests, simply reduce 10 to smaller value. Be careful not to drop this value too low (3 should probably be the limit), or you could cause the game to lock up when it comes to searching for Residents that already don't have quests associated with them when setting up the birthday greetings. Finally, a quick look at getEntityById, in entities.c:
As stated back when we first introduced entities, each one will have a unique id in the game. In order to find an entity, we need to loop through all the entities in the overworld, as well as the towns, to locate one with a matching `id`. Something we could do here is to create an array of pointers to our entities, for a faster lookup. This is the sort of thing that could also apply to towns, quests, etc. As noted in SDL2 Versus, linked lists and array both have their advantages and disadvantages. In this case, I opted for linked lists as my primary data holder. At your option, you may find you would preferred to use arrays through the game instead (but be aware of fitfalls associated with re-ordering arrays that involve pointers). Our basic Quest structure is complete! We can generate quests, assign them to residents, and perform all the tasks necessary to complete them. Ultimately, we will implement three different quest types - a delivery quest, a fetch quest (everyone's favourite!), and a dispose quest. Over the next three parts, we'll add these in, and also introduce the inventory, since it will make for a good addition to see what is being carried by the player. Purchase The source code for all parts of this tutorial (including assets) is available for purchase, as part of the SDL2 tutorials bundle: From itch.io |