24,797 total views, 4 views today
Unity doesn’t have built-in support for rewinding particle systems from an initial offset time. Particles systems will ignore attempts to set any negative simulation values and clamp all properties to 0. We can add support for rewinding particle effects that play in real time through a custom C# script.
See below for some some particle effects that were reversed using a similar script to the one we’ll be working on.
[IMAGE REMOVED]
Let’s begin! Create a new script and call it RewindParticleSystem.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class RewindParticleSystem : MonoBehaviour { // Use this for initialization void Start () { } // Update is called once per frame void Update () { } }
We need to be able to apply the effects of this script to all particle systems in the hierarchy of the game object it’s attached to.
ParticleSystem[] particleSystems;
We also need to track the individual simulation times for all systems (since each one may have a different simulation speed set in the editor).
float[] simulationTimes;
Because we’re working backwards, we’ll need an offset start time. I’ve also thrown in an additional simulation speed scale value which we’ll apply on top of any system’s own simulation speed because a single effect may have many children (and children of children), which can be annoying to work with individually. This way, we can scale the speed for the entire effect if we just have this script on the root parent particle system.
public float startTime = 2.0f; public float simulationSpeedScale = 1.0f;
Let’s initialize some of these in a new function. I first grab all the particle systems in the hierarchy, which includes the component attached to the same object as this script (at index 0). The false indicates I want to ignore inactive instances in the hierarchy. This parameter is optional and defaults to false, but I sometimes prefer to be explicit for clarity. Next, I initialize the simulationTimes array to the number of particle systems.
void Initialize() { particleSystems = GetComponentsInChildren<ParticleSystem>(false); simulationTimes = new float[particleSystems.Length]; }
Replace Start with OnEnable.
This is because we want to be able to restart the simulation if the object is disabled and enabled again. We start off by calling Initialize if particleSystems is null (which it will be the first time).
I also want all the simulation times reset if OnEnable is called, so I loop through and set the value to 0.
Finally, I simulate the particle systems forward to the startTime value by calling Simulate on the root particle system object and passing in true for the first bool parameter which indicates that I also want to include any children for the simulation. The second bool is false to indicate that I don’t want to restart the particle system, and third bool is true to indicate that I want to use a fixed time step. I’ll explain why using a fixed time step is important a bit later.
void OnEnable() { if (particleSystems == null) { Initialize(); } for (int i = 0; i < simulationTimes.Length; i++) { simulationTimes[i] = 0.0f; } particleSystems[0].Simulate(startTime, true, false, true); }
We can now move to Update, where we’ll handle each system in a for-loop. Notice that the loop iterates in reverse order. This is so sub-emitters will simulate correctly with their parent system.
void Update() { particleSystems[0].Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); for (int i = particleSystems.Length - 1; i >= 0; i--) { float deltaTime = particleSystems[i].main.useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime; simulationTimes[i] += (-deltaTime * particleSystems[i].main.simulationSpeed) * simulationSpeedScale; float currentSimulationTime = startTime + simulationTimes[i]; particleSystems[i].Simulate(currentSimulationTime, false, false, true); } }
Let’s go over each part of the loop, starting with the statement below.
I’m essentially forcing the entire particle effect to stop simulating by using the root object at index 0 and passing true for the withChildren parameter. I also want to make sure to clear all existing particles, not just stop their emission since I’ll be calling Simulate with true for the restart parameter later. I could’ve put this statement in the loop and instead used the current loop index with the boolean parameter set to false, but that would mean calling the same function multiple times rather than simply using it once like we’re doing now.
particleSystems[0].Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
Here I’m checking if the current iteration’s particle system is using unscaled time and then selecting the appropriate delta time to use.
float deltaTime = particleSystems[i].main.useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime;
Next, I’m moving the simulation time a step “forward” (actually backwards) based on the delta time scaled by the particle system’s simulation speed, which is scaled again by the “global” simulation speed scale.
simulationTimes[i] -= (deltaTime * particleSystems[i].main.simulationSpeed) * simulationSpeedScale;
By adding this value to the start time, I get the target simulation time of this sytem, which I can then pass in to Simulate. The first bool is false because I’m going to loop over every system in the hierarchy anyway, so simulating with children would be a waste. The second parameter is also false since I’m already stopping and clearing all particle systems, which makes passing in true for restart being redundant. The last parameter being true is important, as we want to explictly use fixed delta time. Remember that intermediate parts of a simulation may not have been calculated with the same time.deltaTime if the frame rate is variable. Using a fixed delta time ensures a more deterministic render for the particle system.
float currentSimulationTime = startTime + simulationTimes[i]; particleSystems[i].Simulate(currentSimulationTime, false, false, true);
The above code will reverse the simulation, but it’s going to be all sorts of freaky…
[IMAGE REMOVED]
Don’t worry, we can fix this. The reason everything appears chaotic is because the random seed changes every time we call Simulate. We just need to turn off automatic seed randomization.
And that’s it, really.
But, what if you want to keep it on? Actually, we can keep track of whatever this value is set to in the editor, temporarily disable it, and then restore it to true if that’s what it was before being disabled. This allows the particle system to remain randomized while not causing any issues during rewind.
We just need to modify what happens inside the for-loop by adding in a bool that stores the current system’s original useAutoRandomSeed value while we disable it. See the highlighted lines below for the additions.
for (int i = particleSystems.Length - 1; i >= 0; i--) { bool useAutoRandomSeed = particleSystems[i].useAutoRandomSeed; particleSystems[i].useAutoRandomSeed = false; particleSystems[i].Play(false); float deltaTime = particleSystems[i].main.useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime; simulationTimes[i] -= (deltaTime * particleSystems[i].main.simulationSpeed) * simulationSpeedScale; float currentSimulationTime = startTime + simulationTimes[i]; particleSystems[i].Simulate(currentSimulationTime, false, false, true); particleSystems[i].useAutoRandomSeed = useAutoRandomSeed; }
If you want to support a particle system’s stop actions, you can throw this into the end of the loop. We first force the system to unpause by calling play, and then fully stop and clear it to trigger the stop action assigned.
if (currentSimulationTime < 0.0f) { particleSystems[i].Play(false); particleSystems[i].Stop(false, ParticleSystemStopBehavior.StopEmittingAndClear); }
The final script will look something like this.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class ParticleSystemReverseSimulationSuperSimple : MonoBehaviour { ParticleSystem[] particleSystems; float[] simulationTimes; public float startTime = 2.0f; public float simulationSpeedScale = 1.0f; void Initialize() { particleSystems = GetComponentsInChildren<ParticleSystem>(false); simulationTimes = new float[particleSystems.Length]; } void OnEnable() { if (particleSystems == null) { Initialize(); } for (int i = 0; i < simulationTimes.Length; i++) { simulationTimes[i] = 0.0f; } particleSystems[0].Simulate(startTime, true, false, true); } void Update() { particleSystems[0].Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); for (int i = particleSystems.Length - 1; i >= 0; i--) { bool useAutoRandomSeed = particleSystems[i].useAutoRandomSeed; particleSystems[i].useAutoRandomSeed = false; particleSystems[i].Play(false); float deltaTime = particleSystems[i].main.useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime; simulationTimes[i] -= (deltaTime * particleSystems[i].main.simulationSpeed) * simulationSpeedScale; float currentSimulationTime = startTime + simulationTimes[i]; particleSystems[i].Simulate(currentSimulationTime, false, false, true); particleSystems[i].useAutoRandomSeed = useAutoRandomSeed; if (currentSimulationTime < 0.0f) { particleSystems[i].Play(false); particleSystems[i].Stop(false, ParticleSystemStopBehavior.StopEmittingAndClear); } } } }
Simply attach this to any game object with a particle system (or particle systems as children) and you’re good to go.
[IMAGE REMOVED]
Happy particle reversing!
Note: All the effects in the previews are prefabs from Ultimate VFX, an asset I made which is available on the Unity Asset Store. You can check it out below through the affiliate link.
Hi Mirza,
First up thanks for posting this and thanks generally for all your tutorials and code-sharing!
We’re trying to use the above rewinding method on particle systems that are attached to objects moving in space that are also being rewound in time.
In other words, we have a gameobject that leaves a trail of particles behind it, and we can rewind time in the game to move the transform of the gameobject back along the path it has just taken. We’d like the particle trail it has left to rewind also, sucking back into the gameobject as it regresses back through time.
When we use the above, the particles just resimulate each frame attached to the object, moving with it as though they are being simualted in Local space (even though they are set to World for leaving the trail). Should the above method work with moving objects and worldspace simulation? Perhaps we’ve just not got thigns set up right.