This page describes how to manage quests at runtime.
Lua functions to check and control quests are summarized in Quest-Related Lua Functions and described in more detail below.
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:
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:
CurrentQuestEntryState("Escape the Prison Planet", 1) == "active"
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:
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:
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.
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") |
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.
You can also modify quest states by adding these trigger components to your scene:
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.
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:
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.
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.
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:
If you want to update the quest state at this point, you can add to the Lua Code:
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:
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:
You'd define your quest like this:
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:
Add these child nodes (responses):
GetItemAmount("!!!FPS Player", "Laptop") > 0
RemoveItem("!!!FPS Player", "Laptop"); SetQuestState("Get The Laptop", "success")
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:
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:
and a script:
And if the player returns without the laptop they just used this condition on the NPC line:
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.
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.
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:
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.
If your quests use quest entries (subtasks), you can set these overrides, too:
<< Quest Design Notes | Quest Log Window >>