Search Unity

Question Determinism with complex collider

Discussion in 'Physics' started by Wiaschtlbert, Jul 29, 2020.

  1. Wiaschtlbert

    Wiaschtlbert

    Joined:
    Dec 1, 2017
    Posts:
    5
    Hi guys, we need help! :)

    We are currently developing a game with time loops, where clones of yourself are executing the actions you have done in the previous loops.
    We need to be determinstic, it seems to work with primitive colliders, with convex mesh colliders sometimes, but not with child colliders. The values are close, but not exactly the same.
    We enabled the Enhanced Determinism flag in the physics settings and disabled Auto Simulate.

    We tried to isolate the problem in a blank project, and came up with this:

    Code (CSharp):
    1. public class DeterminismTest : MonoBehaviour
    2. {
    3.     public int iterations = 1;
    4.     public int LogTick = 100;
    5.     private int currTick;
    6.  
    7.     private Vector3 startPos;
    8.     private Quaternion startRot;
    9.  
    10.     private Rigidbody rb;
    11.     private Vector3 rb_startPos;
    12.     private Quaternion rb_startRot;
    13.  
    14.     private void Awake()
    15.     {
    16.         rb = GetComponent<Rigidbody>();
    17.         startPos = transform.position;
    18.         startRot = transform.rotation;
    19.         rb_startPos = rb.position;
    20.         rb_startRot = rb.rotation;
    21.         ManualReset();
    22.         StartCoroutine(CoManualFixedUpdate());
    23.     }
    24.  
    25.     private IEnumerator CoManualFixedUpdate()
    26.     {
    27.         while (true)
    28.         {
    29.             Physics.Simulate(Time.fixedDeltaTime);
    30.             currTick++;
    31.             if (currTick == LogTick)
    32.             {
    33.                 currTick = 0;
    34.                 Debug.Log($"Position: {ToStringManyDecimals(transform.position)}");
    35.                 ManualReset();
    36.             }
    37.             yield return new WaitForFixedUpdate();
    38.         }
    39.     }
    40.  
    41.     private void ManualReset()
    42.     {
    43.         for (int i = 0; i < iterations; i++)
    44.         {
    45.             transform.position = startPos;
    46.             transform.rotation = startRot;
    47.             rb.velocity = Vector3.zero;
    48.             rb.angularVelocity = Vector3.zero;
    49.             rb.position = rb_startPos;
    50.             rb.rotation = rb_startRot;
    51.         }
    52.     }
    53.  
    54.     private string ToStringManyDecimals(Vector3 v3)
    55.     {
    56.         return $"{v3.x:F10}, {v3.y:F10}, {v3.z:F10}";
    57.     }
    58.  
    59.     private void OnDestroy()
    60.     {
    61.         StopAllCoroutines();
    62.     }
    63. }
    We added this MonoBehaviour to an object with a collider and a rigidbody with the following outcomes:
    • Primitive Collider( Box, Sphere): Works like a charm
    • Convex Mesh Collider: If the object is reset multiple times(iterations > 1) it works?!
    • Collider on parent and child: Does not work. Sometimes it is only the 2nd loop that's different and everything from there is the same, sometimes there simple are multiple different results
    Note: If we reload the scene, the outcomes are exactly the same. So it seems if a combination of collider does not work, at least it does so consistently.
    In the attached project you should be able to observe that each loop is different until at some point it starts to repeat (best observable with the "Collapse" option from the console enabled).
    If you want to modify the attached test scene be aware that you need to delete all other instances of "DeterminismTest" in the scene, otherwise Physics.Simulate() is called multiple times.
    Another problem which seems to destroy the determinism even for simple colliders is setting the rigidbody to kinematic for a certain amount of time. E.g. set kinematic in tick 40 to true and in tick 80 to false.

    Is there a possibility to achieve this kind of determinism with child colliders?

    Tested in unity version 2019.4.4f1
    Any hint or input is very much appreciated!
     

    Attached Files:

    Last edited: Jul 29, 2020
    Daniiii likes this.
  2. Edy

    Edy

    Joined:
    Jun 3, 2010
    Posts:
    2,508
    As for what I've tested, resetting a rigidbody completely involves keeping it kinematic for a couple of frames and repositioning it while it's kinematic. Here's my code for a complete rigidbody reset:
    Code (CSharp):
    1. public class ResetRigidbody : MonoBehaviour
    2.     {
    3.     public Transform target;
    4.  
    5.     IEnumerator ResetInternal (Vector3 position, Quaternion rotation)
    6.         {
    7.         if (target != null)
    8.             {
    9.             Rigidbody rb = target.GetComponent<Rigidbody>();
    10.  
    11.             if (rb != null && !rb.isKinematic)
    12.                 {
    13.                 rb.isKinematic = true;
    14.                 yield return new WaitForFixedUpdate();
    15.                 rb.position = position;
    16.                 rb.rotation = rotation;
    17.                 yield return new WaitForFixedUpdate();
    18.                 rb.isKinematic = false;
    19.                 }
    20.             else
    21.                 {
    22.                 target.position = position;
    23.                 target.rotation = rotation;
    24.                 }
    25.             }
    26.  
    27.         yield return null;
    28.         }
    29.  
    30.     public void ResetRigidbody (Vector3 position, Quaternion rotation)
    31.         {
    32.         StartCoroutine(RespawnInternal(position, rotation));
    33.         }
    34.     }
    Whenever you call ResetRigidbody(position, rotation) in that component the target's rigidbody will be reset completely to the exact given position and rotation.
     
  3. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,073
    If I were you I would just record positions and interpolate between them without any physics involved.
    Do you have to interact with this objects that are repeating actions?
     
  4. Daniiii

    Daniiii

    Joined:
    Nov 13, 2013
    Posts:
    24
    Thank you very much Edy! I'm also working on the game so I'm going to reply here.
    I tried your reset, but for me, it exhibits the same behavior as our previous reset where it works with boxcolliders etc. but more complex ones do not work.
    This is my implementation now:
    Code (CSharp):
    1. public class DeterminismTest : MonoBehaviour
    2. {
    3.     public int iterations = 1;
    4.     public int LogTick = 100;
    5.     private int currTick;
    6.  
    7.     private Rigidbody rb;
    8.     private Vector3 rb_startPos;
    9.     private Quaternion rb_startRot;
    10.  
    11.     private void Start()
    12.     {
    13.         rb = GetComponent<Rigidbody>();
    14.         rb_startPos = rb.position;
    15.         rb_startRot = rb.rotation;
    16.         StartCoroutine(CoManualFixedUpdate());
    17.     }
    18.  
    19.     private IEnumerator ResetRigidbody(Rigidbody rigidbody, Vector3 pos, Quaternion rot)
    20.     {
    21.         rigidbody.isKinematic = true;
    22.         yield return new WaitForFixedUpdate();
    23.         rigidbody.position = pos;
    24.         rigidbody.rotation = rot;
    25.         yield return new WaitForFixedUpdate();
    26.         rigidbody.isKinematic = false;
    27.         yield return new WaitForFixedUpdate();
    28.     }
    29.  
    30.     private IEnumerator CoManualFixedUpdate()
    31.     {
    32.         while (true)
    33.         {
    34.             Physics.Simulate(Time.fixedDeltaTime);
    35.             currTick++;
    36.             if (currTick == LogTick)
    37.             {
    38.                 currTick = 0;
    39.                 Debug.Log($"Position: {ToStringManyDecimals(rb.position)}");
    40.                 yield return StartCoroutine(ResetRigidbody(rb, rb_startPos, rb_startRot));
    41.             }
    42.             yield return new WaitForFixedUpdate();
    43.         }
    44.     }
    45.  
    46.     private string ToStringManyDecimals(Vector3 v3)
    47.     {
    48.         return $"{v3.x:F10}, {v3.y:F10}, {v3.z:F10}";
    49.     }
    50. }
    Yes, unfortunately, we want to interact with our replays. We tried recording all objects, but it has a few annoying edge cases in our game and it takes away some interactivity.
    This is some gameplay of our game
     
  5. Wiaschtlbert

    Wiaschtlbert

    Joined:
    Dec 1, 2017
    Posts:
    5
    Thank you Edy!
    I tried your code and it does exactly what you said: it will reset the rigidbody to the exact given position and rotation. Unfortunately this was not the original problem, the problem was that the position at the end of a loop (e.g. after 100 ticks) differs from time to time, even though the start position and rotation were the same.


    Thanks for the suggestion koirat!
    In each loop, we add a clone of the player. The player should be able to interact with these clones (take away their guns, etc.), so the interaction is a must have.
     
    Last edited: Jul 30, 2020
  6. Daniiii

    Daniiii

    Joined:
    Nov 13, 2013
    Posts:
    24
    So we experimented for a really long time and I think we got a solution that is somewhat working. There is still one problem, I'll touch on that later.

    Turns out Rigidbody resetting has to be done differently, as outlined here for 2D Physics.
    For anyone that also needs a solution, here are our steps. They are almost identical to the 2D steps.
    I attached our project if you want to look at the code.

    Project Settings: Enhanced Determinism: On, Auto Simulation: Off.
    • On Start we record the startposition, rotation, velocity and angularVelocity of our Rigidbodies.

    • 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

    • Then start a Coroutine what yields WaitForFixedUpdate and calls Physics.Simulate for the simulation loop.
    • After enough time/ticks have passed, deactivate all GameObjects with Colliders and Rigidbodies in reverse order.
    • Simulate one physics frame.
    • Then call the Rigidbody reset again.

    Other learnings:
    We had to activate/deactivate not only Rigidbodies but also Colliders without Rigidbodies.
    If you have child colliders, in addition to the parent the children also have to be activated/deactivated in the right order. (SetActivateRecursively and Inactive do this in our code)
    In our tests, the GameObjects didn't have to be inactivate at scene start.
    Every collision detection mode seems to work as far as we tested.

    We still got one problem:
    If we set a Rigidbody to kinematic during the simulation the determinism breaks. Even though it happens in the same tick every run.
    e.g.:

    if (currTick == 40)
    rigidbodies.First().isKinematic = true;

    if (currTick == 80)
    rigidbodies.First().isKinematic = false;

    My guess is that this messes up the internal simulation order.
    Does anybody know if this is possible or how to do it?

    I think we can find a workaround for it, but it would be great if we could just set our Rigidbodies to kinematic during gameplay when we need it.
     

    Attached Files:

    Wiaschtlbert likes this.