Automatic dependency management and ISystemBase support

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

public struct PhysicsWorld : IComponentData
{
    internal PhysicsWorldImposter Imposter;

    public Unity.Physics.PhysicsWorld Value => Imposter;
}

SystemBase Test

public class TestSystem : SystemBase
{
    /// <inheritdoc/>
    protected override void OnUpdate()
    {
        var physicsWorld = this.GetSingleton<PhysicsWorld>().Value;

        this.Job
            .WithCode(() =>
            {
                var raycastInput = new RaycastInput
                {
                    Start = float3.zero,
                    End = new float3(5, 0, 0),
                    Filter = CollisionFilter.Default,
                };

                if (physicsWorld.CastRay(raycastInput, out var closestHit))
                {
                    Debug.Log($"SystemBase hit position:{closestHit.Position}");
                }
            })
            .Schedule();
    }
}

ISystemBase Test

    [BurstCompile]
    public struct UnmanagedTestSystem : ISystemBase
    {
        private Entity physicsEntity;

        /// <inheritdoc/>
        public void OnCreate(ref SystemState state)
        {
            this.physicsEntity = PhysicsSystemUtil.GetOrCreatePhysicsEntity(state.EntityManager);
        }

        /// <inheritdoc/>
        public void OnDestroy(ref SystemState state)
        {
        }

        /// <inheritdoc/>
        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            // Super ugly, but working around ISystemBase limitations since GetSingleton, Query.GetX don't seem to work yet
            var world = state.GetComponentDataFromEntity<PhysicsWorld>(true)[this.physicsEntity].Value;

            var raycastInput = new RaycastInput
            {
                Start = float3.zero,
                End = new float3(5, 0, 0),
                Filter = CollisionFilter.Default,
            };

            if (world.CastRay(raycastInput, out var closestHit))
            {
                Debug.Log($"ISystemBase hit position:{closestHit.Position}");
            }
        }
    }

The magic that makes it all work

public unsafe struct PhysicsWorldImposter
    {
#pragma warning disable 169
#if ENABLE_UNITY_COLLECTIONS_CHECKS
        private fixed byte bytes[1008]; // UnsafeUtility.SizeOf<Unity.Physics.PhysicsWorld>()
#else
        private fixed byte bytes[320];
#endif
#pragma warning restore 169

        public static implicit operator Unity.Physics.PhysicsWorld(PhysicsWorldImposter imposter)
        {
            return UnsafeUtility.As<PhysicsWorldImposter, Unity.Physics.PhysicsWorld>(ref imposter);
        }

        public static implicit operator PhysicsWorldImposter(Unity.Physics.PhysicsWorld physicsWorld)
        {
            return UnsafeUtility.As<Unity.Physics.PhysicsWorld, PhysicsWorldImposter>(ref physicsWorld);
        }
    }

    [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
    [UpdateAfter(typeof(EndFramePhysicsSystem))]
    public class PhysicsWorldSystem : SystemBase
    {
        private BuildPhysicsWorld buildPhysicsWorld;

        /// <inheritdoc/>
        protected override void OnCreate()
        {
            this.buildPhysicsWorld = this.World.GetOrCreateSystem<BuildPhysicsWorld>();
        }

        /// <inheritdoc/>
        protected override void OnUpdate()
        {
            this.Dependency = JobHandle.CombineDependencies(this.Dependency, this.buildPhysicsWorld.GetOutputDependency());

            // We can't set this in a job otherwise it'll have the wrong safety
            this.SetSingleton(new PhysicsWorld { Imposter = this.buildPhysicsWorld.PhysicsWorld });
        }
    }

    /// <summary> This system handles automatically adding all read dependencies to the BuildPhysicsWorld system. </summary>
    [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
    [UpdateBefore(typeof(BuildPhysicsWorld))]
    public class PhysicsWorldDependencySystem : SystemBase
    {
        private BuildPhysicsWorld buildPhysicsWorld;

        /// <inheritdoc/>
        protected override void OnCreate()
        {
            this.buildPhysicsWorld = this.World.GetOrCreateSystem<BuildPhysicsWorld>();

            // This is our forced safety handle
            this.GetEntityQuery(ComponentType.ReadWrite<PhysicsWorld>());

            // Ensure the physics entity exists in the world
            PhysicsSystemUtil.GetOrCreatePhysicsEntity(this.EntityManager);
        }

        /// <inheritdoc/>
        protected override void OnUpdate()
        {
            this.buildPhysicsWorld.AddInputDependencyToComplete(this.Dependency);
        }
    }

   public static class PhysicsSystemUtil
   {
       public static Entity GetOrCreatePhysicsEntity(EntityManager entityManager)
       {
           // This uses EntityManager query to avoid creating a second query in the the system
           using (var query = entityManager.CreateEntityQuery(ComponentType.ReadOnly<PhysicsWorld>()))
           {
               return query.CalculateChunkCount() == 0 ? entityManager.CreateEntity(typeof(PhysicsWorld)) : query.GetSingletonEntity();
           }
       }
   }

Result:
It works in editor
6762166--780931--upload_2021-1-25_22-51-5.png

And in builds
6762166--780925--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.

6 Likes

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)

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

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.

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.

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.

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.

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.

1 Like