Search Unity

Starting off with DOTS + ECS - looking for guidance on my approach

Discussion in 'Entity Component System' started by axxessdenied, Dec 15, 2019.

  1. axxessdenied

    axxessdenied

    Joined:
    Nov 29, 2016
    Posts:
    33
    Hey, guys.

    I'm trying to wrap my head around the ECS approach. It's kind of difficult with things changing so rapidly.

    But, I've got a basic physics system working with super basic collision detection.

    I'm just wondering if I can approach this in a better manner and if I wanted to Jobify a system like this how would I go about comparing the different entity queries? Are the nest ForEach loops the way to go about this? Is there an alternative approach?
    Here is the code I'm working with right now for the physics system

    Code (CSharp):
    1. namespace Chernobog.PlatformerECS.Systems
    2. {
    3.     using Unity.Mathematics;
    4.     using Unity.Entities;
    5.     using Components;
    6.     using UnityEngine;
    7.     using Collider = Components.Collider;
    8.  
    9.     class PhysicsSystem : ComponentSystem
    10.     {
    11.         private EntityQuery moving_group;
    12.         private EntityQuery collider_group;
    13.  
    14.         protected override void OnCreate()
    15.         {
    16.             moving_group = GetEntityQuery
    17.             (
    18.                 ComponentType.ReadWrite<Position>(),
    19.                 ComponentType.ReadWrite<RigidBody>(),
    20.                 ComponentType.ReadOnly<Collider>()
    21.             );
    22.            
    23.             collider_group = GetEntityQuery
    24.             (
    25.                 ComponentType.ReadOnly<Collider>(),
    26.                 ComponentType.ReadWrite<Position>()
    27.             );
    28.         }
    29.  
    30.         protected override void OnUpdate()
    31.         {
    32.             Entities.With(moving_group).ForEach
    33.             (
    34.                 (Entity entity, ref Position position, ref RigidBody rigidbody, ref Collider collider) =>
    35.                 {
    36.                     var p = position;
    37.                     var c = collider;
    38.                     var rb = rigidbody;
    39.                    
    40.                     var deltaTime = UnityEngine.Time.deltaTime;
    41.                     if (rb.Grounded.boolValue == 0)
    42.                         p.Value += new float2(0, (deltaTime * Physics2D.gravity).y);
    43.                    
    44.                    
    45.                     var displacement = rb.Velocity * deltaTime;
    46.                     var steps = math.max(1, (int) math.round(math.length(displacement) / 0.05f));
    47.                     var moveStep = displacement / steps;
    48.                                        
    49.                     var collided = false;
    50.                    
    51.                     for (var s = 0; s < steps; s++)
    52.                     {
    53.                         if (!collided)
    54.                         {
    55.                             Entities.With(collider_group).ForEach
    56.                             (
    57.                                 (Entity cEntity, ref Position cPosition, ref Collider cCollider) =>
    58.                                 {
    59.                                     if (entity != cEntity && !collided)
    60.                                     {
    61.                                         collided = AreSquaresOverlapping(p.Value, c.Size, cPosition.Value,
    62.                                             cCollider.Size);
    63.                                     }
    64.                                 }
    65.                                
    66.                             );
    67.                         }
    68.  
    69.                         p.Value += moveStep;
    70.                         if (!collided)
    71.                         {                          
    72.                             rb.Grounded.boolValue = 0;
    73.                         }
    74.                         else
    75.                         {
    76.                             //no vertical movement
    77.                             p.Value.y = 0f;
    78.                             rb.Grounded.boolValue = 1;
    79.                         }
    80.                    
    81.                         position = p;
    82.                         rigidbody = rb;
    83.                     }                  
    84.                 }
    85.             );
    86.  
    87.         }
    88.  
    89.         static bool AreSquaresOverlapping(float2 posA, float sizeA, float2 posB, float sizeB)
    90.         {
    91.             float d = (sizeA / 2) + (sizeB / 2);
    92.             return (math.abs(posA.x - posB.x) < d && math.abs(posA.y - posB.y) < d);
    93.         }
    94.     }
    95. }
    Any help moving in the proper direction would be much appreciated :)
     
  2. axxessdenied

    axxessdenied

    Joined:
    Nov 29, 2016
    Posts:
    33
    I started this off as a JobComponentSystem implementing an IJobChunk but I had no idea how to do the collider detection with other entities outside of the main query so I switched to the ComponentSystem
     
  3. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    ForEach is a mutation operation, it is surely a friction to bring other entities into calculation by relying that it came from the lambda. (though I heard they are preparing something about bringing foreign entities into ForEach easier)

    What I would do is to linerize and bake all Collider data with eq.ToComponentDataArray, then you can capture the NativeArray<Collider> into a ForEach in JobComponentSystem for parallel read. Use it in place of your inner ForEach.

    Use `out JobHandle` overload of ToComponentDataArray then have your ForEach Schedule depends on that handle. It will help things to be on thread as much as possible and OnUpdate will simply pass by quickly. The Allocator.TempJob is needed because TCDA itself schedule a chunk fetch job inside. Add .WithDeallocateOnJobCompletion on that array so when all parallel jobs ends then it get disposed.
     
  4. axxessdenied

    axxessdenied

    Joined:
    Nov 29, 2016
    Posts:
    33
    Thanks for taking the time to respond!

    I started trying to set it up as a JobComponentSystem with your advice. I think I'm on the right track but haven't got a chance to get any further and actually test it out.

    Code (CSharp):
    1. class PhysicsSystem : JobComponentSystem
    2.     {
    3.         private EntityQuery movingGroup;
    4.         private EntityQuery colliderGroup;
    5.  
    6.         protected override void OnCreate()
    7.         {
    8.             movingGroup = GetEntityQuery(ComponentType.ReadWrite<Position>(), ComponentType.ReadWrite<RigidBody>(), ComponentType.ReadOnly<Collider>());
    9.             colliderGroup = GetEntityQuery(ComponentType.ReadOnly<Collider>(), ComponentType.ReadWrite<Position>());
    10.         }
    11.  
    12.         [BurstCompile]
    13.         struct PhysicsSystemJob : IJobChunk
    14.         {
    15.             public ArchetypeChunkComponentType<Position> positionType;
    16.             public ArchetypeChunkComponentType<RigidBody> rigidbodyType;
    17.             public ArchetypeChunkComponentType<Collider> colliderType;
    18.  
    19.             [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<Collider> collidersToCheck;
    20.             [DeallocateOnJobCompletion] public NativeArray<Position> positionsToCheck;
    21.  
    22.             public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    23.             {
    24.                 var rigidbody = chunk.GetNativeArray(rigidbodyType);
    25.                 var position = chunk.GetNativeArray(positionType);
    26.                 var collider = chunk.GetNativeArray(colliderType);
    27.                 var collidedWith = collidersToCheck;
    28.                 var positions = positionsToCheck;
    29.              
    30.                 //implement some stuff
    31.             }
    32.         }
    33.  
    34.         protected override JobHandle OnUpdate(JobHandle inputDeps)
    35.         {
    36.             var rigidbodyType = GetArchetypeChunkComponentType<RigidBody>();
    37.             var positionType = GetArchetypeChunkComponentType<Position>();
    38.             var colliderType = GetArchetypeChunkComponentType<Collider>();
    39.             var colliderToCheck = colliderGroup.ToComponentDataArray<Collider>(Allocator.TempJob, out var colliderHandle);
    40.             var positionsToCheck = colliderGroup.ToComponentDataArray<Position>(Allocator.TempJob, out var positionHandle);
    41.  
    42.             var job = new PhysicsSystemJob
    43.             {
    44.                 positionType = positionType,
    45.                 rigidbodyType = rigidbodyType,
    46.                 colliderType = colliderType,
    47.                 collidersToCheck = colliderToCheck,
    48.                 positionsToCheck = positionsToCheck
    49.             };
    50.  
    51.             return job.Schedule(movingGroup, JobHandle.CombineDependencies(inputDeps, colliderHandle, positionHandle));
    52.         }
    53.     }
     
    5argon likes this.
  5. axxessdenied

    axxessdenied

    Joined:
    Nov 29, 2016
    Posts:
    33
    Ended up getting some basic collision detection to work with this.

    Code (CSharp):
    1. class PhysicsSystem : JobComponentSystem
    2.     {
    3.         private EntityQuery movingGroup;
    4.         private EntityQuery colliderGroup;
    5.  
    6.         protected override void OnCreate()
    7.         {
    8.             movingGroup = GetEntityQuery(ComponentType.ReadWrite<Position>(), ComponentType.ReadWrite<RigidBody>(), ComponentType.ReadOnly<Collider>(), ComponentType.ReadOnly<GUID>());
    9.             colliderGroup = GetEntityQuery(ComponentType.ReadOnly<Collider>(), ComponentType.ReadOnly<Position>(), ComponentType.ReadOnly<GUID>());
    10.         }
    11.  
    12.         [BurstCompile]
    13.         struct PhysicsSystemJob : IJobChunk
    14.         {
    15.             public float deltaTime;
    16.             public Vector2 gravity;
    17.             public ArchetypeChunkComponentType<Position> positionType;
    18.             public ArchetypeChunkComponentType<RigidBody> rigidbodyType;
    19.             [ReadOnly] public ArchetypeChunkComponentType<Collider> colliderType;
    20.             [ReadOnly] public ArchetypeChunkComponentType<GUID> guidType;
    21.  
    22.             [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<Collider> collidersToCheck;
    23.             [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<GUID> guidsToCheck;
    24.             [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<Position> positionsToCheck;
    25.  
    26.             public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    27.             {
    28.                 var rigidbody = chunk.GetNativeArray(rigidbodyType);
    29.                 var position = chunk.GetNativeArray(positionType);
    30.                 var collider = chunk.GetNativeArray(colliderType);
    31.                 var guid = chunk.GetNativeArray(guidType);
    32.                 var guids = guidsToCheck;
    33.                 var colliders = collidersToCheck;
    34.                 var positions = positionsToCheck;
    35.              
    36.                 //implement some stuff
    37.                 for (var i = 0; i < chunk.Count; i++)
    38.                 {
    39.                     var rb = rigidbody[i];
    40.                     var p = position[i];
    41.                     var c = collider[i];
    42.                     var id = guid[i];
    43.  
    44.                     if (rb.Grounded.boolValue == 0)
    45.                         p.Value += new float2(0, (deltaTime * gravity).y);
    46.  
    47.                     var displacement = rb.Velocity * deltaTime;
    48.                     var steps = math.max(1, (int) math.round(math.length(displacement) / 0.05f));
    49.                     var moveStep = displacement / steps;
    50.                  
    51.                     for (var s = 0; s < steps; s++)
    52.                     {
    53.                         var collided = false;
    54.  
    55.                         for (var x = 0; x < colliders.Length; x++)
    56.                         {
    57.                             if (id.Value == guids[x].Value) continue;
    58.                             collided = AreSquaresOverlapping(p.Value, c.Size, positions[x].Value,
    59.                                 colliders[x].Size);
    60.                             if (collided) break;
    61.                         }
    62.                      
    63.                         p.Value += moveStep;
    64.                         if (!collided)
    65.                         {                        
    66.                             rb.Grounded.boolValue = 0;
    67.                         }
    68.                         else
    69.                         {
    70.                             //no vertical movement
    71.                             p.Value.y = 0f;
    72.                             rb.Grounded.boolValue = 1;
    73.                         }
    74.  
    75.                         p.Value += moveStep;
    76.                     }
    77.  
    78.                     rigidbody[i] = rb;
    79.                     position[i] = p;
    80.                 }
    81.             }
    82.         }
    83.  
    84.         protected override JobHandle OnUpdate(JobHandle inputDeps)
    85.         {
    86.             var rigidbodyType = GetArchetypeChunkComponentType<RigidBody>();
    87.             var positionType = GetArchetypeChunkComponentType<Position>();
    88.             var colliderType = GetArchetypeChunkComponentType<Collider>(true);
    89.             var guidType = GetArchetypeChunkComponentType<GUID>(true);
    90.             var collidersToCheck = colliderGroup.ToComponentDataArray<Collider>(Allocator.TempJob, out var colliderHandle);
    91.             var positionsToCheck = colliderGroup.ToComponentDataArray<Position>(Allocator.TempJob, out var positionHandle);
    92.             var guidsToCheck = colliderGroup.ToComponentDataArray<GUID>(Allocator.TempJob, out var guidHandle);
    93.          
    94.             var job = new PhysicsSystemJob
    95.             {
    96.                 deltaTime = UnityEngine.Time.deltaTime,
    97.                 gravity = Physics2D.gravity,
    98.                 guidType = guidType,
    99.                 positionType = positionType,
    100.                 rigidbodyType = rigidbodyType,
    101.                 colliderType = colliderType,
    102.                 collidersToCheck = collidersToCheck,
    103.                 guidsToCheck = guidsToCheck,
    104.                 positionsToCheck = positionsToCheck
    105.             };
    106.  
    107.             var newHandle = JobHandle.CombineDependencies(colliderHandle, positionHandle, guidHandle);
    108.  
    109.             return job.Schedule(movingGroup, JobHandle.CombineDependencies(inputDeps, newHandle));
    110.         }
    111.  
    112.         static bool AreSquaresOverlapping(float2 posA, float sizeA, float2 posB, float sizeB)
    113.         {
    114.             var d = sizeA / 2 + sizeB / 2;
    115.             return math.abs(posA.x - posB.x) < d && math.abs(posA.y - posB.y) < d;
    116.         }
    117.     }
     
    vildauget likes this.
  6. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    Looks great, you can also try optimize away the if in hot loops to inline-if so they may become an assembly that doesn't require branch prediction (CMOV). If Burst if smart maybe it wouldn't make any difference, you can try and see the assembly in Burst debugger. Or of course you can profile performance with performance testing package.

    e.g. :
    Code (CSharp):
    1. // -- BEFORE --
    2.  
    3. if (rb.Grounded.boolValue == 0)
    4.     p.Value += new float2(0, (deltaTime * gravity).y);
    5.  
    6. if (id.Value == guids[x].Value) continue;
    7.     collided = AreSquaresOverlapping(p.Value, c.Size, positions[x].Value,
    8.         colliders[x].Size);
    9.  
    10.  
    11. if (!collided)
    12. {                    
    13.     rb.Grounded.boolValue = 0;
    14. }
    15. else
    16. {
    17.     //no vertical movement
    18.     p.Value.y = 0f;
    19.     rb.Grounded.boolValue = 1;
    20. }
    21.  
    22. // -- AFTER --
    23.  
    24. p.Value += rb.Grounded.boolValue == 0 ? 0 : new float2(0, (deltaTime * gravity).y);
    25.  
    26. collided = id.Value == guids[x].Value ? collided : AreSquaresOverlapping(p.Value, c.Size, positions[x].Value, colliders[x].Size);
    27.  
    28. rb.Grounded.boolValue = !collided ? 0 : 1;
    29. p.Value.y = collided ? 0f : p.Value.y;
    Then maybe you can skip chunks based on change version. For example a chunk that pos component didn't change its value may not need processing, etc. IJobChunk gives you a chunk one by one, by skipping it means you just return from the function early to go to the next. There are various methods on ArchetypeChunk that could help.
     
    Last edited: Dec 17, 2019
    vildauget and axxessdenied like this.
  7. axxessdenied

    axxessdenied

    Joined:
    Nov 29, 2016
    Posts:
    33
  8. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    axxessdenied likes this.
  9. axxessdenied

    axxessdenied

    Joined:
    Nov 29, 2016
    Posts:
    33
    Ah yes! Trying to google stuff late at night is never a good idea. Thank you :D
     
  10. axxessdenied

    axxessdenied

    Joined:
    Nov 29, 2016
    Posts:
    33
    I applied a RenderMesh just to get a basic sprite on the screen. I have it working.
    I figured I would apply the scale to flip the sprite base on that value. But, I only want it to flip on the x-axis and not the y or z.

    I figured I would just manually apply the math.abs() of those values but when I run the system my sprite disappears. The sprite flips just fine if I don't manually touch the LocalToWorld though. I tried it in a ComponentSystem as well with the same result.

    Code (CSharp):
    1.  
    2. class ScaleToWorldTransformSystem : JobComponentSystem
    3. {
    4.     private EntityQuery _group;
    5.  
    6.     protected override void OnCreate()
    7.     {
    8.         _group = GetEntityQuery(typeof(LocalToWorld), ComponentType.ReadOnly<Scale>());
    9.     }
    10.  
    11.     [BurstCompile]
    12.     struct PositionToWorldTransformJob : IJobForEach<LocalToWorld, Scale>
    13.     {
    14.         public void Execute(ref LocalToWorld world, [ReadOnly] ref Scale scale)
    15.         {
    16.             world.Value.c1.y = math.abs(world.Value.c1.y);
    17.             world.Value.c2.y = math.abs(world.Value.c2.z);
    18.         }
    19.     }
    20.  
    21.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    22.     {
    23.    
    24.         var job = new PositionToWorldTransformJob();
    25.  
    26.         return job.Schedule(_group, inputDeps);
    27.     }
    28. }
    29.  
    The value updates properly in the localtoworld but the sprite doesnt render.


    edit : figured out how to override the localtoworld transform but same problem with the sprite just disappearing despite the values updating correctly

    Code (CSharp):
    1.  
    2. namespace Chernobog.PlatformerECS.Systems
    3. {
    4.     using Unity.Mathematics;
    5.     using Unity.Entities;
    6.     using Unity.Jobs;
    7.     using Unity.Transforms;
    8.     using Unity.Burst;
    9.     using Unity.Collections;
    10.     using Scale = Components.Scale;
    11.  
    12.     class ScaleToWorldTransformSystem : JobComponentSystem
    13.     {
    14.         private EntityQuery _group;
    15.  
    16.        
    17.         protected override void OnCreate()
    18.         {
    19.             var queryDescription = new EntityQueryDesc
    20.             {
    21.                 All = new ComponentType[] {typeof(LocalToWorld), typeof(Scale), typeof(Translation)},
    22.                 Options = EntityQueryOptions.FilterWriteGroup
    23.             };
    24.             _group = GetEntityQuery(queryDescription);
    25.         }
    26.  
    27.         [BurstCompile]
    28.         struct PositionToWorldTransformJob : IJobForEach<LocalToWorld, Scale, Translation>
    29.         {
    30.             public void Execute(ref LocalToWorld world, [ReadOnly] ref Scale scale, [ReadOnly] ref Translation translation)
    31.             {
    32.                 var w = new float4x4(
    33.                     new float4(1,0,0,0),
    34.                     new float4(0,1,0,0),
    35.                     new float4(0,0,1,0),
    36.                     new float4(0,0,0,1));
    37.                 var s = scale.Value;
    38.                 w.c0.x *= s.x;
    39.                 w.c1.y = math.abs(w.c1.y * s.y);
    40.                 w.c3.x = translation.Value.x;
    41.                 w.c3.y = translation.Value.y;
    42.  
    43.                 world.Value = w;
    44.             }
    45.         }
    46.        
    47.         protected override JobHandle OnUpdate(JobHandle inputDeps)
    48.         {
    49.          
    50.             var job = new PositionToWorldTransformJob();
    51.  
    52.             return job.Schedule(_group, inputDeps);
    53.         }
    54.     }
    55. }
    56.  
    here's the component set up
    Code (CSharp):
    1.  
    2. namespace Chernobog.PlatformerECS.Components
    3. {
    4.     using Unity.Entities;
    5.     using Unity.Mathematics;
    6.  
    7.     [System.Serializable]
    8.     [WriteGroup(typeof(Unity.Transforms.LocalToWorld))]
    9.     public struct Scale : IComponentData
    10.     {
    11.         public float2 Value;
    12.     }
    13. }
    14.  
     
    Last edited: Dec 17, 2019
  11. Guedez

    Guedez

    Joined:
    Jun 1, 2012
    Posts:
    827
    ECS feels like programming with a on memory database. If you have experience with databases, you might adapt faster by thinking of ECS like you think of tables and indexes and whatnot, plenty of stuff is analogous to some extent
     
    axxessdenied likes this.
  12. axxessdenied

    axxessdenied

    Joined:
    Nov 29, 2016
    Posts:
    33
    Thanks! It's definitely starting to make a bit more sense as I keep going over it :)
     
  13. axxessdenied

    axxessdenied

    Joined:
    Nov 29, 2016
    Posts:
    33
    I guess the issue is that the scaling of the x,y axis needs to be both positive or both negative for graphics.draw to render the material.
     
  14. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,270
    That sounds like you want to turn off backface culling in the shader.