Hello,
I am working on building a replay system for my game, which uses PhysX. I’m aware of that various challenges of replicating physics across different machines/builds, but for now I’m only concerned with being able to replay locally on the same machine, same build (this has enough value to allow users to create videos, or allow the dev team to capture trailer footage).
There’s lots of discussion online about whether PhysX is/is not deterministic, but the general conclusion seems to be that it is locally deterministic so long as the Enable Enhanced Determinism flag is checked.
I have built a simple test environment where objects are exploded from the center of the scene (code below). When they all come to a rest, their positions and rotations are recorded, the rigidbodies are reset, and the explosion occurs again.
amusingvelvetydrake
After each run the delta position and rotations are compared from the last time to see if they are the same. So far the answer is…sort of.
I get a few misses each time, but they are the exact same misses, with the same deltas. As well, if I manually debug out the position/rotation of the objects that missed, I find that they oscillate between two different orientations:
Rock (1) end of sim status: pos: (0.5302396, 0.9877197, -4.5893560) rot (0.5090225, 0.5429553, 0.6523935, -0.1431022)
Rock (1) end of sim status: pos: (0.5244906, 0.9944359, -4.5884690) rot (0.5166823, 0.5373523, 0.6508234, -0.1439472)
Rock (1) end of sim status: pos: (0.5302396, 0.9877197, -4.5893560) rot (0.5090225, 0.5429553, 0.6523935, -0.1431022)
Rock (1) end of sim status: pos: (0.5244906, 0.9944359, -4.5884690) rot (0.5166823, 0.5373523, 0.6508234, -0.1439472)
(Rock (1) is an object that “missed”). The above is the orientations of the object after each sim.
This has led me to believe I am not correctly resetting the rigidbodies, or not doing it at the right time during the frame, or something along those lines, since it appears that the simulation is deterministic, but oscillating between two different states.
Entire code I am using is below:
using UnityEngine;
using System.Collections.Generic;
public class ExperimentReplaySystem : MonoBehaviour
{
[SerializeField]
int explosions = 10;
[SerializeField]
float explosionForce = 5;
[SerializeField]
float upwards = 5;
[SerializeField]
float radius = 5;
[SerializeField]
bool verbose;
[SerializeField]
bool rigidbodyResetVerbose;
[SerializeField]
bool autoReset;
private class TrackedRigidbody
{
public Rigidbody rb;
public Vector3 initialPosition;
public Quaternion initialRotation;
public Vector3 previousFinalPosition;
public Quaternion previousFinalRotation;
public void LogAndReset(bool verbose)
{
if (verbose)
{
Debug.Log(string.Format("{0} end of sim status: pos: {1} rot {2}", rb.name, rb.position.ToString("F7"), rb.rotation.ToString("F7")));
}
previousFinalPosition = rb.position;
previousFinalRotation = rb.rotation;
rb.position = initialPosition;
rb.rotation = initialRotation;
rb.velocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
}
public float DeltaPosition()
{
return Vector3.Distance(previousFinalPosition, rb.position);
}
public float DeltaRotation()
{
return Quaternion.Angle(previousFinalRotation, rb.rotation);
}
}
private List<TrackedRigidbody> trackedRigidbodies;
private int simulationCount = 0;
private bool simulating = false;
private void Awake()
{
trackedRigidbodies = new List<TrackedRigidbody>();
foreach (Rigidbody rb in FindObjectsOfType<Rigidbody>())
{
TrackedRigidbody trackedRigidbody = new TrackedRigidbody()
{
rb = rb,
initialPosition = rb.position,
initialRotation = rb.rotation
};
trackedRigidbodies.Add(trackedRigidbody);
}
Explode();
}
private void Update()
{
if (simulating)
{
bool stillRunning = false;
foreach (TrackedRigidbody tracked in trackedRigidbodies)
{
if (!tracked.rb.IsSleeping())
{
stillRunning = true;
break;
}
}
if (!stillRunning)
{
simulating = false;
Evaluate();
}
}
if (Input.GetKeyDown(KeyCode.R) && !simulating)
{
Reset();
}
}
private void Explode()
{
Debug.Log(string.Format("Running simulation {0}", simulationCount));
simulating = true;
foreach (TrackedRigidbody tracked in trackedRigidbodies)
{
tracked.rb.AddExplosionForce(explosionForce, transform.position, radius, upwards, ForceMode.Impulse);
}
}
private void Reset()
{
foreach (TrackedRigidbody tracked in trackedRigidbodies)
{
tracked.LogAndReset(rigidbodyResetVerbose);
}
Explode();
}
private void Evaluate()
{
if (simulationCount > 0)
{
int misses = 0;
float largestDeltaP = 0;
float largestDeltaR = 0;
foreach (TrackedRigidbody tracked in trackedRigidbodies)
{
float deltaP = tracked.DeltaPosition();
float deltaR = tracked.DeltaRotation();
bool isMiss = deltaP != 0 || deltaR != 0;
largestDeltaP = Mathf.Max(deltaP, largestDeltaP);
largestDeltaR = Mathf.Max(deltaR, largestDeltaR);
if (isMiss)
misses++;
if (verbose)
{
Debug.Break();
string message = string.Format("Rb {0} DP: {1} DR: {2}", tracked.rb.name, deltaP.ToString("F7"), deltaR.ToString("F7"));
if (isMiss)
Debug.LogError(message);
else
Debug.Log(message);
}
}
if (misses > 0)
{
Debug.LogError(string.Format("Encountered {0} misses. Largest deltaP: {1} Largest deltaR {2}", misses, largestDeltaP.ToString("F7"), largestDeltaR.ToString("F7")));
}
else
{
Debug.Log("Encountered no misses.");
}
}
else
{
Debug.Log("Is first run of simulation. Next run will debug deltas.");
}
simulationCount++;
if (autoReset)
Reset();
}
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, radius);
}
}
I am assuming that resetting position, rotation, velocity, and angular velocity is enough. Am I incorrect?
Thanks for any insight,
Erik