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

Resolved Simple Job not Working (assign values back to entities)

Discussion in 'Entity Component System' started by CaseyHofland, Apr 10, 2021.

  1. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    610
    After having done everything right... this job still has the gal to go wrong. I'm learning ecs right now so I hope someone can help me with this.

    This is the job:
    Code (CSharp):
    1. public struct SpinJob : IJob
    2. {
    3.     public float deltaTime;
    4.     [ReadOnly] public NativeArray<Spin> spins;
    5.     public NativeArray<Rotation> rotations;
    6.  
    7.     public void Execute()
    8.     {
    9.         UnityEngine.Debug.Log($"dt: {deltaTime}, spins: {spins.Length}, speed: {spins[0].speed}"); // dt: 0.02, spins: 10000, speed: float3(1.570796f, 0.7853982f, 0.5235988f) (= 90, 45, 30 in radians)
    10.         UnityEngine.Debug.Log($"rotation: {rotations[0].Value}"); // ALWAYS prints: rotation: quaternion(0f, 0f, 0f, 1f)
    11.  
    12.         for(int i = 0; i < spins.Length; i++)
    13.         {
    14.             var rotation = rotations[i];
    15.             var spin = spins[i];
    16.  
    17.             rotations[i] = new Rotation
    18.             {
    19.                 Value = math.mul(rotation.Value, quaternion.AxisAngle(spin.speed, deltaTime)), // This works.
    20.             };
    21.         }
    22.  
    23.         UnityEngine.Debug.Log($"rotation: {rotations[0].Value}"); // rotation: quaternion(0.0157077f, 0.007853851f, 0.005235901f, 0.99995f)
    24.     }
    25. }
    And this is the OnUpdate inside my system:
    Code (CSharp):
    1. EntityQuery spinQuery = GetEntityQuery(typeof(Rotation), ComponentType.ReadOnly<Spin>());
    2. var rotations = spinQuery.ToComponentDataArray<Rotation>(Allocator.TempJob);
    3. var spins = spinQuery.ToComponentDataArray<Spin>(Allocator.TempJob);
    4.  
    5. var spinJob = new SpinJob
    6. {
    7.     deltaTime = Time.DeltaTime,
    8.     rotations = rotations,
    9.     spins = spins,
    10. };
    11. Dependency = spinJob.Schedule(Dependency);
    12.  
    13. Dependency = rotations.Dispose(Dependency);
    14. Dependency = spins.Dispose(Dependency);
    Note that I know it is being called because of the Debug.Log. I understand that assigning values in jobs is a bit funky because of nativearrays storing pointers, but I understand
    rotations[i] = new Rotation
    should handle that. So what's up?
     
    Last edited: Apr 10, 2021
  2. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,753
    Can you please describe, what is the issue?
    What works and what doesn't?
    What you expect as result?
     
  3. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    610
    The expected result is for the rotations in the job to update. And they do, inside the job... but the next frame they are back to a rotation of 0,0,0.
     
  4. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,753
    Where you got an example from?

    When you use to component data array, you simply copy data to new array. You don't use reference, as if you would do in case of OOP.

    So whatever you calculate in a job in your case, it is not copied back to entities.

    You need pass entities array to the job and assign components data back to relevant entities.

    Otherwise they are never set.
     
  5. Micz84

    Micz84

    Joined:
    Jul 21, 2012
    Posts:
    447
    You can make it much simpler just use Entities for each, code will be much simpler and it will run in parallel.
     
  6. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    610
    oh man, that’s so stupid of me, thank you! I got examples from the ecs examples on github and searching for IJob, but where I was looking they assign the values back outside the job, so I missed it.

    I know, but I’m recreating this same job with every job type so I can get a good sense for them. Plus I want to get a good sense for IJob regardless since Entities is still some years away from being used 'in the field'.
     
    Last edited: Apr 10, 2021
    Nyanpas likes this.
  7. Micz84

    Micz84

    Joined:
    Jul 21, 2012
    Posts:
    447
    OK but you still are using entity query. So I think you should use entities as they would be used or if you do not want to use them then use just the job system and manage your arrays otherwise you shoot yourself in the foot with "errors" like the one you have expirienced.
     
  8. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    610
    Sound advice! I followed it and ended up with an IJobChunk which gives me a wooping ~0.06ms for 10.000 entities without burst (I quadruple checked but WOW, jobs are absolutely insane!)
     
    Last edited: Apr 11, 2021
    Nyanpas likes this.
  9. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    610
    I have one last question, which may be kind of silly. However, for educational purposes, I'd like to know if and how something like this would be possible:

    This job was written for use with standard Unity.
    Code (CSharp):
    1. using Unity.Collections;
    2. using Unity.Jobs;
    3. using UnityEngine;
    4.  
    5. public struct SpinJobParallelFor : IJobParallelFor
    6. {
    7.     public float deltaTime;
    8.     public NativeArray<Quaternion> rotations;
    9.     [ReadOnly] public NativeArray<Vector3> spins;
    10.  
    11.     public void Execute(int index)
    12.     {
    13.         rotations[index] *= Quaternion.AngleAxis(deltaTime, spins[index]);
    14.     }
    15. }
    Is it possible to use this job, without modification, with entities? The use case being 'easily' migrating jobs, even if it may not be the most performant option.

    I have this as a starting point:
    Code (CSharp):
    1. var spinQuery = GetEntityQuery(typeof(Rotation), ComponentType.ReadOnly<Spin>()); // Called in OnCreate.
    2.  
    3. var rotations = spinQuery.ToComponentDataArray<Rotation>(Allocator.TempJob);
    4. var spins = spinQuery.ToComponentDataArray<Spin>(Allocator.TempJob);
    5.  
    6. var spinJob = new SpinJob
    7. {
    8.     deltaTime = Time.DeltaTime,
    9.     rotations = rotations.Reinterpret<Quaternion>(), // The local array will still update correctly.
    10.     spins = spins.Reinterpret<Vector3>(),
    11. };
    12. Dependency = spinJob.Schedule(spins.Length, 64, Dependency);
    13.  
    14. // To Entities code here!!!
    15.  
    16. Dependency = rotations.Dispose(Dependency);
    17. Dependency = spins.Dispose(Dependency);
    I feel like this can be achieved with a specialized 'converter job', but preferably I'd like to know if it can be done using Entities.ForEach, or whatever else the easiest solution may be. "Easiest solution" meaning that the code is in as few places with as few lines as possible.
     
    Last edited: Apr 10, 2021
  10. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,217
    For a single array, there's EntityQuery.CopyFromComponentDataArray. Otherwise, in an Entities.ForEach, you can use int entityInQueryIndex as an argument for working with arrays.
     
  11. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    610
    Thanks, the second way works like a charm!

    The finished OnUpdate
    Code (CSharp):
    1. var spinQuery = GetEntityQuery([URL='http://www.google.com/search?q=typeof+msdn.microsoft.com']typeof[/URL](Rotation), ComponentType.ReadOnly<Spin>()); // Called in OnCreate
    2.  
    3. var rotations = spinQuery.ToComponentDataArray<Rotation>(Allocator.TempJob);
    4. var spins = spinQuery.ToComponentDataArray<Spin>(Allocator.TempJob);
    5.  
    6. var spinJob = new SpinJobParallelFor
    7. {
    8.     deltaTime = Time.DeltaTime,
    9.     rotations = rotations.Reinterpret<Quaternion>(), // rotations will still update correctly.
    10.     spins = spins.Reinterpret<Vector3>(),
    11. };
    12. Dependency = spinJob.Schedule(spins.Length, 64, Dependency);
    13.  
    14. Dependency = Entities.ForEach((int entityInQueryIndex, ref Rotation rotation, in Spin spin) =>
    15. {
    16.     rotation = rotations[entityInQueryIndex];
    17. }).ScheduleParallel(Dependency);
    18.  
    19. Dependency = rotations.Dispose(Dependency);
    20. Dependency = spins.Dispose(Dependency);
    CopyFromComponentDataArray requires to call Complete() on the job, which is obviously undesirable.

    My benchmarks for 10000 spinning cubes were thus:
    The SpinJob took 0.216ms (over 3.5x slower than doing it with an IJobChunk (0.06ms) that's not using Burst), on top of which comes a 1.873ms increase for assigning the values back into entities. So depending on how big the job is, and how critical this area of your code, you might consider rewriting it. Still though, if your game only contains 100 spinning cubes, there's no need to prematurely optimize.
     
    Last edited: Apr 11, 2021
  12. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    610
    Everyone in this chat, thank you so much for helping me! L̶e̶a̶r̶n̶i̶n̶g̶ ̶t̶h̶e̶ ̶e̶n̶t̶i̶t̶i̶e̶s̶ ̶f̶r̶a̶m̶e̶w̶o̶r̶k̶ ̶c̶a̶n̶ ̶b̶e̶ ̶p̶r̶e̶t̶t̶y̶ ̶t̶o̶u̶g̶h̶ ̶u̶s̶i̶n̶g̶ ̶o̶n̶l̶y̶ ̶o̶u̶t̶d̶a̶t̶e̶d̶ ̶t̶u̶t̶o̶r̶i̶a̶l̶s̶ ̶a̶n̶d̶ ̶d̶o̶c̶u̶m̶e̶n̶t̶a̶t̶i̶o̶n̶,̶ ̶s̶o̶ I am really grateful for everyone who can give me a little nudge in the right direction from time to time!
     
    Last edited: Apr 11, 2021
    MNNoxMortem likes this.
  13. Fribur

    Fribur

    Joined:
    Jan 5, 2019
    Posts:
    132
  14. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    610
    Yes, I certainly did, and I can highly recommend it!

    I meant practical examples, though I should have made that more clear. Reflecting on it however, I have to admit there ARE a lot of good practical examples, but unfortunately 'good' ecs is not exactly 'beginner friendly' ecs. Which is not a complaint, I'm all for the runtime philosophy (and job security :D).
     
  15. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    610
    I updated some benchmarks in the previous posts. The new benchmarks were all tested the same, with my laptop plugged in (this contaminated the previous benchmarks) and all threads included (previously I only checked numbers reported on the main thread).

    SpinJobManager: runs an IJobParallelFor inside an Update loop using standard Unity code (no packages needed).
    Job: 0.107ms
    Update: 4.639ms // This is because a 10000 iteration for loop has to assign the values back. This could probably be optimized.
    Total: 4.746ms

    SpinJobConverted: runs a standard Unity IJobParallelFor inside a SystemBase and assigns the values back to their entities.
    Job: 0.216ms // Likely because of calling Reinterpret on the NativeArrays for Rotation and Spin.
    System Update: 0.101ms
    Assign Values: 1.873ms // Same as above. Even though this is scheduled in parallel and I have 16 threads, this is still slow.
    Total: 2.19ms

    SpinJobChunk: runs a specialized IJobChunk inside ecs.
    Job: 0.06ms
    System Update: 0.028ms
    Total: 0.088ms

    I've also added the .pdata which you can use inside the Profile Analyzer (Unity package). I don't know why you would check that, but this is my addendum so I might as well include it.
     

    Attached Files:

    Last edited: Apr 11, 2021