Probably one of the most difficult sections of the RPG series code to debug is the Conditions we use to guide our dialogues and later in the course to allow things like requiring a certain trait or level before equipping an item. Much of this is because the entire condition system becomes a bit obscure to the debugging process once it starts evaluating predicates. There are many things that can go wrong, much of this because of a reliance on magic strings.
The goal of this post is to make some structural changes to the way that the Conditions work, and create a PropertyDrawer to draw those predicates in a way that is easier to view and will select the possible parameters for us.
This one’s not for the faint of heart, we’re going to be creating two property drawers to create our one property drawer which will evaluate conditions…
First things first, we’re going to make a fundamental change to the way we handle predicates. Note that this is going to break all of your existing conditions you have in any dialogues or equipment requirements. This is because we’re going to be changing the predicate itself from an enum to a string. You might take the time to write down all of your conditions, or even consider forking your project at this point into a new project so you can have them side by side if you have a lot of them. (For continuity’s sake, make a copy of your project in a new project folder and add the new project to your Unity Hub. You can then open this project when it’s time to redo all of your conditions when we’re done. Put that copy aside, and work on your original project.)
Either way, BACK UP YOUR PROJECT AND COMMIT THE CURRENT STATE TO SOURCE CONTROL. I only make this reminder once for each major change I do to the code, so make sure you do it.
Predicates Should Be Enums!!!
So one of the biggest problems with the condition system starts in the predicate itself. We are using a string instead of an enum. This means that while you may have written a beautiful IPredicateEvaluator for HasQuest
, if you put Hasquest
in your predicate field, nobody will evaluate it. In the scenarios in which students have said “this isn’t being evaluated”, this accounts for about 1/2 of the issues. Let’s fix it so that we are using an Enum instead of a string. Even if you don’t take the deep dive to the propertydrawer, you can make this simple fix, and you’ll just have to select the predicates in your existing conditions and you’re on your way, knowing you’ll never misspell a predicate again…
Let’s start in our IPredicateEvaluator.cs script. I decided that this is the best place to add the Enum.
namespace GameDevTV.Utils
{
public enum EPredicate
{
Select,
HasQuest,
CompletedObjective,
CompletedQuest,
HasLevel,
MinimumTrait,
HasItem,
HasItems,
HasItemEquipped
}
public interface IPredicateEvaluator
{
bool? Evaluate(EPredicate predicate, string[] parameters);
}
}
If it happens that you’re still in Dialogues and Quests, in the next course, Shops and Abilities, we moved the Conditions code into the GameDevTV.Utils namespace. This allows us to include the conditions in the InventorySystem without relying on your personal code (keeping the inventory system portable and easy to move into other games).
So we’ve created a new Enum, EPredicate. Since we already have a Predicate class in our conditions, I went with the convention of prefixing an enum with an E, which is something I personally do in all of my projects. The EPredicate has all of the possible conditions in your game. If you want to add a condition type, add it here and then make sure you have a component capable of interpreting it.
Next, we change the IPredicateEvaluator interface itself. In this case, we’re changing the predicate from a string to an EPredicate.
At this point, your code editor should be flagging some warnings. Here’s my errors window from JetBrains Rider:
Most of these deal with the methods that implement IPredicateEvaluator. One deals with Condition trying to pass a string instead of an EPredicate. Let’s tackle those one by one. If there’s a component in my list you haven’t implemented yet, don’t worry about it, you can do it when you get to it. If you have a condition not listed here, you should have a good understanding of what needs to be fixed.
Let’s start with the error in Condition:
Argument type 'string' is not assignable to parameter type 'GameDevTV.Utils.EPredicate'
This is a pretty simple fix, just change the declaration in class Predicate:
class Predicate
{
[SerializeField]
EPredicate predicate; //This was string predicate;
Note that at this point, all of your conditions are broken, and you’ll need to reassign the predicates. Trust me, it’ll be worth it for yourself or anybody you allow to wear the designer hat. You’ve misspelled your last predicate!
We still have a lot more errors, though. We’ll start with QuestList since this is the one that everybody has certainly implemented. The compiler’s complaint is:
Interface member 'bool? GameDevTV.Utils.IpredicateEvaluator.Evaluate(EPredicate, string[])' is not implemented
There are two solutions to this: One is to create a new method to cover the interface. Personally, I prefer just editing the method we already have. Here’s the current QuestList.Evaluate() method:
public bool? Evaluate(string predicate, string[] parameters)
{
switch (predicate)
{
case "HasQuest":
return HasQuest(Quest.GetByName(parameters[0]));
case "CompletedQuest":
return GetQuestStatus(Quest.GetByName(parameters[0])).IsComplete();
case "CompletedObjective": //This may not be implemented in your version, consider it a freebie
QuestStatus status = GetQuestStatus(Quest.GetByName(parameters[0]));
if (status==null) return false;
return status.IsObjectiveComplete(parameters[1]);
}
return null;
}
The header doesn’t match, as it’s expecting a string. Here’s my revised script with the EPredicate. I’ve also taken the liberty of doing a little error checking in CompletedQuest.
public bool? Evaluate(EPredicate predicate, string[] parameters)
{
switch (predicate)
{
case EPredicate.HasQuest:
return HasQuest(Quest.GetByName(parameters[0]));
case EPredicate.CompletedQuest:
QuestStatus status = GetQuestStatus(Quest.GetByName(parameters[0]));
if (status == null) return false;
return status.IsComplete();
return GetQuestStatus(Quest.GetByName(parameters[0])).IsComplete();
case EPredicate.CompletedObjective:
QuestStatus teststatus = GetQuestStatus(Quest.GetByName(parameters[0]));
if (teststatus==null) return false;
return teststatus.IsObjectiveComplete(parameters[1]);
}
return null;
}
Simple enough, we’ve replaced the strings with enums here. We’ll do the same thing in the remaining components. (Note: If you don’t have these components/IPredicateEvaluators implemented yet, don’t worry about them, just make the adjustments when you do).
Inventory.cs
public bool? Evaluate(EPredicate predicate, string[] parameters)
{
switch (predicate)
{
case EPredicate.HasItem:
return HasItem(InventoryItem.GetFromID(parameters[0]));
case EPredicate.HasItems: //Only works for stackable items.
InventoryItem item = InventoryItem.GetFromID(parameters[0]);
int stack = FindStack(item);
if (stack == -1) return false;
if (int.TryParse(parameters[1], out int result))
{
return slots[stack].number >= result;
}
return false;
}
return null;
}
Equipment.cs
public bool? Evaluate(EPredicate predicate, string[] parameters)
{
if (predicate == EPredicate.HasItemEquipped)
{
foreach (var item in equippedItems.Values)
{
if (item.GetItemID() == parameters[0])
{
return true;
}
}
return false;
}
return null;
}
TraitStore.cs
public bool? Evaluate(EPredicate predicate, string[] parameters)
{
if (predicate == EPredicate.MinimumTrait)
{
if (Enum.TryParse<Trait>(parameters[0], out Trait trait))
{
return GetPoints(trait) >= Int32.Parse(parameters[1]);
}
}
return null;
}
BaseStats.cs
public bool? Evaluate(EPredicate predicate, string[] parameters)
{
if (predicate == EPredicate.HasLevel)
{
if (int.TryParse(parameters[0], out int testLevel))
{
return currentLevel.value >= testLevel;
}
}
return null;
}
Now is a good time to go through all of your dialogues, and anything else that uses Conditions and change the Predicate from “Select” to the correct predicate. If you didn’t make notes or save a copy of your project to open and refer to, you can usually guess by the context…
So we’ve made the conversion of the Predicate from string to enum, but we still have the issue of the string references (and in the case of InventoryItems, obscure ones) plaguing us.
That’s going to take a bit more work, and we’ll tackle that in the next post.