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
![]()
And in builds
![]()
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.