I took a different approach to handling predicates that I thought I might share, which is to implement the Rules Design Pattern.
All Rules (aka predicates) implement the IPredicate
interface below, which only has one method.
public interface IPredicate {
bool Evaluate();
}
For example, the QuestList might want to define some related rules that could be used anywhere in our game. Many different GameObjects might want to know if certain things about the Player’s QuestList are true or false.
To do this, we can create a base class that inherits from IPredicate
and handles anything common to all rules specific to the QuestList class.
All the Rules which inherit from the base class implement their specific rule logic, and that’s all.
Each Rule can expose more fields as required to the Editor, and the GetData method can be overridden with custom code if the Rule needs more info about the game state.
QuestList.cs
...
public abstract class QuestListPredicate : IPredicate {
protected QuestList questList;
protected List<QuestStatus> questStatuses;
protected virtual void GetData() {
questList = GameObject.FindWithTag("Player").GetComponent<QuestList>();
questStatuses = questList.GetQuestStatuses().ToList();
}
protected abstract bool IsValid();
public bool Evaluate() {
GetData();
return IsValid();
}
}
[System.Serializable]
public class PlayerHasQuest : QuestListPredicate {
[SerializeField] protected Quest quest;
protected override bool IsValid() {
return questStatuses.Any(qs => qs.GetQuest() == quest);
}
}
[System.Serializable]
public class PlayerHasCompletedQuest : QuestListPredicate {
[SerializeField] protected Quest quest;
protected override bool IsValid() {
return questStatuses.Any(qs => qs.GetQuest() == quest && qs.IsComplete());
}
}
...
Now, all a DialogueNode has to do is evaluate the rules have been assigned to it. In fact, anything in the game could evaluate any collection of rules you define so long as it has a list of rules and calls a method similar to the IsValid
method below.
DialogueNode.cs
[SerializeField] List<IPredicate> rules = new List<IPredicate>();
...
public bool IsValid() => rules.All(rule => rule.Evaluate());
IsValid
can be called anytime you want to know if a DialogueNode should be displayed (or included in a List, etc.) I limited my example such that my rules were defined as sub classes on the QuestList class, but the rules can be defined anywhere that makes sense to the developer.
Finally, I really enjoyed Brian’s post about implementing PropertyDrawers, and wanted to add more complex boolean logic right in the Editor, I simply added a few more classes which also inherit from IPredicate
since Rules can contain their own Rules of course.
public class And : IPredicate {
[SerializeField] List<IPredicate> rules = new List<IPredicate>();
public bool Evaluate() => rules.All(r => r.Evaluate());
}
public class Or : IPredicate {
[SerializeField] List<IPredicate> rules = new List<IPredicate>();
public bool Evaluate() => rules.Any(r => r.Evaluate());
}
public class Not : IPredicate {
[SerializeField] IPredicate rule;
public bool Evaluate() => !rule.Evaluate();
}
Here is a good article about the Rules Design Pattern if anyone wants to know more about how it is applied in the non-gamedev world.