How can I make my C++ quest system more lightweight and contain less references outside of itself?

Hello y’all,

TLDR: In UE, how can I make my current quest system more lightweight and discard the current heavy references and includes I currently use with the delegate approach?

Problem: I have made a functioning and modular Quest\task system built in C++. It works perfectly and accounts for any possible objective that can be given. However, some of the objectives have includes of the PLAYER CONTROLLER!! They have these includes because they use the ue delegate system to listen for objective updates (player added item to inventory, player built specific structure, player picked up currency, etc.) Everything works but I would like this system to work without the references to player within the objectives as I want to make the system as lightweight as possible. In UE, how can I make this system more efficient and how can I handle updating objectives and sub objectives in potentially hundreds of quests without referencing the player?

Quests - A container of linked objectives.
Objective - Usually simple logic that waits for a specific event to fire and then checks if a certain requirement has been met. (ex: Has collected 200 gold, has traveled to HyperSweden)
Event - This is less defined. Sometimes an objective is just bound to a trigger box. Other times, I use the ue delegate system on the quest and listen for things the player does.

The quest system is very simple. It is a quest actor, AQuestBase, with an arbitrary number of objectives, UObjectiveBase, than can have and can implement any arbitrary completion structure (linear objectives, multiple objectives, timed objectives). An actor component on the controller simply holds onto a map of incomplete quests and complete quests and handles all the logic around them.

  • My first thought to fix this heaviness was on each event (pick up money, go to location, build a structure,etc) to iterate over each quest and get each active objective and see if we have satisfied it’s requirements. However, this would scale very poorly and would be ridiculous when given a hundred quests and many hundred more objectives.

  • Another thought was to have different arrays of objectives the player has that would be searched through on each event. However, this would mean dozens of unique arrays and would be difficult to manage.

  • Another would be to leverage a world event system plugin I bought from the marketplace but this would require weird roundabout connections and would obfuscate bugfixing as the event system only works with actors.

Is there some special hidden secret ultra rare functionality I have overlooked in UE? It’s a big engine so I wouldn’t be surprised if I overlooked a simple solution.

I would like the objectives to function in some generic way so that it can work equally with my currency system, building system, and state machine system. At the moment I have only been to do this with hard references to the player using the delegate system.

Edit 1: The global event system (observer pattern) option may be a good approach. Have the player simply broadcast to the event with any payload. The quest actor (that is listening for that event) then takes the payload and sends that to it’s objective [Ex: EnterStateObjective->CheckEnteredState(StateEnumPassedByEventObject)]. We still have a reference to an outside object. However, this is a relatively lightweight object (especially compared to the monstrosity that is the player controller and its components.) This also would require a little more code and a partial change to the way the objective class functions. ~also the plugin occasionally causes crashes :upside_down_face:~

1 Like

I don’t see a reason why any of this logic would be written in the controller or character since we have ActorComponents, UObjects and other more fitting classes.

Use an ActorComponent for a character or UObject for generic use which tracks GUIDs of active quests. These GUIDs must be present in a Datatable which is made from a “quest” struct, which holds the GUID and quest completion requirements.

Assuming you went for the ActorComponent, The ActorComponent reads through the quest datatable and matches its current state against the completion state of the tracked quests in the datatable. A completion state could be a line of info like “DialogFinished_BlackSmith_FirstGreeting” or info like having an amount of a certain item, which should be tracked in an inventory system. The ActorComponent can perform this check any time it is relevant (observer pattern), such as when the inventory changes or a dialog finishes.

There is nothing wrong with doing so for hundreds of quests unless you (for some reason) run into memory or fps issues, which is just unlikely if you use datatables for most things.

2 Likes

Thanks for the reply!

The logic is actually taking place within the objective object but is just “listening” for the player controller to broadcast something like “Hey, I just received X item” Or, “Hey, I just entered BlackSmithFirstDialogue”. The only thing the controller (or it’s components) actually does is broadcast. Sorry if that was confusing.

I find it hard to determine event relevancy to the objective using the data table (or data assets in our case) method. While I don’t foresee ever running into memory or fps issues with iterating over a large number of quests, I still would prefer to only check quests or objectives that are interested in the current event like the location the player has traveled to or the NPC’s the player has spoken to. Apologies if this is what you meant.

!!! Perhaps when the quest is added to the player’s quest component it registers it somehow to the relevant function PlayerEnteredNewLocation(), PlayerItemPickup(), etc.
So the actor component in charge of the quests could listen for relevant events, and when those events fire it only looks at the relevant, active quests and sees if any quest requirement has been satisfied?

Reason why I would not involve the character and controller in this is that it is unnecessary while using an ActorComponent instead makes the system portable.

You could store the row names of active quests from the datatable, or use a GUID per entry and scan all rows for a matching GUID which is more reliable. It really does not matter for performance. For a quest / objective to “be interested in the current event” they’d have to be UObjects in order to do so, which is heavier on performance and its complexity is not required.

Yes but instead of processing these events per quest object you could simply process it once on the ActorComponent, which is what I meant by looking up entries from the datatable from within the ActorComponent. It’s much simpler.

Logic:

QuestManager (ActorComponent):
- Holds a list of active quest GUIDs from the datatable.
- Binds to relevant events:
    - DialogComponent::OnDialogFinished  >  QuestManager::UpdateQuestStatus
    - InventoryComponent::OnInventoryChanged  >  QuestManager::UpdateQuestStatus

UpdateQuestStatus() {
  // 1. Loop through datatable rows, every row holds a quest struct.
  // 2. If GUID on the quest struct is stored as atctive quest on the ActorComponent, proceed.
  // 3. Match quest completion requirements from struct against current status (inventory items / completed dialogs etc.)
  // 4. If completion requirement is met, remove GUID from active quest and store the GUID as completed quest.

}
1 Like

Thanks for all the help! :grinning:

I appreciate your walking through this.

If you don’t mind - What benefit does utilizing and iterating over an entire data table and checking our status (UpdateQuestStatus) with each event (OnDialogFinished, OnInventoryChanged,etc) have over using more focused functions that take in or reference specific types (structs, data assets , inventory objects, etc) and compare them against our active quests or objectives of specific requirements? This assumes that we have left out any reference or reliance on the player character/controller.

We never reference a table or the asset manager and instead get each currently subscribed quest (or GUID list if only using a table) and see if each quest has been progressed by the passed in type (enum, object, string, etc).

EX:

DialogComponent: broadcasts end dialog with payload (can be GUID in table of dialogs or anything)

QuestManager has: active quest objects and binds to other components

UpdateQuestStatus_Dialog(DialogStruct DialogReceived)
{
  // Loop over each quest with active dialog objective
  // if DialogReceived satisfies this objectives logic requirements then
  // mark objective as complete and get next objective, 
  // if no further objectives then add this quest to completed quests and fire     
QuestCompleted.Broadcast(CompletedQuestGUID);
}

Really the only difference is that this approach iterates over the 1-10 active quests with an active dialog objective instead of looking at any table or asset manager. This could have any function - UpdateQuestStatus_StateMachineChange(FStateEvent RelevantStates[] ), UpdateQuestStatus_ItemPickup(Item ItemPickedUp), etc.

Sorry, for the questions, your approach looks great! :+1: I just want to understand the rationale behind going through a data table instead of just querying our current quest objects besides heaviness and their logic.

Hopefully, this can somehow help anyone else refining their quest system in the future. I have since removed most of the references to the player in the objectives/quests that had them.

Edit1: Is the implication that all objective logic only exist on the Quest manager component?
There may be 10 different ways of handling dialog, items, buildings, etc and imagining a struct that contains all that relevant data seems staggering. Like, for instance, an objective that requires you to talk to a certain NPC at a certain time at a certain health with a specific item while in combat (ridiculous, I know, but maybe it is a custom made objective that would actually need to iterate over each other actor component) would be very easy to check in one go as a block of logic in the objective class but really throws my brain when trying to figure out how to represent that requirement as pure data in a struct or data asset without ballooning the struct and substructs to comical sizes.

I’m assuming the problem you’re trying to get a grip on, is that you have to #include a bunch of headers in a bunch of places, and you’d like to reduce that, keep it to a minimum. This is known as “reducing physical coupling.”

To include fewer headers everywhere, use the more basic classes in most places, and pass arguments to functions rather than keep state around.
AActor is available everywhere. APlayerController is also pretty common. ASceneComponent, too (or AActorComponent.)

You could build the interface between player and quest system using an AQuestPlayerComponent, which would be added to the player controller, probably in a blueprint. If it’s in blueprint, you actually don’t need to do anything at all in the C++ bit. The behavior of the quest component would still be in C++, it would just be wired up in blueprint. This cuts down on #includes a lot.

However, you still have the problem that “putting gold coins in inventory should be a listenable event for quest progression.”
It turns out, this is also a problem for tutorials, and for things like “add sound effects to these events,” and for a few other reactive interactions. It’s just a known place where things get coupled.

There are three main ways to go about resolving that.
The first is to build a “INotifier” interface that everything needs to implement, and that has a method per “thing” that could happen. Ideally, the interface itself just uses strings, vectors, integers, floats, and actor references, so the INotifier interface doesn’t in turn need to include any other classes. The player, and any interactive object, would then have to look for a INotifier interface on whatever is being interacted with, and call it.
It’s ugly to have an interface with “OnReceivedGold()” right next to “OnReceivedDamage” right next to “OnLeveledUp()” right next to “OnEquippedItem(),” and so on for 50 different things. But it actually reduces physical coupling.

The second way to do it is to have each thing that happens, generate a generic (-ish) event, and then bind these events to generic (-ish) receivers. This gets rid of the “OnEverything()” interface, but then instead the top-level code has to know about both the event source, and the event receiver, so the coupling moves from the names-of-functions-in-interfaces, to the wiring loom that hooks everything up at level load or begin play or whatever.

The third way is to have a global, or perhaps per-player, event bus. Define events that are of the form class UPlayerEvent : public UObject { FString EventType; UObject *Info; AActor *Actor; AActor *Actee; };
Then, whenever anything happens, an event of this class is posted on the bus, and there’s a generic mechanism to register yourself as a receiver of events, either all events, or just events of a particular EventType value.
Now, there’s a very narrow interface of physical coupling – just the UPlayerEvent declaration, and whatever the multicast delegate is for OnEventBusEvent. When you make a new thing that needs to send or receive these events, pass in the appropriate event bus as an argument. That’s the extent of the coupling!

The real drawback of the event bus system, is that you now don’t get type checking. If you send an event named ReceiveGold but register to receive events named GoldReceived, you won’t have the compiler tell you about the mis-match, you just won’t receive the event in question, and some quest will have silently broken.

Event busses are super popular, but I’ve been burned by the un-typed event IDs often enough that I try to avoid them. The wiring loom of generic multicast delegates bound to generic event receivers, know about by some top-level hooking-upping code, ends up being somewhat of a chore, but can work well, and is the easiest to extend – add another delegate somewhere, nothing else needs to know about it unless it wants to use it. The “everything” notification interface, is the most concrete, and gives you good compiler error messages, and as long as you can keep the notification functions from using any too specialized data structures for arguments, it de-couples pretty well. Adding a new kind of notification does mean everyone who implements it needs a re-compile, though.

2 Likes

Thanks for the reply, jwatte!

You’re right on about trying to reduce coupling.

It’s interesting you mention this being a problem for tutorials (assuming you meant actual in game tutorials and not something like YouTube or blog tutorials :sweat_smile:) as that is something else I’ve been trying to fix. As, right now, every single time a “tutorializable” event occurs we check if the tutorial has been viewed and play it if it hasn’t. This a different problem though and could probably be solved in the same method as just listening to “tutorializable” events.

With the quest system - I was thinking of going with the event bus system (I really like the way you laid it out) since I already have one that exists but I also want to keep in mind the “heaviness” on performance Seda145 mentioned in having UObjects. Our subtitle/notification system uses this sort of global typed event system and I think it could be used for the quest system.

The system I had yesterday (but have since fixed) felt like a coupled, horrible Frankensteined version of the “wiring loom” technique you mentioned.

I see you have EventType as an FString - is there a reason for the EventType being an FString? Things silent breaking is a pain I already know well.

Regarding the everything notification interface - How many data structures would you say is “too many specialized data structures”? Do you just mean avoiding references to specific classes? While things like different enums or structs or data assets (that don’t include references) would be okay. We have a lot of different interfaces and data structures for the different mechanics in the game. Our generic interaction mechanic also sounds uncannily similar to your INotifier interface. Perhaps it could be made to work well as it is with the quest system.

I think I have a better understanding of reducing the coupling in the quest system now. Now, Seda145’s replies about the heaviness of the quests objects has me thinking of ways to reduce the impact of having quests as objects. I like the logic of the objective being contained within the objective itself and from a designer standpoint I much prefer composing quests as coded containers of “objective objects” rather than filling out structs in a table or excel spreadsheet.

Thanks again for the reply! :smile:

“heavy” is relative. How many of these events do you get per second? One?
Creating one UObject per particle in a Niagara callback is obviously bad.
Creating one UObject per user-initiated event isn’t a big deal. If you can live entirely with structs and maybe a void* or two, well, that will reduce a little bit of load, but I wouldn’t expect the difference to show up in any real gameplay profile.

FString is the most generic, and removes all physical coupling. (Any other stringly typed object, like FName or FText or FGameplayTag, would be approximately the same.)

The next step is to define a FEventType handle class which wraps a string or something, and where you can have a static member of each event UObject subclass, and compare to that. Anyone receiving event UMyEventObject can compare to UMyEventObject::EventTag and it doesn’t give a lot of extra coupling because you only #include "MyEventObject.h" if you intend to handle it – everything “on the bus” deals with less specialized types.

The next step is to define an enum of all possible event types. Then you end up with having to recompile all receivers each time a new event type is added, which is more coupling.

You can also pass an IEventObject and use dynamic casting, but that’s generally just less efficient overall, so I don’t see that as a win. Might be easier to tie into some specific pre-existing systems, though, so it’s an option to not totally forget about.

If you need to recompile too many things when adding a new kind of event, you have too many specialized structures :slight_smile: The easiest way to not go over the limit, is to not even walk towards it. Define all the base types you need to interact with the event system, in the event system. Use UObject and UActor, and/or define your IEventBase or UEventBase or somesuch. Only the users of a particular specialization, needs to include those specialized types.

Enums are generally not-good from this viewpoint, because you end up having to add more values to them for each new kind of event.

In a game engine where every grenade, sound effect, and sparkle is a new networked AActor object, keeping an extra object to represent “the quest” doesn’t strike me as one of the top-hundred costs :slight_smile:

2 Likes

Thanks for all the replies.

I noticed that I didn’t encounter any big performance hit for even a VERY large number of random quests and events. And since they already work with my save system I was weary on moving away from them. I’m also just trying to be as efficient as possible because there have been times where I could have avoided a lot of headache by making as many optimizations as I could early on in my refactor.

But, yeah, these events are relatively uncommon so I’ll think we’ll be okay.

I’m going ahead with implementing the event system.

Thanks! :+1:

1 Like