Dialogue System Trigger & Quests or Story Progression

Announcements, support questions, and discussion for the Dialogue System.
Mackerel_Sky
Posts: 111
Joined: Mon Apr 08, 2019 8:01 am

Dialogue System Trigger & Quests or Story Progression

Post by Mackerel_Sky »

Hi,

I have been fiddling around with the dialogue system trigger to try and get a story progression going.

From the manual and examples, I understand the basic flow of events being:

- A variable is changed in the dialogue database
- A dialogue trigger that watches the variable through a condition is fired
- The dialogue trigger's action changes something in the scene or a quest to progress the story

From the example kill quest in the demo scene, it seems like each 'step' in a quest or the variable needs a separate trigger to watch- e.g. One trigger watches for Variable 1 = 1 with Action 1, and another one watches for Variable 1 = 2 with Action 2. This would mean that for one quest with a lot of steps or quest events, there would need to be a lot of triggers watching the conditions for each step.

The demo quest had a quest tracker in the same scene as the enemies to be killed, but I'm interested in having potentially several concurrent quests running at the same time, with items to gather/enemies to kill in potentially different locations. I could parent each trigger to a DontDestroyOnLoad gameObject but I think there might be a better way to do it, especially as I'm not sure how many quests or story steps I plan on implementing. Do you have any ideas about a cleaner/simpler way of doing this?

in addition, I understand that I'll need to be using SpawnedObject and SpawnedObjectManager to spawn relevant NPCs, enemies, and other objects on certain story/quest events. I took a look at the documentation but I'm not sure as to how the two interact or work to spawn different objects in the correct position/states. I can see a lot of overlap between them and savers. Could you give me a brief rundown to how I'm supposed to use these classes?

Thanks for your help as always!
User avatar
Tony Li
Posts: 22049
Joined: Thu Jul 18, 2013 1:27 pm

Re: Dialogue System Trigger & Quests or Story Progression

Post by Tony Li »

Hi,

It'll probably be most productive to step through an actual example. (A hypothetical example is fine.)

Are you comfortable with a bit of scripting? It's possible to set up without scripting (using a bunch of Dialogue System Triggers), but it's more compact with a little scripting.

Can you suggest a hypothetical quest and the objects it would involve? For example:
  • Quest: Kill 3 orc warriors OR 2 orc shamans. After that, kill the orc chief.
  • Orc warriors and shamans are in the Forest scene. Orc chief is in the Cave scene.
  • Orcs have a UnityEvent called OnDeath() that they invoke when killed.
Let's ignore SpawnedObjects for now and just set up enemies, etc., so they'll automatically work as SpawnedObjects later.
Mackerel_Sky
Posts: 111
Joined: Mon Apr 08, 2019 8:01 am

Re: Dialogue System Trigger & Quests or Story Progression

Post by Mackerel_Sky »

Hey Tony,

I'm happy to work with scripting.

An example situation would be:

Quest 1: Kill 3 slimes and 2 ghosts, then report back to the scientist.
  • Ghosts are only found in the cave scene. They are a special enemy that spawns when the quest is active.
  • Slimes are found in the cave, beach and forest scenes. They are a regular enemy that spawns regardless of quest states.
Quest 2: Destroy 3 robots then destroy the core.
  • Robots are in the forest and mountain scenes.
  • Core is in the factory scene. Access to it is locked until first phase is completed.
Both quests can be started simultaneously.

Is this what you are looking for?
User avatar
Tony Li
Posts: 22049
Joined: Thu Jul 18, 2013 1:27 pm

Re: Dialogue System Trigger & Quests or Story Progression

Post by Tony Li »

Thanks! Those are perfect examples.

One thing to keep in mind is that the Dialogue System primarily views quests in the context of conversations. It's relatively easy to check and set quest quests inside conversations. Outside of conversations, the Dialogue System expects you to handle a lot of quest stuff manually. (One of the motivations behind Quest Machine is to automate that part.)

Here are three techniques you can use to manually manage quest stuff in the Dialogue System:

1. Add script(s) to the Dialogue Manager or scenes that check specific quest states.

2. Embed Lua code in custom fields in each quest in your dialogue database. (Side note: In the upcoming version 2.2.16, the Dialogue Editor can use multi-line text boxes for custom fields. This makes it easier to edit larger amounts of text.)

3. Use the Dialogue System's Message System. This Message System is in the Dialogue System's Pixel Crushers Common Library. It's what Quest Machine uses, too.

I'll rough out the code below. I'm just typing it into this reply, so it may have typos, and I'm omitting things like 'using' statements for brevity.
User avatar
Tony Li
Posts: 22049
Joined: Thu Jul 18, 2013 1:27 pm

Re: Dialogue System Trigger & Quests or Story Progression

Post by Tony Li »

Let's look at Quest 1 first.

The two primary things to handle are:

1. Spawning.
2. Kill counts.

Spawning:
Add a ghost-spawning script to the cave scene. If Quest 1 is active, start spawning. Otherwise delete any spawned ghosts that the SpawnedObjectManager has restored back into the scene. You can make this a general purpose script, something like:
QuestEntitySpawner.cs

Code: Select all

public class QuestEntitySpawner : MonoBehaviour
{
    [QuestPopup] public string requiredActiveQuest;
    
    public GameObject questEntityPrefab;
    
    IEnumerator Start();
    {
        // Wait until the save system has loaded saved game or applied previous state of scene:
        for (int i = 0; i <= PixelCrushers.SaveSystem.framesToWaitBeforeApplyData; i++)
        {
            yield return null;
        }
        if (QuestLog.IsQuestActive(requiredActiveQuest))
        {
            // (Your code here to start spawning questEntityPrefabs)
        }
        else
        {
            // (Your code here to despawn all instances of questEntityPrefab in scene)
        }
    }
}

Kill counts:
You could use the message system to notify quests when a target is killed, like Quest Machine does. But it might be simpler to use two small custom scripts in combination with some Lua code in your quests. Even so, the description below kind of re-implements the ideas used in Quest Machine, just without the message system.

Add a script to the Dialogue Manager that handles a "quest event." Let's call it QuestEventHandler. I'll sketch that out in a bit.

On the ghosts and slimes, add a script that sends quest events to the QuestEventHandler script. Something like:
QuestEventSender.cs

Code: Select all

public class QuestEventSender : MonoBehaviour
{
    public string questEvent; //<--Set in inspector.
    
    public void SendQuestEvent()
    {
        DialogueManager.instance.GetComponent<QuestEventHandler>().HandleEvent(questEvent);
    }
}
When the player kills a ghost or slime, call this script's SendQuestEvent() method. Let's say you're setting up a slime. In the inspector, set questEvent to "Slime Killed".

The QuestEventHandler script's HandleEvent() method should receive this event and update quests accordingly. First, we'll add some custom quest fields:

quest1.png
quest1.png (17.07 KiB) Viewed 1631 times

The Success Condition field contains a Lua condition to check if the quest requirements are met.

Each entry has 3 custom fields:
  • Entry # Event: The event sent by QuestEventSender that it should listen for.
  • Entry # Variable: The variable to increment when it receives the event.
  • Entry # Goal: When the variable reaches this amount, the entry becomes successful.
QuestEventHandler.HandleEvent() can check all quests for any entries that listen for the specified event. Example:

(Note: The script pulls info directly from the dialogue database, which is a bit faster than pulling from the Lua environment.)
QuestEventHandler.cs

Code: Select all

public class QuestEventHandler : MonoBehaviour
{
    public void HandleEvent(string questEvent)
    {
        foreach (Quest quest in DialogueManager.masterDatabase.items)
        {
            if (quest.IsItem) continue; // Ignore items. Only interested in quests.
            string questName = quest.Name;
            if (!QuestLog.IsQuestActive(questName)) continue; // Only interested in active quests.
            
            // Check all quest entries:
            bool questReceivedEvent = false;
            int entryCount = Quest.LookupInt("Entry Count");
            for (int i = 0; i < entryCount; i++)
            {
                int entryNum = i + 1;
                if (QuestLog.GetQuestEntryState(questName, entryNum) != QuestState.Active) continue; // Only interested in active entries.
                string entryEvent = quest.LookupValue($"Entry {entryNum} Event");
                if (entryEvent == questEvent)
                {
                    questReceivedEvent = true;
                    string entryVariable = quest.LookupValue($"Entry {entryNum} Variable");                    
                    int entryGoal = quest.LookupInt($"Entry {entryNum} Goal");
                    int newValue = 1 + DialogueLua.GetVariable(entryVariable).asInt;
                    DialogueLua.SetVariable(entryVariable, newValue);
                    if (newValue >= entryGoal)
                    {
                        QuestLog.SetQuestEntryState(questName, entryNum, QuestState.Success);
                    }
                }
            }
            
            // Check main quest state:
            if (questReceivedEvent)
            {
                string successCondition = quest.LookupValue("Success Condition");
                if (Lua.IsTrue(successCondition))
                {
                    QuestLog.SetQuestState(questName, QuestState.Success);
                }
            }
        }
        // When finished updating all quests, update quest UIs:
        DialogueManager.SendUpdateTracker();
    }
}
User avatar
Tony Li
Posts: 22049
Joined: Thu Jul 18, 2013 1:27 pm

Re: Dialogue System Trigger & Quests or Story Progression

Post by Tony Li »

You could replace the "Entry # Variable/Goal" fields with a single text field containing Lua code. Then your QuestEventHandler script could just run the Lua code. For example:
  • Entry 1 Update:

    Code: Select all

    Variable["slimesKilled"] = 1 + Variable["slimesKilled"];
     if (Variable["slimesKilled"] >= 3) then SetQuestEntryState("Quest 1", 1, success) end
But from experience almost all quests can get by with variable and goal fields, which are simpler to enter in the editor.

You could also add another custom field named "Success Script". Similar to Success Condition, you could run Success Script through Lua.Run() when the quest becomes successful. For example:
  • Success Script:

    Code: Select all

    ShowAlert("Quest 1 Complete! See Bob for your reward. Activating Quest 2.");
    SetQuestState("Quest 2", active)
Mackerel_Sky
Posts: 111
Joined: Mon Apr 08, 2019 8:01 am

Re: Dialogue System Trigger & Quests or Story Progression

Post by Mackerel_Sky »

Hey Tony,

That's a lot of information! I might have to get around to this on the weekend when I have a bit more time to experiment.

Thanks for writing all this up. I will go through it slowly and come back if I have any issues, For now, I'm starting to put together a spawner to instantiate at start. As I could be spawning stuff both in and out of quests, I want to add a variable check to the spawn function. Do you know if there's a way to get the type and value of a variable in the dialogue database?

I've put the code below, with the variable check towards the bottom.
Spoiler

Code: Select all

public class EntitySpawner : MonoBehaviour
{

    public QuestSpawnData[] objectsToSpawnQuest;
    public VariableSpawnData[] objectsToSpawnVariable;
    
    private void Start()
    {
        StartCoroutine("SpawnObjects");
    }

    IEnumerator SpawnObjects()
    {
        // Wait until the save system has loaded saved game or applied previous state of scene:
        for (int i = 0; i <= PixelCrushers.SaveSystem.framesToWaitBeforeApplyData; i++)
        {
            yield return null;
        }
        foreach(QuestSpawnData data in objectsToSpawnQuest)
        {
            if (QuestLog.GetQuestEntryState(data.quest, data.state) == QuestState.Active)
            {
                for (int i = 0; i <= data.objectList.Length; i++)
                {
                    Instantiate(data.objectList[i].obj, data.objectList[i].pos, Quaternion.identity);
                }
            }
            else
            {
                // don't spawn anything
            }
        }
        foreach (VariableSpawnData data in objectsToSpawnVariable)
        {
            //check if variable is int, bool 
            //then check if matches iCondition or bCondition
            //if true, spawn objects
        }
    }
}

[System.Serializable]
public class QuestSpawnData
{
    public string quest;
    public int state;
    public ObjPos[] objectList;
}

[System.Serializable]
public class VariableSpawnData
{
    public string variable;
    public int iState;
    public bool bState;
    public int iCondition;
    public int bCondition;
    public ObjPos[] objectList;
}

[System.Serializable]
public class ObjPos
{
    public GameObject obj;
    public Vector3 pos;
}
}
User avatar
Tony Li
Posts: 22049
Joined: Thu Jul 18, 2013 1:27 pm

Re: Dialogue System Trigger & Quests or Story Progression

Post by Tony Li »

Hi,

Sorry for that flood of information. I basically ended up re-implementing Quest Machine in the Dialogue System. ;-) Well, minus the quest node editor and procedurally generated quests.

Use DialogueManager.masterDatabase.GetVariable() to get the type of a variable. Then you can check its Type.

Code: Select all

var variable = DialogueManager.masterDatabase.GetVariable("Some Variable Name");
var variableType = variable.Type;
if (variable.Type == FieldType.Number)
{
    Debug.Log($"{variable.Name}: {DialogueLua.GetVariable(variable.Name).asInt;}");
}
else
{
    Debug.Log($"{variable.Name}: {DialogueLua.GetVariable(variable.Name).asString;}");
}
Mackerel_Sky
Posts: 111
Joined: Mon Apr 08, 2019 8:01 am

Re: Dialogue System Trigger & Quests or Story Progression

Post by Mackerel_Sky »

Hi Tony,

Thanks for the tip. I saw GetVariable() in the manual but wasn't sure how to use it.

No hard feelings about the info dump. Thanks for going above and beyond when you could have just recommended Quest Machine, it definitely makes a difference in my eyes.
Mackerel_Sky
Posts: 111
Joined: Mon Apr 08, 2019 8:01 am

Re: Dialogue System Trigger & Quests or Story Progression

Post by Mackerel_Sky »

I managed to get everything working somehow, so I'll leave my processes here for future reference.

I set up a class containing a list of objects and positions I need to spawn for each variable/quest check. On scene start, the entity spawner checks its list of variables and quests and instantiates the correct object lists.

If there are any tracked variables in the scene of the spawner, the spawner game object also has child objects containing dialogue triggers which watch the relevant variable like in the sci-fi demo. The triggers are assigned to a Quest Updater component in the target game objects when they are instantiated. The Quest Updater is responsible for firing the trigger check when required- e.g. They call .OnUse from an IncrementOnDestroy script or some other event.


I'm happy with where I am now regarding object spawns. However, I want to add some additional functionality to doors/scene transitions.

Right now, if my doors are locked they check for a key in the player's inventory, and play a conversation with either 'door is locked' or 'unlocked the door' if the player has the key. The check is done outside of the dialogue system.

I want to add an ability for the doors to check dialogue variables and quests to unlock. In addition, they should be able to use and/or functionality, and hopefully I can expand this to the spawner as well. All I need to do is to check if one or more variable/quest/quest event conditions are true, then I can call LoadScene or StartConversation. I found the Lua section in the manual but it looks tricky to manipulate. What's the best way to add some Lua code to the project?
Post Reply