- Home /
Polymorphism for decisions in a dialogue editor
To give some context, I am creating a custom editor window to create and edit node-based dialogue files that can then be interpreted during run time, using Unity's ScriptableObject class. It assigns each node an ID which is the same as its index in a list of nodes. Each node contains information on what will be said and what the possible responses are, as well as which nodes, defined by their IDs, each possible response links to.
The current issue I have is that, at certain points, there will be decisions that have to be made based on what choices the player has previously made in the game, how far through the game they are etc. as to where the conversation will go, and sometimes I will need to modify variable values based on how the player responds to certain things. However, I am having trouble implementing this sort of functionality. I thought about using a load of classes that derive from a base abstract class, but I ran into numerous problems with this - I didn't know how I would provide some kind of ability to assign it within the custom editor, and I'm not entirely sure how to create the necessary asset files in that case.
Is there any way that I could implement some kind of decision-making functionality in this way? I'm not sure if I've included all of the relevant information, but I'll provide any more as necessary to the best of my ability.
I'm interested in understanding what is going on and feel like I might be slightly out of my depth in what I'm trying to do, so if any solutions could be explained that would be great.
Thanks.
What exactly are you asking for? A way to implement node-specific checks that can be selected (or even edited) in your custom editor?
Yes, some kind of way of executing a piece of code, not necessarily created within the editor itself, that would be able to decide, based on variables within the game, which path the conversation should take. I would also like it to be able to modify existing variables, if, for example, a specific response to a question is chosen that causes the NPC to react differently to the player from then on.
So essentially you need a way to selectively run code based on values you can set in your code editor. There is actually a large number of ways of doing it, but finding a solution that scales well is a bit more tricky. Why exactly aren't ScriptableObjects an option, with different types of ScriptableObjects equaling certain actions? You could create multiple instances of those ScriptableObjects with varying parameters that could then be assigned accordingly to your dialogue 'action slots'.
Answer by Bunny83 · Nov 24, 2016 at 04:19 PM
Well, it all highly depends on how your system is actually used at runtime. Pure dialog-trees are completely static data that is simply used at runtime. However you want now an information flow in the opposite direction. So your static dialog data should reference runtime state. This is in general a bit tricky.
The easiest way would probably be introducing "variables" / parameteres just like the Mecanim system does. It uses this class to represent a parameter which is created inside the mecanim system.
The more variable types you want to support the more complicated / complex things might get. In theory a float would be enough to represent fractional number, whole numbers and booleans.
The link between your dialog nodes and the variables should be made by a string. Indices are dangerous as when you remove an element from the beginning the indices after would be wrong. You also need a name for each variable to actually "set" the value from scripting.
For example something like that:
[System.Serializable]
public class DynVariable
{
public string name;
public float initialValue;
private float currentValue;
public float Value
{
get { return currentValue; }
set { currentValue = value; }
}
}
public class YourDialogMasterScript : ScriptableObject
{
public List<DynVariable> variables;
private Dictionary<string, DynVariable> varLookup = null;
public void SetVariable(string aName, float aValue)
{
if (varLookup == null)
InitVariables();
DynVariable tmp;
if (varLookup.TryGetValue(aName, out tmp))
{
tmp.Value = aValue;
return;
}
Debug.LogWarning("SetVariable: variable with name '" + aName + "' doesn't exist");
}
public float GetVariable(string aName)
{
if (varLookup == null)
InitVariables();
DynVariable tmp;
if (varLookup.TryGetValue(aName, out tmp))
{
return tmp.Value;
}
Debug.LogWarning("GetVariable: variable with name '"+aName+"' doesn't exist");
return 0f;
}
void InitVariables()
{
varLookup = new Dictionary<string, DynVariable>();
for (int i = 0; i < variables.Count; i++)
{
varLookup.Add(variables[i].name, variables[i]);
variables[i].Value = variables[i].initialValue;
}
}
void OnEnable()
{
if (varLookup == null)
InitVariables();
}
}
public enum EComparison
{
Equal,
NotEqual,
Greater,
GreaterOrEqual,
Lower,
LowerOrEqual,
}
[System.Serializable]
public class DynVarCondition
{
public string variableName;
public EComparison comparison;
public float refValue;
public bool roundToInt = false;
public bool ConditionMet(YourDialogMasterScript aMaster)
{
float v = aMaster.GetVariable(variableName);
if (roundToInt)
v = Mathf.RoundToInt(v);
switch(comparison)
{
case EComparison.NotEqual: return v != refValue;
case EComparison.Greater: return v > refValue;
case EComparison.GreaterOrEqual: return v >= refValue;
case EComparison.Lower: return v < refValue;
case EComparison.LowerOrEqual: return v <= refValue;
case EComparison.Equal:
default: return v == refValue;
}
}
}
public class YourDialogNodeTransition
{
public List<DynVarCondition> conditions;
public bool ConditionsMet(YourDialogMasterScript aMaster)
{
for (int i = 0; i < conditions.Count; i++)
if (!conditions[i].ConditionMet(aMaster))
return false;
return true;
}
}
DynVariable represents an actual "variable". They are defined in your dialog master script or any other global script. From scripting you can use the GetVariable and SetVariable methods to read and write the variable at runtime.
Inside your dialog node transitions or other place where you need an external condition you simply add a list of "DynVarConditions". There the user can specify a variable name, a reference value and how they should be compared. At runtime when you would use the "ConditionsMet" method to evaluate if the conditions are currently met or not.
Note that all conditions need to be met. So it's an implicit "and" connection between them. If you want an optional "or" it gets way more complicated as you would need a true evaluation tree. The WarCraft3 map editor uses such a node based evaluation tree, but that's far more than a dialog editor but a whole visual scripting solution. You have to decide how much you want to solve visually and hom much you want to solve via scripting. Some things are way easier to solve with a simple C# script.
As for the usability you could create some custom inspectors / editorwindows for editing those variables / conditions so the user can't create duplicates (with the same name) and can only select existing variables from a list instead of typing in the variable name. But that's not necessary.
How you actually implement that in your current system is up to you ^^.
ps: The warcraft3 condition tree is really easy and straight forward to use, however creating such a system requires a lot of classes and to get the usability right a lot of editor scripting. So it's your decision if it's worth implementing ^^.
I've written this ExpressionParser some time ago, however it only evaluates mathematical expressions and not logical expressions ^^.
Awesome, this looks like it should be able to do everything I need! I probably should have thought of this considering how much I've been using the mecanim system recently.
In response to the concerns about using indices - I have dealt with the problems you listed. when I want to remove a node, I simply set that item in the list to null, and have a check when drawing the nodes for the ones that are null, and when adding a node, I check if there are any null nodes and decide where the new node should be added accordingly - it all seems to be working so far. It might not be incredibly efficient, but it taught me a fair few things working out how to sort out something like that.
I think I've got it sorted in my head how I would implement the system you've suggested, so I'll get on it. Thanks for the help!