godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.13k stars 86 forks source link

Option for physics update every frame. #10015

Open jitspoe opened 3 months ago

jitspoe commented 3 months ago

Describe the project you are working on

Retro FPS with lots of enemies active at once.

Describe the problem or limitation you are having in your project

Updating 100+ enemies every physics tick has huge performance issues, especially if physics update multiple times per frame. See: https://github.com/godotengine/godot/issues/93184

It would be good to stagger the physics updates of things across multiple frames. For example, 5 enemy updates per frame, prioritized by distance, velocity, etc.

The problem is, the physics update at a set rate, which means if I have the physics updating at 60 hz and the user is running at 120hz, they'll only update enemies every other frame, and if players are running at 30fps, they'll be doing 10 enemy updates per frame instead of 5, which further hurts performance on a machine that's already struggling.

Right now the _physics_process() stuff is all or nothing. Either everything updates every physics update, or nothing does.

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

Having a guaranteed physics update (or an update while the engine is in physics mode, _in_physics = true) will allow me to do staggered updates in a controlled manner such that the workload for large numbers of enemies is spread across multiple frames.

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

What I want is an (optional) physics update call that happens every frame. This could possibly be in addition to the regular _physics_process() or an option to make _physics_process() run every frame instead of at a fixed rate.

So currently we have something in the main loop like:

physics_step = physics_ticks_per_second

for 0 to physics_steps:
    in_physics = true
    physics_process(physics_step)
    in_physics = false

process(delta)

What I'm proposing is something like:

physics_step = physics_ticks_per_second

for 0 to physics_steps:
    in_physics = true
    physics_process(physics_step)
    in_physics = false

in_physics = true
physics_frame_process(delta)
in_physics = false
process(delta)

This would add a new function that executed every frame prior to animations and such that could optionally be used instead of the _physics_process on scripts where I want to control things at varying rates.

Or:

if !fixed_physics:
    in_physics = true
    physics_process(delta)
    in_physics = false
else:
    physics_step = physics_ticks_per_second
    for 0 to physics_steps:
        in_physics = true
        physics_process(physics_step)
        in_physics = false

process(delta)

This would add an option to allow the physics to update every single frame at whatever the framerate is at.

This is very similar to the semi-fixed timestep proposal: https://github.com/godotengine/godot-proposals/issues/236

It's also similar to the late physics process: https://github.com/godotengine/godot-proposals/issues/6795

And relevant to the swarm/mob proposal: https://github.com/godotengine/godot-proposals/issues/2380

And the different physics update rates at the same time: https://github.com/godotengine/godot-proposals/issues/439

Edit: Also found this issue which probably more clearly states the issues but was in the main repo, not the proposals, so it got closed: https://github.com/godotengine/godot/issues/24769

So maybe it's not entirely necessary as a separate proposal, but I do want to have some way to guarantee an update while the engine is in physics mode every frame. The semi-fixed one seems like it would do 2 physics updates per frame, but maybe if the physics tick rate was set to 20hz or something, that would mean only one update per frame. Likely resolving one of these proposals would also address a handful of others.

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

It's possible an autoload could handle this in _process(), but I'm not sure if that is the appropriate place to do things like move enemies around, and ideally this would happen before the animations play so there's not a delay in feedback. Could be possible, though.

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

This needs to be in the core loop of the engine in order to do per-frame updates in the physics.

KoBeWi commented 3 months ago

I once used this pattern in my project:

extends Node

var should_emit: bool
var timer: float

signal lazy_process

func _process(delta: float) -> void:
    timer += delta

    if should_emit:
        lazy_process.emit(timer)
        should_emit = false
        timer = 0

func _physics_process(delta: float) -> void:
    should_emit = true

Called it "lazy process". It allows to write logic in process frame, but synchronized with physics frame. Simply connect to the signal to use it. The timer acts as delta parameter.

jitspoe commented 3 months ago

I once used this pattern in my project:

I think this is basically doing the opposite of what I want. This syncs up so process stuff only executes every physics frame. I want physics to execute every process frame.

marcospb19 commented 2 months ago

My 2 cents as a beginner:

1-update-per-frame seems to be simpler to reason about when learning the engine, I'm puzzled thinking about jittering, interpolation and smoothing add-ons instead of just thinking about my "game".


Expanding on your idea:

If you have the option for 1-update-per-frame, what about a lower and upper bound, for minimum and maximum physics FPS regardless of your FPS?

Without flooding your proposal, here is my ideal solution (a superset of what I described).
The 1:1 frame-to-update ratio isn't the best for every project, some might want 1:2, 1:4, 1:8, or the other way around, 2:1, 4:1 and 8:1. However, desired ratios usually change based on framerate. To achieve such granular control, I suggest mapping ranges of framerates. Here is an example, picture a lightweight FPS game for high refresh rate: 1. 0- 30 FPS maps to 30- 30 ups (fixed minimum). 2. 30-120 FPS maps to 30-120 ups (1:1 growth ratio). 3. 120-240 FPS maps to 120-180 ups (after 120, start skipping updates to save unnecessary processing). 4. 240-INF FPS maps to 180-180 ups (fixed maximum) Those values are a bit arbitrary, the point is: you have a LOT of control here. Why not just use a curve then? Well, I find curves to be a bit unpredictable :v.
JoanPotatoes2021 commented 2 months ago

I don't understand this issue 100%, if you want physics to happen at every process frame, can't you just use the _process() function for that? It will execute code at every process frame, or if you want the physics to happen at every process frame, isn't the matter of adapting the physics frames per second to the maximum refresh rate of the user's monitor?

The only problem I see is the user with a varying framerate without Vsync and you wanting to execute physics per process frame, but then again having the physics frame per second capped to the user's max monitor refresh rate would solve this,

Updating 100+ enemies every physics tick has huge performance issues, especially if physics update multiple times per frame

This to me doesn't make sense, only if your frames per second is < physics framerate, but if perfomance is the issue and you want to update enemies at different intervals, we could optmize by allowing objects to skip processing entirely, this allows you to update them at different intervals inside the physics processing function, something like this:

var update_rate:int = randi_range(1,9)
var update_i:int = update_rate
func _physics_process(delta: float) -> void:
    if update_i > 0: update_i -= 1; return;
    update_i = update_rate
    # Execute Physics or related code.
Calinou commented 2 months ago

This should already be achievable by setting Physics Ticks per Second to 1000 and Max Physics Steps per Frame to 1 in the Project Settings (or 2 if you want 2 iterations per frame). However, slowdown will occur if the physics tick rate can't be met, so there would need to be a setting to disable this behavior.

You can set Physics Ticks per Second above 1,000 with a script if needed.

jitspoe commented 2 months ago

I don't understand this issue 100%, if you want physics to happen at every process frame, can't you just use the _process() function for that? It will execute code at every process frame, or if you want the physics to happen at every process frame, isn't the matter of adapting the physics frames per second to the maximum refresh rate of the user's monitor?

That's effectively what I want, but the _physics_process() and _process() happen at different times in the engine update. _process() happens after things like animation, which is useful to do things like post-anim bone corrections and such, but it means if you put your gameplay logic in _process() all the feedback is going to be delayed by an extra frame. I want the equivalent of _process() that gets called when the _physics_process() does. I think there are some other things that are bad to do outside of the physics frame as well, like moving physics bodies and such.

This should already be achievable by setting Physics Ticks per Second to 1000 and Max Physics Steps per Frame to 1 in the Project Settings (or 2 if you want 2 iterations per frame). However, slowdown will occur if the physics tick rate can't be met, so there would need to be a setting to disable this behavior.

This just makes the game run in slow motion. If you don't meet the physics framerate, the game slows down to compensate.

Also, I think it might be good to have a set physics tick rate for things like rigid body simulations, so we have consistency, but the per-frame physics update would be called every frame in addition to that, so you could optionally do additional per-frame calculations and stagger them in your own way.

So something like:

_physics_process(physics_delta) - called every physics update, same as it is now. Could be called 0 times in a frame, or 8+ times in a frame, depending on settings and circumstances. _physics_process_every_frame(frame_delta) - Like _process(), it is called every frame, but while in the physics update stage of the engine. Once per frame every frame. _process(frame_delta) - Called every frame after most of the other stuff has happened, (unchanged).

This would allow:

One drawback I can see is that certain things, like move_and_slide() and is_input_just_pressed() are tied specifically to if they're updating in the physics or process updates, and this new function would be ambiguous.