Improving Conditions: A Property Editor

First of all thank you for this awesome tutorial :slight_smile: :+1:
I was think about QuestCompletition.cs, maybe we could use the same approach to set the objective, or you think that would be excessive?

Also, I take this opportunity to ask about QuestCompletion in inspector it shows Quest as array
image

but if I click on the triangle I get this error:

ArgumentException: Getting control 1's position in a group with only 1 controls when doing repaint
Aborting

Thank you for your infinite patience again :sweat_smile:

Yes, a similar approach can be taken in QuestCompletionā€¦ in fact, hereā€™s my Editor for this:

QuestCompletionEditor.cs
using System.Collections.Generic;
using UnityEditor;

namespace RPG.Quests.Editer
{
    [CustomEditor(typeof(QuestCompletion))]
    public class QuestCompletionEditor:Editor
    {
        private SerializedProperty quest;
        private SerializedProperty objective;

        private void OnEnable()
        {
            quest = serializedObject.FindProperty("quest");
            objective = serializedObject.FindProperty("objective");
        }

        public override void OnInspectorGUI()
        {
            EditorGUILayout.PropertyField(quest);
            if (quest.objectReferenceValue == null)
            {
                EditorGUILayout.HelpBox("Please select a Quest to continue.", MessageType.Info);
                return;
            }
            EditorGUILayout.PrefixLabel("Objective");
            List<string> references = new List<string>();
            List<string> descriptions= new List<string>();
            Quest selectedQuest = (Quest)quest.objectReferenceValue;
            {
                foreach (Quest.Objective questObjective in selectedQuest.GetObjectives())
                {
                    references.Add(questObjective.reference);
                    descriptions.Add(questObjective.description);
                }
            }
            int selectedIndex = references.IndexOf(objective.stringValue);
            int testIndex = EditorGUILayout.Popup(selectedIndex, descriptions.ToArray());
            if (testIndex != selectedIndex)
            {
                objective.stringValue = references[testIndex];
            }
            serializedObject.ApplyModifiedProperties();
        }
    }
}

Iā€™m not sure why your inspector is showing thatā€¦ hopefully the editor nixes that in the bud. If not, there is an issue in your QuestCompletion class.

2 Likes

Now that Iā€™ve implemented the editor, the Quest object is working too :slight_smile:

Thanks again :sweat_smile:

Iā€™m actually experiencing a bug with the Trait Predicate after making the transition from ETrait ā†’ EStat.

Hereā€™s what is getting spat out now when attempting to pickup an Item with condition MinimumTrait

FormatException: Input string was not in a correct format.
System.Number.StringToNumber (System.String str, System.Globalization.NumberStyles options, System.Number+NumberBuffer& number, System.Globalization.NumberFormatInfo info, System.Boolean parseDecimal) (at <695d1cc93cca45069c528c15c9fdd749>:0)
System.Number.ParseInt32 (System.String s, System.Globalization.NumberStyles style, System.Globalization.NumberFormatInfo info) (at <695d1cc93cca45069c528c15c9fdd749>:0)
System.Int32.Parse (System.String s) (at <695d1cc93cca45069c528c15c9fdd749>:0)
Game.Attributes.StatStore.Evaluate (Game.Utils.EPredicate predicate, System.Collections.Generic.List`1[T] parameters) (at Assets/Scripts/Attributes/StatStore.cs:139)
Game.Utils.Condition+Predicate.Check (System.Collections.Generic.IEnumerable`1[T] evaluators) (at Assets/Scripts/Quests/Condition.cs:56)
Game.Utils.Condition+Disjunction.Check (System.Collections.Generic.IEnumerable`1[T] evaluators) (at Assets/Scripts/Quests/Condition.cs:35)
Game.Utils.Condition.Check (System.Collections.Generic.IEnumerable`1[T] evaluators) (at Assets/Scripts/Quests/Condition.cs:17)
Game.Inventories.EquipableItem.CanEquip (Game.Inventories.EquipLocation equipLocation, Game.Inventories.Equipment equipment) (at Assets/Scripts/Inventory/Items/EquipableItem.cs:39)
Game.Inventories.Inventory.AddToFirstEmptySlot (Game.Inventories.InventoryItem item, System.Int32 number) (at Assets/Scripts/Inventory/Equipment/Inventory.cs:164)
Game.Inventories.Pickup.PickupItem () (at Assets/Scripts/Inventory/Items/Pickup.cs:48)
Game.Control.PickupItem.OnTriggerEnter2D (UnityEngine.Collider2D other) (at Assets/Scripts/Inventory/Items/PickupItem.cs:13)

Double-clicking on the error inside Unity brings me to the TraitStore (I renamed to StatStore) evaluate code where itā€™s checking against the parameters[0]

        public bool? Evaluate(EPredicate predicate, List<string> parameters)
        {
            if (predicate == EPredicate.MinimumTrait)
            {
                if (Enum.TryParse<EStat>(parameters[0], out EStat traitStat))
                {
                    return GetPoints(traitStat) >= Int32.Parse(parameters[0]);
                }
            }
            return null;
        }

If Wisdom is set to ā€˜5ā€™ and my Wisdom is 5 or greater it still throws this error. ā€“ end of original post

Found the bug but not sure how to fix it yet. ā€“ edited post

After some digging,

I enabled Debug inside my Inspector and noticed some Items have parameters[0] == "" ā€“ But then some items using the exact same predicate MinimumTrait will have parameters[0] == "Strength"

It appears the ones that are causing that string format error have EStat listed under the parameters[0] == "Strength", "Wisdom" etc.

And on the other hand the items that are blank can be equipped as if no condition was set.

But just to ensure that other Predicates were working, I tried the simplest one ā€“ HasLevel and that works fine. If Iā€™m level 1, and the condition requires the player to be level 2, I can pick the item up no errors and it goes to my bag, attempt to equip, rejects it as intended.

Level up ā€“ can now slot it. So it just seems the MinimumTrait isnā€™t working as it once was

Oh I think Iā€™m suppose to Int32.Parse from parameters[1] not from the parameters[0]. Dā€™Oh!

Now itā€™s no longer producing the string formatted error inside Unity but the Predicate MinimumTrait negate isnā€™t functioning as intended.

If I set it to be false and donā€™t meet the required integer of Wisdom I cannot equip the Item, but can collect the item. However if my Wisdom is now equal or higher, it still wonā€™t allow me to equip it. What a pain in the butt.

WORKED IT OUT!!

  1. Was attempting to Parse from a string. It was suppose to be Parsing from the DrawIntSlider.
  2. On this post I was using the ā€˜GetPoints(stat)ā€™ but I shouldā€™ve been factoring in that baseStats needed to also be calculated. Therefore the fix was to change from GetPoints(stat) to GetTraitStat(stat) which returns the points invested as well as the baseStats integer.

The issue was that with GetPoints(stat) it wasnā€™t factoring in that I was starting off with 5 points into Wisdom. Therefore technically it was comparing 0 to 6 which was making it hard for me to debug. But once I did this Debug.Log

if (Enum.TryParse<EStat>(parameters[0], out EStat stat))
{
   Debug.Log($"Para[0]:{parameters[0]}/Pts:{GetTraitStat(stat)} | Para[1] Points:{parameters[1]}\nIsPara[0] >= Para[1]  ? {GetTraitStat(stat) >= Int32.Parse(parameters[1])}");
   return GetTraitStat(stat) >= Int32.Parse(parameters[1]);
}

It gave me all the information I needed. Case closed!

Good job working through all that. Iā€™m impressed by how you reasoned through the issue, debugged, and found a solution!

1 Like

Hey! Iā€™m trying to follow along with this, and itā€™s helpful. However, I keep getting this error and Iā€™m not sure at all why. The relevant error lines are

NullReferenceException: Object reference not set to an instance of an object
ā€¦(file names and memory addresses)
Assets/Scripts/Core/Evaluator/Editor/PredicatePropertyDrawer.cs:24)

on line 24, I simply have
float propHeight = EditorGUI.GetPropertyHeight(predicate);

from what I can tell, Iā€™m defining the class and the property correctly.

namespace RPG.Core.Evaluator.Editor
{
    [CustomPropertyDrawer(typeof(Condition.Predicate))]
    public class PredicatePropertyDrawer : PropertyDrawer
    {
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            SerializedProperty predicate = property.FindPropertyRelative("predicate");
            SerializedProperty parameters = property.FindPropertyRelative("parameters");
            SerializedProperty negate = property.FindPropertyRelative("negate");

            EditorGUI.PropertyField(position, predicate);

But for whatever reason, every single time I try to reference the predicate attribute it canā€™t ā€œseeā€ it at runtime. Like, the script will compile and wonā€™t show any errors, but then I get errors at any lines like this where Iā€™m trying to get/use the reference to the Predicate. Itā€™s also not correctly drawing the Condition, and will either just draw the area below it as blank, or as ā€œNullā€

Iā€™m probably missing something obvious, but does anyone have any advice?

Most likely, the declaration of the variable is different than the string in FindPropertyRelative
For example:

[SerializeField] EPredicate predication;

would cause the predicate in OnGUI to be null because property.FindPropertyRelative(ā€œpredicateā€) would fail.

I double checked and I had the name right. Just to experiment, and kind of taking an educated guess, I tried changing the PropertyDrawer to just being typeof(Condition) and now suddenly everything is being drawn correctly. Maybe I just did something weird with how I made Predicate a subclass?

Anyways, as long as itā€™s all functioning, can you foresee this potentially causing any issue or do you think itā€™s fine?

Thank you again, as always, for all of your help. Youā€™re seriously awesome Brian.

As long as itsā€™ working, Iā€™d call it a win!

This is awesome.

One recommendation if you intend on the Enum to be constantly changed is to use Scriptable Objects instead of Enums. I do not know how many times I have added an Element to my Enum or removed an Element from an Enum and spent hours trying to hunt down everything that was using that Enum and make sure that they are all set to the correct value.

I have actually done that with Stats, CharacterClasses, and Equiplocations in the pastā€¦ While it solves one problem (easily adding enums), it created a few extra problemsā€¦ Not terrible problems, but things like the Health component needing to know which ScriptableStat was the Health statā€¦

Similarly, you would need to attach the correct ScriptablePredicate(s) to each IPredicateEvaluator class so it would know what things it was testing forā€¦

Something like

[SerializeField] ScriptablePredicate hasQuest;
[SerializeField] ScriptablePredicate completedQuest;
[SerializeField] ScriptablePredicate completedObjective;

public bool? Evaluate(ScriptablePredicate predicate, string[] parameters)
{
     switch (predicate)
            {
                case hasQuest: 
                    return HasQuest(Quest.GetByName(parameters[0]));
                case completedQuest:
                    QuestStatus status = GetQuestStatus(Quest.GetByName(parameters[0]));
                    if (status == null) return false;
                    return status.IsComplete();
                case completedObjective:
                    QuestStatus teststatus = GetQuestStatus(Quest.GetByName(parameters[0]));
                    if (teststatus==null) return false;
                    return teststatus.IsObjectiveComplete(parameters[1]);
            }
            return null;
}

Yes it does make it a little more complicated. I havenā€™t gotten to this portion of the course yet. I am guessing that the use of the magic string was because this was something that was planing on being changed, or we want the designer team to have a little more control. If we are leaving it at this are the only choices then Enum is perfect solution for this. This was just a suggestion as a replacement for magic strings that are magic strings because of the limitation on changing Enums. It all depends on the reason of choosing to use magic strings in the first place.

Amazing Brian. I canā€™t say how much this post has improved quest development and dialogue triggers. Itā€™s self-explaining now and a designer could pick it up without undue frustration.

Quick tip for anyone that may be doing the course on the latest version of Unity. Iā€™ve been upgrading as Iā€™ve gone through the course to see if thereā€™s a point where everything decides to break. One thing I discovered is at some point (guessing 2022.2) the OOTB inspector started acting a little bit wonky with lists and custom inspectors were nearly unusable due to freezing unless you click off the object and then back on with each change (frustrating to say the least with the Predicates). A setting got introduced in Project Settings->Editor->Use IMGUI Default Inspector that needs to be checked on and then everything works perfectly.

2 Likes

The Other way around this is to switch the editor scripts over to UI Elements instead of using IMGU

At some point, I will. Catch is that PropertyDrawers written with UIElements are only shown as UIElements if the Editor they are showing is is using UIElements (that may have changed in 2022/2023, but last time I checked, that was the case. Now it may be that this is happening if newer versions fo Unity are defaulting to using Elements to draw the default inspectors.

This thread triggered me to learn about this. Itā€™s surprisingly easy to get started with using SO as enum. But whatā€™s the advantage of an SO for enum over something like the below? I was about to say the limitation of the below is that you canā€™t re-order items. But it seems an SO doesnā€™t solve that because SO you lose the convenience of a ā€œdrop downā€. But then when you hit select the inspector will sort the assets of the correct type so maybe SO is better?

public enum myEnum
{
  a= 1,
  b= 2,
  c= 3
};

I thought maybe you could reduce the scope of this problem by having a top level ScriptableObject referencing all your ā€œScriptableEnumsā€ and then just pass that one SO around? Youā€™re then only doing that assignment once. You could probably use OnValidate on this top-level SO to sweep up missing enums and throw a warning if you failed to map one.

Hello. Sorry for such kind of question, but should not we close here property with EditorGUI.EndProperty(); command?

That still doesnā€™t really tell you which Enum value represents Health. :slight_smile:

Would something like the below be really dumb? I havenā€™t tried itā€¦ just experimenting around live.

public class EnumReference : ScriptableObject
{
    public ScriptablePredicate HasQuest;
    public ScriptablePredicate CompletedQuest;
    public ScriptablePredicate CompletedObjective;

    public ScriptableStat Health;
    public ScriptableStat Damage;
}

Privacy & Terms