Search Unity

LoadScene, ConvertToEntity, LateUpdate causes headaches

Discussion in 'Entity Component System' started by kablouser, Jul 28, 2021.

  1. kablouser

    kablouser

    Joined:
    Dec 14, 2018
    Posts:
    5
    tl;dr When the game starts, Awake is called before ConvertToEntitySystem is updated. Which works great. But when you load a new scene, ConvertToEntitySystem is updated before Awake is called. Which is a problem if you need MonoBehaviours on LateUpdate to manipulate entities. Because those entities are not converted yet.

    I will describe the problem in detail as far as I understand the systems at play.
    Then I will describe my solutions. And other potential solutions.

    We can attach ConvertToEntity scripts to GameObjects to automatically convert them into entities at runtime. Specifically, when the GameObject Awakes, ConvertToEntity queues its GameObject into ConvertToEntitySystem to be converted later. Then during the InitializationSystemGroup, ConvertToEntitySystem is updated which converts all queued up GameObjects into entities.

    On Game Start:

    The above image shows the order of events when the game starts. So, ConvertToEntity queues up GameObjects and they are converted immediately in the same frame. When LateUpdate is called, the entities are converted and ready to be queried.

    On Load Scene Using Button:

    1. Button clicked event
    2. Ignorable warning. Comes from calling entityManager.DestroyEntity(entityManager.UniversalQuery);
    3. Called SceneManager.LoadScene
    4. LateUpdate called by MonoBehaviours in the old scene (the one to be unloaded). This is fine.
    5. InitializationSystemGroup and ConvertToEntitySystem updates. But cannot convert anything since there's nothing in the queue.
    6. Awake called by MonoBehaviours in the new scene. ConvertToEntity queues up GameObjects to be converted.
    7. LateUpdate called by MonoBehaviours in the new scene. This is where issues arise! Some MonoBehaviour tries to reference some entity that hasn't been converted - but crashes and dies.
    8. InitializationSystemGroup and ConvertToEntitySystem updates. Finally converts the entities. A bit late.
    My Solution
    Code (CSharp):
    1.     // must be the same group as ConvertToEntitySystem
    2.     [UpdateInGroup(typeof(InitializationSystemGroup))]
    3.     [UpdateAfter(typeof(ConvertToEntitySystem))]
    4.     private class ConvertToEntityEventSystem : SystemBase
    5.     {
    6.         protected override void OnCreate()
    7.         {
    8.             base.OnCreate();
    9.             // there is no problem when the game starts
    10.             IsConvertToEntityComplete = true;
    11.             ConvertingFrames = 0;
    12.         }
    13.  
    14.         protected override void OnUpdate()
    15.         {
    16.             Debug.Log("InitializationSystemGroup Update");
    17.  
    18.             if (IsConvertToEntityComplete == false && 1 < ++ConvertingFrames)
    19.             {
    20.                 IsConvertToEntityComplete = true;
    21.                 ConvertingFrames = 0;
    22.             }
    23.         }
    24.     }
    25.  
    26.     public static bool IsConvertToEntityComplete { get; private set; }
    27.     private static int ConvertingFrames;
    28.  
    29.     public static void LoadScene(int buildIndex)
    30.     {
    31.         Debug.Log("Button Clicked");
    32.  
    33.         var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
    34.         entityManager.CompleteAllJobs();
    35.         entityManager.DestroyEntity(entityManager.UniversalQuery);
    36.  
    37.         IsConvertToEntityComplete = false;
    38.         SceneManager.LoadScene(buildIndex, LoadSceneMode.Single);
    39.  
    40.         Debug.Log("Called LoadScene");
    41.     }
    I wait for 1 frame inside the InitializationSystemGroup after the scene has been loaded. After which I set the static boolean to true. And IsConvertToEntityComplete can be used by MonoBehaviours in LateUpdate to determine whether entities are setup or not.

    Other Potential Solutions
    -Delay LoadScene until the very start of a frame. Hopefully GameObjects Awake before InitializationSystemGroup.
    -Forcibly call Awake on all Objects after LoadScene. Then update ConvertToEntitySystem.
    -Add an event on ConvertToEntitySystem called OnConvert. Which is invoked when its Convert() function is called.

    I don't really know why Awake is after InitializationSystemGroup when a scene is loaded. Wouldn't it make more sense for them to happen the other way round?
     
  2. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,778
    You can not rely on late update, u less you call system update from it. Otherwise it runs independently from dots systems.

    Simple solution is, to use component tag, which marks entities as initialising. First job picks entities with that tag. Does whatever it need to do at initialisation of entity. And remove component tag.

    All systems working on these entities, should take into consideration that tag.
     
  3. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,269
    The real issue here is that the process responsible for loading and unloading scenes takes place after InitializationSystemGroup but before FixedUpdate, Mono Update, SimulationSystemGroup, ect. SceneManager.LoadScene simply queues up this process.

    Accessing EntityManager via World.DefaultGameObjectInjectionWorld is usually a sign of an architectural issue. It can be fine for quick hacks and prototyping, but eventually you are going to want to dispatch anything that touches entities exclusively from systems, so that you can more easily control the execution order.
     
    kablouser and Antypodish like this.
  4. kablouser

    kablouser

    Joined:
    Dec 14, 2018
    Posts:
    5
    I don't know what that means. I thought that execution order is controlled by [UpdateBefore], [UpdateAfter], [UpdateInGroup]. And how would I have a button click trigger code in a system without DefaultGameObjectInjectionWorld?
     
  5. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,269
    That's only within ComponentSystemGroups. The top level system groups are injected into the engine loop using the PlayerLoop API. It is an internal engine PlayerLoop SubSystem which handles the scene loading. And that executes between InitializationSystemGroup and SimulationSystemGroup by default.

    You want to cache the events into a MonoBehaviour that gets injected into an entity either by AddHybridComponent or ConvertAndInjectGameObject. Example: https://github.com/Dreaming381/lsss-wip/blob/master/Assets/_Code/Components/TitleAndMenu.cs
    Then a system accesses that MonoBehaviour and polls for the events. Example: https://github.com/Dreaming381/lsss...ode/SubSystems/UI/TitleAndMenuUpdateSystem.cs
     
  6. kablouser

    kablouser

    Joined:
    Dec 14, 2018
    Posts:
    5