[Desperate for help] Change scene in hybrid ECS

So as in title, I couldn’t wrap my head around it and that’s the most hateful hassle in my DOTS project that is basically stopping my college internship’s progress… I’m continuously getting vague errors and crashes when I change a scene. If someone was able to help with this, at least I should virtually owe him a drink!

Code for changing a scene (in LateUpdate() of a monob):

SceneManager.LoadScene("SomeLevel");
_entityManager.DestroyEntity(_entityManager.UniversalQuery);

General architecture: gameobjects follow entities in LateUpdate(), that is necessary because my project would require some complex animations:

public class FollowEntity : MonoBehaviour
{
    public Entity EntityToFollow;
    ...
    void LateUpdate()
    {
        if (_entityManager.Exists(EntityToFollow))
        {
            Translation entityPosition = _entityManager.GetComponentData<Translation>(EntityToFollow);
            transform.position = entityPosition.Value;
        }
    }
}

Spawner: I have one prefab for the entities and one for the gameobjects. It spawns already converted entity prefabs and their follower gameobjects altogether:

 var settings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, _blobAssetStore);
Entity myPrefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(myEntityPrefab, settings); // converts to entity

var myGameObj = Instantiate(myGameObjPrefab);
var myEntity = _entityManager.Instantiate(myPrefab); //instantiates an entity from an already converted entity prefab
myGameObj.GetComponent<FollowEntity>().EntityToFollow = myEntity;

When I switch to an empty scene like a menu, or when I enter play mode, all the levels works fine without errors. Yet when I change to a non-empty scene (such as Level1 → Level1 again), I get a bunch “Entity does not exist” errors (thrown from a well-functioning external navmesh system I’m using, check “dotsnav” on Google if you wish) and a Unity crash.
My hypothesis:

  • the conversion I’m doing (ConvertGameObjectHierarchy in the Start() of a Spawner monob) is not so good and causes issues when respawning the scene. It’s the best method I’ve understood to get things done but I wish to understand a safer and solid conversion method;

If you have any hints about architecture, conversion or an idea of why the errors and crashes, even insulting my architecture if you feel necessary… It’d be of extreme help! As I said before, it’s a worrying issue to me.
well, thank you!

EDIT: this level-switching architecture worked fine until I added follower GameObjects for animation purpose.

Maybe the issue is with using the UniversalQuery there. What I do is to have a SceneLink (custom IComponentData that just contains a Scene field) and then I use SceneManager.sceneUnload event to query all entities with a SceneLink that matches the one being unloaded.

So I selectively chose if an entity is actually linked to a scene or not.

Another option I see there is to just call _entityManager.DestroyEntity(EntityToFollow) inside OnDestroy of your FollowEntity, as it already contains the link to it.

3 Likes

You can’t destroy universal query because it destroys state entities for things like physics that are setup in on create.

Maybe you should just rebuild your entire world in scene change?

Apart from that I don’t have a good solution for you though because I simply don’t change scenes. I only have 1 scene and load subscenes as required.

4 Likes

@tertle I didn’t use subscenes because of time constraints (afaik they also require a different build) but my intention is to explore them maybe because they seems an important feature for the future Unity and because changing traditional scenes becomes messy with entities… if you don’t mind, can you point some code examples of your approach or explain a little further?:slight_smile:
If I get you correct about “rebuilding the world”, I already have _entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; on the Awake() of a gameobject with DontDestroyOnLoad.

@brunocoimbra I tried your last approach but the problem persists, I think what causes it is the rebuilding/respawning of the loaded new scene, rather than a wrong deletion of the previous one (suspect confirmed by the fact that when I switch to an empty scene e.g. Menu things work fine). Is your system hybrid as well? What conversion do you use?

I’m one of the few experienced users on this forum that has never been able to get subscenes to work correctly for my use case (I know why. It is Unity’s fault. And they continue to ignore me.). So for that reason, I can probably provide the best insight.

First, don’t use ConvertGameObjectHierarchy(). Instead, add the ConvertToEntity component and let it automatically convert at the beginning of the next frame. This is way more predictable and reliable, and will force you to structure you code the correct way.

Second, if you are still having problems with scene switching, I have written my own scene manager solution for this problem. Latios-Framework/Documentation~/Core/Scene Management.md at v0.5.0 · Dreaming381/Latios-Framework · GitHub
Note that if you try to use this, this will delete singletons that don’t have specific components. A common one that gets deleted is the Unity Physics world singleton dependency thing. Just add the DontDestroyOnSceneChange to that entity using GetSingletonEntity and you should be good to go.

Also in the latest 0.5, you will have to modify the created bootstrap to invoke the installer for it since the feature is now disabled by default.

3 Likes

I use ConvertToEntity like @DreamingImLatios suggested.

Also, I always link an Entity to a GameObject thorugh IComponentData, instead of linking through a MonoBehaviour (as Entity references are kind of unpredictable when used outside of IComponentData due all the conversion flow.

It looks to me like a bootstrap/world/conversion issue. The new scene doesn’t have any of that set up, so obviously that would need setting up. I think this part of Entities isn’t done yet. Subscenes are working for me but I’m not doing my own world management / bootstrap stuff.

Definitely going to be problems with regular scene loading, especially since Unity said they want everything to go via a subscene in some manner, presumably for this very reason + conversion.

1 Like

Frankly, subscenes / conversion never worked for me properly in hybrid environment either.

What I’ve ended up doing is creating a “container” on the MonoBehaviour side that handles entity creation (stores component hashes in editor-time, and authors an entity based on an archetype in runtime / playmode). This container also provides access to the data, utility methods etc.

Entity lifecycle is bound to the OnEnable / OnDisable.
When object is enabled, entity is created from the cached archetype, and then data is provided via interface from each component attached to that GameObject (or from the hierarchy / child objects);

Entity is created via EntityManager (main-thread) at specific point, and data is updated via ECB. From my testing, in a hybrid environment this is pretty fast. Not as scalable as pure, or converted variant, but still really fast even on mobile.

When OnDisable happens, this container uses ECB to DestroyEntity at the very beginning of the frame, ensuring everything else completes running.

This ensures:

  • Entity lifecycle is determined by GameObject state. If its destroyed or disabled - entity will be cleaned up correctly.
    This also means no such thing is possible as you’re describing here. If scene unloads and GO is gone - entity is gone too. If object is disabled, and re-enabled fresh state of data is always guaranteed, which is a bless when pooling objects.
  • Ease of authoring from MonoBehaviours. No need to create separate editor / authoring scripts, its good old MonoBehaviours.
  • Deterministic creation / data propagation to the ECS side.
  • Standalone can be built via Unity Cloud Build, which is a godsent for IOS and such.

Cons:

  • Its a hybrid approach, pure is or should be faster. Hybrid subscene approach is better at 50k+ entities per frame mark I’d say. (But at the same time… you’re probably not going to author 50k entities per frame)
  • Custom bootstrap required to hack away subscenes / catalog from the build to prevent exceptions when building via default menu / UCB.
  • Requires to modify Entities package to simplify internal access. Some of faster non-alloc paths are plain unavailable, because… unsafe. (This is the main reason why I didn’t uploaded this solution to the Github)

So the Tl;DR of this:
In a hybrid enviroment MonoBehaviour logic should control entity lifecycle.
Otherwise its going to be a real pain to manage as you’ve already figured out.
But at the same time it shouldn’t be tied to purging whole world by the query, as that will cause trouble.

1 Like

Thank you! DOTS is a hard topic but community here is really supportive. I’ve come up with a solution that mixed up your suggestions, especially from @DreamingImLatios and @ and it seems working well:

  1. use Convert to Entity/“convert and destroy” (no more oddities with ConvertGameObjectHierarchy).

  2. The Spawner spawns only GameObjects with Convert To Entity, that will be converted during the next frame.

2.b) Idea: why not spawning the followers sync’d GameObjects during the conversion itself? So I attached this script to my prefabs for the GameObjects described above (those converted to entity):

public class FollowGameObject : MonoBehaviour, IConvertGameObjectToEntity
{
    public GameObject Follower; // references a prefab

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        var follower = Instantiate(Follower, transform.position, Quaternion.identity);
        FollowEntity followEntity = follower.GetComponent<FollowEntity>();
        followEntity.EntityToFollow = entity;    
    }
}

and this script in follower GameObjects not converting (e.g. for animations):

public class FollowEntity : MonoBehaviour
{
    public Entity EntityToFollow;

    void LateUpdate()
    {
        ...
        if (_entityManager.Exists(EntityToFollow))
        {          
             Translation entityPosition = _entityManager.GetComponentData<Translation>(EntityToFollow);
             transform.position = entityPosition.Value;
        }
       ...
    }
}
  1. Change scene still with the same code (universal query):
SceneManager.LoadScene("Level1");
_entityManager.DestroyEntity(_entityManager.UniversalQuery);

I’ve tried with an IComponentData “DestroyOnSceneChangeTag” but wasn’t working as much, still have to experiment more…

  1. static GameObjects in the hierarchy of each scene (like Camera) are instead referenced from a singleton GameManager with DontDestroyOnLoad.

Your comments were of immense help so I posted my solution hoping it may help as well someone!

1 Like

That was a specific detail of my framework. It is implemented like this:
https://github.com/Dreaming381/Latios-Framework/blob/v0.5.1/Core/Core/Systems/Scenes/DestroyEntitiesOnSceneChangeSystem.cs
The key is that I use the UniversalQuery to add and remove a tag component to all entities. Then I can use a more refined EntityQuery that excludes entities with specific components.

1 Like

Mind that some systems create entities in onCreate() and UniversalQuery will also destroy them. Namely, Unity Physics does this. I ended up just destroying and recreating the world on scene changes

    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);
        }
    }
    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);

            return true;
        }
    }
2 Likes