Fixed timestep features coming to Entities / DOTS Physics packages

Hello – my name is Cort, and I’m one of the engineers working on the Entities package at Unity.

One feature we’ve had many requests for in DOTS is the ability to update an ECS system with a constant, frame-rate-independent timestep, similar to the .FixedUpdate() method on MonoBehaviours. In addition to being a critical requirement for stable, deterministic physics simulation, this feature has many applications in gameplay code as well. I’m pleased to report that fixed timestep system updates will be available in an upcoming Entities package release and wanted to give a sneak peek at how it will be exposed and what its initial capabilities and limitations will be.

The groundwork for this feature was laid in the Entities 0.7.0 release, when we added the UpdateCallback property to ComponentSystemGroup objects. If this callback is not set (the default), system groups update as usual. If a callback is added, the system group updates its contents in a loop until the callback returns false.

In the same release, we added the FixedRateUtils class, which contains some sample callbacks intended to be used with this feature. One of them, FixedRateUtils.FixedRateCatchUpManager, essentially configures the system to run like FixedUpdate(): when enabled, the system group temporarily overrides World.Time.ElapsedTime and World.Time.DeltaTime for the duration of its update, and then updates as many times as necessary to “catch up” to the actual elapsed time. The original ElapsedTime and DeltaTime values of World.Time are restored just before the system group update ends.

In an upcoming release (tentatively Entities 0.12.0-preview), we’ll add a new system group that uses this catch-up feature by default. A new ComponentSystemGroup will be added to default DOTS Worlds (tentatively called the FixedStepSimulationSystemGroup). It will update at the beginning of the SimulationSystemGroup (after the BeginSimulationEntityCommandBufferSystem, but before any other systems in the group). It will use FixedRateCatchUpManager, with a default timestep of 1/60th of a second. It will contain begin/end ECB systems so that systems in the group can instantiate/destroy entities and make other structural changes.

In a subsequent release of the DOTS physics packages, physics systems will be modified to update in the new FixedStepSimulationSystemGroup instead of the SimulationSystemGroup. This may require application changes: any user systems that currently order themselves relative to the physics systems will need to update their ordering constraints and/or their system groups.

DOTS Physics systems will refer to World.Time.DeltaTime in their simulation code, instead of UnityEngine.Time.fixedDeltaTime. This puts the timestep used by the physics system completely under the control of its parent system, and breaks the direct dependency on UnityEngine.Time.

If you have any questions about the plan, let me know in the comments. Here’s a few FAQs:

Q: Can I override the default fixed timestep value?
A: Yes, the timestep is a property on the FixedStepSimulationSystemGroup. Changing this property has the same effect for the group as changing UnityEngine.Time.fixedDeltaTime does for MonoBehaviour fixed updates: the next fixed-timestep group update will occur at LastUpdateTime + newTimestep.

Q: What systems will update in the fixed-timestep system group by default?
A: Currently, only the physics systems will move into the fixed-timestep system group. This will happen in a release of Unity Physics following the Entities release where the new group is available.

Notably, the TransformSystemGroup (which is responsible for computing LocalToWorld/LocalToParent matrices and adding/removing Parent/Child relationships) will continue to update in the main SimulationSystemGroup. Systems inside the fixed-timestep system group should not rely on these Transform components having been updated by the next fixed update because the next fixed update might be a catch-up update in the same frame!

For example, if inside the fixed-timestep system group, you add a Parent component to make entity B the parent of entity A, you should not expect entity B to have the Child component by the next fixed update. The Child component is added by the TransformSystemGroup, which is only updated after the fixed-timestep system group has finished all of its updates for the current display frame.

Dynamic rigid bodies affected by physics should present no problems since they read from and write directly to Translation and Rotation components of root-level entities.

Q: Does the FixedStepSimulationSystemGroup support anything like the RigidBodyInterpolation feature on RigidBody components, to provide smoother motion at low simulation tick rates by interpolating/extrapolating the transforms of simulated objects based on previous fixed-timestep results?
A: No. We do not support this feature. Systems running after the fixed-timestep system group (including the rest of the variable-timestep simulation group and the presentation group) will only see the results of the most recent fixed-timestep system group update. If the display frame rate is higher than the fixed-timestep simulation rate, the motion of simulated objects may appear jerky.

The simplest workaround for this issue is to keep the simulation rate and the display rate as close as possible, either by artificially capping the display rate, or manually decreasing the fixed timestep value.
In some cases, applications may want to disable the “catch-up” semantics of the fixed timestep group entirely, and have it update exactly once per display frame; this is an simple change to make during application bootstrap time. The fixed timestep system group can either continue to use a constant deltaTime, or it can fall back on the default, frame-rate-dependent deltaTime. Each approach has pros and cons for the application to consider:

Fixed Timestep with catch-up
PRO: Simulation determinism. Simulation stability. Matches familiar FixedUpdate() behavior. Simulation time tracks “real” elapsed time.
CON: Jerky motion if display rate > simulation rate. Less predictable overall frame rate due to normal frame time fluctuation (even with a ~60fps display rate, the fixed timestep group will update zero times in some frames, twice in others). Running the simulation >1 times per frame can become expensive.

Fixed Timestep without catch-up
PRO: Simulation stability. Predictable frame times (exactly one simulation tick per frame). No jerkiness at high display frame rates.
CON: Simulation rate is locked to display rate; large changes in the display rate will cause the simulation to run correspondingly faster / slower. Simulation’s elapsed time will drift from “real” elapsed time. This approach is best when a stable frame rate can be guaranteed.

Variable Timestep (fixed timestep disabled)
PRO: Predictable frame times (exactly one simulation tick per frame). No jerkiness at high display rates. Simulation time tracks “real” elapsed time.
CON: Potential simulation instability (frame rate spikes → large deltaTime). Non-deterministic simulation.

Q: Can I read / process user input in fixed updates?
A: Yes, if proper care is taken to make sure that input events are applied to the correct simulation tick(s).
Polling an input device returns events that occurred during a particular window of “real” time (since the last poll occured). Without fixed timesteps, this is the same window of time the application is about to simulate, so input events can be consumed immediately. When fixed timesteps are enabled, however, the simulation window no longer necessarily corresponds with the input-polling window; without proper care, input events may be processed multiple times, or processed too early, or ignored entirely. Developers who have worked with input processing in MonoBehaviour.FixedUpdate() may be very familiar with these challenges.

To avoid these problems, input events must be timestamped and bucketed by which simulation frame they should apply to. These events may need to be buffered across multiple display frames until the fixed-timestep system group processes the appropriate simulation tick.

We plan to release sample scenes in the Entities package that demonstrate correct input processing when fixed timesteps are enabled.

28 Likes

Hi. I implemented my own FixedSimulationGroup which is injected to FixedUpdate of PlayerLoop.

Is there any reason for that Unity not using just existing FixedUpdate loop? I think that it is more simple and can easily be integrated with existing PhysX based components (hybrid).

1 Like

Sounds fantastic, thanks for update.

That’s a fair question; certainly the easiest way to get “familiar FixedUpdate()-style behavior” would be to just tick ECS systems from FixedUpdate().

One requirement that pushed us towards the current solution was the need to support the feature in DOTS Runtime / Project Tiny, where there is no UnityEngine functionality to lean on.

We also wanted more flexibility than the existing FixedUpdate() functionality offers. The FixedStepSimulationSystemGroup is meant to provide a reasonable default, but we also wanted the ability for applications to customize (or disable) the fixed timestep system group, or to run the whole simulation with a fixed timestep (including the physics sim), or things we haven’t thought of yet.

Putting the fixed timestep mechanism into the Entities package and under application control addressed both requirements.

To address your use case of interacting with PhysX objects in DOTS, I can’t make any specific promises, but we do have some improvements to the DOTS player loop manipulation code in mind that would make it easier for applications to configure which system groups are added to the player loop (and where).

5 Likes

Seems like we will need to choose between:

  1. Jerky motions to have proper frame-rate independent simulation
  2. Frame-rate dependent simulation to not have jerky motions

Isn’t it possible to have something that supports something like Rigidbody Interpolation at all?

1 Like

I think that’s a physics implementation detail and thus not subject of this fixed timestep system. The physics systems probably will, or already do that.

My question is, why the odd default? I mean the built-in PhysX system runs on 1/50. Is there any consideration behind it? Or do you expect to be the 60FPS the standard and don’t expect that the physics would handle the interpolation? (Well, maybe the above question isn’t that clean cut after all?)

Would it be possible to define our own fixed-step simulations (multiple) on different fixed time-step? Or it is not the plan at first?

It doesn’t seem like as previously stated:

But I may have misunderstood the situation.

@ is correct that an interpolation feature is beyond the scope of the Entities-related changes here, and might be better exposed as a feature of the physics package.

@brunocoimbra is also correct, the DOTS Physics packages do not support this feature yet. It’s definitely on their radar and being discussed, but once again, I can’t make any specific promises about features or timelines on that front.

I have seen reports (e.g. this tweet) that nobody at Unity can remember why the default fixed-update rate is 50 Hz, and can confirm that this has been my experience as well. The most plausible (but still unsubstantiated) theory I’ve heard is that it was originally targeting the PAL 50Hz video standard. Literally everybody we consulted internally agreed that a 60Hz default would make more sense in 2020. Most of them were also in favor of changing the MonoBehaviour.FixedUpdate() default to 60Hz as well, but agreed that it would cause too many breaking changes in existing projects. Fortunately…

…yes, you can change the timestep value of the group with a single line of code (something like group.Timestep = 1.0 / 50.0). You can also define your own fixed-timestep system groups, or customize/disable the one we provide by default; the core feature is available to any ComponentSystemGroup.

Having multiple fixed-timestep groups running at different rates in the same frame is possible, but tricky; if implemented naively, they won’t interleave their updates the way you might expect. This could potentially cause problems if the outputs of one group feed into the inputs of the other. The application could implement proper interleaving behavior in a more complex FixedRateManager; in theory you could attach an individual Timestep and LastUpdateTime on every system in a group, throw them into a priority queue, and figure out exactly which systems need to tick (and in which order) for a given “real” elapsed deltaTime. But currently that’s left as an exercise for the reader.

8 Likes

In other words, all components used in building physics world and being exported as result will be updated from “catch up” to “catch up” update?

Does this still apply if the one is using manual InputSystem update? If I remember correctly, somewhere on forum has been mentioned that update provides an access to a buffer of all the input that occurred since the last call, so having that in mind it sounds that there should not be an issue, unless someone desires to have “less precise” update relaying on frame-rate rather then real-time update.

In any case, rigidbody interpolation is a fairly simple feature to implement:

  • remember current & previous pos/rot of the transform at every fixed update

  • at every variable update:

  • float t = timeSinceLastFixedUpdate / fixedTimeStep;

  • pos = math.lerp(prevFixedUpdatePos, currentFixedUpdatePos, t)

  • rot = quaternion.slerp(prevFixedUpdateRot, currentFixedUpdateRot, t)

Or something like that. Someone could probably release a simple system that does that soon after the fixedUpdate feature lands

5 Likes

Another possible decision factor is that you can’t have exactly 60 Hz. Timestep would be 0.01666667, which translates to 59.99999 Hz. Physics must be as deterministic as possible, and you can achieve exactly 50 Hz with timestep = 0.02.

Not surprisingly, many monitors with “60 Hz” refresh rates are actually 59.94 Hz and sometimes even plainly displayed as 59 Hz. I’d be against setting the physics default rate to 60 Hz because calculations won’t be as precise as with 50 Hz, and in practice you’ll never have a 1:1 correspondence with the monitor refresh rate. For example, think that just drifting one frame out of those 60 frame per second mean one visual stutter per second.

In my opinion a correct solution forcefully requires interpolation. Choose a timestep that doesn’t involve periodic decimals for precise calculations, and let the interpolation provide the visually smooth results.

4 Likes

Interesting approach! Thanks for the update on this, and, despite what I am saying below, I like the approach and transparency.

I do admit that it seems a lot more complicated than before (pre-DOTS), though.

And of the options you list, I will basically have to pick the last one. Frame rate affecting simulation speed is an absolute no-go and feels like a throwback to old game design that’s really, really bad (like old racing games that are so difficult to play at current framerates), while jerykness doesn’t seem ideal either.

That said, having the option is good, because the option means entirely different use caes will work. Single player games that probably don’t care for determinism (as there’s no need to sync anything) will probably default to the third option, won’t they? Or am I misunderstanding something here? I’d love to hear if my logic has a huge flaw!

I can’t imagine a single scenario in a single-player game where I would even care for determinism, especially if the cost for it is “frame rate affects simulation speed”.

  • We have separated our server / client logic into a Server world and a Client world.
  • A game client will be able to run both worlds simultaneously.
  • Each world would preferrably be able to have individual fixed frame rates (server update rate can be much lower)

Would this be possible with the planned approach? The FixedStepSimulationSystemGroup would have be non-static for that to be supported, is that the case? Or is it a static field, used for all instances of FixedStepSimulationSystemGroup?

please don’t do that ,in order to fixedupdate or separate simulation and presentation ,I just need to set world.quite update to true ,and update the three group :initialize,simulation,present my self。Actually,I have made a game use DOTS this way ,and it worked fine.

    public void Update()
    {
        if (!LoadComplete)
            throw new Exception("BattleWorld has not been Initialized ,check value: BattleWorld." + nameof(LoadComplete));

        m_AccumulateTime += UnityEngine.Time.deltaTime;
        m_World.QuitUpdate = false;


        while (m_AccumulateTime > ConstantsOfWorld.FixedDeltaTime)
        {
            m_InitializationSystemGroup.Update();
            m_SimulationSystemGroup.Update();
            CheckEnd();
            m_AccumulateTime -= ConstantsOfWorld.FixedDeltaTime;
        }


        m_World.GetExistingSystem<TransformPresentationSystem>().DeltaTime = m_AccumulateTime;
        m_PresentationSystemGroup.Update();
        m_World.QuitUpdate = true;
    }
1 Like

why your guys spend time do the meaningless thing,when do you fixed the world memory leak problem. until now,there is even not exist a simple way to convert array of prefabs to entity prefabs… :face_with_spiral_eyes:

You can disable the fixed timestep “looped update” behavior on the FixedStepSimulationSystemGroup with a single line of code, at which point it will behave just like a regular ComponentSystemGroup. The manual init/sim update approach you describe would still work at that point.

1 Like

The FixedStepSimulationSystemGroup is instantiated as part of the default World initialization. If you’re using default world initialization on both the client and server Worlds, you’ll get a unique instance of the fixed timestep group in each World. If not, it should be straightforward to instantiate the group in custom Worlds, or create your own group with similar update semantics. The FixedStepSimulationSystemGroup doesn’t rely on any special internal magic to implement, only public Entities features; it’s 45 lines of new code in DefaultWorld.cs, most of which are comments :slight_smile:

All fixed-timestep-related state (last update time, timestep value, etc.) in the group is stored per system instance; it’s not static.

1 Like

Except 0.2 isn’t really a precise floating point number, either. The literal 0.2f will actually be 0.200000003 in single precision IEEE754, aka float.

1 Like

What about 0.02f?

Ooops, good find, same problem, but the error is outside the Mantissa limit, so it doesn’t really show up in float.

1 Like