Improving Conditions: A Property Editor

Yes, we should!

1 Like

This is one of the advantages of using an enum as it works out of the box that way. One of the disadvantages is that you have things set up and working.

One if the disadvantages of using an enum.
You change that enum to be

  c
  g
  b

This will cause you to have to go through all of your game objects and make sure that they are no correct. Not a big deal on a small prototype, but on a huge project or even with a team of people working together it becomes easy to miss one.
Using SO solves this issue, just rename the SO as you need.

If you want a drop down list for your SO you can create a Property Drawer for it, in the Property Drawer it collects all of the available SO of that type and displays them in a list.

1 Like

The standard inspector (or in an editor, an ObjectField) will properly show all ScriptableObjects of the same type, so

[SerializeField] ScriptableStat statToUse

will provide a standard object picker which contains only ScriptableStats.

If only Unity would provide the same Functionality for MonoBehaviours attached to prefabs.

1 Like

I like using the standard object picker, but it doesnā€™t show as a drop down selection list like enums which for some reason people like better.

I use Jet Brains Rider as my IDE which you can find all Unity usage. They also have a Asset called Rider Flow, I believe that this is included the ability to find all MonoBehaviour that is attached to a prefab.

I wish that this was something built straight into Unity, right click on the MonoBehaviour and find all Game Objects.

This is something that can be done with their search feature. You have to have properties indexed in the index manager.

Windows >Search>Index Manager
Under Options make sure Properties is selected.

t: prefab has:ScriptableStat

I wonder if you need to specify a prebfab or if you can just use the has: like

has:ScriptableStat

I know you can search for properties that contain a certain value

statToUse:exactNameOfScriptableStat

There is a whole list of search filters that you can use replace 2021.2 with 2022
.3 to see those that work in 2022

There is also a whole list of search providers that we can use. Like p: for project

Unity has a power full built in search tool. I forget that these filters are available when I am searching my project for things.

I just stumbled upon an Attribute that you can use when I was looking up the filters that can be used, I havenā€™t used it yet.

Scroll down to the Search Picker Heading.

OK so I was playing around a little bit with the code. It works beautifully, but the ā€˜HasLevelā€™ option isnā€™t working for some reason. I tried a few of the other options, they seem to work well, but ā€˜HasLevelā€™ isnā€™t working for some reason. Where can we investigate this from?

Did you set up your BaseStats.cs to be an IPredicateEvaluator and add in the needed code to test for HasLevel?

Forgot to type in ā€˜IPredicateEvaluatorā€™ when defining the ā€˜BaseStats.csā€™ classā€¦ Thanks again Brian :sweat_smile:

Oh my goodness you saved me. This not only fixed a problem with this specific tutorial but it makes other parts of the unity inspector (lists, etc) work so much better.

For anyone else, if youā€™re taking this tutorial and youā€™re seeing stuff like the below, read the post I quoted. I was literally about to put in a magic number hack ā€œposition.x += 50ā€ until I saw this post.

Also - I am excited about this tutorial but also a little (a lot, actually) hesitant to proceed with anything past ā€œPart 1ā€ Predicates as Enums. On the other hand I know Iā€™ll incur some debt if I keep proceeding without finishing this. Iā€™m just concerned about my ability to further enhance Editor scripts on my own as I add more predicates. For now Iā€™m shelving my work but I want to come back to this once I get some more core functionality done.

Iā€™m using the Unity 2021 LTS version. It worked perfectly fine till the end for me personally, if it helps anyone reading this in the future in anyway

There are two ways to get good at enhancing the Editor scripts:

  • Work through this tutorial on Editor Scripts, paying attention to the tricks along the way.
  • Work through this tutorial on Editor Scripts, paying attention to the tricks along the way.

The good news is, while Iā€™ve tried to think of most of the possibilities, if youā€™ve got one youā€™re working on, thereā€™s a fairly decent chance that if youā€™ve got a new condition I havenā€™t thought of yet, that Iā€™ll be around to give you pointers.

Thank you Brian. I canā€™t tell you how much I appreciate your contributions. I may try the Inventory Item editor since that seems easier than this one and potentially addresses a bigger issue for me.

I took the conditions on a tangent by creating a conditional quest objective SO. This class is functions like an objective but with some bonuses - it automatically sets up predicates, parameters, achievement counters based on how the objective is configured. It has a built in Check method so it also functions like a Condition. I also use QuestList.CompleteObjectivesByPredicate and a modification on your AchievementCounter and so that combination dramatically cuts down the number of magic strings I need to worry about.

I initially wanted to complete this tutorial in its fullest before expanding game features like that (to not create complex technical debt to unwind) but now I think a better plan is to get the features working at the risk of carrying a bit of tech debt.

Brian,

I ended up trying this ScriptableObject as Enum ā€“ not for predicates but for something else where I needed a ā€œdistributedā€ for lack of a better word way of expanding the possible list of enum values. I implemented some code that looks very much like your example but I got this error in my IDE (Rider).

A constant value is expected

I understand the problem but I donā€™t know how to fix it. I tried searching in a few places but came up blank. My code looks similar to yours. What am I doing wrong?

It would help to know which specific script yielded the error, and more about the ScriptableObject as Enum situation that youā€™re usingā€¦

One thing about ScriptableObject as Enumā€¦ I have used them effective, but it comes with some extra troublesome work that can beā€¦ tediusā€¦

For example: I converted the Stat enum to be a ScriptableStat, which was great because you can create new Stats on the fly in the Editor. The problem came when it was time to use themā€¦
For instance, you have to set up serialized fields in each of the components that use stats to tell them which stats to useā€¦ like

[SerializeField] ScriptableStat healthStat; //Link health ScriptableStat here

or a damage stat in Fighter, etc.

Forgetting to do this on a compnent usually spelled trouble.

Anyways, more information on the stat, and the script that Rider is (most likely correctly) giving you an error (and mark the line!).

Itā€™s very similar to your example on Evaluate. Sorry I should have mentioned it was the switch statement. It just looks like you canā€™t use a ScriptableObject in a switch statement. It happens on any line starting with the word case.

My code

    public class ContentPageType : ScriptableObject
    {
    }
    public class CountryTopic : AbstractTopic
    {
        [SerializeField] private ContentPageType homePage;

        public PageContentData GetContentFor(ContentPageType contentPagetype)
        {
            switch (contentPagetype)
            {
                case homePage: //this line has the error "A constant value is expected"
                        Debug.Log("Home Page");
                        break;
            }
        }
    }

Yep that wonā€™t work. A switch on contentPageType would only work on subclasses of ContentPageTypeā€¦ for example:

public void DoSomethingWithAnInventoryItem(InventoryItem item)
{
    switch(item)
    {
          case EquipableItem: //only checks for polymorphism, doesn't cast a new value
                Debug.Log("This is an EquipableItem");
                break;
           case ActionItem:
               //you get the idea

You can, however, switch on strings, but they must be string constantsā€¦

public class ContentPageType : ScriptableObject
{
     [SerializeField] private string identifier;
     public string Identifier => identifier;
}
public class CountryTopic : AbstractTopic
    {
         private const string homePage = "Home";

        public PageContentData GetContentFor(ContentPageType contentPagetype)
        {
            switch (contentPagetype.Identifier)
            {
                case homePage: //this would only work with homePage as a const!
                        Debug.Log("Home Page");
                        break;
            }
        }
    }

This, however, makes your code reliant on magic strings, which is likely what youā€™re NOT looking for. The switch/case statement is not appropriate for this type of comparison.
What you really want is

public PageContentData GetContentFor(ContentPageType contentPage)
{
      if(contentPage == homePage) 
      {
            Debug.Log("Home page");
      } //else next condition check if any
}
1 Like

Yeah I ended up using if statements instead. So what do you do if you have 100s of ScriptableObjects (letā€™s say lots and lots of StatsEquipableItems that are imported via CSV) that each need a reference back to the various ScriptableStats to know which is which.

Now what? If I had a static definition that applied to all StatsEquipableItems that could work but Unity provides no mechanism for doing so. The other option is to map them during the import process I guess.

That gets a bit more complicated, to say the leastā€¦
In fact, any external thing like a .CSV is going to have trouble linking to a distinct scriptableObject unless that SO had some sort of unique identifier as wellā€¦

I noticed weā€™re also having part of this conversation on my ResourceRetriever, and that may be a big clue about how you would do thisā€¦

While a UUID might be overkill for ScriptableStats, assigning each one to itā€™s own unique identifier isnā€™t that hard to do, either automatically or manuallyā€¦

If, for example, you used the ResourceRetriver version I just posted there

ResourceRetriever
public class ResourceRetriever<T> where T : ScriptableObject, IHasItemID
    {
        //Dictionary already holds all items, no need for separate list.
        static Dictionary<string, T> itemLookupCache;
        
        /// <summary>
        /// Retrieves item represented by itemID.  
        /// </summary>
        /// <param name="itemID">Must be unique across all instances of T</param>
        /// <returns>cached item represented by itemID or null if not in Dictionary</returns>
        public static T GetFromID(string itemID)
        {
            BuildLookup();
            if (itemID == null || !itemLookupCache.ContainsKey(itemID)) return null;
            return itemLookupCache[itemID];
        }

        /// <summary>
        /// All items of the Retriever's type (readonly). 
        /// </summary>
        public static IEnumerable<T> Values
        {
            get
            {
                BuildLookup();
                return itemLookupCache.Values;
            }
        }
        /// <summary>
        /// ItemIDs of all items of the Retriever's type (readonly)
        /// </summary>
        public static IEnumerable<string> Keys
        {
            get
            {
                BuildLookup();
                return itemLookupCache.Keys;
            }
        }
        
        private static void BuildLookup()
        {
            if (itemLookupCache == null)
            {
                itemLookupCache = new Dictionary<string, T>();
                var itemList = Resources.LoadAll<T>("");
                foreach (var item in itemList)
                {
                    if (itemLookupCache.ContainsKey(item.GetItemID()))
                    {
                        Debug.LogError(string.Format("Looks like there's a duplicate  ID for objects: {0} and {1}",
                            itemLookupCache[item.GetItemID()], item));
                        continue;
                    }
                    itemLookupCache[item.GetItemID()] = item;
                }
            }
        }
    }

You could use OnValdiate() in the ScriptableStat to ensure that no other items with that key existed in a simlar way to what we use in SaveableEntity

void OnValidate()
{
    if(string.IsNullOrEmpty(itemID) || (ResourceRetriever<ScriptableStat>.GetFromID(itemID)!=this)
    {
         int highestValue = 0;
         foreach(string key in ResourceRetriever<ScriptableStat>.Keys)
         {
              if(int.TryParse(key, out int value)
              {
                   if(value > highestValue) highestValue = value;
              }
         }
         itemID = $"{highestValue+1}");
         ResourceRetriever<ScriptableStat>.BuildLookup(); //change BuildLookup to public in ResourceRetriever
    }
}

This would give you a consistent autoindexed value, much like a standard databases autonumber feature.
With that number in hand for each of the ScriptableStats, you can then put those numbers in your Google Sheet.

1 Like

Yes! Brian this looks like it will address the need. Your tips and tricks section is amazing.

With the help of ChatGPT + what I learned from this course + the tips and tricks, Iā€™m building an EditorWindow to import bulk items via CSV and have them appropriately cross referenced. Hope to get it working tonight.

The guidance above from Brian was helpful but I also ended up finding a simpler way to fix this.

To do the below where I have to set the same exact three ScriptableObject references across 100s or 1000s of imported Scriptable Objects, I just did a multiselect of all my applicable SOs and did three drag and drop operations (one for each ScriptableStat / ScriptablePredicate / ScriptableX).

Totally forgot about multiselect but that was the trick

Privacy & Terms