Weird Quest Reward bug at Close Dialogue button

OK so this bug has been around in my project for a while, but I’m starting to see some patterns…

Apparently I have this issue where if I try to do the sole quest in my game (for now), at a high combat level (my game is temporarily limited to 11 levels, I still didn’t work much with my progression bar), my game has an infinite reward glitch that keeps rewarding me everytime I hit the quit button (it’s a completely random bug, but it exists). It doesn’t stop, and keeps going on and on and on forever, unless I restart my gameplay. Sometimes it happens (30-40% of the time), sometimes it doesn’t… How do I fix this problem?

P.S: I sincerely apologize for the high number of questions today :slight_smile:

Not enough information…
Let’s see the details of the quest (the inspector, with any conditions highlighted)
And since this is apparently a reward given through the dialogue, the DialogueNode that it is awarded in with it’s Enter and Exit triggers.

Sure, here is my Quest Reward System Inspector:

In order to enter the dialogue that triggers, the quest, there are 2 conditions I have set up for my player to meet, as follows:

If he has been going through the Quest, but has not completed it, nor does he have the item, he will say “I have a few updates for you”, as follows:

The Missing Sword Quest Updates

And then you have the ‘GiveQuest’ Node, at the end of the initially agreed upon conversation:

The Missing Sword GiveQuest Node

We also have the Node conditions to give the QuestGiver his sword:

The Missing Sword Sword Giving Conditions

And finally, the Node that outputs the Quest Completion System:

The Missing Sword Quest Completion

I didn’t want to invest a lot more into this early on, I just wanted to make sure it all works

Is the sword being taken from the inventory?

Yes it is, and we get the reward. Problem is, at the end of the Quest, occasionally the ‘X’ button fails to just shut the dialogue down. Instead, it keeps the dialogue up and reward the player over and over and over again, sometimes 3-4x the provided quantity at the end of the Quest, per click of the ‘Quit Button’

Ok, and that’s with or without an error message (say a null reference error, or a missing reference error?)

As far as I remember, I think there was a null reference exception error. I’ll probably have to play the game again a few times before the issue pops up again

Important information. :slight_smile: With any odd bug report, I’ll need to see things like any error messages, along with their details.

A couple things to try…
Put the CompletedQuest action in the Enter Action instead of the Exit Action
Add a node after the completion node, “Thanks again, and good luck with your future adventures!”

Welp, I managed to catch the bug. Here you go (Apparently I noticed it only happens when I play the game for a while before getting to the Quest). I also just noticed that trying to walk away from the dialogue gives the player triple the reward, and it still doesn’t shut the Dialogue Down:

And here is the Stack Trace, if it helps in anyway:

MissingReferenceException: The object of type 'QuestItemUI' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
UnityEngine.Object.Instantiate (UnityEngine.Object original, UnityEngine.Transform parent, System.Boolean instantiateInWorldSpace) (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.Object.Instantiate[T] (T original, UnityEngine.Transform parent, System.Boolean worldPositionStays) (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.Object.Instantiate[T] (T original, UnityEngine.Transform parent) (at <ba783288ca164d3099898a8819fcec1c>:0)
QuestListUI.Redraw () (at Assets/Project Backup/Scripts/UI/Quests/QuestListUI.cs:37)
RPG.Quests.QuestList.AddQuest (RPG.Quests.Quest quest) (at Assets/Project Backup/Scripts/Quests/QuestList.cs:30)
RPG.Quests.QuestGiver.GiveQuest () (at Assets/Project Backup/Scripts/Quests/QuestGiver.cs:16)
UnityEngine.Events.InvokableCall.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.Events.UnityEvent.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
RPG.Dialogue.DialogueTrigger.Trigger (System.String actionToTrigger) (at Assets/Project Backup/Scripts/Dialogue/DialogueTrigger.cs:19)
RPG.Dialogue.PlayerConversant.TriggerAction (System.String action) (at Assets/Project Backup/Scripts/Dialogue/PlayerConversant.cs:180)
RPG.Dialogue.PlayerConversant.TriggerExitAction () (at Assets/Project Backup/Scripts/Dialogue/PlayerConversant.cs:168)
RPG.Dialogue.PlayerConversant.Quit () (at Assets/Project Backup/Scripts/Dialogue/PlayerConversant.cs:46)
RPG.UI.DialogueUI.<Start>b__8_1 () (at Assets/Project Backup/Scripts/UI/DialogueUI.cs:30)
UnityEngine.Events.InvokableCall.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.Events.UnityEvent.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.UI.Button.Press () (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Button.cs:70)
UnityEngine.UI.Button.OnPointerClick (UnityEngine.EventSystems.PointerEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Button.cs:114)
UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IPointerClickHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:57)
UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:272)
UnityEngine.EventSystems.EventSystem:Update() (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:514)

I’ll give the solutions you mentioned above a try, and see if any of them fixes the problem

Um… that’s quite an interesting stack…
I thought you were clearing a quest objective, not Giving another quest…
Let’s take a look at your DialogueTrigger and it’s UnityEvents…

Sure, here is my Quest Giver Inspector (with all the Dialogue Triggers I have applied to him):

And here is my DialogueTrigger.cs Code:

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using UnityEngine.Events;

using RPG.Respawnables;

using RPG.Combat;

namespace RPG.Dialogue {

    public class DialogueTrigger : MonoBehaviour

    {

        [SerializeField] string action;

        [SerializeField] UnityEvent onTrigger;

        public void Trigger(string actionToTrigger) {

            if (actionToTrigger == action) {

                onTrigger.Invoke();

                // if the action we want to activate is equal to our 'actionToTrigger',

                // we will launch that action (using 'Invoke()')

            }

        }

    }

}

(I don’t know if now is the right time to mention a second bug, now that we’re speaking of the Quest Giver, but if the player had a fight anywhere close to this guy, with another NPC, he just wants to go and bash the Quest Giver as well right after that for some reason (I’m guessing it’s because of an added component from our scripts)… Let’s keep this until the end)

This one just gives me the Reward earlier, and now I can’t even access the last node

Add a node after the completion node, “Thanks again, and good luck with your future adventures!”

As for the second solution above, it gives me an ‘IndexOutOfRangeException’ error:

IndexOutOfRangeException: Index was outside the bounds of the array.
RPG.Dialogue.PlayerConversant.Next () (at Assets/Project Backup/Scripts/Dialogue/PlayerConversant.cs:120)
RPG.Dialogue.PlayerConversant.SelectChoice (RPG.Dialogue.DialogueNode chosenNode) (at Assets/Project Backup/Scripts/Dialogue/PlayerConversant.cs:99)
RPG.UI.DialogueUI+<>c__DisplayClass11_0.<BuildChoiceList>b__0 () (at Assets/Project Backup/Scripts/UI/DialogueUI.cs:107)
UnityEngine.Events.InvokableCall.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.Events.UnityEvent.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.UI.Button.Press () (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Button.cs:70)
UnityEngine.UI.Button.OnPointerClick (UnityEngine.EventSystems.PointerEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Button.cs:114)
UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IPointerClickHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:57)
UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:272)
UnityEngine.EventSystems.EventSystem:Update() (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:514)

I’m baffled… It looks from the stack trace like you called GiveQuest instead of CompletedQuest…

I’d like to add a Debug.Log to the DialogueTrigger

public void Trigger(string actionToTrigger)
{
    Debug.Log($"DialogueTrigger.Trigger({actionToTrigger}), this Action = {action}");
    //rest of method

In terms of the other error, somethings very wrong here…
Paste in your Dialogue.cs, AIConversant.cs, and PlayerConversant.cs.
Quick hint: type the ``` first, then on the next line paste the method… if you paste the method and then add the ``` later, it sometimes double spaces the lines.

I won’t be able to get back on this until tomorrow, it’s sleep time.

Here are the results of the debugger, followed by all 4 stack traces (P.S: this one was working perfectly fine, I didn’t catch the error this time… Logically speaking though, the second call was NEVER supposed to return ‘CompletedQuest’ as far as I believe, neither was the third call supposed to output ‘GiveQuest’. The same issue exists with my dialogue guard, where the attack trigger gets activated early in the dialogue (he doesn’t start a fight from the first trigger though, only the second one), as well as the conversation ending trigger, and I have no clue why this happens):

DialogueTrigger.Trigger(GiveQuest), this Action = GiveQuest
UnityEngine.Debug:Log (object)
RPG.Dialogue.DialogueTrigger:Trigger (string) (at Assets/Project Backup/Scripts/Dialogue/DialogueTrigger.cs:17)
RPG.Dialogue.PlayerConversant:TriggerAction (string) (at Assets/Project Backup/Scripts/Dialogue/PlayerConversant.cs:180)
RPG.Dialogue.PlayerConversant:TriggerExitAction () (at Assets/Project Backup/Scripts/Dialogue/PlayerConversant.cs:168)
RPG.Dialogue.PlayerConversant:Quit () (at Assets/Project Backup/Scripts/Dialogue/PlayerConversant.cs:46)
RPG.UI.DialogueUI:<Start>b__8_1 () (at Assets/Project Backup/Scripts/UI/DialogueUI.cs:30)
UnityEngine.EventSystems.EventSystem:Update () (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:514)

DialogueTrigger.Trigger(GiveQuest), this Action = CompletedQuest
UnityEngine.Debug:Log (object)
RPG.Dialogue.DialogueTrigger:Trigger (string) (at Assets/Project Backup/Scripts/Dialogue/DialogueTrigger.cs:17)
RPG.Dialogue.PlayerConversant:TriggerAction (string) (at Assets/Project Backup/Scripts/Dialogue/PlayerConversant.cs:180)
RPG.Dialogue.PlayerConversant:TriggerExitAction () (at Assets/Project Backup/Scripts/Dialogue/PlayerConversant.cs:168)
RPG.Dialogue.PlayerConversant:Quit () (at Assets/Project Backup/Scripts/Dialogue/PlayerConversant.cs:46)
RPG.UI.DialogueUI:<Start>b__8_1 () (at Assets/Project Backup/Scripts/UI/DialogueUI.cs:30)
UnityEngine.EventSystems.EventSystem:Update () (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:514)

DialogueTrigger.Trigger(CompletedQuest), this Action = GiveQuest
UnityEngine.Debug:Log (object)
RPG.Dialogue.DialogueTrigger:Trigger (string) (at Assets/Project Backup/Scripts/Dialogue/DialogueTrigger.cs:17)
RPG.Dialogue.PlayerConversant:TriggerAction (string) (at Assets/Project Backup/Scripts/Dialogue/PlayerConversant.cs:180)
RPG.Dialogue.PlayerConversant:TriggerExitAction () (at Assets/Project Backup/Scripts/Dialogue/PlayerConversant.cs:168)
RPG.Dialogue.PlayerConversant:Quit () (at Assets/Project Backup/Scripts/Dialogue/PlayerConversant.cs:46)
RPG.UI.DialogueUI:<Start>b__8_1 () (at Assets/Project Backup/Scripts/UI/DialogueUI.cs:30)
UnityEngine.EventSystems.EventSystem:Update () (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:514)

DialogueTrigger.Trigger(CompletedQuest), this Action = CompletedQuest
UnityEngine.Debug:Log (object)
RPG.Dialogue.DialogueTrigger:Trigger (string) (at Assets/Project Backup/Scripts/Dialogue/DialogueTrigger.cs:17)
RPG.Dialogue.PlayerConversant:TriggerAction (string) (at Assets/Project Backup/Scripts/Dialogue/PlayerConversant.cs:180)
RPG.Dialogue.PlayerConversant:TriggerExitAction () (at Assets/Project Backup/Scripts/Dialogue/PlayerConversant.cs:168)
RPG.Dialogue.PlayerConversant:Quit () (at Assets/Project Backup/Scripts/Dialogue/PlayerConversant.cs:46)
RPG.UI.DialogueUI:<Start>b__8_1 () (at Assets/Project Backup/Scripts/UI/DialogueUI.cs:30)
UnityEngine.EventSystems.EventSystem:Update () (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:514)

As for my scripts, here they are:

AIConversant.cs:

using RPG.Control;
using UnityEngine;
using RPG.Attributes;

namespace RPG.Dialogue {

    public class AIConversant : MonoBehaviour, IRaycastable
    {

        [SerializeField] Dialogue dialogue = null;
        [SerializeField] string conversantName;
        [SerializeField] PlayerController player;
        [SerializeField] Health thisCharacterHealth;

        public CursorType GetCursorType()
        {

            return CursorType.Dialogue;

        }

        public bool HandleRaycast(PlayerController callingController)
        {

            if (dialogue == null) {

                return false;

            }

            if (thisCharacterHealth && thisCharacterHealth.IsDead()) { return false; }  // if the NPC is alive and has health, ignore raycast (i.e: show the dialogue Cursor)
            
            if (Input.GetMouseButtonDown(0))
            {
                
                transform.LookAt(callingController.transform);  // This line is solely responsible for ensuring our 'Dialogue-Triggered' guard can be a 'Respawn Manager' child, making him able to engage in conversation, even if he is respawnable
                callingController.GetComponent<PlayerConversant>().StartConversation(this, dialogue);
                
            }

            return true;

        }

        public string GetName() {

            return conversantName;

        }

    }

}

Dialogue.cs:

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;  // to be able to access the 'Undo' class (line 81)



namespace RPG.Dialogue {

    [CreateAssetMenu(fileName = "New Dialogue", menuName = "Dialogue", order = 0)]
    public class Dialogue : ScriptableObject, ISerializationCallbackReceiver    // ISerializationCallbackReceiver Interface avoids errors in Adding object to asset (line 90)
    {

        [SerializeField]
        List<DialogueNode> nodes = new List<DialogueNode>();

        [SerializeField]
        Vector2 newNodeOffset = new Vector2(250, 0);

        // key = string, value = DialogueNode (Dictionaries are used because of their ability to quickly find value of stuff, based on given keys)
        Dictionary<string, DialogueNode> nodeLookup = new Dictionary<string, DialogueNode>();

        private void OnValidate() {

            nodeLookup.Clear();

            foreach (DialogueNode node in GetAllNodes()) {

                nodeLookup[node.name] = node;

            }

        }

        public IEnumerable<DialogueNode> GetRootNodes() {

            foreach (DialogueNode node in GetAllNodes()) {

                bool isChild = false;

                foreach (DialogueNode lookupNode in GetAllNodes()) {

                    if (lookupNode.GetChildren().Contains(node.name)) {

                        isChild = true;
                        break;

                    }

                }

                if (!isChild) yield return node;

            }

        }


        // IEnumerables are an Interface (implemented by the List of Dialogue Nodes in this case), allowing objects to do 'for' loops over them:
        public IEnumerable<DialogueNode> GetAllNodes() {

            return nodes;

        }

        public DialogueNode GetRootNode() {

            return nodes[0];

        }

        public IEnumerable<DialogueNode> GetAllChildren(DialogueNode parentNode)
        {
            
            List<DialogueNode> result = new List<DialogueNode>();

            foreach (string childID in parentNode.GetChildren()) {

                if (nodeLookup.ContainsKey(childID)) {
                
                yield return nodeLookup[childID];
                
                }
            }

        }

#if UNITY_EDITOR

        public void CreateNode(DialogueNode parent)
        {

            DialogueNode newNode = MakeNode(parent);

            Undo.RegisterCreatedObjectUndo(newNode, "Created Dialogue Node");
            Undo.RecordObject(this, "Added Dialogue Node");

            AddNode(newNode);

        }

        

        public void DeleteNode(DialogueNode nodeToDelete)
        {

            Undo.RecordObject(this, "Deleted Dialogue Node");
            nodes.Remove(nodeToDelete);
            OnValidate();
            CleanDanglingChildren(nodeToDelete);    // cleans up children (further nodes connected to node to delete down the line) of parent nodes that have been deleted
            Undo.DestroyObjectImmediate(nodeToDelete);  // destroys our node (but also saves a backup, just in case you need to Undo your changes)


        }

        private DialogueNode MakeNode(DialogueNode parent)
        {

            DialogueNode newNode = CreateInstance<DialogueNode>();
            newNode.name = Guid.NewGuid().ToString();

            if (parent != null)
            {

                parent.AddChild(newNode.name);
                newNode.SetPlayerSpeaking(!parent.IsPlayerSpeaking());
                newNode.SetPosition(parent.GetRect().position + newNodeOffset);

            }

            return newNode;

        }

        private void AddNode(DialogueNode newNode)
        {
            nodes.Add(newNode);
            OnValidate();
        }

        private void CleanDanglingChildren(DialogueNode nodeToDelete)
        {
            foreach (DialogueNode node in GetAllNodes())
            {

                node.RemoveChild(nodeToDelete.name);

            }
        }
#endif

        public void OnBeforeSerialize()
        {

#if UNITY_EDITOR

            if (nodes.Count == 0)
            {

                DialogueNode newNode = MakeNode(null);
                AddNode(newNode);
            }
            
            // For saving files on the Hard Drive
            if (AssetDatabase.GetAssetPath(this) != "") {

                foreach(DialogueNode node in GetAllNodes()) {

                    if (AssetDatabase.GetAssetPath(node) == "") {

                        // For Loading a file off the Hard Drive/SSD
                        AssetDatabase.AddObjectToAsset(node, this); // adds the new nodes in the same dialogue under the original Dialogue Node


                    }

                }

            }
#endif

        }

        public void OnAfterDeserialize()
        {

        }

        public IEnumerable<DialogueNode> GetPlayerChildren(DialogueNode currentNode)
        {
            
            foreach (DialogueNode node in GetAllChildren(currentNode)) {

                if (node.IsPlayerSpeaking()) {

                    yield return node;

                }

            }

        }

        public IEnumerable<DialogueNode> GetAIChildren(DialogueNode currentNode)
        {

            foreach(DialogueNode node in GetAllChildren(currentNode)) {

                if (!node.IsPlayerSpeaking())
                {

                    yield return node;

                }

            }

        }
    
    }

}

PlayerConversant.cs:

using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System;
using RPG.Core; // for the Evaluator of the Dialogues (line 133)
using RPG.Movement;
using GameDevTV.Utils;

namespace RPG.Dialogue {

    public class PlayerConversant : MonoBehaviour, IAction
    {

        [SerializeField] string playerName;
        
        Dialogue currentDialogue;
        DialogueNode currentNode = null;
        AIConversant currentConversant = null;  // the conversation being occured between the Player and the AI (to trigger events during the conversation for instance)
        bool isChoosing = false;

        // OUT OF COURSE CONTENT ------------------------------------------------------------------------------------------------
        private Dialogue targetDialogue;    // the Dialogue our Player is aiming to go to, for conversation purposes
        private AIConversant targetConversant;  // the Target Conversant of our Quest Giver = Player Conversant
        public float acceptanceRadius; // the minimum distance our Player has to be from the NPC before they can talk
        // ----------------------------------------------------------------------------------------------------------------------

        public event Action onConversationUpdated;

        

        public void StartDialogue(AIConversant newConversant, Dialogue newDialogue) {

            currentConversant = newConversant;
            currentDialogue = newDialogue;
            currentNode = currentDialogue.GetRootNode();
            TriggerEnterAction();
            onConversationUpdated();    // this line subscribes our Dialogue to the event Action we created above, so that it follows along when something new to that event occurs

        }

        public void Quit() {

            // When the conversation is over, or the 'X' button is clicked, this function is called

            currentDialogue = null; // no node available
            TriggerExitAction();

            // Test
         /* Debug.Log("Quit Dialogue - Before TriggerExitAction");
            TriggerExitAction();
            Debug.Log("Quit Dialogue - After TriggerExitAction"); */

            currentNode = null; // no Nodes available to process through
            isChoosing = false; // no choices of conversations available
            currentConversant = null;   // quitting the AI Conversant Dialogue
            onConversationUpdated();    // makes the dialogue hide itself when the chat is over

        }

        public bool IsActive() {

            // This function (a 'getter' function) returns whether we have a current Dialogue to refer to (after the 2 seconds of the IEnumerator)
            return currentDialogue != null;
        }

        public bool IsChoosing() {

            // getter
            return isChoosing;

        }

        public string GetText() {

            // getter
            if (currentDialogue == null) {

                return "";

            }

            return currentNode.GetText();

        }

        public IEnumerable<DialogueNode> GetChoices() {

            return FilterOnCondition(currentDialogue.GetPlayerChildren(currentNode));

        }

        public void SelectChoice(DialogueNode chosenNode) {

            currentNode = chosenNode;
            TriggerEnterAction();
            isChoosing = false;

            // OPTIONAL: Implement this line only if you don't want the next conversation to display what your button just had written on it:
            Next();
            // if we didnt call 'Next()', which calls 'onConversationUpdated()' event subscription, we could've called it here instead

        }

        public void Next() {

            int numPlayerResponses = FilterOnCondition(currentDialogue.GetPlayerChildren(currentNode)).Count();

            if (numPlayerResponses > 0) {

                isChoosing = true;
                TriggerExitAction();
                onConversationUpdated();
                return;

            }

            DialogueNode[] children = FilterOnCondition(currentDialogue.GetAIChildren(currentNode)).ToArray();  // filters our Player <-> Quest Giver responses based on the process of our Quests
            int randomIndex = UnityEngine.Random.Range(0, children.Count());    // UnityEngine is mentioned here because Random.Range comes from both UnityEngine and System (which we need for event Action), hence we need to specify which function we are calling
            TriggerExitAction();
            currentNode = children[randomIndex];
            TriggerEnterAction();
            onConversationUpdated();

        }

        public bool HasNext() {

            return FilterOnCondition(currentDialogue.GetAllChildren(currentNode)).Count() > 0;

        }

        private IEnumerable<DialogueNode> FilterOnCondition(IEnumerable<DialogueNode> inputNode) {

            // This function ensures we can play different Nodes on our dialogues, based on the Progress Status of our Quests (Start, Pending, Complete, etc)
            foreach (var node in inputNode) {

                if (node.CheckCondition(GetEvaluators())) {

                    yield return node;  // if a condition (E.g: A quest has been done) is met, we include it in our filter, otherwise it's excluded from the Filter

                }

            }

        }

        private IEnumerable<IPredicateEvaluator> GetEvaluators() {

            return GetComponents<IPredicateEvaluator>();

        }

        private void TriggerEnterAction() {

            if (currentNode != null) {

               TriggerAction(currentNode.GetOnEnterAction());

            }

        }

        private void TriggerExitAction() {

            if (currentNode != null)
            {

                TriggerAction(currentNode.GetOnExitAction());

            }

        }

        private void TriggerAction(string action) {

            if (action == "") return;

            foreach(DialogueTrigger trigger in currentConversant.GetComponents<DialogueTrigger>()) {

                trigger.Trigger(action);

            }

        }

        public string GetCurrentConversantName()
        {
            
            if (isChoosing) {

                return playerName;

            }

            else {

                return currentConversant.GetName();

            }

        }

        // MORE OUT OF COURSE CONTENT (RUNNING TO CLICKED NPC BEFORE INTERACTING IN DIALOGUE WITH THEM) -----------------------------------------------------------------------------

        public void Cancel()
        {
            Quit(); // if you cancel a dialogue, you just Quit it...
            targetConversant = null;    // ... and you also ensure that you're not talking to anyone in-game
        }

        public void StartConversation(AIConversant conversant, Dialogue dialogue) {

            GetComponent<ActionSchedular>().StartAction(this);
            targetConversant = conversant;
            targetDialogue = dialogue;

        }

        void Update() {

            if (!targetConversant) return;
            if (Vector3.Distance(transform.position, targetConversant.transform.position) > acceptanceRadius) {

                transform.LookAt(targetConversant.transform);
                GetComponent<Mover>().MoveTo(targetConversant.transform.position, 1.0f);

            }

            else {

                GetComponent<Mover>().Cancel();
                StartDialogue(targetConversant, targetDialogue);
                targetConversant = null;

            }

        }

        // -----------------------------------------------------------------------------------------------------------

    }

}

In terms of both triggers appearing in the console, this was expected. I was looking to see that the triggers were getting and using the information they were supposed to.

I can’t figure out why the CompletedQuest trigger would call the QuestGiver method…

But it’s possible that this error was encountered when you were getting the quest in the first place?

In any event, the null referene itself is in QuestListUI… so that will be the next thing we look at.

On a related note, I recreated the quest/dialogue in my copy, with the quest being to bring a wooden dagger to the quest giver, in exchange for a Sword of Plot Device.

I’ll walk through the best practice of how I set it up:
First, the Quest itself:
image
Fairly straightforward, give a dagger to Sam is what it all boils down to.
Now let’s look at Sam’s dialogue:
We always start with a neutral greeting. No conditions here, you always get the greeting.
image
The greeting itself has three children, which are all speaker dialogues, not Player
image
Now which dialoguenode is displayed is set by the conditions. It’s absolutely vital that GetFilteredItems does not return no items here, so you have to set your conditions up in a way that at least one option will be available

I can’t seem to find my dagger. Can you help me out?
image
This is basically if(!HasQuest(MissingDagger))

Have you found my Dagger?
image
if(HasQuest(MissingDagger) && !CompletedQuest(MissingDagger));

I’m so grateful that you found my dagger!
image
`if(HasQuest(MissingDagger) && CompletedQuest(MissingDagger))

That’s it for the conditions, all handled in the opening salvo.

Now let’s look at the dialogue after the I can’t seem to find my dagger. At this point, there are no conditions remaining, so a screenshot should help for the most part:

The top option heads straight to Thank you so much. The On Enter Action for this is GiveQuest.
The middle option lets Rick explain a bit more.
You would be helping out a friend, and I’d probably give you this sword I can’t lift.
That leaves the player with two options, to accept or not. The Accept goes to the Thank you node (with that GiveQuest Enter action. The sorry goes to a thanks anyways node and out.

The last option goes to a that’s ok node.


Now for the payoff…
No, I don’t have your dagger has no conditions whatsoever. It just leads out to the I hope you find it soon.
The Yes, here it is! node has the condition if(HasItem(WoodenDagger)). This gives the player the option of not turning in the quest if he doesnt’ want to (maybe he still needs the wooden dagger?) so both No and yes are choices when the player has the dagger.
I put the TakeDagger action in the ExitAction of the Yes. Thank you So Much is just dialogue.
And finally, here’s all the support components I used on Sam:

Best Practice:

  • Always try to end on the speaker speaking, not a Player choice.
  • When there are conditions, always ensure that there is at least one condition that will always be true. This can mean one node with conditions, or it can mean having a node for each logical possibility.

In the case of the first three speaker nodes, every logical possibility is covered. There are three possible states at any given time:

  • The player does not have the quest
  • The player has the quest and has not completed it
  • The player has the quest and has completed it.
    These are covered by each of the three nodes above.
    In the second conditions example, there are two possibilities
  • The player has the dagger
  • The player does not have the dagger.
    It can be safely assumed that the player can’t get to this node unless he also Has the quest and has not completed it because the only way to get to that branch is through that condition.
    I chose to leave the first option, don’t have the dagger, with no conditions. This satisfies the rule that if there are filtered conditions, there must be one node that passes.

Good morning Brian, thank you for the detailed answer (just woke up… :sweat_smile:). So from what I understood, the error is probably being given off because of how I set my conditions up, rather than the code itself?

That’s how it appears (and has often been the case with students who have had issues). That’s sort of where I developed the best practices list related to dialogues.

Ahh that’s fair. The bug disappeared for a while though, so if it shows up again I’m probably going to keep the best practices in list. If we are to move on to another bug, my ‘Triggered-by-Dialogue’ mixed with Respawn Manager latest attempt on Udemy has been left on unread for a while :stuck_out_tongue_winking_eye: (I’ll comment on that over there)

It’s infinitely easier to help with problems in these forums than in the Udemy QA (the editor in there gets very wanky if I want to paste images or code.

Fair enough, I’ll open a follow-up Question here :slight_smile: (and let’s keep this forum open until it automatically closes, just in case)

Privacy & Terms