Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Resolved Entities.ForEach outside of SystemBase.OnUpdate

Discussion in 'Entity Component System' started by mbaker, Apr 6, 2022.

  1. mbaker

    mbaker

    Joined:
    Jan 9, 2013
    Posts:
    52
    I'm working on a solution to move entities between two worlds and want to give systems a chance to run some pre-move and post-move logic.

    To make sure that the entities aren't moved mid update I kick off the operation in the LateUpdate playerloop phase. After all of the ECS system groups have been updated and scheduled their work for the frame.

    The high level setup is:
    1. Systems register to be "World Change" aware and implement an interface that requires them to implement a Pre and Post world change handling method.
    2. At some point a world change is requested and we schedule the operation to run during LateUpdate
    3. During LateUpdate, from the main thread, we loop through all systems that are world change aware providing a JobHandle to depend on and allowing them to return a JobHandle for the world change to wait on.
    4. We run JobHandle.CompleteAll() with all of the returned JobHandles from the previous step.

    Generally this works well but I'm having issues with component read/write dependencies when the systems schedule their pre/post work move work. Scheduling work outside of a system's OnUpdate doesn't seem to build off any existing dependencies and I get errors that we're trying to access components without depending on their existing read/write handles from the main update phase.

    Not surprisingly, calling EntityManager.CompleteAllJobs() before scheduling the pre/post world move work solves the issue but this isn't ideal.


    Context:
    Step 3: Kick off World change
    Code (CSharp):
    1. LateUpdate()
    2. {
    3.    DestinationWorld.GetOrCreateSystem<EntityLifecycleSystem>().ImportEntitiesFrom(sourceWorld);
    4. }

    Step 3: Prepare Entities for World Eviction and move them.
    Code (CSharp):
    1.  
    2. // Methods in EntityLifecycleSystem.cs
    3. public void ImportEntitiesFrom(World sourceWorld)
    4. {
    5.    sourceWorld.GetOrCreateSystem<EntityLifecycleSystem>().PrepareEntitiesForEviction();
    6.    EntityManager.MoveEntitiesFrom(sourceWorld.EntityManager);
    7. }
    8.  
    9. public void PrepareEntitiesForEviction()
    10. {
    11.    // Uncommenting this fixes the issue
    12.    // EntityManager.CompleteAllJobs();
    13.  
    14.    JobHandle rootDependency = default;
    15.    NativeArray<JobHandle> observerDependencies = new NativeArray<JobHandle>(m_Observers.Count, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
    16.    for(int i=0; i<observerDependencies.Length; i++)
    17.    {
    18.       observerDependencies[i] = m_Observers[i].OnEnvironmentEvictionPending(rootDependency);
    19.    }
    20.  
    21.    JobHandle.CompleteAll(observerDependencies);
    22. }

    OnEnvironmentEvictionPending Example
    There are other jobs in the main update loop that make use of the SomeComponent type below. Scheduling the work below outside of the OnUpdate stack doesn't seem to chain the component's dependency off the last access from the main loop.
    Is there a way I can do this?

    Code (CSharp):
    1. public JobHandle OnEnvironmentEvictionPending(JobHandle dependsOn)
    2. {
    3.    // Exception: A previously scheduled job reads from SomeComponent...
    4.    return Entities.ForEach((ref SomeComponent myComponent) =>
    5.       {
    6.          // some work...
    7.       })
    8.       .Schedule(dependsOn);
    9. }
     
  2. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    If you don't want to sync on all jobs before these systems run their special callbacks, then you need these special callbacks to be invoked from OnUpdate. Some base class cleverness could dispatch the appropriate callback from OnUpdate based on an enumeration. Then to update the system, the enum for the callback entry point is set and then Update() gets called on the system.
     
  3. mbaker

    mbaker

    Joined:
    Jan 9, 2013
    Posts:
    52
    Thanks, that's what I was afraid of. It's too bad the per-component type read/write fences aren't exposed to us or at least the tip of the JobHandle chain in a world.

    Yea, that was an alternative approach I had in mind. It's going to get a bit messy coordinating that work between two Worlds but I'll give it a shot and we'll see if I can come up with a reasonably clean solution.

    I'm hoping I can get away with calling my special callbacks from the OnUpdate of my EntityLifecycleSystem rather than having to call Update on all of the systems that are interested in pre/post world move events. That would kind of defeat the purpose having to wait on the complete of those systems before scheduling their world move work.
     
  4. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    3,983
    Dependency just isn't updated with the new JobHandles (there are multiple) until inside SystemBase.Update just before OnUpdate gets invoked.
     
  5. mbaker

    mbaker

    Joined:
    Jan 9, 2013
    Posts:
    52
    Yea, looks like we'd just need a way to flip the SystemState.NeedToGetDependencyFromSafetyManager flag...maybe call m_JobHandle.Complete() on the existing handle.

    Or gain access to EntitiyDataAccess.DependencyManager
     
  6. mbaker

    mbaker

    Joined:
    Jan 9, 2013
    Posts:
    52
    Ok so calling my observer methods (which schedule jobs) from the OnUpdate method of my EnvironmentLifecycleSystem didn't work. However find a solution that's only a little bit of extra work for the consumer. Add the query's dependency before scheduling the job.

    In practice, this is a minor change to my last code snippet
    OnEnvironmentEvictionPending Example (Updated)
    Code (CSharp):
    1. private EntityQuery m_Query;
    2. public JobHandle OnEnvironmentEvictionPending(JobHandle dependsOn)
    3. {
    4.    dependsOn = JobHandle.Combine(m_Query.GetDependency(), dependsOn);
    5.    return Entities
    6.       .WithStoreEntityQueryInField(ref m_Query)
    7.       .ForEach((ref SomeComponent myComponent) =>
    8.       {
    9.          // some work...
    10.       })
    11.       .Schedule(dependsOn);
    12. }
    13.