Search Unity

How we did deterministic time loops in Unity

Discussion in 'Physics' started by Wiaschtlbert, Mar 28, 2022.

  1. Wiaschtlbert

    Wiaschtlbert

    Joined:
    Dec 1, 2017
    Posts:
    5
    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 :D


    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:
    1. Initialize: On Start we record the startposition, rotation, velocity and angularVelocity of our Rigidbodies. You can instantiate physics objects here.
    2. Then the Rigidbody Reset:
      1. Activate all Rigidbodies and Colliders
      2. Set all Rigidbodies to kinematic.
      3. Apply the recorded start values
      4. Set all Rigidbodies to not kinematic
      5. Deactivate GameObjects with Colliders and Rigidbodies again in reverse order (reverse order not needed I think)
      6. Simulate one physics frame (call
        Physics.Simulate()
        )
      7. Then activate all GameObjects with Colliders and Rigidbodies again
      8. Call WakeUp on all Rigidbodies
    3. Actual Loop: Then start a Coroutine that yields WaitForFixedUpdate and calls Physics.Simulate for the simulation loop.
    4. After Loop: After enough time/ticks have passed, deactivate all GameObjects with Colliders and Rigidbodies in reverse order. After that, simulate one physics frame.
    5. Repeat with step 2: Rigidbody reset.

    I hope this writeup was interesting and can maybe help someone if they’re working on something similar.
     
    Last edited: Mar 30, 2022
  2. Edy

    Edy

    Joined:
    Jun 3, 2010
    Posts:
    2,508
    Thank you so much for sharing this wonderfully detailed information!

    Physics.Simulate is already called after each FixedUpdate loop. Does calling it in the WaitForFixedUpdate stage does so significant difference?

    Wow! This means that some state variables relevant to physics are actually stored in the GameObject itself. As you use an object pool, does it mean that a GameObject may be used by different physics objects at different times, as long as when replaying an action each physics object receives the same GameObject the action was recorded with?

    I set Rigidbody.isKinematic via code all the time. Do you mean that doing so breaks the determinism? So when the procedure says "Set all Rigidbodies to kinematic" you're actually modifying constraints and collision detection.
     
    keeponshading likes this.
  3. Wiaschtlbert

    Wiaschtlbert

    Joined:
    Dec 1, 2017
    Posts:
    5
    For some reason it was not deterministic when AutoSimulate was enabled, so we had to manually simulate the physics.

    Using the exact same GameObject was mostly for the collisions/triggers to be resolved in the same order. We had many iterations where we were deterministic over 90% of the time. Using the exact same GameObject fixed that, probably because of collisions with multiple objects within one physics frame. I'm not aware of any physics state variables stored in GameObjects that are not exposed like the velocity and so on.

    Not sure what you mean by "used by different physics objects". A bullet for example could theoretically be used multiple times and by different weapons, yes. As long as the exact same bullet that was used while "recording" is used in the next loop, the simulation should be deterministic. Is that what you meant?

    Yes, by that I mean that doing so breaks determinism, but only if done so during the actual loop. At least in our case.
    When I said that you are not allowed to activate or deactivate (physics) objects, as well as set .isKinematic, I meant only during the actual loop. In the Reset Rigidbody step you have to use activate and deactivate objects, as well as set isKinematic. When you need to set isKinematic during the actual loop, you have to set the constraints and collision detection instead. Apologies if that was not clear.

    If anything else is unclear, I'll be happy to try to provide further answers :)
     
    keeponshading and Edy like this.
  4. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,456
    That just cannot be true!

    All the "auto-simulate" does is see if the flag is on and call the exact same call you do i.e. "Physics.Simulate". This is the same for both 2D and 3D physics. The call you make from script, calls directly into the native call that we make for you. There's not a separate code-path for this, that would be an awful lot of duplicated code if that were the case.

    Bear in mind that like other systems, 2D/3D physics hooks into the FixedUpdate player-loop. This runs after the script fixed-update in the player-loop. All it does is then check if that flag is one and call the internal simulate method. It's not a special fixed-update or anything. To the player-loop, lots of things are hooked into the fixed-update such as animation, 2D/3D physics, script callbacks etc.
     
    keeponshading likes this.
  5. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,456
    Just to qualify what I said a little...

    This is the native method that is hooked into the player-loop fixed-update call. It's called immediately after the script fixed-update too.

    The "Simulate" call has the scene handle and the time-delta.
    Code (CSharp):
    1. void PhysicsManager::FixedUpdate()
    2. {
    3.     if (!m_AutoSimulation)
    4.         return; // in this mode the users are expected to call Simulate themselves
    5.  
    6.     float dt = GetTimeManager().GetFixedDeltaTime();
    7.     Simulate(GetDefaultPhysicsSceneHandle(), dt);
    8. }
    9.  
    10. void PhysicsManager::Simulate(PhysicsSceneHandle sceneHandle, float dt)
    11. {
    12.     // Fetch the physics scene.
    13.     PhysicsScene* physicsScene = GetPhysicsScene(sceneHandle);
    14.     if (physicsScene == NULL)
    15.     {
    16.         WarningString("Physics.Simulate(...) was called with an invalid scene handle therefore the simulation was not run.");
    17.         return;
    18.     }
    19. ...
    20. ...
    21.  
    Here's where the "Physics.Simulate" call in the script goes. It's the exact same method as the above call for "auto simulation".
    https://github.com/Unity-Technologi...ics/ScriptBindings/Dynamics.bindings.cs#L1474

    It's called for both PhysicsScene.Simulate and Physics.Simulate.

    This is the same for both 2D/3D physics.
     
  6. Wiaschtlbert

    Wiaschtlbert

    Joined:
    Dec 1, 2017
    Posts:
    5
    Thank you for your insights. I'll edit the original post.
    Either way we found it more comfortable to work when we call Phyiscs.Simulate manually, so we haven't tried to switch back in a while.
     
    MelvMay likes this.
  7. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,456
    Not a problem and I didn't want to "stomp" on what you said, more that I really didn't understand how it would be possible given the short code-paths that both take. :) I can understand that, for the most part, this is black-boxed so it's not easy to figure it out at all. Hopefully what I said clarifies that part at least.

    Whilst I'm not a 3D physics dev (2D physics here) I'm always happy to try to give you some inside black-box info if I can figure out what 3D physics is doing.

    Good luck and thanks for taking the time on your original post.
     
    Wiaschtlbert likes this.
  8. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,073
    I have to say this is surprising and in a very bad way.
    Another trap placed by unity.
     
  9. hippocoder

    hippocoder

    Digital Ape

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    How so? Vector3Int could be compared but any non-zero Vector3 can't be compared without at least checking the square magnitude is under a suitable threshold.
     
    DragonCoder and AlTheSlacker like this.
  10. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,073
    When I use == I want exact comparison, by the logic of "==".

    It's not like it cannot be compared. It's more like you usually don't do this except for some cases when you want exact comparison.

    Do you use "==" for vectors in your code ?
    In what cases ? (except Vector3.zero ==)

    Also since they broke "==" they had to break "!=" .

    In the end.
    Code (csharp):
    1.  
    2.  
    3. Vector3 a;
    4. Vector3 b;
    5.  
    6. (a == b)
    7.  
    8. is different than
    9.  
    10. (a.x == b.x && a.y ==b.y && a.z == b.z)
    11.  
    12.  
    It does not look good to me.
     
  11. DragonCoder

    DragonCoder

    Joined:
    Jul 3, 2015
    Posts:
    1,696
    When it comes to floats, "exact comparison" can be unintuitive.
    @koirat you surely are aware of: https://bitbashing.io/comparing-floats.html
    That's a trap that programming languages have and Unity just decided to cover that for Vector3s. Say they exchanged one trap for another if you really want to nitpick xP
    However this is quite OT here.

    @Wiaschtlbert
    Really nice guide! It will come useful.
     
    Wiaschtlbert likes this.
  12. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,456
    You've stated your opinion but could we leave that point there or not take it much further or take it elsewhere because it'd be a shame for this thread to end up being a discussion mostly on this; that subject has been done to death on these forums so very many times.

    I'm honestly not trying to squash discussion at all, just that sometimes useful thread info gets lost in the noise of opinions on what people think is good or bad. I'd personally prefer this thread to stay more physics based if I'm honest.

    Thanks.
     
  13. umairu172

    umairu172

    Joined:
    Aug 24, 2022
    Posts:
    1
    Hi, I am working on a 3D physics-based simulation game in unity and have been having problems solving determinism issues. My game uses the same approach to playability i.e( Multiple 3d objects are thrown into the scene at certain fixed times and the player has to catch these objects, most objects collide mid-air, and change their trajectory after a reset of the level is done) to keep the levels behaving the same way as they played the first time (trajectory after mid-air collision).

    A hard reset of the scene is done while enabling all the rigid bodies under fixed update, using a Coroutine enabled from the fixed update loop having a WaitForFixedUpdate(willing to share the code for anyone interested).
    Most levels play out deterministically (checking is done using Vector3.Equals as admin pointed out) but some levels break determinism even if the approach is the same for them. I think it is due to the high amount of collisions between objects or the number of joints (i am not sure!).

    Another issue that I am facing is that the game is to be published on Android & IOS Platform, which means cross-platform determinism which is not supported by Unity's Phyx due to floating point accuracy. To resolve that issue means to use a fixed point math library. As the game is a single player I haven't tried using server-related things which use fixed floating point accuracy.

    I am using auto-simulate and set the fixed-timestep value to 0.01666667(1/60) the value of fixed-timestep is lowered from default 0.02 to the above value because the objects under collision were passing thru each other, even with the detection mode set to Continuous Speculative. And the max allowed timestep defaults to 0.3333333.

    Any help related to this will be greatly appreciated!!
     
  14. topitsky

    topitsky

    Joined:
    Jan 12, 2016
    Posts:
    100
    This post is great. I was able to get deterministic replays work by essentially loading the scene one extra time (after editor play). I think Unity should include some kind of setting in physics that does this without having to load the scene one extra time.
     
  15. topitsky

    topitsky

    Joined:
    Jan 12, 2016
    Posts:
    100
    edit: The scene reload differs from when you restart the computer, now I put all the dynamic bodies under one transform and loop through them to get a deterministic load
     
  16. topitsky

    topitsky

    Joined:
    Jan 12, 2016
    Posts:
    100
    Does anybody have an idea, why replays saved in editor mode, only work in builds when I have the scripting backed set as mono, not il2cpp? Is this because the editor player itself uses Mono?
     
  17. Daniiii

    Daniiii

    Joined:
    Nov 13, 2013
    Posts:
    24
    I don't know how you're serializing replays, but maybe you're serializing some data type that is not supported by il2cpp. Check if you see any errors in the build that might hint at this
     
  18. topitsky

    topitsky

    Joined:
    Jan 12, 2016
    Posts:
    100
    im using a binary writer with floats and bools so i think it should be fine.