Display readonly fields of an immutable(readonly) struct inside the inspector

Viewed 2255

I have the following immutable struct.

[Serializable]
public readonly struct Wind
{
    /// <param name="windSpeed">The speed of the wind</param>
    /// <param name="rho">The density of the air, if not provided 15 degrees assumed aka 1.225f</param>
    public Wind(Vector3 windSpeed, float rho = 1.225f)
    {
        Speed = windSpeed;
        Rho = rho;
    }

    /// <summary>
    /// The speed of the wind [m/s]
    /// </summary>
    [DisplayReadOnly]
    public readonly Vector3 Speed;

    /// <summary>
    /// Density of the air [kg/m^3]
    /// </summary>
    [DisplayReadOnly]
    public readonly float Rho;
}

The problem with this is that, Unity can not serialize readonly fields so i followed Anton Semchenko's guide on how to make an attribute to only display these fields in the inspector for the ease of debugging.

These are the scripts i made:

The Custom property drawer

[CustomPropertyDrawer(typeof(DisplayReadOnlyAttribute))]
public class DisplayReadOnlyAttributeDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        DisplayReadOnlyAttribute att = (DisplayReadOnlyAttribute)attribute;
        object obj = property.serializedObject.targetObject;
        Type type = obj.GetType();
        FieldInfo field = type.GetField(property.propertyPath, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
        object val = field?.GetValue(obj);

        if (att.warningIfNull && (val == null || val.ToString().Equals("null")))
            val += " <-This value should NOT be NULL!";

        EditorGUI.LabelField(position, string.Format("{0}: {1}", label.text, val));
    }
}

The attribute that signals if a readonly property should be displayed in the inspector

public class DisplayReadOnlyAttribute : PropertyAttribute
{
    /// <summary>
    /// Writes a warning if the value of this field is null
    /// </summary>
    public readonly bool warningIfNull = false;

    public DisplayReadOnlyAttribute(bool _warningIfNull = false)
    {
        warningIfNull = _warningIfNull;
    }
}

The problem is that this attribute does not work if it is used on a readonly field it only get's called when used on a non readonly field public float Rho; I'm not just only interested in the above solution if you do know how to display readonly fields in another way please don't keep it for yourself

1 Answers

So in the end i went with a custom inspector GUI since Unity is not able to hook PropertyAttributes to readonly fields because of limitations.

[Serializable]
public readonly struct Wind
{
    /// <param name="windSpeed">The speed of the wind</param>
    /// <param name="rho">The density of the air, if not provided 15 degrees assumed aka 1.225f</param>
    public Wind(Vector3 windSpeed, float rho = 1.225f)
    {
        Speed = windSpeed;
        Rho = rho;
    }

    /// <summary>
    /// The speed of the wind [m/s]
    /// </summary>
    public readonly Vector3 Speed;

    /// <summary>
    /// Density of the air [kg/m^3]
    /// </summary>
    public readonly float Rho;

#if UNITY_EDITOR
    [CustomPropertyDrawer(typeof(Wind))]
    public class WindDrawer : PropertyDrawer
    {
        public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
        {
            return EditorGUIUtility.singleLineHeight * 4 + 6;
        }

        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            // Readonly fields are initialized at construction time so we need to wait for them
            bool readonlyInitialized = GameManager.LevelController;
            // Getting the readonly fields values
            var rho = typeof(Wind).GetField(nameof(Rho));
            var speed = typeof(Wind).GetField(nameof(Speed));

            var windRect = new Rect(position.x, position.y + 18, position.width, 16);
            var rhoRect = new Rect(position.x, position.y + 42, position.width, 16);
            var speedRect = new Rect(position.x, position.y + 66, position.width, 16);

            EditorGUI.BeginProperty(position, label, property);
            {
                EditorGUI.LabelField(windRect, label);
                EditorGUI.indentLevel++;

                EditorGUI.BeginDisabledGroup(true);
                {
                    EditorGUI.Vector3Field(rhoRect, nameof(Speed),
                    // Default value is Vector3.zero when the readonly field cannot be defined
                        readonlyInitialized ? (Vector3)speed.GetValue(GameManager.LevelController.WeatherController.Wind) : Vector3.zero);
                }
                EditorGUI.EndDisabledGroup();
            }
            EditorGUI.EndProperty();

            EditorGUI.BeginProperty(position, label, property);
            {
                EditorGUI.BeginDisabledGroup(true);
                {
                    EditorGUI.FloatField(speedRect, nameof(Rho),
                       // Default value is zero when the readonly field cannot be defined
                       readonlyInitialized ? (float)rho.GetValue(GameManager.LevelController.WeatherController.Wind) : 0.0f);
                }
                EditorGUI.EndDisabledGroup();
            }
            EditorGUI.indentLevel--;
            EditorGUI.EndProperty();
        }
    }
#endif
}

This produces the following editor GUI: enter image description here

Voilá: You have readonly struct showing up in the editor window

I don't think it worth the effort tho maybe if you really need performance otherwise just use this, and attach a readonly attribute to the backingfield

[Serializable]
public struct Wind
{
    /// <param name="windSpeed">The speed of the wind</param>
    /// <param name="rho">The density of the air, if not provided 15 degrees assumed aka 1.225f</param>
    public Wind(Vector3 windSpeed, float rho = 1.225f)
    {
        Speed = windSpeed;
        Rho = rho;
    }

    /// <summary>
    /// The speed of the wind [m/s]
    /// </summary>
    public Vector3 Speed { get; }

    /// <summary>
    /// Density of the air [kg/m^3]
    /// </summary>
    public float Rho { get; }
}
Related