Search Unity

Question In DOTS, how to properly use external data in Entities.ForEach and share data between systems?

Discussion in 'Entity Component System' started by CydonianKnight, Jan 23, 2022.

  1. CydonianKnight

    CydonianKnight

    Joined:
    May 19, 2012
    Posts:
    54
    Hey guys,

    I'm just embarking on my journey with ECS and I'm generally struggling a bit to find good examples to lean on, especially since the API has changed in such a way that some stuff from 2019 or later is already deprecated. I managed to get something done but I'm not sure if I did it the right way and I'm generally a bit confused about what I have to do to make external data available within an Entities.ForEach. I'm not planning on going full ECS yet, I rather want to unload some stuff on ECS and feed back some data. So, in the code sample below I'm processing entities, reducing a value on a component and if the value has reached 0, I want to store some associated data in a queue that I am dequeueing in another system that runs afterwards. The latter will not be jobified and update a tilemap on the main thread. After some trial and error, this stuff works. I'm just wondering if there are other/simpler/more efficient ways of doing this and what might be redundant in my code. As mentioned before, I'm mostly confused about usage of reduceAmount/dataReduceAmount within the foreach in the code below. I'm pretty sure I read somewhere that you have to pack data into these native collections (even if it's just a single value) for it to be usable for the jobs or to make sure they can concurrently access it. However, I can leave out all the dataReduceAmount stuff and directly use reduceAmount in the foreach and it all still works. I didn't notice any performance differences either. So, do I have to pack the data in native arrays or not? And the NativeQueue ECSManager.queuedActions is actually a static variable (not of the system but another class). I couldn't use that directly in the foreach, because "Loading from a non-readonly static field is not supported" by Burst and I'm wondering if the stuff I'm doing with the "AsParallelWriter" etc is the proper way to go. And what other/better ways are there to pass data between systems? Having it in a static variable somewhere for different systems to access doesn't really feel like the way to go. Sorry that this post is a bit all over the place but I'm happy for any suggestions, pointers etc that you can provide. Thanks a lot in advance!

    Cheers

    Code (CSharp):
    1. protected override void OnUpdate()
    2.     {
    3.         float deltaTime = Time.DeltaTime;
    4.         float reduceAmount = Time.DeltaTime * fertilizerDecaySpeed;
    5.  
    6.         EntityCommandBuffer.ParallelWriter commandBuffer = commandBufferSystem.CreateCommandBuffer().AsParallelWriter();
    7.  
    8.         NativeArray<float> dataReduceAmount = new NativeArray<float>(1, Allocator.TempJob);
    9.         dataReduceAmount[0] = reduceAmount;
    10.  
    11.         NativeQueue<TilemapAction>.ParallelWriter queue = ECSManager.queuedActions.AsParallelWriter();
    12.  
    13.         Entities
    14.             .WithReadOnly(dataReduceAmount)
    15.             .WithNativeDisableContainerSafetyRestriction(queue)
    16.             .ForEach((ref Fertilizer fertilizer, in GridPosition gridPosition, in int entityInQueryIndex, in Entity entity) =>
    17.             {
    18.                 fertilizer.amount -= dataReduceAmount[0];
    19.                 if (fertilizer.amount <= 0.0f)
    20.                 {
    21.                     commandBuffer.DestroyEntity(entityInQueryIndex, entity);
    22.  
    23.                     queue.Enqueue(new TilemapAction
    24.                     {
    25.                         position = gridPosition.value,
    26.                         type = TilemapActionType.Remove,
    27.                         tileID = TileID.Topping_Fertilized
    28.                     });
    29.  
    30.                 }
    31.             }).WithDisposeOnCompletion(dataReduceAmount)
    32.               .WithBurst()
    33.               .ScheduleParallel();
    34.  
    35.         commandBufferSystem.AddJobHandleForProducer(this.Dependency);
    36.     }
     
  2. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    For readonly data you don't have to use NativeContainers. Think of it like this. When you schedule a job, it copies the job structs contents to another instance for each worker thread that can work on it. But when the job finishes, the values never get copied back. So it is only outputs that need to be stored in NativeContainers, unless you are using .Run() in which case the values do get written back.

    Lambda jobs need to capture local variables. So assigning it to a local like you did is correct. You shouldn't need NativeDisableContainerSafetyRestriction anymore.

    People have differing opinions on this. The one true rule is that wherever you store the container, you should also store JobHandles with it so that jobs that use it have the correct dependencies. I personally wrote a custom mechanism for automatically managing containers and their dependencies between systems: https://github.com/Dreaming381/Lati...e/Collection and Managed Struct Components.md
     
    charleshendry and Krajca like this.
  3. CydonianKnight

    CydonianKnight

    Joined:
    May 19, 2012
    Posts:
    54
    Thanks a lot for the information. I followed most of your suggestions, however leaving out NativeDisableContainerSafetyRestriction actually seems to have some negative impact on the performance, so I'm leaving that in for now.
    Concerning your last section, I still have to properly look through the link you provided (thanks for sharing). I think this might be a bit above my level of understanding as of now but I'll dig into it. To get me started, is there a very basic example you can give for storing a container plus associated JobHandles when using it across multiple systems?
     
  4. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    That's probably due to safety checks. Make sure you profile with safety checks disabled.

    Are you asking for an example of using my system or a more basic example? Regardless, I have both.

    For using my system, you can refer to this video and jump ahead to the Grid demo, specifically at the 9:45 mark is when I go through this particular feature:


    Otherwise, the Scriptable Object implementation for my Hearts demo shows how to explicitly manage JobHandles and why they are cumbersome (wrapping up the dependency management in a better API is an exercise for the reader): https://github.com/Dreaming381/HeartPromoDOTS/tree/main/HeartsCleanup
     
    bb8_1 likes this.