Search Unity

Can such mechanics (herbivores eat plants) be reliably run in parallel?

Discussion in 'Entity Component System' started by Antypodish, Oct 21, 2019.

  1. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,778
    Imagine you have plants and herbivore animals.

    Lets take into consideration group of 5 animals close to each other.
    Each searches for plants in reach.
    It can happen, that multiple animals find same plant at the same time.
    If animal eat plant, plants available resources decreases.
    So for example, there may be enough food resources for 2 animals, but not for 3 or more.

    All plays nicely, when I do such mechanics in single threaded job. Which is fine.

    Now lets say, I run animals in IJobChunk.
    By my understanding is that, if parallel chunks are running, it may happen that animal from each chunk pick same plant at the same time. So that of course is the problem, as I can run into race condition, when number of animals eat more, than plant can supply.

    The way I can think of, is using multi hash map in parallel to store information, which animal tried eat which plant. At this point, no actual resources consumption would occur, just checks and pairing. Need to indicate, not every animal will eat plant at the same frame. Not sure, if that is right application for multi hash maps in parallel.

    And even then, I would need iterate through multi hash map elements in next single threaded job, to check and consume resources, if available.

    Another problems is, that as far I am aware, cleaning hashamps is not as performant, or I need allocate memory for hashamp. I would need do that every time, I want to run system.

    Any other suggestions?
     
  2. Ziboo

    Ziboo

    Joined:
    Aug 30, 2011
    Posts:
    356
    For this kind of thing I tend to use an event system.
    In you're job your create an Event Entity with a ComponentData like

    Code (CSharp):
    1. public struct AnimalEatFoodEvent : IComponentData
    2. {
    3.     public Entity Animal
    4.     public Entity Plant
    5. }
    Then in a simple ComponentSystem that run on the main thread and at the end of the frame, you collect all the events, and do you're logic. Like removing resources on the plant, or giving it to the animal. Then you just destroy the EventEntity.

    The nice thing with that also, is that you don't modify any archetypes on the plants or animals. You just create 1 frame entity.

    I'm using the awesome https://github.com/tertle/com.bovinelabs.entities library to do that, which by pass the slow CommandBuffer, and actually batchs all the events at the end of the frame.
     
    florianhanke and Antypodish like this.
  3. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,778
    Last edited: Oct 21, 2019
  4. It depends on what this system will be for. If you are working on an AUX AI, you don't need to keep 100% realistic your data in all frames.
    If I were making this I would do this (I don't need every frame precise):
    - The animals select plant to eat, regardless of each other, store reference to plant on animal and reference to the animals on plant (the animals will prepare to move to the plant and eat in the subsequent frames)
    - a sorting job (animals have strength or power or whatever attribute) will be run on the plants selected by any animal, the job will sort the animals referenced on the plant by this power attribute DESC and store which animals will be able to feed on it (if there is room for two animals and four selected this, the two most powerful will win)
    - the animal plant-picking job run tests periodically and checks if the animal is still in the allowed pool otherwise resets such animals' plant-selection and for those animals the selection process will start again

    this has some very nice side effects: every animal which needs to feed will start to act, in the mean time you have time to check if everything is valid, if not, the less powerful animals will forced to alter their plans

    The con: it is not working for decision in the same frame (fast simulation).
     
  5. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,778
    @Lurking-Ninja fortunately I don't need strictly execute this behaviour in same frame. So I can spread it a bit.
    While your concept sounds resealable, my only concern is regarding
    My trouble is, if I run animals in chunks, how do you propose store group of animals in corresponding plant, without race condition?
     
  6. Yeah, that's a problem.
    I think the NativeList allows parallel writing. So you will end up something like a ECB but hand-made. Which means after animals schedule their plants, you need to sync this to the plants as well before you run the check jobs, but this cannot be parallelized. This mean, you need to do this in a sync-point (end of frame?).

    Yeah, I'll think about this more at home. At work, my parallel brain processes are busy with work too. :D
     
  7. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,778
    No stressing out :)

    I am keep comming back and looking int event system, which was proposed earlier. It apparently work somehow similarly as ECB, but allow to work in burst as well.

    However, I haven't yet fully wrapped my brain around the concept.
     
    MNNoxMortem likes this.
  8. Radu392

    Radu392

    Joined:
    Jan 6, 2016
    Posts:
    210
    As far as I know, you can only do this reliably with some type of event system. The way I do it is, inside a bursted parallelized job, I can request an 'eat' event and put it into a hashmap. I then pass this hashmap to another parallelized but not bursted job which will pass along the event to a List or Queue of the relevant system, in your case it would be EatSystem. That system will then schedule a new and final job with a new hashmap containing all the requested events, which can be multiple per frame. This job will make sure that the plant you're eating has enough 'hp' to be consumed and if not, simply ignore the event and move on. If it could be consumed, it would then request a new event to your animal system to receive whatever boost it should get from eating the plant.

    Almost everything here is bursted and everything is parallelized. The only 'catch' is that this can be executed over more than one frame. This however doesn't cause any race issues and everything works as it logically should, meaning that plants can't be overconsumed and animals wouldn't get more food than they should.
     
  9. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,778
    @Radu392 Since mentioning HashMaps, which now occurred few times in this thread already, I am just thinking, since we got now GetValues and GetKeys from native hash map, we can do something like following.

    Pseudo code ..

    Before passing hashMap into job
    Code (CSharp):
    1. // Prepare maximum size of Native HashMap.
    2. var nhm_animalsTryingEat = new NativeHashMap <Entity, Entity> ( totalNumberOfAnimals, Allocator.TempJob ) ;
    3.  
    Assign animals-plants pairs in parallel job with burst.
    Food check job for each animal.
    Code (CSharp):
    1. // Here my parallel job ...
    2. // Check if animal can potentially eat plant.
    3. // If so, assign animal-plant pair to hash map.
    4.  
    After parallel job done, move animal-plant pairs to native array.
    Code (CSharp):
    1. NativeArray <Entity> na_animalsTryingEat = nhm_animalsTryingEat.GetKeyArray ( Allocator.TempJob ) ;
    Run this in single threaded job with burst.
    Each filtered animal eats food if enough.
    Code (CSharp):
    1. for ( int i = 0; i < na_animalsTryingEat.Length; i ++ )
    2. {
    3.     Entity animalEntity = nhm_animalsTryingEat [i] ;
    4.     Entity plantEntity ;
    5.     nhm_animalsTryingEat.TryGetValue ( animalEntity, out plantEntity ) ;
    6. }
    One thing is, since following is deprciated
    nhm_animalsTryingEat.ToConcurrent ()
    correct way would be using?
    nhm_animalsTryingEat.AsParallelWriter ()

    Does that sounds any reasonable?
    Or still would advise go with some event system?


    Edit:
    To avoid needing for sync points, possibly using hash map as Allocator.Persistent.
    Then dispose and reallocate / clean before each new search food job is executed.

    Then eating food job can be executed in next frame.
     
    Last edited: Oct 22, 2019
  10. Ziboo

    Ziboo

    Joined:
    Aug 30, 2011
    Posts:
    356
    Here is an example:

    Code (CSharp):
    1. // Event Component
    2. public struct AnimalEatPlantEvent : IComponentData
    3. {
    4.     public Entity Animal;
    5.     public Entity Plant;
    6. }
    7.  
    8.  
    9. //Job System
    10. public class AnimalAiSystem : JobComponentSystem
    11. {
    12.      protected override JobHandle OnUpdate(JobHandle inputDeps)
    13.         {
    14.             //Your code ....
    15.             var animalAIJob = new AnimalAiJob
    16.             {
    17.                 EventQueue = World.Active.GetOrCreateSystem<EntityEventSystem>().CreateEventQueue<AnimalEatPlantEvent>().AsParallelWriter()
    18.             };
    19.  
    20.             return animalAIJob.Schedule(this,inputDeps);
    21.         }
    22.  
    23. }
    24.  
    25. //Job
    26. [BurstCompile]
    27. private struct AnimalAiJob : IJobForEachWithEntity<Animal>
    28. {
    29.     public NativeQueue<AnimalEatPlantEvent>.ParallelWriter EventQueue;
    30.  
    31.     public void Execute(Entity entity, int index, ref Animal animal)
    32.     {
    33.      
    34.         //Your Ai that check if animal near a plant and wants to eat it...
    35.         //if yes then Enqueue an Event
    36.      
    37.         this.EventQueue.Enqueue(new AnimalEatPlantEvent()
    38.         {
    39.             Animal = animal,
    40.             Plant = myPlant
    41.         });
    42.      
    43.     }
    44. }
    45.  
    46. //Event Logic
    47. public class AnimalEatPlantEventSystem : ComponentSystem
    48. {
    49.     protected override void OnUpdate()
    50.     {
    51.         //Here you are on the main Thread. Thoses Events only live for 1 frame, cause the Queue is clear by the system automaticaly at the end of the frame
    52.      
    53.         this.Entities.ForEach((Entity entity, ref AnimalEatPlantEvent animalEatPlantEvent) =>
    54.         {
    55.             var plant = animalEatPlantEvent.Plant;
    56.             var animal = animalEatPlantEvent.Animal;
    57.             var plantHealth = this.EntityManager.GetComponentData<Health>(plant);
    58.          
    59.             if (this.EntityManager.Exists(plant) && this.EntityManager.Exists(animal) && plantHealth.Value > 0)
    60.             {
    61.                 plantHealth.Value -= 1;
    62.                 this.EntityManager.SetComponentData(plant, plantHealth);
    63.              
    64.                 var animalHealth = this.EntityManager.GetComponentData<Health>(animal);
    65.                 animalHealth.Value += 1;
    66.                 this.EntityManager.SetComponentData(animal, animalHealth);
    67.             }
    68.         });
    69.     }
    70. }
    71.  
     
    Antypodish likes this.
  11. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,778
    @Ziboo this looks really nice and look simple. Thx lot for sharing.

    I just got a thought here.
    When you run
    Code (CSharp):
    1. this.Entities.ForEach((Entity entity, ref AnimalEatPlantEvent animalEatPlantEvent) =>
    2. {
    Does it have to use this.EntityManager inside?
    I am asking, because if ComponentFromEntity could be used, then burst would be applicable to the job?
     
  12. Ziboo

    Ziboo

    Joined:
    Aug 30, 2011
    Posts:
    356
    I never tried in a job.. But I guess it's doable.

    I was doing it in a simple component system, because you're sure it's on the main Thread, then you don't have any race condition.

    Also, keep in mind that you can make this system run only when such Events exists, with a Query and RequireForUpdate.

    Also, we are talking about a one frame system that executes on some entities.
    Try it, but I'm pretty sure it will not blow up your framerate ^^

    I know with DOTS, we all want to super optimize the code, but remember that you profile first, and if needed, optimize.
     
  13. Ziboo

    Ziboo

    Joined:
    Aug 30, 2011
    Posts:
    356
    Maybe you could try with a ScheduleSingle to run the job on a single thread.
    But I don't know in that case if Burst will be usefull at all. Never tried.
    If you do, let me know.
     
  14. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,778
    Yep. There is also IJob, which I think is sensible, to allow burst.
    My question was rather toward, if the event forces somehow EntityManager to be there.
    If not, I just pointing into nice optimization possibility. Specially if there is such massive difference between burst and none burst job :)

    However, if this event job is burstable I will definitely give a go. Looks more optima, than using HashMap.
    Until now, I was collecting all possible options, for given problem.

    Of course, still open for other propositions :)

    I will play with events system, as soon as I can, and I post with results.
     
  15. Ziboo

    Ziboo

    Joined:
    Aug 30, 2011
    Posts:
    356
    Nop the event doesn't force the EntityManager.

    The system only store Events in a queue. Batch them, and create Entities with this component. Then destroy the entities at the end of frame and clear the Queue.
     
  16. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    So my event system ziboo talked about above does work quite well here, especially if you want to have other systems also act when a plant is eaten (play a sound, record stats, etc).

    However if you don't intend to build your application modular like this and all that would be handled in just a single system there is a much more general pattern you can use (and is something I use regularly) and will be more performant as you don't need to create any entities. Just do the work over 2 jobs.

    It ends up being pretty much exactly the same but instead of creating entities, you just pass what was the event queue directly between the 2 jobs, with the first one executing in parallel.

    Code (CSharp):
    1.  
    2.     public struct AnimalEatPlantEvent
    3.     {
    4.          public Entity Animal;
    5.          public Entity Plant;
    6.     }
    7.  
    8.     protected override JobHandle OnUpdate(JobHandle handle)
    9.     {
    10.         handle = new AnimalAiJob
    11.             {
    12.                 EventQueue = this.queue.AsParallelWriter()
    13.             }
    14.             .Schedule(this,handle);
    15.      
    16.         handle = new EatPlantsJob
    17.             {
    18.                 EventQueue = this.queue,
    19.                 Health = this.GetComponentFromEntity<Health>(),
    20.             }
    21.             .schedule(handle);
    22.  
    23.         return handle;
    24.     }
    25.     [BurstCompile]
    26.     private struct AnimalAiJob : IJobForEachWithEntity<Animal>
    27.     {
    28.         public NativeQueue<AnimalEatPlantEvent>.ParallelWriter EventQueue;
    29.    
    30.         public void Execute(Entity entity, int index, ref Animal animal)
    31.         {
    32.             //Your Ai that check if animal near a plant and wants to eat it...
    33.             //if yes then Enqueue an Event
    34.        
    35.             this.EventQueue.Enqueue(new AnimalEatPlantEvent()
    36.             {
    37.                 Animal = animal,
    38.                 Plant = myPlant
    39.             });
    40.        
    41.         }
    42.     }
    43.  
    44.     [BurstCompile]
    45.     private struct EatPlantsJob : IJob
    46.     {
    47.         public NativeQueue<AnimalEatPlantEvent> EventQueue;
    48.  
    49.         // pass in component data
    50.         public GetComponentFromEntity<Health> Health;
    51.      
    52.         public void Execute()
    53.         {
    54.             while(EventQueue.TryDequeue(out var e)
    55.             {
    56.                 var plantHealth = this.Health[e.Plant];
    57.                 // do the work here
    58.             }
    59.         }
    60.     }
    I would still consider using events with this but instead I'd put them in the second EatPlants job for when plants are confirmed eaten. Then you can react to these events in for example a AudioSystem to play a sound or a StatSystem to record plants eaten, types, anaytics etc so you don't have to crowd your EatPlants job and AI system with all sorts of external systems.

    It really just depends how you want to design your application.
     
  17. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,269
    I apologize if I come off as that guy who shows up late to the party and tells everyone they are wrong, but I do feel like you all are misunderstanding the ECS way of doing things for this particular problem.

    Let's suppose you have a bunch of animals that go around searching for food. When they "see" food, they target it and move towards it. So the animals discover food, and move towards food. Those are straight-forward ECS problems most of us have figured out. Now the next step is to make the animals devour the food, while also alerting everything which food was devoured, and trying to do that all in parallel. The problem is that you are trying to make the animals do all the work, will the food just disappears passively. That's an actor model.

    So instead, let's look at our data from a different perspective. To help paint a mental picture, lets assume there is a goddess overseeing the entire food consumption. She is our "systems". When multiple animals try to consume the same food, she decides which animal earned the food, essentially writing the deserving animal's name on the food.

    In practice what you would do is have a component or buffer on the food entities named "Claimed" or something and have an Entity field. Instead of looping over each animal, you loop over each food entity and collect the animals close enough to the food to eat it. Then you filter out for only the animals targeting that specific food entity. With those results, if you still have animal entities, you assign the entity references to the Claimed components. Afterwards, you run a loop over each animal and check the targeted food entity. The food's Claimed component will either match the animal's in which case the animal may eat it, or it may remain unclaimed (null entity) and the animal will continue to pursue it, or it may be claimed by another animal. In that final case, the current animal knows to give up, or as a bonus mechanic, it can see which animal "stole" its food and go attack that other animal.

    This approach does not require any event system nor sync point, and can be completely parallelized. All I did was look at the problem from a different perspective and turned a write dependency into a read dependency.
     
    MNNoxMortem and florianhanke like this.
  18. Ziboo

    Ziboo

    Joined:
    Aug 30, 2011
    Posts:
    356
    Btw @tertle thanks for your repo ! Could you update the readme though, it seams outdated:
    Code (CSharp):
    1. NativeQueue<T> EntityEventSystem.CreateEventQueue<T>(JobComponentSystem componentSystem)
    CreateEventQueue() doesn't take any parameter.

    I'm stuck on something and can't find the right solution.

    Thanks
     
  19. Ziboo

    Ziboo

    Joined:
    Aug 30, 2011
    Posts:
    356
    Thanks for the other point of view.
    Will that modify Archetypes though ?

    It's mostly what I'm trying to avoid, cause that what's slowing down the most the ECS
     
  20. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,269
    This algorithm does not require any sync points, if that's what you are asking.

    You obviously need to make sure that food has the Claimed components for the algorithm to work, but they don't need to be added and removed at runtime. Their life-cycle would be the life-cycle would be the life-cycle of the food. For recharging food sources (if that's what you need), you can have some "Recharge" entity value or use a bool in the Claimed component. Or if you go dynamic buffers, a dynamic buffer with a length of zero could mean that there is no food left and when the food replenishes, the buffer fills.
     
  21. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    You can see at Nordeus demo from earlier official Unity DOTS presentations, where they using exactly same event approach as above - writing to queue in parallel and attack processor, later, dequeue it in bursted job and apply damage. Similar thing - multiple affectors, single consumer. This is not going against “ECS way of doing things” at all :) Moreover you not spend chunk size for additional components.
     
    Antypodish likes this.
  22. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    The repo has a pretty old version. I haven't updated anything in there in a while (the latest version can be found in the forum thread, I should get around to making the repo it lives in public at some point.)
    You need to use, AddJobHandleForProducer(JobHandle), now. Similar to EntityCommandBuffer.
    Anyway it's a bit off topic, if you want more help just throw up a post in the original thread.
     
    Antypodish likes this.
  23. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,778
    @DreamingImLatios thank you for your suggestions.
    However, iterating over every food, is exactly what I want to avoid. Otherwise I wouldn't need ask for this problem.

    To clarify reason, why animal is priority over food, is in case for example, where I have 10k plants and only 100 animals. It makes massive difference, in terms how things are computed. If that makes sense.

    @tertle, you library looks super cool. The only my concern is (maybe unnecessary?) DOTS evolve rapidly. While you library is not massive, I think maintaining it may be the issue? I mean, as long you keep updating is fine. But what if you decide no more support for it, for any reason? Chances are it may become quickly obsolete, unless updated by myself?

    You know all magic on low level very well. I am not that clever to keep up. Just a thought :)
     
  24. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    I can't see much breaking it, I'd expect instead there would be more opportunities to optimize it in the future.

    Anyway, I probably wouldn't use it for this specific problem anyway instead I'd personally just do the 2 job solution I posted earlier. It may not have 100% optimal thread performance but it is very straight forward and easy to understand what is going on.

    DreamingImLatios is quite an elegant idea that I hadn't really considered before; I quite like the idea of inverting the problem. However, I do have certain concerns with it (not really for this specific problem) but it is something I'm definitely going keep in mind in the future when solving problems.
     
  25. Radu392

    Radu392

    Joined:
    Jan 6, 2016
    Posts:
    210
    DreamingImLatios idea's is very similar in the way you acquire a target, but inverted. It sounds good in theory, but a little complicated in practice.

    First, if you use a Claimed component with only a single Entity field, it would mean that a food piece can only be eaten by only one animal at the same time. I assume the OP would like to have a food piece be eaten by multiple animals at the same time. A solution to that would be a buffer to keep track of multiple claims, but this is where it gets complex. Instead of dealing with the usual 'one to one' situation, you'd have to manage a 'one to many' situation. You would have to manage the buffer correctly in many possible situations, i.e. identify the correct index to remove in case an animal decides not to go for the plant anymore. And that is only one case. In that case, the plant would still need to know of the animal's intention, so you'll still need communication between the 2, which as stated by myself and others in this thread, is best done by some sort of event system.

    It could also be done without events however, but would require extra computation. I'm currently experimenting with a system that refreshes the data of an entity's 'Claimed' component every frame as long as the claimed entity is in sight. In my case, that component is called 'HasTarget', which contains position of the target and other stats. As far as I know, you would need to do this in cases where the target can move away from its original position of acquisition. In the case of plants however, this wouldn't be required so the events would still be the best way to go imo.
     
  26. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,778
    Yep.

    If I want go per plant checks, rather per animal, I just iterate through plants, and checks if there is food for next near animal. If is, eat. Then check next near animal. If there is available food eat and so on, until no nearby animals, or no food. In this case, I don't need any claim tags etc.

    But then, I need mark again an animal that it has been eating already, since if I go to next plant, It can happen I will check already feasted animal. Either if has been tagged as already eaten, or need check, if is still hungry.

    And more over, if I do that in parallel, I still run into potential race conditions.

    So in the end, I will need end up with some sorts of event system anyway.
    Only that, If I got much more plants than animals, I have much more iterations to compute.
    So I don't feel like win win situation.

    However, if I know, I got more-less same amount of animals as available plants, or much more animals than plants, then should be fine. Is just inversion.



    Can anyone comment please, upon using NativeHashMap approach, similar as my post #9 please?
    In this case, using nhm_animalsTryingEat.AsParallelWriter () for checking available food in one parallel job, then passing keys to NativeArray for next job, possibly in next frame, to avoid needing for sync points.

    nhm_animalsTryingEat would need be cleaned / disposed, before food search job.
     
    Last edited: Oct 22, 2019
  27. Radu392

    Radu392

    Joined:
    Jan 6, 2016
    Posts:
    210
    The approach in #9 sounds correct, though you have to experiment yourself. Even though DOTS is more than a year old now, it's still very much new territory. It has many little restrictions but also possibilities and you won't know what they are until you actually start coding yourself. Since you can go so low level with DOTS approaches in general, it's also much harder to find the optimal one size fits all solution. So you'll have to see how the solution works out for your particular case.

    Btw, off the top of my head, I can't recall which one it is: AsParallelWrtiter() or ToConcurrent(). You'd have to check the docs for that. Even if you use wrong/deprecated one, Unity will be complaining about it so you'll know it's the other one.
     
  28. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,778
    I checked already, AsParallelWrtiter() should be used now.
    I executed this approach for now, so it does works. I need profile it yet.

    Still I think I should implement @tertle suggestion, just to test it out.


    Just a side note, I don't think any of our suggestion, are suitable for deterministic behavior, if considering run in parallel jobs. Single threaded jobs would be more advisory I think, in that case.
    Unless, as @DreamingImLatios mentioned a bit, we got some prioritization mechanics, which defines, who it first.
     
  29. Ziboo

    Ziboo

    Joined:
    Aug 30, 2011
    Posts:
    356
    After some thoughs about the solution from DreamingImLatios.
    I'm not sure it's super explicit in term of understanding the code base.

    I think it's easier when you talk about AI that the decision comes from the Actor.
    Having a FoodSystem deciding the logic on how an animal might eat would soon be overloaded if you have many types of different Animals.
    Puting the logic around the Animal Systems is better I think.
    The FoodSystem might just sort race condition and choose the winner.
     
  30. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,269
    DOTS is all about using the right tool for the right job. In the case of Nordeus, the problem may have been different enough that the queue solution made sense. Or it may have been that they were still learning (that project was made nearly two years ago) and made some mistakes. Just because Unity was involved doesn't mean it was perfect. I admittedly haven't spent enough time in that code to come to conclusion on that.
    I would probably rely on a physics broadphase to identify which food entities were in range of being consumed by animals using triggers. In that case, the food entities iterated over would actually be on average less than the number of animals. Of course if you are using raw position checks rather than some spatial query engine AND have significantly fewer animals than food entities, then using an event system or an IJob or IJFE.ScheduleSingle makes more sense here, as this becomes a much lower frequency problem. Also I have not spent enough time with Unity.Physics to see how intuitive it is for these sorts of problems.

    The "Claimed" component (or buffer element) will only ever be not NullEntity the very frame that it is consumed. You can think of it like an event in that regard.
    Can an animal eat more than one plant per frame? I assumed no, which is why the Claimed component solved a lot of the race condition problems you described.
    That's an actor model. Not ECS. In ECS, logic is separated from the data. The moment you start talking about Animal Systems and Food Systems, you might be thinking about it wrong. In this case what I am proposing is a FoodIsConsumedByAnimals system. Note that I loop over food, and then loop over animals. I made the goddess analogy because I find it easier to put myself in the correct mindset when I write all my systems from the perspective of an omnipotent being deciding everything.
     
    vildauget and florianhanke like this.
  31. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,778
    Animal can eat one plant per frame. But multiple animals can eat same plant.

    My understanding was from your initial post, you set claimed on the plant, not the animal. Still, in your proposal, multiple plants nearby will potentially search through same animal. So is the same problem, as searching food by animals, where multiple animals can find same plant. On single threaded job is not so much issue.

    In my case, If I go with event system alike, food should not care, what eats it. It is a resource. Is just system, which process all already filtered data. No need for logic, which decide what type of animal, or type of food eats what. Same system can allow eat animal by other animal, or animal by plant.
    Animal system just defines, weather type of food / condition are met to eat it. I can add other type of animals eating different type of food, with their corresponding systems.
    So it fits ECS indeed.

    Sure, you can go with god food system. Which may be fine in your case.
    But then logic of type of foods and relation what can eat what, will make it looks bulky, when decide to expand functionality / types.
    Instead adding just a new type of food, you now need modify system, to accommodate new food consumption correlation.

    So I am not convinced in that proposed mechanics for my purpose. It doesn't feel to bet modular.

    In my case, animal checks chunk in which is located. It checks for food in that chunk, within a range. Some animals may search few chunks in certain conditions. But not going necessarily into details.

    However, I expect total number of plants, being significantly higher, than animals count that they can eat it. This is why I am focusing on per animal search, rather per food.
    More over, I can ignore animals, which are not hungry.
     
    Last edited: Oct 22, 2019
  32. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,269
    Alright. You gave a lot more information about a much more specific problem. Here are the requirements and assumptions I am now working with. For the sake of English being an awkward language, I'm going to call the animals "hunters" and the food "prey".
    • There are a relatively small number of hunters.
    • There are a relatively large number of prey.
    • The spatial query system in the game is low res and may generate many false positives.
    • Hunters eat prey.
    • A hunter can only ever eat one prey in a frame.
    • A prey has multiple servings.
    • A hunter can eat multiple servings.
    • Multiple hunters can eat different servings of the same prey at the same time.
    • A hunter can only target one prey in a frame.
    • A hunter can only eat the prey it is targeting.
    Those last two assumptions might seem limiting, in practice it does not limit you in any practical way as you can choose the target right before running this algorithm.

    So the first thing I ask: How many ArchetypeChunks of hunters do you have? If the answer is 1 or maybe 2, use IJFE.ScheduleSingle with some ComponentDataFromEntity with write capabilities and call it a day.

    However, if you have enough data to warrant making this algorithm parallel, here's how I would go about it:

    First, the spatial query. The built-in systems aren't all that useful. So instead of doing a gather of all hunters from the preys' POV, we will have to perform a scatter from the hunters' POV. A NativeMultiHashMap with the prey as the key and the hunter as the value works here. Make sure only hunter-prey entries are entered when the hunter is actually close enough to eat the prey. I'm guessing this is a pretty simple distance check.

    Second, assigning claims. Use a parallel job to iterate over all unique keys in the NativeMultiHashMap. For each key you iterate over the Prey's servings, which is a dynamic buffer with each element containing an Entity field. In this job, you have ComponentDataFromEntity read access to the hunters, so you can assign each hunter to multiple servings based on how hungry each hunter is. A hunter can be assigned multiple servings, and several hunters can be assigned servings of a single prey.

    Third, the feasting. Iterate over each hunter, and use a readonly BufferFromEntity to read the target prey's servings buffer. For each serving that has the hunter's name on it, eat. You can also determine that the prey is gone if all the servings are claimed.

    Fourth, prey updates. Once again, iterate over each unique key in the NativeMultiHashMap. Remove each claimed serving from the buffer, then do any other simulation and presentation updates needed on the prey.

    Fifth, enjoy your perfectly parallel, sync-point-less, fully Bursted, and potentially deterministic (depends on order-independent resolution in the second part) algorithm.
     
    psuong likes this.
  33. MNNoxMortem

    MNNoxMortem

    Joined:
    Sep 11, 2016
    Posts:
    723
    In case you are able to determine the count of hunters which eat a single prey (#claims/plant) after claim phase and before the feasting phase you can use fractional consumption to solve some problems, by having each hunter eat 1/n of the plant and this way do violate energy conservation less.

    Fractional arithmethics obviously have a whole tail of other problems...