godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.16k stars 97 forks source link

Add late physics process #6795

Open KeyboardDanni opened 1 year ago

KeyboardDanni commented 1 year ago

Describe the project you are working on

2D sidescroller

Describe the problem or limitation you are having in your project

A continuation of https://github.com/godotengine/godot/issues/19228 which oddly enough, has not seen a feature proposal here yet.

Right now Godot has several issues with things happening "one frame too late" because they aren't getting processed in the optimal order each frame. In particular, anything that should occur as a result of physics engine updates only gets a chance to run in the same frame if it's done in idle process, but doing this has the potential to make the playsim non-deterministic.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

It would be great if Godot had a "late physics process" - i.e. an update that occurs after _physics_process but before _process, and only on physics frames. This would allow things to be processed in the correct order:

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

Introduce a new function, _late_physics_process, to go alongside _physics_process and _process. Nodes would be able to implement this new function for anything that should happen after the physics engine step, but only on physics frames.

Additionally, built-in nodes like Camera and AnimationPlayer would have a new "Late Physics" process mode which would use _late_physics_process.

My only concern with this change is that it might cause extra object iteration overhead. So it might be a good idea to maintain a list of objects that opt into late physics process and only iterate through those.

If this enhancement will not be used often, can it be worked around with a few lines of script?

Workarounds exist but they're kinda iffy, and since this issue affects built-in nodes too, you can't really script around the problem for those.

Is there a reason why this should be core and not an add-on in the asset library?

Issue affects core nodes and the core engine loop, so can't really be fixed with an add-on.

KoBeWi commented 1 year ago

I think you can use process_priority for this. According to the documentation, it should also affect built-in nodes.

KeyboardDanni commented 1 year ago

My understanding is process_priority only affects the order of execution of _physics_process in relation to other nodes, but can't be used to make a node get processed after the physics engine update. For example, when using a Camera2D to track a CharacterBody2D, no matter what I set the process_priority to, the camera always lags behind the target object by a frame if the camera's process mode is set to "Physics". The issue doesn't occur if the process mode is "Idle". But I'd like to have everything use "Physics" if possible. Using "Idle" for Camera2D is probably innocuous, but having an AnimationPlayer process outside of physics could desync the playsim if any logic is dependent on the current animation frame.

smix8 commented 1 year ago

That issue that you describe is the common misconception of using physics objects for something unrelated to physics e.g. visual updates or rendering.

If Godot had a default physics tickrate of e.g. 10, people would immediately spot how wrong using physics for a camera or a visual animation is but since the default physics tickrate is 60 it goes unnoticed for long until the complains popup. Even with physics interpolation this is never really solved, it is just the wrong usage.

That late update idea will not solve your issue because physics runs at 0 - 8 ticks each rendered frame. While users will always only see the new frame rendered there is no guaranteed that anything physics related has been updated properly or updated at all in that frame when it comes to visuals.

So don't use physics_process for visual updates when you want smooth and accurate camera and animations, especially on high frame rate with a lower physics tickrate. Make your visual objects follow / react smoothly to your physics objects, not push the physics results to your visuals.

KeyboardDanni commented 1 year ago

That issue that you describe is the common misconception of using physics objects for something unrelated to physics e.g. visual updates or rendering.

Camera updates are not unrelated to physics, because if the camera position is used to handle activation/deactivation of objects, you may end up with objects spawning on different physics frames each time you play, which can (will) desync the playsim. Animation updates are not unrelated to physics because the same thing can happen. Attaching an object to a player's arm, for example, may cause said object to move to a different position on the wrong physics frame, and cause a playsim desync.

If Godot had a default physics tickrate of e.g. 10, people would immediately spot how wrong using physics for a camera or a visual animation is but since the default physics tickrate is 60 it goes unnoticed for long until the complains popup. Even with physics interpolation this is never really solved, it is just the wrong usage.

SNES, Genesis games didn't use a tickrate of 10, they used a rate of 60. Nobody uses a physics rate of 10 unless they're either targeting very old PC hardware or they're making something esoteric on purpose. Even then, it's more typical to use a tickrate of 30 or even 20.

Also, you do not want a 60 FPS camera following a 10 FPS object. That would be massively jittery.

That late update idea will not solve your issue because physics runs at 0 - 8 ticks each rendered frame. While users will always only see the new frame rendered there is no guaranteed that anything physics related has been updated properly or updated at all in that frame when it comes to visuals.

I'm not sure how this is an issue here. If physics runs twice a frame, we can just run _physics_process followed by _late_physics_process, then do the same thing again before the idle process.

So TLDR don't use physics_process for visual updates. That is the wrong use of physics_process when you want smooth and accurate camera and animations, especially on high frame rate with a lower physics tickrate. Make your visual objects follow / react smoothly to your physics objects, not push the physics results to your visuals.

If anything, the camera jitter I've seen in Godot has largely been due to 1. improper combined use of physics and idle (updating positions on physics and camera on idle), and 2. bad framepacing in Godot (which should be fixed by using physics interpolation, waiting to do at least one physics update before rendering, and using the native platform swapchain, not by papering over it with idle process).

Edit: This can also happen due to improper (or lack of) rounding of positions to integer coordinates.

RedMser commented 1 year ago

They don't literally mean 10 fps physics update. Rather, having a lower physics rate than process rate. You would have the same issue on 60 fps physics and 144 Hz rendering rate, but most people have only 60 Hz monitors and thus never run into this.

Calinou commented 1 year ago

you may end up with objects spawning on different physics frames each time you play, which can (will) desync the playsim

Godot's physics engine is not deterministic, and neither is Bullet in 3.x. While we intend to make GodotPhysics slightly more deterministic to allow for better client-side prediction, there are no plans to make it 100% deterministic in a way that would be reliable for storing input-based replays (it's recommended to store transforms instead). Therefore, I wouldn't rely on any form of "incidentical" determinism that may occur.

Also, you do not want a 60 FPS camera following a 10 FPS object. That would be massively jittery.

With physics interpolation in place, this would look just fine (and better than an interpolated 10 FPS camera following a 60 FPS object). This is especially the case when mouselook is involved with a third-person character. Games like Heretic 2 or Anachronox come to mind here, as they inherit from Quake 2's 10 Hz tickrate.

KeyboardDanni commented 1 year ago

They don't literally mean 10 fps physics update.

Oops, my bad. I do maintain that you'd have the same issue with 60 FPS at 144hz, as you mentioned.

most people have only 60 Hz monitors and thus never run into this.

Not necessarily. A few things: first, not all 60hz configurations are equal. Back in the CRT days, different resolutions all had different ideas of what 60hz was exactly. For example, 800x600 was actually closer to 61hz, from what I recall. This is still somewhat true today, though it will vary based on your display and graphics hardware. You also have the NTSC standard which is "not quite" 60hz but rather 59.94hz. Here's the list of refresh rates I get on my machine:

2023-05-03 18_57_23-Settings

Second, and probably more important, is that framepacing issues can happen even at 60hz. If the engine decides not to process a physics frame, but then does a Vsync, you end up with zero physics frames and one idle frame. Because of Vsync, you end up waiting 16ms even if the next physics frame was only going to be in 2ms. Then physics has to catch up, so next frame you get two physics frames and one idle frame. This is the single biggest cause of framepacing issues I've seen in games (that isn't related to the compositor). Workarounds exist, such as framepacing extensions that let you query timings, plus triple buffering, though the latter may come at the cost of increased input lag. Not to mention if your OS doesn't have direct scanout, it'll probably go through the compositor, which means another potential source of framepacing woes.

Godot's physics engine is not deterministic, and neither is Bullet in 3.x. While we intend to make GodotPhysics slightly more deterministic to allow for better client-side prediction, there are no plans to make it 100% deterministic in a way that would be reliable for storing input-based replays (it's recommended to store transforms instead). Therefore, I wouldn't rely on any form of "incidentical" determinism that may occur.

I believe I know what causes this (acceleration structure resulting in objects being ordered differently), which is unfortunate, but at least it's not related to timing. If there's a bit more randomness in a complex sim, so be it I guess. My big concern however is two users getting different results based solely on timing issues (different refresh rates, different compositor configuration, different OS, game hitched in a different spot for one user). I've seen how games can break when physics and idle get out of sync and it often isn't pretty.

My focus is less on the "old" input replay netcode from the Doom era. Which, you're right, it's not great. Not too long ago I tried to play a netgame on ZDoom with friends and it had Problems.

No, I'm personally more interested in stuff like precision platformers, speedrunning, that sort of thing. If a game like Celeste had issues with non-deterministic physics, I think people would pick up on that pretty quick.

With physics interpolation in place, this would look just fine (and better than an interpolated 10 FPS camera following a 60 FPS object). This is especially the case when mouselook is involved with a third-person character. Games like Heretic 2 or Anachronox come to mind here, as they inherit from Quake 2's 10 Hz tickrate.

This would probably depend on when the camera is getting repositioned. In some cases it might help, but not completely eliminate the jitter issue. But as long as the target and camera are getting repositioned on the physics tic and are both being interpolated the same way, it should be fine in theory.

For reference, here is a watered down version of what I'm currently using for the camera:

func _physics_process(_delta):
    if is_instance_valid(target):
        position.x = floor(target.position.x);
        position.y = floor(target.position.y);

If "Process Callback" is set to "Idle", it works fine. If it's set to "Physics", it lags behind by a frame. But I feel like this setup only really works as well as it does because the camera's transform is changed on the physics tic, and the idle process happens to be after the physics engine update.

Edit: I do think that there's one good exception to handling the camera on physics tic and that's mouselook. Ideally, set its position on physics and adjust rotation every display frame. Doing mouselook through physics tic and then interpolating will probably make the motion feel sluggish, since it will exhibit about half a frame of latency.

wagnerfs commented 1 year ago

Just dropping to add my 2 cents here! I noticed something after someone ran my game on a slow machine, at like, 20 FPS, all my bones logics like jiggle physics and bone look at got all wonky, after doing some research I realized the issue is because _physcis_process where the code is supposed to run, happens BEFORE the animation updates the bones chain, so by the time it reaches rendering, there will be a gap between bone positioning and structure.

Godot has a signal called frame_pre_draw from RenderingServer, which's basically a post physics/animation and right before everything is rendered, by moving my code there all the bone positioning issues stopped, so in short, godot does have a late update method you can connect, but you'd still need to limit it to the same physics_process frame rate, and also get the delta yourself as well.

So yeah, I do agree we need a proper _late_physics_process with the delta included and preferebly right before the render code for comodity instead of writing everything I mentioned by ourselves in gdscript.

KoBeWi commented 1 year ago

If you do call_deferred() in _physics_process(), it will run after everything else.

wagnerfs commented 1 year ago

If you do call_deferred() in _physics_process(), it will run after everything else.

Pretty sure that only queues the call for the next idle frame, which has nothing to do with execution order between process and animations.

KoBeWi commented 1 year ago

It's a common misconception about deferred calls. The docs were updated recently to clear that.