Using Value Resolvers

Value resolvers take a string and resolve it into a value of a specified type. They're used extensively throughout Odin's attribute implementations to increase the flexibility and ease-of-use of attributes.

They're quite easy to use - let's make a simple attribute that will color a property if a given condition is true.

First, we start with the basics - making an attribute and a drawer for it. You can see this tutorial for the basics of creating attribute drawers:

// ColorIfAttribute.cs
using System;

public class ColorIfAttribute : Attribute
{
    public string Color;
    public string Condition;

    public ColorIfAttribute(string color, string condition)
    {
        this.Color = color;
        this.Condition = condition;
    }
}

// ColorIfAttributeDrawer.cs
using Sirenix.OdinInspector.Editor;
using Sirenix.Utilities.Editor;
using UnityEngine;

public class ColorIfAttributeDrawer : OdinAttributeDrawer<ColorIfAttribute>
{
    protected override void DrawPropertyLayout(GUIContent label)
    {
        bool condition = false; // TODO: Get condition boolean
        
        if (condition)
        {
            Color color = default(Color); // TODO: Get color
            GUIHelper.PushColor(color);
        }
        
        this.CallNextDrawer(label);

        if (condition)
        {
            GUIHelper.PopColor();
        }
    }
}

Now we have an attribute that can conditionally color the GUI of the property you put it on - but it doesn't do anything! Now we just need to get values out of the Color and Condition strings! For this, we need to add the Sirenix.OdinInspector.Editor.ValueResolvers namespace, and create value resolvers for the Color and Condition strings in the drawer's Initialize() method, and then get the condition and color from them:

// ColorIfAttributeDrawer.cs
using Sirenix.OdinInspector.Editor;
using Sirenix.OdinInspector.Editor.ValueResolvers;
using Sirenix.Utilities.Editor;
using UnityEngine;

public class ColorIfAttributeDrawer : OdinAttributeDrawer<ColorIfAttribute>
{
    private ValueResolver<Color> colorResolver;
    private ValueResolver<bool> conditionResolver;

    protected override void Initialize()
    {
        this.colorResolver = ValueResolver.Get<Color>(this.Property, this.Attribute.Color);
        this.conditionResolver = ValueResolver.Get<bool>(this.Property, this.Attribute.Condition);
    }

    protected override void DrawPropertyLayout(GUIContent label)
    {
        bool condition = this.conditionResolver.GetValue();
        
        if (condition)
        {
            GUIHelper.PushColor(this.colorResolver.GetValue());
        }
        
        this.CallNextDrawer(label);

        if (condition)
        {
            GUIHelper.PopColor();
        }
    }
}

This is enough to get it working! We can now make use of the ColorIf attribute, and do things like reference a method or use an expression to get the condition and color:

// Example.cs
using UnityEngine;

public class Example : MonoBehaviour
{
    [ColorIf("@Color.green", "ColorCondition")]
    public string coloredString;

    private bool ColorCondition()
    {
        // Color the property if the string has an even number of characters
        return coloredString?.Length % 2 == 0;
    }
}

However, there's an issue here! Currently, if we pass in a bad string, we won't get any visual feedback at all - the attribute will just silently do nothing! The attribute does nothing because, if there is an error resolving the string, then the resolver will simply pass out the default value of the type it resolves

  • such as false for a bool, or default(Color) for a Color. You can also pass in specific default values to ValueResolver.Get() that will be used in case of errors.

We need to tell the resolvers to draw errors, if there are any. They will generate decent error messages, and provide the necessary feedback. This is simply done by calling DrawError() on the resolver; if there is an error, it will be drawn.

When we have several resolvers, we can also call the ValueResolver.DrawErrors() method and pass in all our resolvers, for convenience. We can also check HasError on resolvers to see if they had an error or not.

// ColorIfAttributeDrawer.cs
using Sirenix.OdinInspector.Editor;
using Sirenix.OdinInspector.Editor.ValueResolvers;
using Sirenix.Utilities.Editor;
using UnityEngine;

public class ColorIfAttributeDrawer : OdinAttributeDrawer<ColorIfAttribute>
{
    private ValueResolver<Color> colorResolver;
    private ValueResolver<bool> conditionResolver;

    protected override void Initialize()
    {
        this.colorResolver = ValueResolver.Get<Color>(this.Property, this.Attribute.Color);
        this.conditionResolver = ValueResolver.Get<bool>(this.Property, this.Attribute.Condition);
    }

    protected override void DrawPropertyLayout(GUIContent label)
    {
        ValueResolver.DrawErrors(this.colorResolver, this.conditionResolver);
        
        // If there is a condition error, this value will default to default(bool), which is false in this 
        // case. Therefore, we don't need to check if there is an error to set the bool to false - it will 
        // always be false when there is an error.
        bool condition = this.conditionResolver.GetValue();
        
        // However, we *do* need to check for errors in the color resolver, because default(Color) is the
        // same as new Color(0, 0, 0, 0) - a color with 0 alpha! If we draw using that color, then the UI
        // will become completely transparent, IE, invisible! Therefore, we never color anything if the 
        // color resolver has an error.
        // 
        // The alternative to this is to pass in Color.white as the default fallback when we create the
        // color resolver in Initialize(). Which method you choose is simply a matter of taste.
        if (this.colorResolver.HasError)
        {
            condition = false;
        }

        if (condition)
        {
            GUIHelper.PushColor(this.colorResolver.GetValue());
        }
        
        this.CallNextDrawer(label);

        if (condition)
        {
            GUIHelper.PopColor();
        }
    }
}

And now if we pass in a bad string to the attribute, we will get a helpful error message in the inspector that explains which kinds of strings can be resolved as values if there was a failure to resolve the string, or if a possible resolution had an error, such as an expression compiler error:

// Example.cs
using UnityEngine;

public class Example : MonoBehaviour
{
    [ColorIf("@Color.thisIsAnError", "Bad String!")]
    public string coloredString;
}

And there we have it. As you can see, using value resolvers practically couldn't be easier!

Resolving string values

But wait, we're not done yet! There is one special case that calls for an additional example. Specifically, if you are resolving a string into the type string, then there are helpful utilities to use that get you the default behaviour that you are used to from attributes like LabelText and Title.

What we're talking about here is the fact that strings are a bit of a special case. After all, if you're resolving a string to a string, then the resolved string might itself be a valid result without any "resolving" happening at all. When you write [Title("My Custom Title")] you expect the title to be "My Custom Title". But if you resolve a string like in the examples given above, then you will simply get an error instead.

The solution for cases where this behaviour is wanted is simply to pass in the string as the default fallback value:

// The third optional argument is the default fallback value to use if string resolution fails
this.someStringResolver = ValueResolver.Get<string>(this.Property, this.Attribute.SomeString, this.Attribute.SomeString);

The above code where you pass in the same argument twice looks a bit strange, though, so we've added a special GetForString method that amounts to the exact same code above:

// The GetForString method is the equivalent of the above code example
this.someStringResolver = ValueResolver.GetForString(this.Property, this.Attribute.SomeString);

Finally, when you resolve strings as in these examples, then it also enables a special behaviour in the value resolver system: prepending a string with "$" turns it into a member reference. As such, the string "MyTitle" will resolve to "MyTitle", but the string "$MyTitle" will resolve to the contents of the MyTitle field, property or method - or an error if no such member exists!

Only in this one case will "$" ever receive special treatment when prepended to a resolved string. However, you will see it used quite often, as it is a very common thing in Odin to resolve strings into labels, titles, and the like.

More to learn

And with that, we've covered the basic usage of value resolvers - but there's still more learn! As you can see in some of the example error messages above, they sometimes give a list of something called "named values" - just what are those? Well, they're a huge part of the power and convenience of value and action resolvers.

Let's go take a look at Named Values, or perhaps you want to check out Using Action Resolvers next?