How To Manage Quests

This page describes how to manage quests at runtime.


Working with Quests in Conversations

Lua functions to check and control quests are summarized in Quest-Related Lua Functions and described in more detail below.

Checking Quest State

To check the state of a quest inside a conversation, you'll specify a simple Lua condition. This condition will usually be in a dialogue entry's Conditions field. You can use the Lua Wizards, so you don't have to type a single character, just pick the conditions from a dropdown menu.

The Dialogue System now provides CurrentQuestState and SetQuestState Lua functions (and equivalents for quest entries), so you usually don't need to work with Lua tables directly. If you do use Lua tables, remember when referencing table indices in Lua code that you must replace spaces and hyphens with underscores as described in Important Note About Table Indices.

Using the quest defined in Example 1: Kill 5 Rats, say the baker has a dialogue entry asking the PC to start the quest. You only want to show this entry if the quest is not assigned yet. Set up the dialogue entry like this:

  • Dialogue Text: (Baker to PC) "If you bring me 5 dead rats, I'll bake us a pie."
  • Condition: CurrentQuestState("Kill 5 Rats") == "unassigned"

The dialogue entry will only be shown if the condition is true – that is, if the quest's state is "unassigned".

To check a quest entry (a sub-task in a quest), check its state field:

  • Condition: CurrentQuestEntryState("Escape the Prison Planet", 1) == "active"

Setting Quest State

To set the state of a quest, you'll write a simple Lua statement for a dialogue entry's Script field.

Using the same "Kill 5 Rats" example, say the PC accepts the quest. Your dialogue entry will look like this:

  • Dialogue Text: (PC to Baker) "Yum! I'll return with those rats."
  • Script: SetQuestState("Kill 5 Rats", "active")

When the player selects this dialogue entry, the PC will say this line, and the quest state will be set to "active".

To set a quest entry's state:

  • Script: SetQuestEntryState("Escape the Prison Planet", 1, "success")

Since the Quest[] table is a normal table in Lua, you can use any Lua commands on it – for example, to add new quests, remove quests, change descriptions, etc. You can even add new quest entries by increasing Entry_Count and adding the additional entry fields.

However, the Lua functions SetQuestState and SetQuestEntryState also update the quest tracker and send the OnQuestStateChange message that your scripts can handle.

You can also set quest states using Timeline Support.

Quest-Related Lua Functions

Lua Function Description Example
CurrentQuestState(questName) Returns a quest state as "unassigned", "active", "success", or "failure" CurrentQuestState("Kill 5 Rats") == "active"
SetQuestState(questName, state) Sets a quest state SetQuestState("Kill 5 Rats", "success")
CurrentQuestEntryState(questName, entryNum) Returns a quest entry state CurrentQuestEntryState("Escape", 2) == "active"
SetQuestEntryState(questName, entryNum, state) Sets a quest entry state SetQuestEntryState("Escape", 2, "success")

Quests in Lua

You can also update quests in Lua code elsewhere, for example using the Lua On Dialogue Event component or in your own scripts. Most often you'll use the SetQuestState and SetQuestEntryState functions as described above, but you can also work with other fields:

Quest["Kill_5_Rats"].XP_Reward = 500;

Reminder: When referencing table indices in Lua code, you must replace spaces and hyphens with underscores as described in Important Note About Table Indices.


Quest Triggers

You can also modify quest states by adding these trigger components to your scene:

Using Condition Observers in Quests

The Condition Observer component is very useful to monitor quest activity during gameplay. For example, you can use Increment On Destroy to increment a Lua variable when the player kills or collects a quest target. Then use Condition Observer to check the status of these variables on a regular frequency, update the quest state, show a gameplay alert message, and more.

Condition Observer Quest Example

Say you're writing a real time strategy (RTS) game in which workers can harvest wood, gold, and oil. You've defined a quest called "Master Harvester" in which the player must establish a harvest rate of at least 100 units/minute among wood, gold, and/or oil. The rates will ebb and flow depending on how many workers are assigned to harvesting, monsters interrupting their work, and other factors similarly unrelated to the Dialogue System.

In this example scenario, let's say you have a non-Dialogue System script that keeps track of the harvest rates for wood, gold, and oil. In this script, whenever you update the rate, also set a corresponding Lua variable. Using our hypothetical example, the code might look like this:

float woodRate = MyComplexCalculation(ResourceType.Wood);
float goldRate = MyComplexCalculation(ResourceType.Gold);
float oilRate = MyComplexCalculation(ResourceType.Oil);
MyGameplayHUD.UpdateHarvestText(woodRate, goldRate, oilRate);
DialogueLua.SetVariable("woodRate", woodRate);
DialogueLua.SetVariable("goldRate", goldRate);
DialogueLua.SetVariable("oilRate", oilRate);

Note that if you don't want to maintain synchronized Lua variables, you can register your C# methods as Lua functions (see Registering Functions) and call those functions directly in the Lua condition.

Now set up a Condition Observer similar to the one below:

The relevant sections of this component are enumerated below:

  1. A Lua condition checks if the total harvest rate is at least 100.
  2. A quest condition checks that the "Master Harvester" quest is active.
  3. If the condition is true, it sets the quest state to success and shows an alert message "Achievement: Master Harvester".
  4. It also sends the message "GiveBonus" with the parameter "archers" to a GameObject named Bonus Giver.

In script, #4 would look similar to this:

GameObject.Find("Bonus Giver").SendMessage("GiveBonus", "archers");

Say the Bonus Giver GameObject has a script with this method:

void GiveBonus(string bonus) {
    if (string.Equals(bonus, "archers")) {
        EnableTechTree(BuildableUnits.Archers);
    } else if (string.Equals(bonus, "footmen")) {
        et cetera...
    }
}

When the player reaches a harvest rate of 100, it will open up archers in the tech tree.

Condition Observer Efficiency: Polling vs. Events

Polling is when you check a condition repeatedly on a regular frequency. Event-based checking is when you check a condition only after a specific event has occurred, such as the end of a conversation.

The Condition Observer component uses polling, which can be an inefficient approach. For example, say you poll the value of a "kill count" variable every 1 second. If the player takes an hour to reach the required kill count, the Condition Observer will have evaluated the condition 3,600 times. Whenever practical, use an event-based trigger such as Set Quest State On Dialogue Event. However, sometimes it's not practical. If the event occurs very frequency, for example, you might as well poll.

If you want to use event-based checking with Condition Observer, you can set the frequency to a very high value (e.g., 999999) and manually run ConditionObserver.Check() whenever your event occurs.


Quest Case Study

This section describes some approaches a team took to implement a quest in the Dialogue System using Realistic FPS Prefab and S-Inventory. The mission is to kill an enemy and retrieve a laptop. The initial model was Sad Robot's quest to bring him a shotgun in the Realistic FPS Prefab integration example scene.

Loot Drop

First was the loot drop. Options discussed were:

  • Option #1: Set the enemy's Dead Replacement to a prefab that has an S-Inventory ItemGroup. This way, the player can loot the body just like a chest.
  • Option #2: Set the Dead Replacement to a prefab with two children: a corpse and a pickup item (the laptop). This way, the player can just pick up the laptop that spawns on the floor next to the corpse.
  • Option #3: Add Usable and Lua Trigger components to the corpse. Set the Lua Trigger to OnUse, and set Lua Code to:
AddItem("!!!FPS Player", "Laptop"); Variable["Alert"] = "Got the laptop! Bring it back now."

If you want to update the quest state at this point, you can add to the Lua Code:

SetQuestState("Get The Laptop", "success")
  • Option #4: Add Usable and Conversation Trigger components to the corpse. Set up the conversation just like the "Dead Guard" conversation in the S-Inventory example scene (not the RFPS + S-Inventory example scene).

Quest State Update

The Sad Robot's "Shotgun" quest doesn't update the quest state until you actually hand in the shotgun. It's the easiest way to do it.

If you want to update the quest state immediately, you can add it to a Lua script (as described in Option #3 above) or use a Condition Observer. If you use a Condition Observer, manually enter this Lua condition:

GetItemAmount("!!!FPS Player", "Laptop") > 0

Or, if you use loot drop Option #3 above, you could add a Quest Trigger and set it to OnUse.

You can make the quest more complex by adding quest entries (subtasks). Instead of this:

  • Quest: "Get the Laptop" [State: unassigned -> active -> success]

You'd define your quest like this:

  • Quest: "Get the Laptop" [State: unassigned -> active -> success]
    • Quest Entry 1: "Kill the enemy that has the laptop." [State: active -> success]
    • Quest Entry 2: "Find the laptop." [State: unassigned -> active -> success]
    • Quest Entry 3: "Bring the laptop back." [State: unassigned -> active -> success]

In this case, when the player kills the enemy, set Quest Entry 1's state to success, and set Quest Entry 2's state to active. When the player returns the laptop, set Quest Entry 3's state to success, and set the quest's State to success.

Turn-In

Option #1: Say you frame the quest as a laptop collection quest instead of a kill quest, and you have this NPC dialogue entry:

  • Dialogue Entry:
    • Actor: NPC
    • Dialogue Text: "Do you have the laptop?"

Add these child nodes (responses):

  • Dialogue Entry:
    • Actor: Player
    • Dialogue Text: "Here's the laptop."
    • Conditions: GetItemAmount("!!!FPS Player", "Laptop") > 0
    • Script: RemoveItem("!!!FPS Player", "Laptop"); SetQuestState("Get The Laptop", "success")
  • Dialogue Entry:
    • Actor: Player
    • Dialogue Text: "Sorry, I don't the laptop yet."
    • Conditions: GetItemAmount("!!!FPS Player", "Laptop") == 0

Option #2: If you use quest entries, you can add more dialogue. For example, if the player has done quest entry 1 (kill the enemy) but hasn't found the laptop yet, the conversation can reflect this:

  • Dialogue Entry:
    • Actor: Player
    • Dialogue Text: "No, I don't have the laptop yet. But I killed the enemy already."
    • Conditions: CurrentQuestEntryState("Get The Laptop", 2) == "active"

You can also add Variable["Alert"]="xxx" Lua commands to the Script fields to show updates.

Final Implementation

In the end, they decided to make the laptop a quest and added it as a pick up to the body. They also added the laptop as a weapon in Realistic FPS Prefab so the player can carry it around (which looks cool). When the player returns to the quest giver, they just called a condition:

GetItemAmount("!!!FPS Player", "Laptop") > 0

and a script:

Variable["Laptop"] = false
SetQuestState("Get The Laptop", "success")
Variable["Alert"] = "Quest Complete: LAPTOP"
RemoveItem("!!!FPS Player", "Laptop", 1)

And if the player returns without the laptop they just used this condition on the NPC line:

GetItemAmount("!!!FPS Player", "Laptop") == 0

Working with Quests in Scripts

Script reference: PixelCrushers.DialogueSystem.QuestLog

The QuestLog class provides methods to add and remove quests, get and set their state, and get their descriptions. This is a static class, so you can call its methods without having to create a QuestLog object.

Note that quest states are usually updated during conversations. In most cases, you will probably set quest states in Lua code during conversations, so you may never need to use many of the methods in this class.

If you do use these methods, converting spaces and hyphens to underscores (as described in Important Note About Table Indices) is optional, since the QuestLog class will automatically do this for you.

For descriptions of each method, see the PixelCrushers.DialogueSystem.QuestLog reference page.

Setting Quest State Observers

The Dialogue System now sends an OnQuestStateChange message when quest states change. (See Script Messages) This is generally more efficient and easier to use than quest state observers.

However, you can still set watches on quest states using these methods:

QuestLog.AddQuestStateObserver(): Adds a watch on a quest that will be checked on a specified frequency. The frequency can be EveryUpdate, EveryDialogueEntry, or EndOfConversation. If the expression changes, the quest log system will invoke a delegate that takes the form:

void MyDelegate(string title, QuestState newState) {...}

Example:

QuestLog.AddQuestStateObserver("Kill 5 Rats", LuaWatchFrequency.EveryDialogueEntry, OnQuestStateChanged);
 
void OnQuestStateChanged(string title, QuestState newState) {
    if (newState == QuestState.Success) {
        xp += 500;
        DialogueManager.ShowAlert("+500 XP");
    }
}

You can remove watches using QuestLog.RemoveQuestStateObserver() or QuestLog.RemoveAllQuestStateObservers().

Note: For best performance, limit the number of watches you set, especially when the frequency is EveryUpdate. Each watch requires an extra Lua call to evaluate the current quest state.


Authoritative Multiplayer Quests

How you manage quests in a multiplayer game highly depends the design of your game.

In many cases, you can simply maintain the Dialogue System environment (including quests) in each client for each player.

However, if you're using an authoritative master server, such as with an MMO, you may want to validate quest states on the master server to prevent cheating. To do this, assign override methods to these delegates:

  • QuestLog.SetQuestStateOverride
  • QuestLog.CurrentQuestStateOverride

In the SetQuestStateOverride method, contact the master server to confirm that the player is allowed to set the requested state. If so, call QuestLog.DefaultSetQuestState(), which will set the quest state locally, update the tracker, and inform listeners.

In the CurrentQuestStateOverride method, contact the master server to confirm the authoritative quest state for the player. The use of this method may be more complicated than SetQuestStateOverride. Communication with the master server is usually asynchronous; your override method will probably not be able to return the quest state immediately because it needs to wait for a response from the master server. Instead, your method can return a string reference to an async operation. The code that invokes CurrentQuestState() can then wait for the async operation to complete and retrieve the quest state from the response, instead of immediately using the override method's return value as the quest state.

Very often, quest states are checked and set during conversations. When using async override methods that don't return a value immediately, you may want to configure your dialogue entry's Sequence to wait for a sequencer message that indicates that the async method is done. In the dialogue entry, use the WaitForMessage() sequencer command to wait for the sequencer message. In your async method, use the Sequencer.Message() method to send the sequencer message. If the dialogue entry is configured to wait for a quest state (i.e., you've set CurrentQuestStateOverride), you can register an additional Lua function that you can use in the next dialogue entry to return the value received from the master server.

Handling Quest Entries (Subtasks)

If your quests use quest entries (subtasks), you can set these overrides, too:

  • QuestLog.SetQuestEntryStateOverride
  • QuestLog.CurrentQuestEntryStateOverride

<< Quest Design Notes | Quest Log Window >>