Skip to content

RuntimeSingleton: entering Play Mode destroys scene-authored instances #582

Description

RuntimeSingletonRegistry clears its instances from a
[RuntimeInitializeOnLoadMethod(BeforeSceneLoad)], and RuntimeSingleton<T>.ClearInstance destroys
every live instance's GameObject, not just the one it cached:

T[] liveInstances = UnityObjectExtensions.FindObjectsOfTypeShim<T>(true);
for (int i = 0; i < liveInstances.Length; i++)
{
    liveInstances[i].StopAllCoroutines();
    liveInstances[i].gameObject.Destroy();
}

In a player that is harmless: nothing is alive when BeforeSceneLoad runs. In the Editor, pressing
Play on a scene that already carries a RuntimeSingleton is not harmless
— the open scene's
objects are alive at that moment, so the sweep destroys objects it did not create, and the scene runs
without the component it authored.

Measured

Unity 6000.5.2f1, m_EnterPlayModeOptionsEnabled: 1, m_EnterPlayModeOptions: 0.

A level scene with eleven root GameObjects, two of which carry a RuntimeSingleton subclass. In Play
mode exactly those two roots are absent and the other nine are present. Nothing else in the scene
selects on "is a RuntimeSingleton", and FindObjectsByType<T>(FindObjectsInactive.Include) returns
zero for both types for the whole session.

The consequence is not an absence, because Instance fabricates one:

moment the LevelBounds the game sees
first frame none
after any consumer asks Instance (x:0, y:0, width:0, height:0)

The authored value is (0, 0, 76.8, 43.2). The consumer downstream clamps a camera to the centre of
that rectangle, so the camera left the level and held on the world origin — 62.56 world units in one
move, in a take where nothing else moved.

Why this is worth fixing rather than documenting

Every symptom is silent. Instance cannot fail, the fabricated object answers every call, and the
authored values it replaces are defaults that look plausible. A consumer written the obvious way has
no way to tell which one it is holding.

Suggested shapes

  • Have ClearInstance drop the cached reference and destroy only instances the getter created —
    track those, rather than sweeping by type.
  • Or skip the sweep when Application.isPlaying is already true at BeforeSceneLoad, which is the
    Editor-only state that makes open-scene objects visible to it.

The clear-the-cache half is what the callback needs; the destroy half is what a test fixture wants,
and the two look worth separating.

Found from Ambiguous-Interactive/IshoBoy#694, where it put both marketing captures of a level on the
world origin for most of their take with every gate still green.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions