DEV Community

Cover image for SimpleQuest - Dev Log - Entry #1
Greg Bussell
Greg Bussell

Posted on

SimpleQuest - Dev Log - Entry #1

GitHub Repo | SimpleQuest


"Just rip the guts out, tape it back up, and ship it already."

With an hour to go before the final deadline and our mission manager in pieces on the table in front of me, there was really no other option. It simply can’t blow up the game, not this close to the end of the jam. We have to ship something. So I did the only thing left: I tore out the interfaces, left the soft references bleeding on the floor, and bound every single class together so tight that any sailor would be impressed.

We made it. Just under the wire! USS Proteus would embark, complete with the friendly voice of mission control to guide you on your fantastic voyage through the anatomical wonders within the human body, step by step. Our week-long sprint ended with a shipped game jam entry that landed roughly in the same ballpark as our shared vision, a success by any measure.

And yet. I couldn’t help but look at the dead code in the mission manager, like a heap of parts - bits and pieces of some motor that I’d failed to assemble correctly and had to replace with a hand crank. Those comment blocks shamed me in clear green letters. The gall of it. So about twenty minutes after the deadline, I went back to work.

About two months before the game jam, I’d started thinking about the idea of goal management and sequencing. Of objectives. Of progressive chains of events that depend on each other’s outcomes. What does it mean to be a mission, in the object-oriented programming sense?

I’d settled on the idea that a mission - or a quest as I’d come to call it - is really a series of smaller discrete steps. Go here, pick that up, go over there, give it to that NPC, fight some bad guys, profit. Rinse and repeat. Sounds good as a starting point. We can encapsulate the idea of a quest in some class that owns an array of structs. But maybe a mission doesn’t have to be completed in a particular order. Maybe some missions have several steps you can do in any order or not at all. Maybe making a certain choice about how to progress should affect your ability to progress along certain storylines.

And maybe you won’t know the answers to any of that before you get your hands dirty and start trying to put it all together, both narratively and mechanically.

That led to an idea:

A generic system of goal management that allows the designer flexibility to implement linear or branching progressions with equal ease.

Seems attractive in principle, but how do you build such a thing? Well I suppose we ought to think about all those ‘maybes.’

To cover all cases, it should be possible for several quest steps to be active simultaneously. They should each be able to activate any number of next steps on completion. On top of that, quest steps should be able to gate activation based on the completion status of any number of other quest steps. We could even store all that per step. Each step itself could know what to activate next, and it could know what prerequisites need to be completed before it can start. If we were to chain together steps that each know where to go next and under what conditions to activate, then we could create complex branching and converging movements through a chain of quest steps where none of them really knows anything about each other. And that sounds like the kind of good old fashioned separation of concerns that might turn a conscientious gameplay programmer’s head.

Well it turns out that’s such a good idea that it’s called a directed graph and the concept is older than I am.

“Hey, ma, look! I call it ‘the wheel.’”

To be honest, that’s likely to be the throughline of this dev log: discovering the architecture based on need. I’m not a trained programmer. I got my degree in theater and worked as an actor and a writer for a number of years. Now I’m a customer service manager by day and a game dev by night. Guess I’m just a nerd who’s hooked on telling stories.

The directed graph architecture moved slowly at first (oh, if only I’d known to Google that term, the hours I could have saved!). In its first iteration, each quest was still a big array of structs, but the steps keyed one another by index, meaning they each held sets of integers that directly referenced each other’s indices in the quest step array, one forward looking (NextSteps) and one backward looking (PrerequisiteSteps). It was functional, but only just. ‘Clunky’ was a generous description. And all the more sophisticated parts of it - the soft references, delegate management, the communication interface - were on the cutting room floor.

Yet the essence was there. Nodes on a graph, even if you couldn’t really see it, and a game instance subsystem working as a manager that extracted the data from the graph and broadcast it to the rest of the game as needed.

I tinkered with it here and there where time would allow. I fixed the loading order issues that were behind the pre-deadline failures within a day or two. Part of the solution would form the basis of the late state-registration feature that now serves dynamic actor spawning and level streaming and allows an interested observer to receive the current quest state at registration time, whatever the current progress. I cleaned up the relationship between the quest manager and quests themselves, defining a policy for loading and unloading of quests and establishing a properly inverted dependency. I developed a series of actor components that wrapped the quest event interface and provided default plug-and-play functionality for quest targets, quest givers, and quest watchers - actors serving different roles in the context of the system.

In the midst of that, it found a home in another game jam project, this one a cooperative online multiplayer puzzle game called Rad Crew about cleaning up an irradiated industrial disaster zone. Bit on the nose now that I look back at it.

Together, the game jam projects validated that the system works in unrelated gameplay contexts and clarified many of the remaining questions regarding a multiplayer implementation. The functional directed graph architecture proved that the idea was sound, though clearly crying out for a cleaner user interface. Soon that would become the hallmark of the project. But not yet.

The most interesting point of the design to emerge from this period was, without a doubt, the evolution of the way in which tasks are tracked and where that logic came to reside.

In the original implementation, the logic for the completion of each step in a quest lived outside the system. For every task, some actor - the target actor itself, the player or player controller, the game mode perhaps - had to define what it meant for the task to be completed. The quest manager could tell an actor that some quest had been activated, but beyond that every quest required a bespoke solution somewhere in the project to track progress and call back into the quest manager when complete.

I feel you recoil in horror, and reader, I share your disgust for this obvious tangle of dependencies. It makes no sense that a door should know about the mission that advances when you open it. It’s bizarre to imagine a goblin diligently logging their death so the hero can be paid. And why should a treasure chest need to know the address of a mission manager? But alas, such is the tyranny of the hard deadline.

The targets must obviously all share the idea of being ‘triggered’ and own their definition for it. A door opens. A goblin dies. A chest is looted. But this isn’t the same thing as a specific task being completed. The task may depend on a number of other conditions. Perhaps the door must be opened during a certain window in the game’s day/night cycle. Maybe the player has to kill a certain number of goblins before the quest is completed. Maybe the chest has to be the third one looted and the first two, whichever they are in a room full of chests, don’t count. Where does this logic live? Should the door be aware of the system running the game’s day/night cycle? Do the goblins synchronize their states somehow? Are the chests all connected to some invisible manager singleton? What if the same item is involved in several different quests? Do we use the world’s worst switch statement?

“We’ll just move that logic to the manager,” I hear you say, “it’s already quest-aware!” And at first glance that seems logical. The quest manager subsystem knows how to start quests. It knows how to progress and end them and it coordinates the broadcasts to other game systems. We could extend it to actually perform filtering according to various conditions and increment the necessary counters. We could even give it a number of different policies it could swap out as needed based on some property set at the step level. We could allow it to read all of the properties on our steps and contextualize what they mean and process the data every time a target says it’s been triggered. And yes, we could do all that, committing to the maintenance required each time we want to add something new. Unfortunately, dear reader, that’s merely the world’s worst switch statement with extra steps.

“Well what about the quests? We can keep from building a monolith if we use the Quest objects.” Aha! Now we’re on to something. We move it to each individual quest object. Then we can define the logic for each individual step in the same class where its context lives. That seems a little better, but then remind me: where did we put the mission to kill 12 goblins and then drink a potion of strength? Quest B, right? No, that’s the one where you befriend 9 gremlins and eat a portion of stew. Really it’s still the same problem. We’ve just distributed it over several classes scattered across the solution and the content browser that now contain code geared toward specific game scenarios and numerous questionable dependencies. Possibly the worst of all worlds, we gain enormous organizational complexity along with the maintenance burden of all those unfortunate couplings: a bunch of tiny, decentralized monoliths. A graveyard perhaps where we can lay to rest any dreams we may have had of keeping our code organized and our system scalable.

However imperfect, the last instance allows us to make an interesting observation. There seems to be an awful lot in common between those two example missions. In both cases we interacted with a set number of a specific type of actor and then consumed a particular item. What if we extract what the two have in common into some ruleset? Then we could reuse it. We could pass in a target class or a set of actors. When something tells us they’ve been triggered - remember, each target can still define what it means to be triggered - we could check if that event was sent by the right type of thing. We could increment a counter if it was. When we hit some predetermined number of triggers, we could simply tell the quest manager about it.

And so the Quest Objective class was born. With the logic for the completion of a step extracted into its own UObject, a clear separation of concerns emerges:

  • Quest targets only care about the condition that triggers them, broadcasting their ‘Get Triggered’ event.
  • The Quest Manager Subsystem directs the loading, activation, and progression between Quests, keeping track of completion statuses as they’re finished. It receives the trigger event and passes it to all active Quest objects.
  • Quests act as a sub-manager for the steps, instantiating the chosen Quest Objective class for each step as it becomes active. Each active quest passes the trigger event to every active Quest Objective.
  • Steps themselves are merely UStructs that each hold a reference to a Quest Objective class along with numerous optional parameters that provide context to that ruleset.
  • Quest Objectives hold the logic that determines when to call Complete Objective, which signals completion back up the chain for broadcast to interested observers.

With the whole point of the plugin being the ease of setup for any set of linked goals, Blueprints is a natural fit as the place to define this logic, so Quest Objectives host a pair of Blueprint Native Events. Intended to be overridden in child classes, they provide one access point for the designer to define what happens when a step begins and another that fires every time a Quest Target is triggered. It’s this second event that allows elements to be filtered, counted, and handled as needed. Once the conditions needed to complete the mission objective have been satisfied, this event should call a function, Complete Objective, that signals completion up the chain and out into the game via the Quest Manager Subsystem.

Proud of this hard-won architecture, I brought the project to the attention of a community of other game devs I’m a part of. The Druid Mechanics Discord server has been a fantastic resource over my three years in game dev. Run by Stephen Ulibarri, a top-notch Unreal instructor, the courses offered emphasize SOLID principles throughout the data-driven design of a number of game systems. So I was duly excited to have the opportunity to show off what I’d worked out at one of the periodic, open-mic progress meetings.

I mean why shouldn’t I be? I’d come up with the idea of discrete encapsulated behaviors with their own activation conditions, targets, and completion logic that are managed by an external system and then wrapped in handy reusable blueprints for ease of access.

“They kind of remind me of GAS abilities,” Stephen said, meaning well, I’m sure.

“…”

Look, ma! This one’s called a ‘bicycle.’


Greg Bussell is the author of the SimpleQuest and SimpleCore plugins, a source-available work in progress. This dev log functions as both a semi-regular record of project development and as a retrospective account of the rationale and experiences behind the major design decisions.

Thanks for reading!

Top comments (0)