Working with physics right way

I’ve been working with Unity Physics for a little while, and came to conclusion that I’m not sure If I have clear understanding of how basic physics features work and how I should place my own systems interacting with physics relatively to to core ECS physics systems. The best source I could learn such kind of things from is Physics Samples git repository and some tests around things I wasn’t sure about.
After FixedStepSimulationSystemGroup was added, we should place our own fixed step systems around several “key” Systems - BuildPhysicsWorld, StepPhysicsWorld and ExportPhysicsWorld. I ended up with the following set of physics-related usecases and thoughts about how these things work and would appreciate if someone could correct me If I’m wrong.

I. Accessing rigid body properties

  • PhysicsVelocity component is RW-accessible up until BuildPhysicsWorld. After that it is RO up until ExportPhysicsWorld, and then it is again RW-accessible. So everything that modifies PhysicsVelocity should be normally scheduled before BuildPhysicsWorld. In between BuildPhysicsWorld and ExportPhysicsWorld, PhysicsVelocity may not reflect actual state of rigidbody, because at this interval all computations are performed based on data inside PhysicsWorld. Suppose that same logic can be applied to others components which duplicate data contained by PhysicsWorld
  • Accessing rigidbody data by index from PhysicsWorld should be done after BuildPhysicsWorld and before StepPhysicsWorld, as it is the only interval when data is guaranteed to be valid. We can use burst for that, but can access this data only from main thread, so no jobs.
  • RW access to rigidbody data by index is VERY slow compared to RW access by component. It is probably not going to be a problem on a scale of hundreds of rigidbodies, but will become a problem on a scale of thousands and tens of thousands. So I should generally avoid accessing rigidbody data by index. In my test I had 10k cubes falling down without collisions between each other. RW-accessing their physics velocities by rigidbody index appeared much slower than I initially expected.

So the question from this block is if i understand everything described correctly.

II. Raycasts

  • I can immediately raycast with CollisionWorld.Cast anywhere relatively to BuildPhysicsWorld or StepPhysicsWorld, but until I try to create or destroy any physics entities in runtime.
  • I can do manual raycast batching, for example if I use CollisionWorld.Cast inside IJobParallelFor. If I need results immediately, I call Dependency.Complete().
  • Immediate raycasts can cause excess sync points, so it is better to batch them and use dependencies. I’m not getting any errors when scheduling raycasts anywhere relatively to BuildPhysicsWorld or StepPhysicsWorld, so I suppose it’s ok to raycast from anywhere.
  • Raycast results may change depending on when I do them. In series of tests I was raycasting vertically down from fixed point. Raycast hits Entity with collision (cube). Entity is placed under raycast point and falling down freely affected by gravity. Not sure if my attempt to describe results is clear enough, but the main point is that I can get different results depending on when I do raycasts.

  • Changing entity position 3 frames after game start - I do that via Translation component before BuildPhysicsWorld. If I schedule raycast before BuildPhysicsWorld, I get raycast results as if I haven’t changed entity position at all - I’ll get changed results next frame only.

  • If I schedule raycast between BuildPhysicsWorld and StepPhysicsWorld, then I get relevant result, which takes into account the fact that I changed Entity position manually. So I guess raycast uses latest data from BuildPhysicsWorld under the hood, even if this data is from previous simulation frame (suppose that it is correct after reading this thread https://discussions.unity.com/t/822780 ).

  • Changing entity rigidbody velocity between BuildPhysicsWorld and StepPhysicsWorld via rigidbodyIndex and PhysicsWorld 3 frames after game start. raycast before BuildPhysicsWorld - ok. raycast between BuildPhysicsWorld and StepPhysicsWorld required manual dependency management with system that changes rigidbody velocity, but still works

  • Things are getting a bit more complicated if I create/destroy physics entities at runtime, for example when couple of fixed ticks is passed.

  • if created at SimulationSystemGroup, no matter if done before or after FixedStepSimulationSystemGroup - then I get errors while trying to raycast before BuildPhysicsWorld.

InvalidOperationException: The previously scheduled job RaycastBeforeBuildPhysicsWorld:RaycastJob reads from the Unity.Collections.NativeArray`1[Unity.Physics.RigidBody] RaycastJob.World.m_Bodies.

  • Creating physics entity at FixedStepSimulationSystemGroup before BuildPhysicsWorld - ok. I get errors if create entity between BuildPhysicsWorld and ExportPhysicsWorld - this looks reasonable. Also getting errors if entity is created after ExportPhysicsWorld - not sure why that happens.
  • So it looks that if I want create some physics-related entities - I should do that In FixedStepSimulationSystemGroup and before BuildPhysicsWorld? or raycasting before BuildPhysicsWorld is totally wrong? Does it happen this way because any physics entity created elsewhere in simulation frame “breaks” something in otherwise valid PhysicsWorld?

So the questions from this block are:
- should I use raycast batching every time it is possible, or more generally - when I should and shouldn’t batch raycasts?
- is it ok to do raycasts anywhere in fixed simulation frame, or there are moments when I should or should not do them?
- if it is ok to raycast anywhere in frame, then I suppose that one usecase for raycasts before BuildPhysicsWorld is slowly changing or not so precision-or-latency critical situations. Fast racing game with complex road collision geometry may require raycasting between BuildPhysicsWorld and StepPhysicsWorld, thus accessing rigidbody properties via index, which is much slower - I think something must be wrong with my understanding of raycasts.
- is there really some limitations on when I can create physics-related entities in simulation frame, or I’m mistaken somewhere?

|||. Collision queries
1. PhysicsSamples have good example of stateful trigger and collision events. Not taking into account fact that this implementation gives us very useful ability to detect OnEnter/OnExit "events", handling all trigger/collision events in one place and one single job looks like much better idea compared to other option - scheduling multiple ITriggerEventsJob/ICollisionEventsJob every time I need collision/trigger result. In second case every time I schedule such job, I must pass over all collision/trigger events and filter out ones I dont need in exact system every time. This looks like a lot of excess work in case If my game heavily relies on trigger/collision events.

So the questions are:
- is the way to handle all collision/trigger events in one place (as shown In PhysicsSamples) is better from performance point of view, compared to case when each system handles collision/trigger events individually?
- Besides performance questions, there must be some limitations for centralized processing of collision/trigger events, otherwise I cant understand why something similar to what we can find in PhysicsSamples is not already included in physics package? Especially taking into account that we are getting OnEnter/OnExit which we are used to after working with PhysX.

IV. PhysicsSimulation Call-backs

  • I can schedule and enqueue Physics SimulationCallbacks anywhere in FixedStepSimulationSystemGroup. For me it makes make most sense if done before StepPhysicsWorld. I checked that only for PostSolveJacobians, suppose that should be true for other phases?

Hi @Rekart , sorry for taking such a long time to respond. Let me address the first 2 sections of your post and the rest will follow today or tomorrow.

I Accessing rigid body properties

  1. Correct

  2. RigidBody, MotionData and MotionVelocity arrays on CollisionWorld and DynamicsWorld are formed in jobs spawned by BuildPhysicsWorld (last job handle retrieved via BuildPhysicsWorld.GetOutputDependency()) and the order of their elements does not change during the step. That’s the physics runtime data, so it is read/written by StepPhysicsWorld jobs and finally written back to PhysicsVelocity, Translation and Rotation components in ExportPhysicsWorld. RigidBody contains body’s position at the start of the step and is not changed during the step, so you can read it from either main thread or jobs. MotionData and MotionVelocity are being read and written to during the step, so it is not advisable to read or write them. Note that there are callbacks you can use to tweak some things during physics step.

  3. Rigid body data arrays (mentioned in #2) are contiguous in memory, so guidelines for accessing them are same as any other arrays - it’s advisable to iterate through them separately and sequentially. What’s your use case?

If you’re storing bodyIndex and accessing RigidBody array just to get Entity for that body, I can share the good news that the post-january release (probably late feb or march) will have body index - entity map, so you won’t have to do that.

II Raycasts

  1. When you have lots of raycasts, it makes sense to cast from an IJobParallelFor (please see Raycasting Is Super Slow )

  2. Casts are performed on Broadphase data and Broadphase building is the last job spawned by BuildPhysicsWorld. So, if you do casts before BuildPhysicsWorld, results will be based on the data from the start of the previous step. If you cast after Broadphase build job is complete, results will be based on the data from the start of the current step.
    If you want to cast against data from the end of the physics step, you’ll need to make StepPhysicsWorld update Broadphase with latest body positions at the end of the physics step by ticking “Synchronize Collision World” in PhysicsStep component (note that it incurs some time cost). That way you’ll be able to perform casts on fresh data between current step’s StepPhysicsWorld.GetOutputDependency()) and next step’s BuildPhysicsWorld.OnUpdate().
    It’s important to hook up your cast jobs properly in your system’s OnUpdate:
    a. m_BuildPhysicsWorld.AddInputDependencyToComplete(myCastJob)
    b. myCastJob.Schedule(…, m_StepPhysicsWorld.GetOutputDependency()) or myCastJob.Schedule(…, BuildPhysicsWorld.GetOutputDependency())

The time interval after BuildPhysicsWorld.OnUpdate starts and before BuildPhysicsWorld.GetOutputDependency() ends is not good for casts, since they’ll be working on empty/incomplete Broadphase.

  1. Of course, all of the above is affected if you change physics-related entities and components. Any changes should be done after the last job in ExportPhysicsWorld ends (use ExportPhysicsWorld.GetOutputDependency()) and before next step’s BuildPhysicsWorld.OnUpdate starts (use m_BuildPhysicsWorld.AddInputDependencyToComplete()). Don’t forget that it’s on BuildPhysicsWorld to query for physics Entities and build the Broadphase, so that’s the time when the changes will get picked up.

General note: I’m assuming that you know how to specify SystemGroup for your system and chain it with other systems (using UpdateBefore and UpdateAfter). Those jobHandle dependencies I mentioned above need to be handled in addition to system “chaining”, they are not a replacement. This is something many people miss, until they gain a solid amount of experience with DOTS.

Please let me know if this helps. I’ll start working on the rest of your questions immediately.

3 Likes

Thank you very much, @[milos85miki]( Working with physics right way members/milos85miki.4040651/), that definitely adds lot of clarity!

Regarding my attempts to access rigidbody data from CollisionWorld instead of components - I tried to run 2 jobs, one of them working with components, no questions here:

Entities.WithName("AccessBeforeBuildPhysicsWorld").ForEach((ref PhysicsMass m) =>
{
    float3 mass = m.InverseMass;
}).Schedule();

And another job which I could not Schedule(), just Run(). I thought that combining dependencies in the beginning of update and adding our dependency to StepPhysics world will be enough, but I’m getting job scheduling errors.
It is about 3 times slower with Run() compared to previous case.

[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
[UpdateAfter(typeof(BuildPhysicsWorld))]
[UpdateBefore(typeof(StepPhysicsWorld))]
public class QueryRigidBody : SystemBase
{
    protected override void OnUpdate()
    {
        Dependency = JobHandle.CombineDependencies(Dependency, World.GetExistingSystem<BuildPhysicsWorld>().GetOutputDependency());
      
        var physicsWorld = World.GetExistingSystem<BuildPhysicsWorld>().PhysicsWorld;
        Entities.ForEach((Entity e, in PhysicsMass m) =>
        {
            var rbIndex = physicsWorld.GetRigidBodyIndex(e);
            var mass = physicsWorld.GetMass(rbIndex);
        }).Run();//.Schedule(); //Can't schedule
      
        World.GetExistingSystem<StepPhysicsWorld>().AddInputDependency(Dependency);
    }
}

Hi @Rekart ,

I'm glad the first answer helped and will try to address the rest now. I carefully read your last questions in initial post only after replying, sorry if I didn't pay enough attention to the scheduling part and please don't hesitate to raise more questions there.

physicsWorld.GetRigidBodyIndex() - the code in the current version (0.5.1-preview.2) iterates through all PhysicsWorld.Bodies and is thus very slow (your 2nd job effectively had a nested loop, all rigid bodies for each entity). That will change in 0.6.0 or the version after that (not sure yet), we'll be keeping a map between entity and rigid body index.
Anyway, in your case I see 2 options:
a) before BuildPhysicsWorld: iterate on Entities, like you did in the first job ("AccessBeforeBuildPhysicsWorld")
b) between BuildPhysicsWorld and StepPhysicsWorld: create a custom job that will iterate through physicsWorld.MotionVelocities and take mv.InverseMass (just make sure to mark readonly fields of the job). That will cover dynamic bodies (physicsWorld.NumDynamicBodies), while static bodies have infinite mass.

Note that you're getting inverse mass in both cases and can calculate var mass = 0 == mv.InverseMass ? 0.0f : 1.0f / mv.InverseMass; . Infinite mass is denoted by 0 for both mass and inverse mass.

Side tip: you could save refs to BuildPhysicsWorld and StepPhysicsWorld as class fields in onCreate() for your system (an example in UnityPhysicsSamples\Assets\Tests\BehaviorUnitTests\Events\VerifyCollisionEvents.cs).

I don't see why .Schedule wouldn't work for your 2nd job, it hooks up nicely between BuildPhysicsWorld and StepPhysicsWorld. Could you please share the error message?

III Collision queries

  1. "Monolithic" event handling is not necessarily more performant than "separated" handling. Yes, separated will iterate over each event multiple times, but it all depends on what needs to be done. Imagine that there are 5 types of event handling jobs, each one doing non-negligible work and dealing with data that is organized per type in memory (data for same event type is at the same place in memory). Monolithic approach would be constantly jumping around the memory, while separated approach would use the same (cached) data and thus be faster in total. So, it all depends on the use case and it's best to be open to all options.

  2. The stateful events using dynamic buffers are not in the physics package because they are stateful. The point of UnityPhysics is to have minimal state (only the necessary physics data), in order to be network-friendly. On the other hand, we realized many people need stateful events and might not care about network physics, so we created those samples.

IV PhysicsSimulation Call-backs

  1. Yes, all callbacks are scheduled in the same way and it's best to do it before StepPhysicsWorld.OnUpdate (use UpdateBefore attribute on your system). Alternatively, you could do it after StepPhysicsWorld.OnUpdate and they will get called in the next physics step. So, as long as you don't call EnqueueCallback() while StepPhysicsWorld.OnUpdate is running, you should be fine.
3 Likes

@[milos85miki]( Working with physics right way members/milos85miki.4040651/) Thank you very much again for detailed reply! Regarding job scheduling issue - my mistake, missed another job accessing PhysicsWorld at the same moment, chaining them fixed issue.

2 Likes