Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

ecs jobs - Interactions between entities

Discussion in 'Entity Component System' started by OnceUponADev, Feb 7, 2020.

  1. OnceUponADev

    OnceUponADev

    Joined:
    Dec 5, 2018
    Posts:
    12
    Dear DOTS fans,

    Let's say that I have a bunch of cars that arrive on screen and need to find an empty parking space

    for the sake of argument I have the following components (I have simplified my problem, my components are not these ones):
    - IsACarComponent (int x,int y, bool hasParkingSpace)
    - IsAParkingSpaceComponent (int x,int y,bool assignedToACar)

    and I want a system that simply takes all the cars with no parking space and assign one to them.

    I cannot figure out how to make a parallel job,

    all my solutions included
    [NativeDisableParallelForRestriction] and had bad performance ...

    any idea or general concept?

    ECS was absolutely magical until I had to interact between two different entities :-(

    Thank you for any help!
     
  2. Vanamerax

    Vanamerax

    Joined:
    Jan 12, 2012
    Posts:
    938
    I am running into a similar problem as well, trying to assign idle workers to available tasks. Currently using ComponentSystem and an Entities.Foreach within an Entities.Foreach, but there is probably a better (more parallelizable) way.

    If anybody has suggestions or best practices for this, I am interested as well.
     
  3. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,223
    Does it have to be a parallel job? It might just be faster to make this a single-threaded job and optimize the Burst-compiled code.
     
  4. calabi

    calabi

    Joined:
    Oct 29, 2009
    Posts:
    232
    You cant make everything a parallel job. I did something sort of similar but it was putting entities into buffers while checking whether they had already been assigned to a buffer. I couldn't see any way of doing it without having to check all entities and ending up with conflicts between threads checking and assigning buffers.

    So I could only get it to run single threaded bursted, but it still takes a somewhat long time with a lot of entities, but still way faster than mono and without burst.

    I think there are ways of doing it multi-threaded you could separate the parking spots into zones/chunks and have each thread handle a zone, but that's beyond my knowledge at the moment.
     
  5. OnceUponADev

    OnceUponADev

    Joined:
    Dec 5, 2018
    Posts:
    12
    Actually I just realised that what I was doing with my pathfinding was the same.
    Basically you can IJobForEach the cars, put them in a state like isCalculating, take it's position, then in the main thread send that to a list, and outside of ECS you can do some "normal" non ECS thread that calculates what is the destination the closest, and another job then reinsert the results into ECS.
    A bit convoluted but it does work ...

    When someone find a better solution I will had it back to ECS :)
     
  6. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,223
    So you can have one job build up a freelist of empty parking spots and then a single-threaded ForEach job to assign cars to each index in the freelist incrementally. That's two single-threaded jobs with Burst that could run alongside other jobs scheduled in your simulation.

    If your simulation is huge and you need parallel jobs, you can build up a list of cars needing parking in parallel using IJobChunk and NativeStream and do the same for your free spots. Then you can use a parallel job to map cars to parking spots for each stream index and write the extras to another pair of NativeStreams. And then lastly use an IJob to cleanup the mimatches.

    But profile first and make sure you need parallel before you use it.
     
  7. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    I might be wrong since I'm very new to DOTS, but here's my idea:

    Components (might want more specific names):
    • Car
    • TargetParkingSpot
    • Parked (tag)
    • Path (with Dynamic Buffer of target positions)
    • ParkingSpot
    • TargetedByCar
    • Occupied (tag)
    CarParkingSpotAssignmentSystem
    NativeArray<Entity> CarsNeedingParkingSpot = Entities with Car and without TargetParkingSpot, Parked.

    NativeArray<Entity> NontargetedUnoccupiedParkingSpots = all entities with ParkingSpot and without TargetedByCar, Occupied

    Create a concurrent NativeHashMap <Entity, Entity> called CarSpotPairings

    In a job, perform logic for pairing Cars with Spots using the two arrays, assigning to the NativeHashMap.

    In a main thread job, iterate the CarSpotPairings (this could be an ECB followed by a Barrier)
    • assign TargetParkingSpot to Cars and TargetByCar to ParkingSpot
    dispose CarsNeedingParkingSpot, NontargetedUnoccupiedParkingSpots, and CarSpotPairings​

    CarPathfindingSystem (after CarParkingSpotAssignmentSystem)
    NativeArray<Entity> PathlessCars = Entities with Car, TargetParkingSpot without Parked, Path + Buffer

    create a concurrent EntityCommandBuffer

    job: generate pathfinding data per Car, assign to Car via ECB path component and Buffer

    Dispose PathlessCars​

    CarParkingPathfindingECBBarrier (after CarPathfindingSystem)
    Entity Command Buffer Barrier for car pathfinding. (unsure about this ordering)​

    CarPathfindingSystem (after CarPathfindingSystem or CarParkingPathfindingECBBarrier )
    job: update all Entities with Car, Path + Buffer, and TargetParkingSpot without Parked. This system will 'move' your cars. When a car arrives at the parking spot: Car gets Parked and ParkingSpot gets Occupied.​



    If this doesn't make any sense please let me know as I'm still learning.
     
    Last edited: Feb 7, 2020
  8. OnceUponADev

    OnceUponADev

    Joined:
    Dec 5, 2018
    Posts:
    12
    @TheGabelle
    Thanks a lot for the detailled reply!
    I did try the approach with NativeHashMap, but the issue was that you need it to be either readonly or writeonly, so I can write to it (assigning to a spot) but I cannot check if the spot was already assigned as it would be a read, and therefor two cars close enough will receive the same spot :-(

    @DreamingImLatios reading your answer I guess the idea here (also solve what I am writing to TheGabelle) is that instead of having a parallel job that assign, I could do it in the main thread with a foreach

    Let me try that and come back to you guys :)
     
    Last edited: Feb 7, 2020
  9. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    Fair point. I don't know of a work-around for that situation at this time. This step might have to be main thread for now.
     
  10. thebanjomatic

    thebanjomatic

    Joined:
    Nov 13, 2016
    Posts:
    36
    To do this in parallel you can use tricks like spatially partitioning the data into buckets using their positions on a grid, and then process each bucket in parallel.

    If you went this route, you'd still have to handle the case where a bucket doesn't have enough open spaces for the cars. You could just do the global pass checking all remaining cars vs all remaining spaces as a last resort, but I suspect that whatever your actual use-case is, its probably not worth the hassle.
     
  11. OnceUponADev

    OnceUponADev

    Joined:
    Dec 5, 2018
    Posts:
    12
    I actually do partitions, I am using a grid so I have partitions of say 50 cells, and I do first the partition next to the car.
    but I hit the same issue, on the same partition I need a way to say that one is used.

    I think I nearly have everything working now, once it work I will post it!
     
    TheGabelle likes this.
  12. OnceUponADev

    OnceUponADev

    Joined:
    Dec 5, 2018
    Posts:
    12
    I post my code just for the sake of it, basically I
    - gather all the "cars" in a NativeQueue in an IJobForEach (so it is done in parallel)
    - I dequeue all the NativeQueue to a c# ConcurrentQueue
    - outside of the system I create threads to find dequeue the ConcurrentQueue and make all the complex calculation
    - the thread push to another ConcurrentQueue (results)
    - In the system I then dequeue the results and send it to a NativeArray
    - my system then apply the native array to the cars

    It was seriously more complicated than anything I can think of, but it does work lol, I guess at some point I will find a better way, or maybe ECS will provide new ways!

    Thanks all :)

    Code (CSharp):
    1.  
    2.  
    3. namespace Systems
    4. {
    5.     class NeedDestinationSystem : JobComponentSystem
    6.     {
    7.  
    8.         public static NativeQueue<Holder> toCalculate = new NativeQueue<Holder>(Allocator.Persistent);
    9.  
    10.         protected override void OnDestroy()
    11.         {
    12.             toCalculate.Dispose();
    13.         }
    14.  
    15.         protected override JobHandle OnUpdate(JobHandle inputDeps)
    16.         {
    17.             var jobGather = new jobGather()
    18.             {
    19.                 ret = NeedDestinationSystem.toCalculate.AsParallelWriter()
    20.             };
    21.  
    22.             var jobGatherHandle = jobGather.Schedule(this, inputDeps);
    23.  
    24.             jobGatherHandle.Complete();
    25.  
    26.             while (NeedDestinationSystem.toCalculate.TryDequeue(out Holder element))
    27.             {
    28.                 DestinationManager.threadQueue.Enqueue(element);
    29.             }
    30.  
    31.             // dequeue results
    32.             var archetype = World.DefaultGameObjectInjectionWorld.EntityManager.CreateArchetype(new ComponentType[] { typeof(TempTileMapCellToBeRenderedComponent) });
    33.  
    34.             NativeHashMap<int, Holder> results = new NativeHashMap<int, Holder>(10000, Allocator.TempJob);
    35.             while (threadResultQueue.TryDequeue(out Holder element))
    36.             {
    37.                 // we are here 100% sure that it is a unique place, ensured by .FindClosestFieldCell()
    38.                 results.Add(element.HasActionQueueComponentId, element);
    39.             }
    40.             var h = new JobApply()
    41.             {
    42.                 results = results
    43.             }.Schedule(this, inputDeps);
    44.  
    45.             results.Dispose(h);
    46.  
    47.  
    48.             return h;
    49.         }
    50.  
    51.         [BurstCompile]
    52.         struct JobApply : IJobForEachWithEntity<NeedDestinationComponent, HasActionQueueComponent>
    53.         {
    54.             [ReadOnly]
    55.             public NativeHashMap<int, Holder> results;
    56.  
    57.             public void Execute(Entity entity, int unused, ref NeedDestinationComponent NeedDestinationComponent, ref HasActionQueueComponent HasActionQueueComponent)
    58.             {
    59.                 if (results.ContainsKey(HasActionQueueComponent.id))
    60.                 {
    61.                     NeedDestinationComponent.found = 1;
    62.                     NeedDestinationComponent.active = 0;
    63.                     NeedDestinationComponent.foundX = results[HasActionQueueComponent.id].foundX;
    64.                     NeedDestinationComponent.foundY = results[HasActionQueueComponent.id].foundY;
    65.  
    66.                     HasActionQueueComponent.actionCurrentHasFinished = (byte)1;
    67.                 }
    68.             }
    69.         }
    70.  
    71.         [BurstCompile]
    72.         struct jobGather : IJobForEachWithEntity<NeedDestinationComponent, HasActionQueueComponent, Translation>
    73.         {
    74.             public NativeQueue<Holder>.ParallelWriter ret; // ParallelWriter is MANDATORY
    75.  
    76.             public void Execute(Entity entity, int index, ref NeedDestinationComponent NeedDestinationComponent, ref HasActionQueueComponent HasActionQueueComponent, ref Translation Translation)
    77.             {
    78.                 if (NeedDestinationComponent.active == 1 && NeedDestinationComponent.found == 0)
    79.                 {
    80.                     NeedDestinationComponent.found = 2;
    81.                     ret.Enqueue(new Holder()
    82.                     {
    83.                         x = (int)Translation.Value.x,
    84.                         y = (int)Translation.Value.y,
    85.                         type = NeedDestinationComponent.type,
    86.                         tried = 0,
    87.                         HasActionQueueComponentId = HasActionQueueComponent.id
    88.                     });
    89.                 }
    90.             }
    91.         }
    92.     }
    93. }
    94. //*/
     
    TheGabelle likes this.