As I’ve been finishing up the technical debt of my rpg, one of the things that jumped out at me was how there is no memory of conversation threads, so we couldn’t have a thread with NPC A and when we come back, the options and dialogue are different or having the conversation with NPC A allowing us to open up new dialogue with NPC B(without making it part of a quest objective). I put something together that is minimally invasive to the greater dialogue system by utilizing a DialogueStore, IItemStore interface and the HasInventoryItems Predicate, but definitely there may be a better way that doesn’t tie so closely to the Inventory system that I’d be very open to. Any advice would be super helpful.
With that said, here it goes
Setting up the groundwork, each NPC that we want to save dialogue branches will have a DialogueItem created. The npc string is their SaveableEntity ID that without prefabbing each NPC means it needs to be copy and pasted onto the item. I’m not making a Bethesda scale game, so I can live with that. At runtime the item is set to Stackable, so we can use the number being passed in the HasInventoryItems predicate that we’ll see later.
using UnityEngine;
using GameDevTV.Inventories;
namespace RPG.Inventories.Items
{
[CreateAssetMenu(fileName = "NPC Dialogue Item", menuName = ("RPG/Inventory/Create New NPC Dialogue"))]
public class NPCDialogueItem : InventoryItem
{
[SerializeField] string npc;
public string NPC { get { return npc; } }
}
}
Adding a bunch of DialogueBranchFinished triggers to our TriggerType enum
namespace RPG.Dialogue
{
public enum TriggerType
{
Select, // Default State that returns immediately from TriggerAction
Attack, // References AggroGroup component
GiveItem, // References ItemGiver Component (NPC item->Player)
GiveQuest, // References QuestGiver Component
CompleteObjective, // References QuestCompletion Component on NPC or Child Object
CompleteObjective1, // References QuestCompletion Component on Child Object
CompleteObjective2, // References QuestCompletion Component on Child Object
CompleteObjective3, // References QuestCompletion Component on Child Object
CompleteObjective4, // References QuestCompletion Component on Child Object
ReceiveItem, // References ItemGiver Component (Player->NPC)
TriggerPortal, // References SetPortalState on Portal Object
TriggerShop, // References SetActiveShop on Player Object
DialogBranchFinished1, // References DialogueItemGiver component
DialogBranchFinished2, // References DialogueItemGiver component
DialogBranchFinished3, // References DialogueItemGiver component
DialogBranchFinished4, // References DialogueItemGiver component
DialogBranchFinished5, // References DialogueItemGiver component
DialogBranchFinished6, // References DialogueItemGiver component
DialogBranchFinished7, // References DialogueItemGiver component
DialogBranchFinished8, // References DialogueItemGiver component
DialogBranchFinished9, // References DialogueItemGiver component
DialogBranchFinished10, // References DialogueItemGiver component
} // Use an OnDie call to CompleteObjective to complete kill quests
}
A DialogueItemGiver component that will be triggered and contain the NPC we are talking to and the branch number we want to remember. This is where it can get a little bit messy in implementation as each branch will have to have the number manually assigned.
using GameDevTV.Inventories;
using RPG.Inventories.Items;
using UnityEngine;
namespace RPG.Inventories
{
public class DialogueItemGiver : MonoBehaviour
{
// CONFIG DATA
[SerializeField] NPCDialogueItem item;
[SerializeField] int dialogBranchNumber;
// PUBLIC
public void GiveDIalogueItem()
{
Inventory playerInventory = GameObject.FindWithTag("Player").GetComponent<Inventory>();
playerInventory.AddToFirstEmptySlot(item, dialogBranchNumber);
}
}
}
On to the meat of the Store. The nice thing about the store is we never have to take information out of it or do anything with the InventoryItem itself, so all we need to store is the npc ID and an integer list of branches that have been visited. This also allows us to implement probably the easiest Capture/Restore in the entire RPG. I added GetItemCount(InventoryItem item) to IItemStore for use by the Predicate Evaluator for some other Stores I created, which isn’t used in this class, but am using the one that takes the number.
using System;
using System.Collections;
using System.Collections.Generic;
using GameDevTV.Inventories;
using GameDevTV.Saving;
using RPG.Inventories.Items;
using UnityEngine;
namespace RPG.Inventories
{
public class DialogueStore : MonoBehaviour, IItemStore, ISaveable
{
Dictionary<string, List<int>> slotDictionary = new Dictionary<string, List<int>>();
public int AddItems(InventoryItem item, int number)
{
NPCDialogueItem dialogueItem = item as NPCDialogueItem;
if (slotDictionary.ContainsKey(dialogueItem.NPC))
{
foreach (int branchNumber in slotDictionary[dialogueItem.NPC])
{
if (branchNumber == number)
{
//Debug.Log("Branch Exists");
return number; // Branch already exists in the dictionary for the object
}
}
List<int> branchNumbers = new List<int>();
branchNumbers = slotDictionary[dialogueItem.NPC];
branchNumbers.Add(number);
//Debug.Log("Added Branch to existing NPC");
return number; // NPC slot exists and new Branch is added
}
List<int> newBranchNumber = new List<int>();
newBranchNumber.Add(number);
slotDictionary[dialogueItem.NPC] = newBranchNumber;
//Debug.Log("Added New Branch to New NPC");
return number;
}
public int GetItemCount(InventoryItem item)
{
return 0;
}
public bool GetItemCount(InventoryItem item, int number)
{
NPCDialogueItem dialogueItem = item as NPCDialogueItem;
if (slotDictionary.ContainsKey(dialogueItem.NPC))
{
foreach (var branchNumber in slotDictionary[dialogueItem.NPC])
{
if (branchNumber == number)
return true;
}
}
return false;
}
object ISaveable.CaptureState()
{
return slotDictionary;
}
void ISaveable.RestoreState(object state)
{
slotDictionary = (Dictionary<string, List<int>>)state;
}
}
}
I made a small change to Inventory.Evaluate for this (and larger changes for other stores). This is the only place of integration outside of the dialogue system.
case EPredicate.HasInventoryItems: //Only works for stackable items.
// This will work for Gold and Experience
int otherStoreCount = CheckForConditionsInStores(InventoryItem.GetFromID(parameters[0]));
if (otherStoreCount > 0 && (int.TryParse(parameters[1], out int storeResult)))
return otherStoreCount > storeResult;
// This will work for Dialogue Branches
if (InventoryItem.GetFromID(parameters[0]) is NPCDialogueItem)
{
int dialogueBranch;
int.TryParse(parameters[1], out dialogueBranch);
return GetComponent<DialogueStore>().GetItemCount(InventoryItem.GetFromID(parameters[0]), dialogueBranch);
}
// This will work for aggregating stackable items in ActionStore and Inventory
int actionStoreCount = 0;
if (InventoryItem.GetFromID(parameters[0]) is ActionItem)
actionStoreCount = CheckForConditionsInActionStore(InventoryItem.GetFromID(parameters[0]) as ActionItem);
InventoryItem item = InventoryItem.GetFromID(parameters[0]);
int stack = FindStack(item) + actionStoreCount;
if (stack == -1) return false;
if (int.TryParse(parameters[1], out int result))
{
return slots[stack].number + actionStoreCount >= result;
}
return false;
Not directly related, but I added the Predicate Evaluator check into Dialogue.GetRootNode which allows you to put conditions on the root node, so if you have visited certain branches (or other conditions), the entry point may change.
public DialogueNode GetRootNode()
{
foreach(DialogueNode node in nodes)
{
if (!HasParent(node))
{
if (node.CheckCondition(GameObject.FindGameObjectWithTag("Player").GetComponents<IPredicateEvaluator>()))
return node;
}
}
return nodes[0];
}
From a configuration perspective, the only things that are important on the NPCDialogueItem are the Display Name, Stackable set to true, and NPC GUID.
On the NPC, the DialogueTrigger “should” match the Dialogue Branch Number.
And then the Node condition just uses the standard Has Inventory Items with the NPC and branch number selected.