Unity Field Validation
Open Snippit
The Problem
When you expose a field in unity:
class World : MonoBehaviour
{
Player _player;
}
you don't want it to be public, but you do want to validate it. So
class World : MonoBehaviour
{
[SerializeField]
Player _player;
}
you want to ensure it gets assigned/validated.
You could:
void Awake() => _player = GetComponent<Player>();
That works...but I don't want to have scripts doing work at start, that could be
done at edit time easily enough and save performance.
or you could do the equivalent of:
[RequireComponent(typeof(Player))]
class World : MonoBehaviour
{
[SerializeField]
Player _player;
}
That will enforce it... but it means it HAS to be on this object and gets
annoying removing components because if a dependency chain.
What I Want
I want to ensure that I didn't forget to drag it in the inspector, but isn't a lot of
work to either write or to maintain.
an earlier solution I have used was:
[RequireComponent(typeof(Player))]
class World : MonoBehaviour
{
[SerializeField]
Player _player;
void Awake()
{
if(_player == null) Debug.LogError("Player cannot be null!");
}
That does what I want, but is a lot of messy code to add, especially since I
need to write one of those for every object.
I can simplify this again with a simple extension method:
public static void RequiredBy<T>(this T t,MonoBehaviour item) where T : Component
{
if (t == null)
Debug.LogError($"<color=yellow>{typeof(T).Name}</color> required by
<color=blue>{item.name}</color>", item);
}
Which allows the much cleaner syntax of:
void Awake()
{
_player.RequiredBy(this);
}
This looks a lot nicer but I have to remember to add a line for every single
required field. Seeing as I do this a lot, the end result starts to look pretty
heavy:
class World : MonoBehaviour
{
[Header("Dependencies")]
[SerializeField]
Player _player;
[SerializeField]
Shop _shop;
[SerializeField]
Wallet _wallet;
void Awake()
{
_player.RequiredBy(this);
_shop.RequiredBy(this);
_wallet.RequiredBy(this);
}
}
Cleaning the Syntax
Personally I don't like giving so much real estate to something as non
descriptive to my actual intention as a developer as "validation".
So what if we take a queue from the [SerializedField] and make our own
attribute:
[AttributeUsage(AttributeTargets.Field)]
public class RequiredAttribute : Attribute { }
So now with the attribute we can simplify the check of what is required:
public static void Validate<T>(this T obj) where T : MonoBehaviour
{
var req = RequiredFields(obj);
foreach (var rInfo in req)
{
var val = rInfo.GetValue(obj);
switch (val)
{
case Object unityObj when !unityObj:
case null:
Debug.LogWarning($"Required <color=yellow>
{rInfo.Name</color> missing from {obj.name}", obj);
break;
}
}
}
So now instead of having to write a new validation line for each field we can
search for each [Required] attribute and apply it to all of them, so now the
code is a lot cleaner..
class World : MonoBehaviour
{
[Header("Dependencies")]
[SerializeField,Required]
Player _player;
[SerializeField,Required]
Shop _shop;
[SerializeField,Required]
Wallet _wallet;
void Awake() => this.Validate();
}
this is a lot better, one line per script to validate all the fields!
... but what if we forget to write that line? we can do better:
#if UNITY_EDITOR
public class CheckMissingFields
{
[RuntimeInitializeOnLoadMethod]
static void TryValidate()
{
foreach (var behaviour in Object.FindObjectsOfType<MonoBehaviour>())
behaviour.Validate();
}
}
#endif
Now we can ask unity to automatically validate all the fields if we try to load
the project!
The Final Result
So the final validation code looks like:
collapse:open
class World : MonoBehaviour
{
[Header("Dependencies")]
[SerializeField,Required]
Player _player;
[SerializeField,Required]
Shop _shop;
[SerializeField,Required]
Wallet _wallet;
}
And whenever a field is missing it will automatically display an error in the
inspector, an error that tells you what field, on what script is missing and if
you click it... will ping the script in the hierarchy no matter where it is located
in the hierarchy!
The Source Code
Open the File