Here’s a post about how we did deterministic physics for our time loop game. Hopefully it can be helpful to someone out there
What is this post about?
We built a game with time loops. Let’s say you have 20 seconds. You can throw stuff around, load a weapon, shoot with it, and do much more stuff, mostly involving (3D) physics. When the time runs out, everything is reset back to the start and you start again. But: Every action you did in the previous loop is now precisely replicated by a clone of yourself.
And that’s the catch: the word “precisely”. If a bullet is reflected at a slightly different angle, it may look the same, but if it is reflected a few more times, the error can add up, and in the second loop, an enemy that would have been hit in the previous loop is now incorrectly missed.
Why would a bullet be reflected at a slightly different angle? The answer is determinism. “Determinism” means that the same input results in the exact same output.
Unity’s 3D Physics engine (PhysX) can be deterministic, but there are a lot of traps to be avoided. The solutions we found work on the same machine at least. For different machines we are not aware of a deterministic solution involving PhysX.
So if you are in a similar situation as us, and you want to create deterministic time behaviour on the same machine, we hope to be able to help with this post. For context, our game is called “We Are One”, and it is a VR puzzle shooter.
Disclaimer
What we’re writing here might not be 100% correct. While we did a lot of tests to isolate specific behaviors and scenarios, we cannot guarantee that our findings are applicable to other situations.
Note that if the Physics Time Step cannot be maintained for whatever reason (e.g. you placed 10.000 cubes with rigidbodies inside one another), both presented solutions will probably be nondeterministic.
Why did we not just record everything?
To preserve interactivity. Future loops should be able to interact with past loops, so you can take a gun out of the hands of a past clone and replace it with something different. Or a past clone can shoot with an empty gun, but in a future loop you can reload the gun, with which the past clone is now shooting. While this would theoretically be possible with records, it would get very complex pretty fast.
Why not DOTS physics?
When we started the project back in 2020 I checked out the Unity DOTS physics but found that it wasn’t quite ready for what we wanted to do. I think it didn’t have a fixed time step from what I can remember. Besides that we were all not that familiar with DOTS and I didn’t think it was quite production ready, so we went with the default Unity physics.
If you’re starting now, the DOTS physics might be worth checking out because it is probably less hassle than fighting with PhysX and Unity.
Prerequisites
If you want to achieve a deterministic simulation there’s a few general things you should do.
First there’s an enhanced determinism option in the physics settings that you should enable.
In the physics settings turn off auto simulation and instead call Physics.Simulate in your own custom loop (This might not be necessary).
We call our own simulation loop in our game which is a Coroutine that waits for fixedupdate. Our scripts implement an interface with an update method. In the loop we then iterate over each object that implements the interface. This way we can guarantee the order in which our scripts are updated.
For all scripts that you want to behave the same way make sure that you’re using a fixed update loop and not any frame rate dependent stuff.
Another important point here is testing if the simulation is deterministic. Start with a minimal test project to figure out how everything works or why it doesn’t work.
When you compare the positions of objects to check if they behave deterministically don’t use Vector3 == Vector3
as that uses Vector3.Approximately
to check if they are the same.
Instead use Vector.Equals
to compare them. The same goes for Quaternions I think. Debug.Log(Vector3)
also doesn’t show all decimal places by default.
A thing we messed up quite a few times is checking if the code that verifies the determinism is correct. Make sure that this works, you don’t wanna work on your game for 2 months and then figure out that it doesn’t actually work, like we did
Two options for determinism with PhysX
We know of two approaches to achieve a deterministic simulation using PhysX (default Unity physics).
Option A is reloading the scene (hard reset).
Option B involves a soft reset by enabling and disabling rigidbodies and colliders in a specific order (soft reset). This is the option we currently use in our game.
Back when we started the project we weren’t fully aware that you could use the scene reloading in combination with multi scene physics to achieve deterministic physics. That’s why we didn’t go with Option A but instead went with Option B.
We tried to switch our project over to scene reloading but that caused a crash in the build we couldn’t get to the bottom of, so we just reverted back to our working soft reset. But if I were to start the project anew I’d probably go with Option A.
Option A: Scene reload (hard reset)
When you reload a scene Unity will destroy and recreate the physics world. This is deterministic on the same machine. So when you play the scene, bounce a ball off a corner, reload the scene and then do the same thing it will bounce in exactly the same way.
This approach is probably the easiest way to do deterministic physics using the default Unity PhysX engine.
With multi scene physics you can create a separate physics scene and let all your physics objects live in there. If you don’t want to destroy all your gameobjects when resetting the physics scene you can move your physics objects between the physics scene and another scene when destroying and reloading the physics scene.
If you use a different scene for your physics you’ll have to replace all your Physics.Simulate or Physics.Raycast calls with direct calls to the physics scene you’re using.
Option B: soft reset
This is the option we use in the game, it is based on this blog post about 2D physics: https://support.unity.com/hc/en-us/articles/360015178512-Determinism-with-2D-Physics
For this we enable/disable rigidbodies and colliders in a specific way when we want to reset the physics. This option comes with some caveats that make working with it a bit of a pain:
- With this option you can’t instantiate objects during a loop. So all objects with physics you want to spawn during the loop, like bullets or clones, have to be created before the first loop. For that we used object pooling (which is great for multiple reasons).
- Also you can’t enable and disable colliders, at least not by calling
Collider.enabled = false
. Workaround: You can add a deactivated layer though, which does not collide with any other layer. - You can’t set
RigidBody.isKinematic
via code. Workaround: You can set the RigidBodyConstraints to FreezeAll and set the detectCollision of the rigidbody to false. - If you want to replay the same actions again - like a clone performing the same actions as you did - you need to use the exact(!) same gameobject. We implemented a deterministic ObjectPoolGetter for that
- All discrete and continuous collision options work
- Child colliders work
- Rigidbodies childed under other rigidbodies do not seem to work
- Don’t have two colliders on the same gameobject
With Option A (scene reload) setting the kinematic value during the loop or disabling a gameobject doesn’t seem to be a problem for determinism. For all other points we do not know.
The actual procedure:
-
Initialize: On Start we record the startposition, rotation, velocity and angularVelocity of our Rigidbodies. You can instantiate physics objects here.
-
Then the Rigidbody Reset:
-
Activate all Rigidbodies and Colliders
-
Set all Rigidbodies to kinematic.
-
Apply the recorded start values
-
Set all Rigidbodies to not kinematic
-
Deactivate GameObjects with Colliders and Rigidbodies again in reverse order (reverse order not needed I think)
-
Simulate one physics frame (call
Physics.Simulate()
) -
Then activate all GameObjects with Colliders and Rigidbodies again
-
Call WakeUp on all Rigidbodies
-
Actual Loop: Then start a Coroutine that yields WaitForFixedUpdate and calls Physics.Simulate for the simulation loop.
-
After Loop: After enough time/ticks have passed, deactivate all GameObjects with Colliders and Rigidbodies in reverse order. After that, simulate one physics frame.
-
Repeat with step 2: Rigidbody reset.
I hope this writeup was interesting and can maybe help someone if they’re working on something similar.