Search Unity

Automatic dependency management and ISystemBase support

Discussion in 'Physics for ECS' started by tertle, Jan 25, 2021.

  1. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    Background:
    So I haven't been a fan of how dependency management works with physics from pretty much the start and with the introduction of ISystemBase and thinking about architecture of a new project I decided to experiment with an alternative.

    Goals:
    - Handle dependency management automatically when doing spatial queries (remove the need for GetOutputDependency and AddInputDependency)
    - Remove coupling systems to physics systems and instead use only the data
    - Work with ISystemBase
    - Avoid making any changes to the physics package

    Solution:
    Make PhysicsWorld an struct based IComponentData singleton so it's safety is automatically handled by the built in safety system instead of having to pass dependencies around.

    Implementation:
    Component
    Code (CSharp):
    1. public struct PhysicsWorld : IComponentData
    2. {
    3.     internal PhysicsWorldImposter Imposter;
    4.  
    5.     public Unity.Physics.PhysicsWorld Value => Imposter;
    6. }
    SystemBase Test
    Code (CSharp):
    1. public class TestSystem : SystemBase
    2. {
    3.     /// <inheritdoc/>
    4.     protected override void OnUpdate()
    5.     {
    6.         var physicsWorld = this.GetSingleton<PhysicsWorld>().Value;
    7.  
    8.         this.Job
    9.             .WithCode(() =>
    10.             {
    11.                 var raycastInput = new RaycastInput
    12.                 {
    13.                     Start = float3.zero,
    14.                     End = new float3(5, 0, 0),
    15.                     Filter = CollisionFilter.Default,
    16.                 };
    17.  
    18.                 if (physicsWorld.CastRay(raycastInput, out var closestHit))
    19.                 {
    20.                     Debug.Log($"SystemBase hit position:{closestHit.Position}");
    21.                 }
    22.             })
    23.             .Schedule();
    24.     }
    25. }
    ISystemBase Test
    Code (CSharp):
    1.     [BurstCompile]
    2.     public struct UnmanagedTestSystem : ISystemBase
    3.     {
    4.         private Entity physicsEntity;
    5.  
    6.         /// <inheritdoc/>
    7.         public void OnCreate(ref SystemState state)
    8.         {
    9.             this.physicsEntity = PhysicsSystemUtil.GetOrCreatePhysicsEntity(state.EntityManager);
    10.         }
    11.  
    12.         /// <inheritdoc/>
    13.         public void OnDestroy(ref SystemState state)
    14.         {
    15.         }
    16.  
    17.         /// <inheritdoc/>
    18.         [BurstCompile]
    19.         public void OnUpdate(ref SystemState state)
    20.         {
    21.             // Super ugly, but working around ISystemBase limitations since GetSingleton, Query.GetX don't seem to work yet
    22.             var world = state.GetComponentDataFromEntity<PhysicsWorld>(true)[this.physicsEntity].Value;
    23.  
    24.             var raycastInput = new RaycastInput
    25.             {
    26.                 Start = float3.zero,
    27.                 End = new float3(5, 0, 0),
    28.                 Filter = CollisionFilter.Default,
    29.             };
    30.  
    31.             if (world.CastRay(raycastInput, out var closestHit))
    32.             {
    33.                 Debug.Log($"ISystemBase hit position:{closestHit.Position}");
    34.             }
    35.         }
    36.     }
    The magic that makes it all work
    Code (CSharp):
    1. public unsafe struct PhysicsWorldImposter
    2.     {
    3. #pragma warning disable 169
    4. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    5.         private fixed byte bytes[1008]; // UnsafeUtility.SizeOf<Unity.Physics.PhysicsWorld>()
    6. #else
    7.         private fixed byte bytes[320];
    8. #endif
    9. #pragma warning restore 169
    10.  
    11.         public static implicit operator Unity.Physics.PhysicsWorld(PhysicsWorldImposter imposter)
    12.         {
    13.             return UnsafeUtility.As<PhysicsWorldImposter, Unity.Physics.PhysicsWorld>(ref imposter);
    14.         }
    15.  
    16.         public static implicit operator PhysicsWorldImposter(Unity.Physics.PhysicsWorld physicsWorld)
    17.         {
    18.             return UnsafeUtility.As<Unity.Physics.PhysicsWorld, PhysicsWorldImposter>(ref physicsWorld);
    19.         }
    20.     }
    21.  
    22.     [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
    23.     [UpdateAfter(typeof(EndFramePhysicsSystem))]
    24.     public class PhysicsWorldSystem : SystemBase
    25.     {
    26.         private BuildPhysicsWorld buildPhysicsWorld;
    27.  
    28.         /// <inheritdoc/>
    29.         protected override void OnCreate()
    30.         {
    31.             this.buildPhysicsWorld = this.World.GetOrCreateSystem<BuildPhysicsWorld>();
    32.         }
    33.  
    34.         /// <inheritdoc/>
    35.         protected override void OnUpdate()
    36.         {
    37.             this.Dependency = JobHandle.CombineDependencies(this.Dependency, this.buildPhysicsWorld.GetOutputDependency());
    38.  
    39.             // We can't set this in a job otherwise it'll have the wrong safety
    40.             this.SetSingleton(new PhysicsWorld { Imposter = this.buildPhysicsWorld.PhysicsWorld });
    41.         }
    42.     }
    43.  
    44.     /// <summary> This system handles automatically adding all read dependencies to the BuildPhysicsWorld system. </summary>
    45.     [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
    46.     [UpdateBefore(typeof(BuildPhysicsWorld))]
    47.     public class PhysicsWorldDependencySystem : SystemBase
    48.     {
    49.         private BuildPhysicsWorld buildPhysicsWorld;
    50.  
    51.         /// <inheritdoc/>
    52.         protected override void OnCreate()
    53.         {
    54.             this.buildPhysicsWorld = this.World.GetOrCreateSystem<BuildPhysicsWorld>();
    55.  
    56.             // This is our forced safety handle
    57.             this.GetEntityQuery(ComponentType.ReadWrite<PhysicsWorld>());
    58.  
    59.             // Ensure the physics entity exists in the world
    60.             PhysicsSystemUtil.GetOrCreatePhysicsEntity(this.EntityManager);
    61.         }
    62.  
    63.         /// <inheritdoc/>
    64.         protected override void OnUpdate()
    65.         {
    66.             this.buildPhysicsWorld.AddInputDependencyToComplete(this.Dependency);
    67.         }
    68.     }
    69.  
    70.    public static class PhysicsSystemUtil
    71.    {
    72.        public static Entity GetOrCreatePhysicsEntity(EntityManager entityManager)
    73.        {
    74.            // This uses EntityManager query to avoid creating a second query in the the system
    75.            using (var query = entityManager.CreateEntityQuery(ComponentType.ReadOnly<PhysicsWorld>()))
    76.            {
    77.                return query.CalculateChunkCount() == 0 ? entityManager.CreateEntity(typeof(PhysicsWorld)) : query.GetSingletonEntity();
    78.            }
    79.        }
    80.    }
    81.  
    Result:
    It works in editor
    upload_2021-1-25_22-51-5.png

    And in builds
    upload_2021-1-25_22-50-1.png

    This was just a quick throw together in a free evening and it hasn't been thoroughly tested yet but from first appearances it works as intended and safeties working properly.

    I don't like the fact I used the same name for the component but I couldn't think of something better.

    I also don't like that it's using GetSingleton instead of GetSingletonEntity as it will cause a sync point on the physics system. However this is required to properly inject safety handles. In reality it shouldn't really matter and you could cleverly order your systems a bit more

    Anyway would like to hear what people think of this as an idea.

    FAQ:
    What's the imposter and why is it needed? Unity.Physics.PhysicsWorld has safety handles which makes it a managed object and unable to be placed on an IComponentData. The imposter is a bit of memory magic to allow this to work.

    Will this work for systems in fixed update? In theory if you [UpdateAfter(typeof(PhysicsWorldSystem))] though I haven't tested it.
     
    Last edited: Jan 25, 2021
  2. TRS6123

    TRS6123

    Joined:
    May 16, 2015
    Posts:
    246
    I second this. Any data used by multiple systems should be stored in a component (and NOT a system) whenever possible. The netcode package also suffers from a similar issue (e.g. ClientSimulationSystemGroup.ServerTick, GhostPredictionSystemGroup.PredictingTick)
     
  3. thelebaron

    thelebaron

    Joined:
    Jun 2, 2013
    Posts:
    857
    im not really sure if this was the plan anyway as its only something I noticed recently but there is a
    CollisionWorldProxy : IComponentData
    inside the PhysicsComponents.cs file
     
  4. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    Had a quick look at that source. It is a much cleaner/safer way the of storing it on a component than reinterpreting memory that I'm using but sadly the setup is internal.

    I did consider my own version of a component like this but the things I need are internal and I would have to make changes to the physics package.
     
  5. jasons-novaleaf

    jasons-novaleaf

    Joined:
    Sep 13, 2012
    Posts:
    181
    Do you have any links where I can learn about storing shared data in entities instead of systems? In my prototyping I keep having shared native queues or arrays in systems, so wonder how alternatives might work out.
     
  6. milos85miki

    milos85miki

    Joined:
    Nov 29, 2019
    Posts:
    197
    Nice work, @tertle! Seems like this will work and make it easier for people to handle dependencies (with some constraints, probably acceptable for most).

    I agree that it's currently quite inconvenient to schedule systems and jobs that work with physics, but it comes from a performance-driven decision to perform physics step on "physics runtime" data (RigidBody, MotionData, etc.) instead of ECS components (Translation, Rotation, etc.). As you probably know, BuildPhysicsWorld creates the runtime data based on ECS data and ExportPhysicsWorld writes back from runtime into ECS. Dependencies between jobs are automatically added when reading/writing ECS data, but not for runtime. So, the 2 "kinds" of data coexist and can be altered from your systems/jobs, thus requiring extra care when scheduling.
     
    Last edited: Jan 26, 2021
  7. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    I recommend against wildly sharing data directly between systems that isn't settings (just IComponentData) and standalone simulations, such as physics and navigation grids.
    To do this you need to be comfortable with unsafe code and pointers and avoid native containers.

    I'm quite the fan of the package and reading through the source has taught me far more tricks and given more more inspiration for patterns than any other package around.

    My only complaint has been the resulting dependency management mess but the fact the default workflow doesn't work with ISystemBase was really the kick that got me looking into alternatives though. This was just an attempt at writing a layer to solve this all for me.
     
  8. Bas-Smit

    Bas-Smit

    Joined:
    Dec 23, 2012
    Posts:
    274
    I use a singleton entity with any number of dynamic buffers to communicate between systems. (note that a "singleton entity" is purely conceptual, there are no APIs to specifically support this)

    If I need a queue for collecting a parallel job's results I run a single threaded job that empties the queue into a dynamic buffer. As the single threaded job can be scheduled from the same system the queue remains local.
     
    jasons-novaleaf likes this.