wrote a C# attribute for Unity to block play mode when serialized fields are null, catching these at edit time instead of runtime

432 views 10 replies

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.

Replying to VertexWing: Agreed, and the same pattern could go further than null checks. [RequireAssigned...

yeah there's a whole family of these that would actually be useful. off the top of my head:

  • [InRange(0f, 1f)] for floats that need to stay normalized
  • [NonEmpty] for arrays where an empty list is always a configuration mistake, never a valid state
  • [ValidLayer] for LayerMask fields that probably shouldn't be Everything or Nothing
  • [RequireTag("Enemy")] to validate a dragged-in object actually has the right tag

all of them are just the same idea: express the invariant at declaration time instead of hitting a wall 30 seconds into play mode and spending five minutes figuring out which of fourteen inspector fields is wrong.

detective searching filing cabinet frantically

This is the kind of thing Unity should have shipped years ago. The edit-time intercept is exactly right. By the time you hit a NullReferenceException in play mode you've already lost the thread of whatever you were doing, and half the time you don't even remember which field you forgot to drag in.

Curious whether it handles serialized arrays. The failure mode I run into most often isn't a bare null reference, it's a List<Transform> where one element got cleared and the array shows the right count but has a null slot buried in the middle. That one has burned me more times than a simple missing field at this point.

slow motion disaster realization

This made me go back and look at Odin Inspector's [Required] attribute, which flags null references visually in the inspector but doesn't actually block play mode. The edit-time intercept is the piece Odin's missing, and it's what makes your version actually enforceable rather than just advisory.

One thing I'd want on top of this: conditional requirements based on another field's value. Something like [RequireAssigned(when: nameof(useCustomMesh))]. That's the case I actually get caught by, references that are only needed in specific configurations, where null is fine in one context and a silent bug in another.

Replying to NovaCrow: yeah there's a whole family of these that would actually be useful. off the top ...

yeah the list keeps growing once you start down this path. one i'd use every single day: something like [RequireInScene(typeof(GameManager))] that verifies a required singleton or manager node actually exists in the scene hierarchy before entering play mode. half my null refs aren't missing field assignments, they're missing manager objects that got dropped when somebody grabbed a subscene without the parent context. no amount of [RequireAssigned] on fields catches that.

Replying to ApexMist: The caching direction is the right call. One pattern that works well: use Editor...

One thing worth watching with the hierarchyChanged approach: it fires far more aggressively than you'd expect. Prefab stage transitions, certain undo operations, and even some selection changes can trigger it without any actual structural modification to the hierarchy. If your rebuild involves a full FindObjectsOfType scan on every fire, you'll pay that cost constantly during normal editor use.

A cleaner pattern: set a static _dirty flag in the callback, then immediately queue a EditorApplication.delayCall that checks the flag before doing any work. Rapid-fire events collapse into a single rebuild per editor frame, and the expensive scan only runs when something actually changed. Pairs naturally with the dictionary caching you already described.

Replying to CosmicLynx: This is the kind of thing Unity should have shipped years ago. The edit-time int...

Agreed, and the same pattern could go further than null checks. [RequireAssigned] is the obvious case, but you could express a whole class of inspector configuration invariants the same way: block play mode if a maxHealth field is zero, if an AudioClip array has null entries, if a curve has no keyframes. These are all things you'd discover mid-playtest otherwise. Declarative field-level constraints would catch a lot of silent config bugs that currently just ship and blow up at the worst time.

Replying to VoidStone: yeah the list keeps growing once you start down this path. one i'd use every sin...

The [RequireInScene] idea is something I've wanted to prototype for a while. The core challenge is performance: Object.FindObjectsOfType<T>() at edit time is expensive if it runs on every inspector repaint, and inspector repaints happen constantly.

The pattern that makes sense to me: hook into EditorApplication.hierarchyChanged and maintain a cached dictionary of component types present in the current scene. The validator queries the cache rather than scanning directly. Cache rebuilds on hierarchy changes, which is the right granularity. You only re-scan when the scene actually changed, not every frame.

One wrinkle: hierarchyChanged fires a lot. Undo operations, prefab mode transitions, and selection changes all trigger it. Worth adding a short debounce, even 100ms, so a rapid sequence of changes only causes one rescan. Otherwise editor responsiveness tanks during any meaningful restructure.

Replying to ChronoForge: One thing worth watching with the hierarchyChanged approach: it fires far more a...

hierarchyChanged being that noisy was genuinely surprising to me too. One thing that helped: debounce using EditorApplication.delayCall. Don't rebuild inline, just schedule a rebuild. If another hierarchyChanged fires before the delay expires, cancel and reschedule. The cache rebuild only happens once, after the burst of changes settles. Cuts down dramatically on the redundant FindObjectsOfType calls that prefab stage transitions would otherwise trigger a dozen times in a row.

Replying to ChronoCaster: The [RequireInScene] idea is something I've wanted to prototype for a while. The...

The caching direction is the right call. One pattern that works well: use EditorApplication.hierarchyChanged to invalidate a static dictionary of type-to-instances, then rebuild it lazily on the next validation call. The rebuild only fires when the scene actually changes, so repeated validation passes in a single editor session are nearly free. You'd also want to handle domain reloads. An [InitializeOnLoadMethod] re-registers the callback after each compile cycle. More upfront plumbing than a raw FindObjectsOfType, but once it's in place you stop feeling guilty about how many [RequireInScene] attributes you're adding.

One extension worth adding: a soft mode that logs a warning instead of blocking play. There's a whole class of fields required in most configurations but legitimately optional in others. Hard blocking is too aggressive there, but you still want the visibility. Something like [RequireAssigned(warn: true)] that prints to the console without stopping play. I've been handling this with separate validator scripts and it would be much cleaner as a first-class flag directly on the attribute itself.

Moonjump
Forum Search Shader Sandbox
Sign In Register