Search code examples
c#unity-game-enginestructunity3d-editorunity-editor

Unity - Custom drawing of a struct in the inspector


I have a custom struct, with the following code :

[Serializable]
public struct HexPoint : IEquatable<HexPoint>
{
    public readonly int x;
    public readonly int y;
    public readonly int z;
    
    // Some custom methods for initializations and operators
}

If I make the x, y, and z variables non-readonly, they are displayed in the unity inspector just fine. However, I have some rules that they need to satisfy (actually x+y+z=0), so I added readonly to prevent people to mess with it.

But as readonly variables, they are not displayed (as they can't be modified)! :(

I was wondering if they are a way for me to display them in the unity inspector, with something similar to a PropertyDrawer. I know that I could switch my struct to a class, as PropertyDrawer is reserved for classes, but I'd like to keep it as a struct.

So, is there a way to display the values? And eventually to modify them using the custom initializers?

Thanks a lot!


Solution

  • readonly makes them also non-serialized -> not displayed in the Inspector

    Note that PropertyDrawer is not limited to class types but can also be used for struct types.


    There is actually no need for a CustomPropertyDrawer.

    You can have public readonly properties to access private fields and to display them in the Inspector use [SerializeField] which makes them editable only via the Inspector but not via other classes.

    [Serializable]
    public struct HexPoint : IEquatable<HexPoint>
    {
        // Those are not displayed in the inspector, 
        // readonly and accessible by other classes
        public int x { get { return _x; } }
        public int y { get { return _y; } }
        public int z { get { return _z; } }
    
        // if you prefer you can also use the expression body style instead
        //public int x => _x;
        //public int y => _y;
        //public int z => _z;
    
        // Those are displayed and editable in the Inspector
        // but private and therefor not changeable by other classes
        [SerializeField] private int _x;
        [SerializeField] private int _y;
        [SerializeField] private int _z;
    
        public bool Equals(HexPoint other)
        {
            return _x == other._x && _y == other._y && _z == other._z;
        }
    
        public override bool Equals(object obj)
        {
            return obj is HexPoint other && Equals(other);
        }
    
        public override int GetHashCode()
        {
            unchecked
            {
                var hashCode = _x;
                hashCode = (hashCode * 397) ^ _y;
                hashCode = (hashCode * 397) ^ _z;
                return hashCode;
            }
        }
    }
    

    Another (not documented) option is also to use serialized auto-properties

    [field: SerializeField] public int x { get; private set; }
    [field: SerializeField] public int y { get; private set; }
    [field: SerializeField] public int z { get; private set; }
    

    Note though that should you ever require a custom editor or property drawer for these the name of an automatically generated backing field will not be e.g. x but rather "<x>k__BackingField"


    If you really want to use a PropertyDrawer to additionally also disallow to edit these values in the Inspector but still save and see them you could add one like e.g.

    [Serializable]
    public struct HexPoint : IEquatable<HexPoint>
    {
        // Those are not displayed in the inspector, 
        // readonly and accessible by other classes
        public int x { get { return _x; } }
        public int y { get { return _y; } }
        public int z { get { return _z; } }
    
        // if you prefer you can also use the expression body style instead
        //public int x => _x;
        //public int y => _y;
        //public int z => _z;
    
        // Those are displayed and editable in the Inspector
        // but private and therefor not changeable by other classes
        [SerializeField] private int _x;
        [SerializeField] private int _y;
        [SerializeField] private int _z;
    
        public bool Equals(HexPoint other)
        {
            return _x == other._x && _y == other._y && _z == other._z;
        }
    
        public override bool Equals(object obj)
        {
            return obj is HexPoint other && Equals(other);
        }
    
        public override int GetHashCode()
        {
            unchecked
            {
                var hashCode = _x;
                hashCode = (hashCode * 397) ^ _y;
                hashCode = (hashCode * 397) ^ _z;
                return hashCode;
            }
        }
    
    #if UNITY_EDITOR
    
        [CustomPropertyDrawer(typeof(HexPoint))]
        public class HexPointDrawer : PropertyDrawer
        {
            public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
            {
                    return EditorGUIUtility.singleLineHeight * (EditorGUIUtility.wideMode ? 1 : 2);
            }
    
            public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
            {
                // Find the SerializedProperties by name
                var x = property.FindPropertyRelative(nameof(_x));
                var y = property.FindPropertyRelative(nameof(_y));
                var z = property.FindPropertyRelative(nameof(_z));
    
                // Using BeginProperty / EndProperty on the parent property means that
                // prefab override logic works on the entire property.
                EditorGUI.BeginProperty(position, label, property);
                {
                    // Makes the fields disabled / grayed out
                    EditorGUI.BeginDisabledGroup(true);
                    {
                        // In your case the best option would be a Vector3Field which handles the correct drawing
                        EditorGUI.Vector3IntField(position, label, new Vector3Int(x.intValue, y.intValue, z.intValue));
                    }
                    EditorGUI.EndDisabledGroup();
                }
                EditorGUI.EndProperty();
            }
        }
    
    #endif
    }
    

    Hint for checking the values after a change MonoBehaviour.OnValidate might be interresting for you