Tired of hitting play, getting a NullReferenceException 30 seconds in, and tracing it back to a reference I forgot to drag into the inspector. Wrote a [RequireAssigned] attribute that intercepts the play mode transition and validates before Unity actually enters play mode.
Usage is just tagging whatever fields actually need to be assigned:
public class EnemyController : MonoBehaviour
{
[RequireAssigned, SerializeField] private NavMeshAgent _agent;
[RequireAssigned, SerializeField] private Animator _animator;
[RequireAssigned, SerializeField] private EnemyData _data;
}
If anything's null when you hit play, it logs all violations and cancels the transition:
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;
[AttributeUsage(AttributeTargets.Field)]
public class RequireAssignedAttribute : PropertyAttribute { }
#if UNITY_EDITOR
[InitializeOnLoad]
public static class RequireAssignedValidator
{
static RequireAssignedValidator()
{
EditorApplication.playModeStateChanged += Validate;
}
static void Validate(PlayModeStateChange state)
{
if (state != PlayModeStateChange.ExitingEditMode) return;
var issues = new List<string>();
foreach (var mb in Resources.FindObjectsOfTypeAll<MonoBehaviour>())
{
if (!mb.gameObject.scene.IsValid()) continue;
var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
foreach (var field in mb.GetType().GetFields(flags))
{
if (field.GetCustomAttribute<RequireAssignedAttribute>() == null) continue;
var val = field.GetValue(mb);
if (val == null || val.Equals(null))
issues.Add(string.Format(" {0} ({1}): {2}",
mb.gameObject.name, mb.GetType().Name, field.Name));
}
}
if (issues.Count == 0) return;
Debug.LogError("[RequireAssigned] " + issues.Count +
" unassigned field(s):\n" + string.Join("\n", issues));
EditorApplication.isPlaying = false;
}
}
#endif
Setting EditorApplication.isPlaying = false inside ExitingEditMode cancels the transition cleanly. No hard abort, just returns to edit mode with everything logged in the console. Resources.FindObjectsOfTypeAll picks up scene instances but skips loose prefabs in the asset database, which is the behavior I wanted.
One known gap: if you mark a Transform[] with [RequireAssigned], it only checks whether the array reference itself is null, not the individual elements. Considering a [RequireAssignedElements] variant but not sure it's worth the extra complexity for most setups.
Anyone doing edit-time validation beyond Awake() assertions? Curious if there's a cleaner approach I'm missing, or whether people mostly just accept the inspector-wiring null ref as a fact of life.