Which statics survive Enter Play Mode without domain reload in Unity
Why singletons, static events and counters keep stale values when domain reload is off, how to find every static in your project, and how to reset them.
Turn off domain reload and Unity enters Play Mode in well under a second instead of several. In Unity 6.6 and newer this is how new projects start out. The price is that the editor no longer throws away your managed state between runs, and the symptoms arrive one at a time: a singleton that points at an object from the previous run, a button whose click handler fires twice, a score that starts where the last session ended, a scene that behaves differently on the second Play than on the first.
This article explains exactly what Unity stops resetting, how to find every static in your project, and how to reset them so the fast Play Mode is safe to leave on.
What domain reload does, and what you lose without it
Unity runs your scripts in a managed scripting domain. By default it tears that domain down and builds it again on every Play, which is why entering Play Mode takes seconds in a large project, and also why every static field starts from its initializer each time. Unity's manual states the consequences of turning the reload off plainly:
- Static variables keep their values between Play Mode sessions.
- Static events keep their registered subscribers between Play Mode sessions.
- Non-serialized fields keep the values assigned to them during Play Mode on returning to Edit Mode.
That is the whole mechanism. Nothing is "leaking"; the objects simply still exist because the domain they live in was never destroyed. Two things are still reset: scene objects, because Unity reloads the scene (unless you turn that off too), and anything that is reinitialized by code at the start of a run.
Note that Unity still performs a domain reload when it recompiles scripts. So the problem only shows between two Play sessions with no code change in between, which is exactly the loop you are in while tuning gameplay.
The four kinds of static, and which ones matter
Not every static is a problem. Sort them by what can change after the type is initialized:
- Mutable value. A static field or static auto-property that is not
readonly:static int SpawnCount,public static Player Instance { get; private set; }. It keeps whatever it held when the last run stopped. This is the singleton-to-a-destroyed-object case:Instanceis not null, so yourAwakeguard decides the singleton already exists and destroys the new one. - Event. A static event or a static delegate field. Subscribers added by
OnEnablein run one are still attached in run two, so handlers run twice, then three times. Each stale handler also holds a reference to a destroyedMonoBehaviour, which is where theMissingReferenceExceptionon the second run comes from. - Reference to a mutable object.
static readonly List<Enemy> All = new();cannot be reassigned, but it can be filled, and it keeps its contents. Treat it like a mutable value: clear it at the start of a run. - Constant. A
readonlyvalue type, string or delegate instance set once, and anythingconst. These cannot change after type initialization and are safe. They are also the majority of statics in a typical project, which is what makes the dangerous ones hard to see.
The rule of thumb: anything that is assigned, incremented, subscribed to or added to during play needs a reset.
Resetting: the attribute Unity recommends
Unity's answer is a static method that runs at the start of every Play session, with or without domain reload, before the first scene loads:
using UnityEngine;
public class Spawner : MonoBehaviour
{
public static Spawner Instance { get; private set; }
public static int SpawnCount;
public static event System.Action<Spawner> Spawned;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetStatics()
{
Instance = null;
SpawnCount = 0;
Spawned = null;
}
}
SubsystemRegistration is the earliest load type, which matters: a reset that runs at BeforeSceneLoad or later is too late for code that touches the static during scene loading. Unity's manual also shows resetting on exit through EditorApplication.playModeStateChanged, and recommends unregistering static event handlers on exiting Play Mode, but it adds a caveat worth repeating: code that also runs in Edit Mode can modify a static after your exit handler ran, so the entry reset is the one you can rely on.
Three details that come up in practice:
- Reset to the declared value, not to
default.static float Gravity = -9.81f;must go back to-9.81f. If the initializer is the only assignment anywhere, mark the fieldreadonlyinstead and it stops being a problem at all. readonlycollections cannot be reassigned, so callClear()on them.- Editor-only statics (in editor assemblies, or inside
#if UNITY_EDITOR) are editor state, not play state. Unity offers[InitializeOnEnterPlayMode]for those, but the runtime attribute is the one that fixes gameplay symptoms.
Unity 6.6: letting a generator write the reset
From Unity 6.6 there is a second route. Mark a static member with [AutoStaticsCleanup] and a source generator resets it on entering Play Mode: for a non-readonly field it captures the initializer expression and re-evaluates it on every transition, and for a readonly collection it keeps the instance and calls Clear(). Members that genuinely should persist are marked [NoAutoStaticsCleanup]. The matching analyzer (rules UAL0010 to UAL0014) reports statics that carry neither attribute.
Read the constraints before adopting it project-wide, because they are the reason the manual route still exists:
- The class must be
partialso the generator can inject code (UAL0012). - The generator never re-runs the type's static constructor (UAL0014). Static setup that is not a field initializer runs once per editor session and has to be moved into an
[OnEnteringPlayMode]method. - Code generation is on by default, but the analyzer that tells you about unmarked statics is off by default. It is switched on per assembly definition with a
.globalconfigfile containingbuild_property.UnityEnableAutoStaticsCleanupAnalysis = true, placed next to the.asmdef, or in a subfolder ofAssetsfor scripts without one. - It is per member and per assembly, so it does nothing for statics in packages you do not control.
Both routes work on 6.6. On 6.3 LTS the attribute route is the only one.
Finding them all
The hard part is not the reset, it is the inventory. A project of moderate size has hundreds of statics spread over your own code, third-party assets and packages, and the editor has no window that lists them. Three ways to build the list:
- Search. A regex over
Assetsforstatic\s+(?!readonly|void|class|.*\()finds declarations, but also local functions, nested types and constants, and it cannot tell areadonlyreference whose contents change from a real constant. Good enough for a small project. - Reflection. An editor script that loads every assembly from
CompilationPipeline.GetAssemblies(), walksAssembly.GetTypes()and listsGetFields,GetPropertiesandGetEventswithBindingFlags.Static | Public | NonPublic | DeclaredOnly. SkipIsLiteralfields (those areconst), skip compiler-generated types, and classify byIsInitOnlyand by the field type'sIsValueType. Half a day, and you own it. - Run it and look. Put a counter in
Awake, press Play twice, and watch the second run. This finds the symptom, not the cause, but it is the fastest way to confirm you have the problem at all.
Whatever you use, check the result once with domain reload off: enter Play Mode twice without recompiling, and anything that differs between the two runs is a static you missed.
Why this now matters beyond the editor
Two external reasons to do this work this year rather than later. Unity 6.6 made the fast Play Mode the default for new projects, so anyone who opens your package in a fresh project gets the stale-statics behavior out of the box. And the Asset Store's submission guidelines, updated in October 2026, require packages submitted from Unity 6.6 to support Play Mode without domain reload; a package whose statics survive between runs is now a review failure, not a quirk.
Where Static State Auditor fits
Static State Auditor is the reflection approach from the list above, packaged as a free editor window. Press Scan and every static field, auto-property and event in your assemblies is listed with its classification (mutable, event, reference, constant), whether its class already has a SubsystemRegistration reset, and a double-click that opens the declaration. The status line reads your Enter Play Mode settings and says what they mean for the list, and the view exports to CSV with the project, Unity version and settings in the header. Editor-only, no runtime code, no dependencies, Unity 6.3 LTS and newer.
Static State Auditor Pro (14.99 USD) writes the reset method, either into a generated file with one SubsystemRegistration method per class or straight into your class, and takes a snapshot of every static when Play starts and stops so it can show which ones actually changed and which survived the run. That turns the list into evidence.
Both are submitted to the Asset Store and in review as of this writing. What neither does: they do not change your code without a click, and they do not scan packages by default (a toggle includes them).
Sources
- Unity 6.3 manual, Domain reloading: https://docs.unity.com/en-us/engine/6000.3/manual/programming-environment/code-reload-serialization/code-reloading-editor/domain-reloading
- Unity 6.6 manual, Domain reloading (AutoStaticsCleanup, analyzer rules, .globalconfig): https://docs.unity.com/en-us/engine/6000.6/manual/programming-environment/code-reload-serialization/code-reload/code-reloading-editor/domain-reloading
- Unity Asset Store submission guidelines (section 2.5.h): https://assetstore.unity.com/publishing/submission-guidelines