"Static registry persists across editor Play restarts — gate on live objects"
▸ SYMPTOM
After stopping and restarting Play in the editor (or after a code hotload), a static list or registry populated in the previous session is still full — but its entries point at GameObjects and Components that were destroyed with the old scene. Code that iterates the registry and acts on its contents (spawning relative to registered positions, querying registered state) operates on dead references, causing silent wrong behavior: objects teleport to destroyed positions, routines append to stale data, or fall-through failures cascade.
The registry reports a plausible count (e.g. 30 entries), masking the fact that every entry is stale.
▸ CAUSE
An editor Play stop → start is a scene reload, not an assembly reload. C# static fields survive scene reloads. s&box's code hotload also preserves statics by design. So a static List<T> or static Dictionary<K, V> populated by Components during Play session N is still populated at session N+1's boot — but every GameObject / Component reference in it was destroyed when the old scene tore down.
Clearing the static on the teardown path (e.g. in OnDestroy or a reset method) helps but is not sufficient alone: the static also survives a fresh session that never called the clear path (first Play after a hotload, or a session that didn't trigger the specific teardown flow).
▸ FIX
Gate on live-object validity, not static contents. When iterating a static registry, skip any entry whose Component.IsValid() or GameObject.IsValid() returns false. If no valid entries remain, treat the registry as empty:
static readonly List<MyRegisteredThing> Registry = new();
// When consuming the registry:
var live = Registry.Where( r => r.Component.IsValid() ).ToList();
if ( live.Count == 0 )
{
// Registry is effectively empty — handle the absent case
return;
}
// Operate only on live entriesOptionally, prune dead entries lazily during iteration to keep the list from growing unboundedly across sessions. And still clear on teardown as good hygiene — but never rely on it as the sole guard.
▸ WHY IT WORKS
IsValid() checks both nullity and the alive/destroyed flag on the underlying engine object. A destroyed GameObject's managed wrapper is non-null (it survives in the static list) but IsValid() returns false, correctly identifying it as unusable. This makes the guard robust against every scenario that leaves stale entries: scene reloads, hotloads, partial teardowns, and sessions that skip the explicit clear path.