Skip to content

Implement semi-fixed timestep / manual stepping #236

@lawnjelly

Description

@lawnjelly

Describe the problem or limitation you are having in your project:
Godot currently only supports fixed timestep. While this is my preferred method, in most cases it requires the use of fixed timestep interpolation in order to prevent jitter due to aliasing between physics ticks and frames.

This interpolation is now supported via godotengine/godot#30226 ,
example addon here: https://github.com/lawnjelly/smoothing-addon

During the development of the above, a simpler alternative strategy was also discussed (used by default in some engines, e.g. Unreal) to overcome this same problem of physics ticks / frame synchronisation - the use of semi-fixed timestep. This can be simpler to work with, particularly for beginners and game jams, and can provide a more responsive input experience in certain circumstances.

On the other hand, semi-fixed can suffer from lack of deterministic behaviour. This can make debugging, testing and QA difficult in some types of game (hence why I personally prefer using fixed timestep interpolation). This is a trade off.

Anyway in the interests of a rounded approach to the problem I investigated semi-fixed as well as fixed.

Describe how this feature / enhancement will help you overcome this problem or limitation:
Semi-fixed time step overcomes the need to use fixed timestep interpolation. Semi-fixed timestep can be used to limit the problem of physics 'explosion' due to too high deltas, when stepping physics by frame deltas, and can also be used to lock physics ticks to frames, or the frame rate.

Show a mock up screenshots/video or a flow diagram explaining how your proposal will work:
I've already implemented semi-fixed using a hard coded path.

Semi-fixed logic is something like this:

float MAX_PHYSICS_DELTA_MS = 16.0f;

main::frame_iteration(float deltaMS)
{
// how many whole ticks
int nPhysicsTicks = floor(deltaMS / MAX_PHYSICS_DELTA_MS);
for (int n=0; n<nPhysicsTicks; n++)
    _physics_process(MAX_PHYSICS_DELTA_MS);

// always do a fractional physics tick
float leftMS = deltaMS - (nPhysicsTicks * MAX_PHYSICS_DELTA_MS);
_physics_process(leftMS);

// frame update
_process(deltaMS);
}

This is the semi-fixed timestep selectable in project settings (note that delta smoothing is not part of this PR):
timesteps

If we do decide to add semi-fixed, it is notable that it can either be hard coded (as I have done already), or implemented as a customizable callback in e.g. gdscript.

func iteration(frame_delta):
    for i in range (4):
        Engine.step_physics(0.02)
 
    Engine.step_frame(frame_delta)

A customizable approach also has the potential for a tie in to solve the issue of the desire to manually step the physics in network games, both at the server and the client:
godotengine/godot#25068

Describe implementation detail for your proposal (in code), if possible:
I've already implemented semi-fixed timestep, as a hard coded solution, selectable from project settings:
godotengine/godot#30798

Alternatively custom manual stepping can be implemented as a callback (I've already done this in another area for delta smoothing), which also has the potential to provide a mechanism to allow manual stepping for multiplayer. However this would require some investigation because although running the main iteration from a callback is feasible, multiplayer may better be accomplished by allowing stepping from within _process during the frame update, which may or may not be feasible.

If this enhancement will not be used often, can it be worked around with a few lines of script?:
No, in both cases this would need core support.

Is there a reason why this should be core and not an add-on in the asset library?:
It cannot be implemented as an add-on.

Extra
I originally wrote the PR before godot proposals, but it seems a good idea to discuss the whole area here, as there must be overall support of the idea if we are to go ahead with it (or similar).

There are 3 possible options here:

  1. Continue to only support fixed timestep
  2. Add semi-fixed hard coded as an option in addition to fixed
  3. Add customizable timestepping (possibly with a tie in for the multiplayer issue)

Probably strangely for a 'proposer', I am equally happy with any of these. It really boils down to the Godot mission statement, where we want to go with the engine - become highly focused for making single player games via a common method, or make it more adaptable. This involves trade offs, more options can bring in greater complexity and surface for bugs.

Realistically, if we did add semi-fixed I would tend towards the KISS principle, keep it simple stupid and go for the hard coded approach. I think most people for whom semi-fixed would be useful would be far more likely to use it if they simply had to switch it on in project settings, then forget about it, rather than write or copy some custom scripts.

Addendum

Just to add a little as we may get to discuss this soon:

Fixed to refresh rate

Another additional option that reduz is keen on, which is changing the fixed tick rate at runtime to match the refresh rate of the monitor.

This has some advantages - it is simple to use and does not require interpolation. On the downside, it means that game behaviour will be different on different machines, and may not play nicely with variable refresh rate monitors.

Delta smoothing

For best results with interpolation and semi fixed it can be a good idea to consider delta smoothing as an additional step. This is an attempt to compensate for the sources of error in making delta measurements to drive timesteps. This is fairly easy to implement and I got this working while I was working on the timestepping last year (both hard coded and with custom script interface), and is fairly simple to add, I didn't make a PR because I was waiting for decisions on timestepping.

Fixed timestep without interpolation offers some insulation against this problem. There are also some newer APIs in vulkan and android for improving frame timing information.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions