Correctly Changing Scenes

Is there a best practice on how to change Scenes?
Until now i was just destroying all Entities with EntityManager.UniversalQuery and loading a new Scene.
This won’t work anymore in 0.50 since now the Physics System instantiates a Singleton Entity to hold a reference for Dependency resolving. When i destroy that entity a new Sceneload obviously throws an Error that GetSingleton() can’t find an Entity anymore. Of course i could just exclude that Entity from the Query and be done with it but i would like to not think about every implementation detail of every external System whenever i tear down my Scene.

There are some reasons (necassary Hybrid workflows…) why i can’t switch to Subscenes right now but ultimately that’s the goal. I’d be open for suggestions for both ways (Subscenes or ConvertToEntity)

What’s the best way of changing Scenes and cleaning up Entities?

I just encountered the same problem.
And the issue goes even deeper than that.
Even if you wanted to exclude PhysicsSystemRuntimeData from a query you cant.

I am not using the universal query but a custom one and would like to just add the physics component to the list of ignored components. But there is no way as its internal and I am not allowed to access the type:

var queryDescription = new EntityQueryDesc {None = new ComponentType[] {typeof(Prefab),typeof(DontDestroyOnLoadTag)}};

Oh that makes it much worse. I didn’t even try to exclude it yet because it seemed like a really bad solution anyways.
I really don’t want to tag every Entity with a “DestroyOnSceneChange” Tag just to be able to query them all at once.

Even when using SubScenes and unloading them, don’t you run into problems when you instantiate new Entities at runtime that do no get a SubScene Component? How are they cleaned up in the “official” SubScene Workflow?

I really struggled with scene loading when getting started (still do, so I avoid it where possible).

I think the key thing to remember is that Scenes are not tied to Worlds and you need to architect your solution inline with this fact.
Really what you want is a section of the world that can be marked as dependant on the scene so that when the scene is unloaded, you go and destroy all those entities.
If you want this behaviour unfortunately you have to do it manually and it’s not exactly easy.

My advice is don’t have multiple scenes with entities that need loading/unloading.
At most have main menu scene that is non-entities and game scene that contains subscenes with entities and just obliterate the entire world when you want to unload the game scene.

I use netcode and there you need to tackle 3 worlds.
But I still use the same process, I just recreate the Client world when I go back to the main menu and keep the logic that handles this outside of any world.
I know its not great advice, but I haven’t heard of a better way of doing things.

1 Like

I see. I have two problems with destroying all Worlds when i switch to the menu scene :

  1. I would like to stick with the use of the DefaultGameObjectInjectionWorld to avoid the overhead and headaches of creating my own World (if custom worlds are fully compatible with all Editor tools by now i might reconsider). So if I destroy the DefaultWorld is there a way to get it back?

  2. Even if I try “World.DisposeAllWorlds();” I just get alot of those Errors afterwards:

InvalidOperationException: system state is not initialized or has already been destroyed
Unity.Entities.ComponentSystemBase.CheckedState () (at Library/PackageCache/com.unity.entities@0.50.0-preview.24/Unity.Entities/ComponentSystemBase.cs:29)
Unity.Entities.SystemBase.OnBeforeDestroyInternal () (at Library/PackageCache/com.unity.entities@0.50.0-preview.24/Unity.Entities/SystemBase.cs:473)
Unity.Entities.World.DestroyAllSystemsAndLogException () (at Library/PackageCache/com.unity.entities@0.50.0-preview.24/Unity.Entities/World.cs:474)
UnityEngine.Debug:LogException(Exception)
Unity.Debug:LogException(Exception) (at Library/PackageCache/com.unity.entities@0.50.0-preview.24/Unity.Entities/Stubs/Unity/Debug.cs:19)
Unity.Entities.World:smile:estroyAllSystemsAndLogException() (at Library/PackageCache/com.unity.entities@0.50.0-preview.24/Unity.Entities/World.cs:478)
Unity.Entities.World:smile:ispose() (at Library/PackageCache/com.unity.entities@0.50.0-preview.24/Unity.Entities/World.cs:216)
Unity.Entities.World:smile:isposeAllWorlds() (at Library/PackageCache/com.unity.entities@0.50.0-preview.24/Unity.Entities/World.cs:234)
LevelManager:ChangeScene(Int32) (at Assets/Scripts/Core/LevelManager.cs:15)
InputGatheringSystem:InputActions.IGameplayActions.OnQuitScene(CallbackContext) (at Assets/Scripts/InputEngine/InputGatheringSystem.cs:350)
UnityEngine.InputSystem.LowLevel.<>c__DisplayClass7_0:<set_onUpdate>b__0(NativeInputUpdateType, NativeInputEventBuffer*)
UnityEngineInternal.Input.NativeInputSystem:NotifyUpdate(NativeInputUpdateType, IntPtr) (at /Users/bokken/buildslave/unity/build/Modules/Input/Private/Input.cs:120)

So what am I supposed to clean up? Destroy every single System first and then dispose the World?

As we have multiple entities that should not be destroyed on scene change I already introduced a DontDestroyOnLoadTag earlier in analogy to flagging GOs as DontDestroyOnLoad.

I now got a workaround by adding this DontDestroyOnLoadTag to the PhysicsSystemRuntimeData entity.
I just have an extra system that handles adding it if it doesnt exist. So it still requieres special code to handle this entity but I dont need to adjust the unloading query.

Adding the component was tricky though as you cannot query an internal component.
So I used a AssemblyDefinitionReference to “patch” the Physics Assembly.

Another solution which might work would be to create a type by string with Type.GetType() and use it in the query.

Imho components should never be internal as this kind of deafeats the purpose of ECS.

1 Like
  1. The editor workflow for 0.50 is pretty good regardless of which worlds you have.
    You can use DefaultWorldInitialization.Initialize() to recreate the default world (this is how unity code does it).
    It will call your implementation of ICustomBootstrap.Initialize() using reflection magic where you need to set the DefaultGameObjectInjectionWorld. (you can just do World.DefaultGameObjectInjectionWorld = new World()).

  2. You may need at least the default world, I’m not sure…
    In my game I just dispose of the worlds one at a time.
    I would set the default world to the new one first then try disposing the old default world.

Sorry this is a little late.

I also used this methode. To make it work with 0.50 I copied the physics package to the packages folder, made the PhysicsSystemRuntimeData ‘public’ and use the following semi-universal query (in the options you could also include the prefabs if you want):

EntityManager.DestroyEntity(GetEntityQuery(new EntityQueryDesc() { None = new ComponentType[] { typeof(PhysicsSystemRuntimeData) }, Options = EntityQueryOptions.IncludeDisabled }));

Yeah, this was annoying. I ended up just destroying and recreating the world

    public sealed class Bootstrap
    {
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
        public static void InitializeBeforeSceneLoad() { SceneManager.sceneUnloaded += OnSceneUnloaded; }

        private static void OnSceneUnloaded(Scene scene)
        {
            var oldWorld = World.DefaultGameObjectInjectionWorld;
            oldWorld.Dispose();
            var world = new World("Custom world");
            World.DefaultGameObjectInjectionWorld = world;
            var systems = DefaultWorldInitialization.GetAllSystems(WorldSystemFilterFlags.Default);

            DefaultWorldInitialization.AddSystemsToRootLevelSystemGroups(world, systems);
            ScriptBehaviourUpdateOrder.AppendWorldToCurrentPlayerLoop(world);
            World.DefaultGameObjectInjectionWorld.GetOrCreateSystem<FixedStepSimulationSystemGroup>().FixedRateManager =
                null;
        }
    }
    public class CustomWorldBootstrap : ICustomBootstrap
    {
        public bool Initialize(string defaultWorldName)
        {
            var world = new World("Custom world");
            World.DefaultGameObjectInjectionWorld = world;
            var systems = DefaultWorldInitialization.GetAllSystems(WorldSystemFilterFlags.Default);

            DefaultWorldInitialization.AddSystemsToRootLevelSystemGroups(world, systems);
            ScriptBehaviourUpdateOrder.AppendWorldToCurrentPlayerLoop(world);
            World.DefaultGameObjectInjectionWorld.GetOrCreateSystem<FixedStepSimulationSystemGroup>().FixedRateManager =
                null;
            return true;
        }
    }
2 Likes