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;
}
}
// -----------------------------------------------------------------------------------------------------------
}
}