I can’t seem to find any good resources utalising the Unity Physics simulationworlds to reverse the physics state back a few ticks and resimulating those ticks to the current tick, any information anyone has on this would be greatly appreciated!
It depends on what you mean by rolling back. People use this to mean different things. You can easily maintain an old copy of the world and roll back to that.
There’s two key pieces of tech. One is saving the world state serializing it and restoring it.
And the second is simulating the world.
There’s lots of discussion of ways to do these things but I haven’t found a great source of examples or definitive solutions.
So I have tried to tried to implement the sort of system I’m refering to in it’s most basic sense for a rolling ball. At tick 10 the PhysicsWorld is supposed to revert back to the PhysicsWorld state at tick 5 and simulate the next five ticks to catch up. The code I have wrote does not work how I would expect it to (I’m not as farmiliar with DOTS and ECS as I would like, so I’m certain I am missing some crucial steps). When I revert the physics world back, the ball position does not seem to update and when using the StepImmidiate the ball just seems to stay in the same position (Look at the image of the console to get a sense of what I mean). Any information anyone can give me about how to progress further would be greatly appreciated!
using UnityEngine;
using Unity.Entities;
using Unity.Physics;
using Unity.Transforms;
using System.Collections.Generic;
using Unity.Physics.Systems;
[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
public class RollBacker : ComponentSystem
{
public List<PhysicsWorld> pastWorldInstances = new List<PhysicsWorld>();
BuildPhysicsWorld m_BuildPhysicsWorld;
protected override void OnStartRunning()
{
m_BuildPhysicsWorld = World.GetOrCreateSystem<BuildPhysicsWorld>();
}
protected override void OnUpdate()
{
Debug.Log("Tick " + pastWorldInstances.Count + " ball position is" + GetBallPosition());
pastWorldInstances.Add(m_BuildPhysicsWorld.PhysicsWorld.Clone());
if (pastWorldInstances.Count == 10)
{
RollBackTo(5);
}
}
void RollBackTo(int tickNum)
{
Debug.Log("Changing world back to tick " + tickNum);
//Changes current physicsworld
ChangePhysicsWorld(pastWorldInstances[tickNum]);
Debug.Log("BallPostion after world change " + GetBallPosition());
//Steps the physics until catches up to current tick
StepWorldXTimes(pastWorldInstances.Count - tickNum);
}
private SimulationContext SimulationContext;
void StepWorldXTimes(int stepAmount)
{
PhysicsStep stepComponent = PhysicsStep.Default;
var stepInput = new SimulationStepInput
{
World = m_BuildPhysicsWorld.PhysicsWorld,
TimeStep = Time.DeltaTime,
NumSolverIterations = stepComponent.SolverIterationCount,
SolverStabilizationHeuristicSettings = stepComponent.SolverStabilizationHeuristicSettings,
Gravity = stepComponent.Gravity,
SynchronizeCollisionWorld = true
};
for (int i = 0; i < stepAmount;i++)
{
SimulationContext.Reset(stepInput);
Simulation.StepImmediate(stepInput, ref SimulationContext);
Debug.Log("Ball position in simulation " + GetBallPosition());
}
}
void ChangePhysicsWorld(PhysicsWorld physicsWorld)
{
m_BuildPhysicsWorld.PhysicsWorld = physicsWorld;
//m_BuildPhysicsWorld.Update();
}
Vector3 GetBallPosition()
{
Vector3 pos = Vector3.zero;
Entities.ForEach((ref Translation position) =>
{
pos = new Vector3(position.Value.x,position.Value.y,position.Value.z);
});
return pos;
}
}
So I have tried to set it up so that I have a specific world for running the simulation, However, when I use simulationWorld.Update() it gives me an error saying
“AssertionException: Assertion failure. Value was False
Expected: True
PushTime without matching PopTime”
Is this something that is expected to happen? I tried using simulationWorld.PopTime() in different places but it doesn’t seem to help the error, how am I properly supposed to use PopTime() and PushTime()?
[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
public class RollBacker2 : ComponentSystem
{
static World simulationWorld;
static List<float3> positions = new List<float3>();
static List<float3> velocites = new List<float3>();
static bool isSimulating = false;
protected override void OnStartRunning()
{
if (isSimulating){ return;}
simulationWorld = new World("lockStepWorld", WorldFlags.Simulation);
var systems = DefaultWorldInitialization.GetAllSystems(WorldSystemFilterFlags.Default);
DefaultWorldInitialization.AddSystemsToRootLevelSystemGroups(simulationWorld, systems);
FixedStepSimulationSystemGroup fixGroup = simulationWorld.GetExistingSystem<FixedStepSimulationSystemGroup>();
fixGroup.FixedRateManager = new FixedRateUtils.FixedRateSimpleManager(Time.DeltaTime);
}
protected override void OnUpdate()
{
if (isSimulating)
{
Debug.Log("Simulating " + GetBallPosition());
}
else
{
Debug.Log("Normal " + positions.Count + " " + GetBallPosition());
AddBallStateToLists();
if (positions.Count == 10)
{
RollBackTo(5);
}
}
}
void RollBackTo(int tickToRollBackTo)
{
Debug.Log("Rolling back to " + tickToRollBackTo);
SetBallPosAndVelToTick(tickToRollBackTo);
simulationWorld.EntityManager.DestroyAndResetAllEntities();
simulationWorld.EntityManager.CopyAndReplaceEntitiesFrom(World.EntityManager);
simulationWorld.SetTime(new Unity.Core.TimeData(World.Time.ElapsedTime, World.Time.DeltaTime));
SimulateTicks(positions.Count - tickToRollBackTo - 1);
World.EntityManager.DestroyAndResetAllEntities();
World.EntityManager.CopyAndReplaceEntitiesFrom(simulationWorld.EntityManager);
}
void SimulateTicks(int tickNumber)
{
isSimulating = true;
Debug.Log("Simulating the next " + tickNumber + " ticks");
for (int i = 0; i < tickNumber; i++)
{
simulationWorld.Update();
}
isSimulating = false;
}
public void SetBallPosAndVelToTick(int tick)
{
float3 ballPosition = positions[tick];
float3 ballVelocity = velocites[tick];
Entities.ForEach((ref PhysicsVelocity physicsVelocity, ref Translation position, ref Rotation rotation) =>
{
position.Value = ballPosition;
physicsVelocity.Linear = ballVelocity;
});
}
void AddBallStateToLists()
{
positions.Add(GetBallPosition());
velocites.Add(GetBallVelocity());
}
}
Set your FixedRateManager. I do most stuff in a fixed time step because I am just Simulating physics and related gameplay logic in ECS. You may want to enable and disable systems or use [DisableAutoCreation] depending on your use case.
void Start()
{
locECSWorld = new World("lockStepWorld", WorldFlags.Simulation);
var systems = DefaultWorldInitialization.GetAllSystems(WorldSystemFilterFlags.Default);
DefaultWorldInitialization.AddSystemsToRootLevelSystemGroups(locECSWorld, systems);
FixedStepSimulationSystemGroup fixGroup = locECSWorld.GetExistingSystem<FixedStepSimulationSystemGroup>();
fixGroup.FixedRateManager = new FixedRateUtils.FixedRateSimpleManager(MyFixedTimeStep);
}
private void updateOnce(inputData myInputData)
{
//do input stuff here
locECSWorld.Update();
}
For example I disable and enable the system that send positions to my graphics so it only runs once per update even though I may have multiple fixed updates. If you don’t care how many fixed updates you can use FixedRateCatchUpManager.
public void UpdateWorld(List<inputData> myInputData)
{
if (myInputData.Count == 1)
{
updateOnce(myInputData[0]);
}
else if (myInputData.Count > 1)
{
myGxSys.Enabled = false; // because we only want to move gx after all calls to update
for (int i = 0; i < myInputData.Count; i++)
{
if (i == (myInputData.Count - 1))
{
myGxSys.Enabled = true;
}
updateOnce(myInputData[i]);
}
}
}
This is exactly what I need to do! I’m new to ECS and need to reset an extensive world (millions of entities) to start state… It would even be better if I could save and restore this copy as it takes quite a while to generate this world. Could you please give me a quick hint on how to approach this?
One possible approach is to maintain two physics worlds, one which represents the current time, and a delayed one which represents the world some time ago. For instance, your delayed world could represent the state of the world 15 seconds ago. Then, as your game runs, you maintain a queue of all the changes made to your current world and apply them to the delayed world 15 seconds later. If you ever need to roll back, say by 5 seconds, you discard the current world, make a copy of the delayed world to replace the current world, and apply all changes made between 15 seconds and 5 seconds ago to the new current world.
I’ve never written this kind of system in ECS, so I’m not sure if it’s possible to copy a physics world’s state over to a new one. But I have to imagine that it’s possible since ECS is just a collection of entities and components.
However there’s an active bug that it doesn’t work depending on your world. For me it starts failing when I add a sharedcomponents even then it only fails after hundreds of successful rollbacks.