Unity Tips: Properties and the Inspector
Tags: csharp, dotnet, unity3d
Published: 2021-11-20 19:03:20

Unity is a powerful game engine, but many of its quirks make it difficult to write clean code. In this article we are going to address one of the most fundamental issues: replacing public member variables with properly scopeable properties.

Look at nearly any Unity documentation, tutorial, or project, even much of the built-in code, and you will find public member variables used everywhere.

Defining data as public member variables is the standard convention in Unity because such variables are automatically detected and serialized by the Unity editor, allowing them to be displayed in the Inspector for reading and writing. This is great for the experience of using the editor, but from a coding perspective it is horrible design. Public member variables give up control of who can change the state of the object and that introduces an opportunity for bugs.

## Example Script

<small>**NOTE: All example code uses standard C# naming conventions, not Unity's incorrect style. Also, `this` is used religiously.**</small>

Here we have a simple example script with one public member variable, `Health`, that can be raised and lowered using the `HealHealth` and `TakeDamage` methods, both of which cause side effects based on the new `Health` value. The `Health` value can be changed directly by the Inspector, but it can also be changed directly by any code in the project, even though that would bypass the side effects of the `HealHealth` and `TakeDamage` methods.

Instead of a public member variable, `Health` should be a public property with a private setter that is displayed by and can be changed in the Inspector.

```csharp
public class HealthScript : MonoBehaviour
{
    public float Health;

    public void HealHealth(float healAmount)
    {
        this.Health += healAmount;
        if (this.Health > 100)
        {
            // Do something based on excessive health.
        }
    }

    public void TakeDamage(float damageAmount)
    {
        this.Health -= damageAmount;
        if (this.Health <= 0)
        {
            // Do something like destroy the owning object.
        }
    }
}
```
![Inspector Health Value Enabled](https://i.ibb.co/J5vp7zj/prop-enable.png)

## Using Properties

The first step is to convert the member variable into an auto-implemented property. After changing `Health` to a property, the code will behave the same, but the property won't be shown in the Inspector.

```csharp
//public float Health;
public float Health { get; set; }
```
![Inspector Health Value Not Shown](https://i.ibb.co/y4yzHts/prop-none.png)

The Inspector can only display values that are serialized by Unity and Unity doesn't auto-detect properties like it does public member variables. This means we have to tell it which properties to serialize so they will be displayed in the Inspector.

Unfortunately, Unity won't serialize properties directly, but we can serialize the property's backing field. In fact, any private member variable can be serialized by Unity if we annotate it with the `SerializeField` attribute. Since the attribute has to go on a member variable, not a property, we'll rewrite the auto-implemented property to use a manually declared backing field.

```csharp
//public float Health { get; set; }
public float Health { get => this._health; set => this._health = value; }
private float _health = 0;
```
![Inspector Health Value Not Shown](https://i.ibb.co/y4yzHts/prop-none.png)

That done, annotate the `_health` backing field with the `SerializeField` attribute so Unity will detect it.

```csharp
public float Health { get => this._health; set => this._health = value; }
[SerializeField]
private float _health = 0;
```
![Inspector Health Value Enabled](https://i.ibb.co/J5vp7zj/prop-enable.png)

Now the `Health` value can be read and changed in the Inspector. While properties can now be used instead of public member variables, it is a shame to give up auto-implemented properties and return to the stone age of manually declaring backing fields. Thankfully, there is a solution.

Attributes can be applied to the backing fields of auto-implemented properties by prefixing the term `field:` to the annotation.

Rewrite the `Health` property back to an auto-implemented form, annotate it with the `SerializeField` attribute, and add the `field` prefix to the annotation.

```csharp
//public float Health { get => this._health; set => this._health = value; }
//[SerializeField]
//private float _health = 0;
[field: SerializeField]
public float Health { get; set; }
```
![Inspector Health Value Enabled](https://i.ibb.co/J5vp7zj/prop-enable.png)

Again, the `Health` value can be read and changed in the Inspector, but, unlike the manually declared backing field, the `InspectorName` attribute is no longer needed to display the correct label in the Inspector.

The one downside to this strategy is that exception messages will display the compiler mangled name of the backing field, something similar to `<Health>k__BackingField`, instead of a more readable name of `Health`.

It is important to note that C# only generates a usable backing field if the property has a getter and a setter. An auto-implemented property with only a getter will not be serialized by Unity, even if it is annotated with the `SerializeField` attribute.

```csharp
// This will not be serialized by Unity.
[field: SerializeField]
public float Health { get; }
```
![Inspector Health Value Not Shown](https://i.ibb.co/y4yzHts/prop-none.png)

## Controlling Code Access

Applying serialization to the property's backing field means that the Inspector can access the value regardless of the access scope of the property. In other words, the property and its getter and setter can be public, protected, private, or any valid combination without affecting the Inspector's ability to read and change the value.

So now we can change the property setter to be private, allowing any code to read the value, but only code in the same class can change it. This only affects use of the value in code, the Inspector can still read and change the value.

```csharp
[field: SerializeField]
public float Health { get; private set; }
```
![Inspector Health Value Enabled](https://i.ibb.co/J5vp7zj/prop-enable.png)

## Controlling Inspector Access

The code access can be controlled through the scoping of the property and its getter and setter, but in some cases one may want to limit the Inspector's ability to change a value. Unfortunately, there is no built-in way to accomplish this, but it can be accomplished with a relatively simple custom property drawer.

A property drawer is a class that tells Unity how a type should be displayed in the inspector. There is a property drawer that displays arrays with their length and a dropdown of their items, another drawer that displays `float` values as a number that can be dragged left and right, and a property drawer that displays `Vector2` values as a combination of X and Y values.

In this case, we want a custom property drawer that displays values as they would normally be shown, but disabled so that they can not be changed. We also need a custom attribute that will invoke our drawer when it is applied to a field.

### ReadOnlyField Attribute

The attribute is fairly straight forward. It should inherit from `PropertyAttribute`, the Unity base class for all attributes. The attribute should be restricted to fields only, as Unity only deals with fields. Lastly, the attribute should be inherited and should not allow multiple declarations on the same field. The class itself has no functionality and will only serve as a marker to invoke our custom drawer.

```csharp
[System.AttributeUsage(System.AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
public class ReadOnlyFieldAttribute : PropertyAttribute { }
```

### ReadOnlyField Drawer

The custom property drawer takes a little more code than the attribute, but not much. The class should inherit from `PropertyDrawer`, the Unity base class for all property drawers. The class should be annotated with two attributes; `JetBrains.Annotations.UsedImplicitly`, which will keep the compiler from complaining about the class not being directly used in the project; and `CustomPropertyDrawer` with the type of our attribute, which tells Unity to use the class to draw any fields annotated with our attribute.

```csharp
[UsedImplicitly, CustomPropertyDrawer(typeof(ReadOnlyFieldAttribute))]
public class ReadOnlyFieldAttributeDrawer : PropertyDrawer
{
}
```

The first method that needs to be overridden is `GetPropertyHeight` and it simply wraps a call to `EditorGUI.GetPropertyHeight` so that we preserve the desired height of the property as displayed by the normal drawer.

```csharp
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    => EditorGUI.GetPropertyHeight(property, label, true);
```

The other method that has to be overridden is `OnGUI` to do the actual drawing. Since we don't want to change the display of the value, delegate the drawing down to the built-in `EditorGUI.PropertyField` method. To accomplish our goal, disabling the drawn fields, we can use the `GUI.enabled` field, setting it to `false` (disabled) before the drawing and back to `true` (enabled) after the drawing.

```csharp
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
    GUI.enabled = false;
    EditorGUI.PropertyField(position, property, label, true);
    GUI.enabled = true;
}
```

### Using ReadOnlyField

Now that we have the attribute and custom property drawer, the latter can be invoked by adding the former to a field, the same as was done with the built-in `SerializeField` attribute. Note that the `field:` expression doesn't have to be repeated before each attribute as it applies to all attributes that follow it. That done, the `Health` value will now be displayed in the Inspector, but it will be displayed as disabled and won't be editable.

```csharp
public class HealthScript : MonoBehaviour
{
    [field: SerializeField, ReadOnlyField]
    public float Health { get; private set; }
}
```
![Inspector Health Value Disabled](https://i.ibb.co/5j4bqVR/prop-disable.png)

## Usage Examples

Here are four examples of exposing properties to the Unity Inspector with various combinations of the value being writable from code and/or the Inspector.

### Example 1

* Inspector: Read and Write
* Code: Read and Write

This example is a plugin replacement for the standard public member variable. It will be readable and writable by both the Inspector and any code in the project.

```csharp
public class HealthScript : MonoBehaviour
{
    [field: SerializeField]
    public float Health { get; set; }
}
```
![Inspector Health Value Enabled](https://i.ibb.co/J5vp7zj/prop-enable.png)

### Example 2

* Inspector: Read and Write
* Code: Read Only

In this example, the Inspector can still access the value as normal, both reading and writing it, but the value is now restricted to only being writable from inside the same class. One could instead mark the setter as `protected` if the value can be modified by derived classes.

```csharp
public class HealthScript : MonoBehaviour
{
    [field: SerializeField]
    public float Health { get; private set; }
}
```
![Inspector Health Value Enabled](https://i.ibb.co/J5vp7zj/prop-enable.png)

### Example 3

* Inspector: Read Only
* Code: Read and Write

Here the Inspector can show the value, but will show it disabled so that it can not be changed in the Inspector. However, any code in the project is allowed to change the value.

```csharp
public class HealthScript : MonoBehaviour
{
    [field: SerializeField, ReadOnlyField]
    public float Health { get; set; }
}
```
![Inspector Health Value Disabled](https://i.ibb.co/5j4bqVR/prop-disable.png)

### Example 4

* Inspector: Read Only
* Code: Read Only

This example is only readable in the Inspector and can only be changed by code in the same class. One could instead mark the setter as `protected` if the value can be modified by derived classes.

```csharp
public class HealthScript : MonoBehaviour
{
    [field: SerializeField, ReadOnlyField]
    public float Health { get; private set; }
}
```
![Inspector Health Value Disabled](https://i.ibb.co/5j4bqVR/prop-disable.png)

## Conclusion

Controlling the access scope of values is compartmentalization 101 and rampant use of public member variables throws that out the window. With the use of the `SerializeField` attribute on backing fields, it is trivial to separate code access from Inspector access and return some sanity to the code.

With the addition of the custom `ReadOnlyField` attribute, it is also possible to display values in the Inspector and restrict them from being changed. More importantly, that restriction can be imposed regardless of property's declared access scope.

* Copyright (c) 2021 Jay Jeckel
* [Article License: CC BY](https://creativecommons.org/licenses/by/4.0/)
* [Code License: CC0](https://creativecommons.org/publicdomain/zero/1.0/)