I have already understood the following about delta :
it is the time (in seconds) elapsed since the last frame (physic frame in _physics_process)
when using move_and_collide it is useful in order to have the same behavior regardless of FPS
when using move_and_slide it is not needed
Question
When adding a script to a CharacterBody2D, I can choose the following model.
extends CharacterBody2D
const SPEED = 300.0
const JUMP_VELOCITY = -400.0
func _physics_process(delta: float) -> void:
# Add the gravity.
if not is_on_floor():
velocity += get_gravity() * delta
# Handle jump.
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
velocity.y = JUMP_VELOCITY
# Get the input direction and handle the movement/deceleration.
# As good practice, you should replace UI actions with custom gameplay actions.
var direction := Input.get_axis("ui_left", "ui_right")
if direction:
velocity.x = direction * SPEED
else:
velocity.x = move_toward(velocity.x, 0, SPEED)
move_and_slide()
Here move_and_slide() is used to move the body, but delta is still used at line 11: velocity += get_gravity() * delta. I am wondering why.
move_and_slide makes use of velocity in pixels per second; itās the odd-one-out function automatically taking delta into account.
Gravity is a constant, it isnāt modified by the current framerate on itās own you must calculate that along with it. Generally any value moving towards another or adding up should be multiplied by delta to produce frame-independent code.
This line for instance is frame-dependent because itās moving SPEED pixels per-frame towards zero, if your current velocity.x was very high then how fast you decelerate would be based on your framerate. The template really should use acceleration and deceleration as pixels per second
# where ACCELERATION is much higher than SPEED
velocity.x = move_toward(velocity.x, direction * SPEED, ACCELERATION * delta)
Hereās a quick explanation of what multiplying by delta actually does:
For the sake of this reply, 1 unit = 1 pixel in 2D or 1 meter in 3D.
An increment needs to be added to your nodeās position each frame so that it can move and, because it is added each frame, it always represents āunits per frameā.
If this increment is velocity, than that implies that velocity = units per frame.
Naturally, that means that the size of the increment will never change even if the frame-rate does. That is what frame-rate dependence means.
To fix it, we need to instead treat velocity as units per second and convert it into units per frame to get the actual increment. We do this by multiplying velocity by the elapsed time since the previous physics process iteration: delta.
You should never multiply anything by delta unless it is being added to something.
You also should never use delta with move_and_slide() because it already multiplies velocity by delta before adding it to the nodeās position. The one exception is acceleration (which is any time you write velocity += <something>), but thereās a catch to it. A typical script in Godot tutorials looks something like this:
The problem here is that using delta this way is actually still frame-rate dependent. You should apply 50% before and 50% after updating the position to achieve frame-rate independence, like so:
You can move the acceleration into a separate function and pass delta * 0.5 to it instead of duplicating your code. Just be aware that you should save the values of all tests like is_on_floor()beforemove_and_slide() or they may change before the second pass and cause bugs.
P.S. A trick that can come in handy is dividing by delta to convert units per frame into units per second. Iāve only used this a few times, but itās a handy thing to have in your tool belt.
Also, velocity += get_gravity() * delta is done that way because gravity is acceleration.
The example you gave is actually still frame-rate dependent because itās not doing the 50% before and after thing with gravity.
Could you explain more why one should apply acceleration half before move_and_slide and half after?
Iād argue strictly against this as delta will be the same value after move_and_slide; no time as passed while the function is processing. I cannot think of what scenario in which accounting for collisions between accelerations would greatly matter. This merely doubles your additions and makes your code harder to manage.
delta is an approximation and only of the previous framesā time, in physics steps most game engines do keep delta as consistent as possible this also keeps physics calculations consistent which may prevent anomolies such as projectiles passing through targets. If correct delta in _physics_process should almost never deviate from 0.1666 when targeting 60 fps.
Thank you for your reply, this confirm my suppositions.
However, I would also need more explanations about why we should apply acceleration half before and half after calling move_and_slideā¦
delta can change value each frame. So, with @ikanaut idea, between two calls of move_and_slide, velocity will increase of two halves of two different deltas. So maybe, it changes somethingā¦
But I still need clarifications.
No, itās still within a frame. Some people have trouble grasping that all the work you do in a game loop step is at a fixed point in time. Conceptually, time is frozen, like when youāre hand painting animation cells : together they give the illusion of movement,.but each is a frozen instant.
Itās exactly the same with a game loop and the world youāre simulating with a game.
Youāre only working on discrete steps, and so is the engine. The rest is taken care by the passage of time.
Hence, why so many simplifications are possible for real time calculations, compared to the integrations necessary for some more serious simulations.
Keep having fun,
Cheers !
Edit : Iām also curious and willing to be proven wrong in the splitting of the application of Delta calcs
This is a good explanation !
But your reply begins with āNo, ā¦ā. Itās like you are contradicting someone and as my reply comes just before, I feel like it can be myself. But as what you say is already clear to me, I donāt understand.
I see what you are saying, and it does ultimately end up using half of a frame before and half of this frame, but in the big picture what does that change, and especially for acceleration? With physics steps being fixed in mind, does half of 0.01666 plus half of 0.01666 ever change? What about the floating point loss of precision? Do we want acceleration to be a half frames behind itās perceived value?
I know it changes code structure significantly which should be a major concern until proven to provide a great benefit.
I just choose to implement it the way I suggested in my original reply because itās a little more convenient in some implementations because a lot of times you want different forms of acceleration handled differently in different situation (like move_toward() for walking vs a simple += for gravity).
The important part is that you add 50% of the acceleration before, and 50% after, updating the position. This allows it to update at a independently of the frame-rate. Itās been a while since I learned this trick⦠if I recall, the reason had something to do with acceleration being units per second per second and this technique letting it play catch-up the same way delta gives correct results over time despite its being 1-frame out of date.
In any case, I did prove its effectiveness against both updating position before the acceleration and after the acceleration at multiple frame-rates and found it was the only method that wasnāt frame-rate dependent.
You can try it for yourself with this code:
print("\nAll before.")
var delta := 1.0 / 60.0
var position := 0.0
var velocity := 0.0
for i in 60:
velocity += 1.0 * delta
position += velocity * delta
if i == 59:
print("60fps")
print(position)
delta = 1.0 / 6.0
position = 0.0
velocity = 0.0
for i in 6:
velocity += 1.0 * delta
position += velocity * delta
if i == 5:
print("6fps")
print(position)
print("\n50/50 before and after.")
delta = 1.0 / 60.0
position = 0.0
velocity = 0.0
for i in 60:
velocity += 1.0 * delta * 0.5
position += velocity * delta
velocity += 1.0 * delta * 0.5
if i == 59:
print("60fps")
print(position)
delta = 1.0 / 6.0
position = 0.0
velocity = 0.0
for i in 6:
velocity += 1.0 * delta * 0.5
position += velocity * delta
velocity += 1.0 * delta * 0.5
if i == 5:
print("6fps")
print(position)
Incidentally, updating the velocity once before moving vs once after leads to two different outcomes, both of which were incorrect, but I left that part out because nobody updates velocity after the position.
I learned about this from an old gamedev.net C++ discussion from like 2004, if I recall. Iāve found that 90% of the time, if you want the correct answer to a question, you should look for C/C++ threads made before the year 2010 or somewhere thereabout. Everything after that mostly regurgitates bad practices by Unity users.
Itās not always a big deal, but it can be a problem. Using lerp() instead of something like move_toward() is very frame-rate dependent and (believe it or not) could potentially lead to someone getting hurt IRL when applied to something that affects camera movement.
I improved the script to make it easier to try different values:
extends Node
func _ready() -> void:
try([3, 10, 30, 60, 120], 5)
func try_before(fps: float, acceleration: float = 1):
var delta := 1.0 / fps
var position := 0.0
var velocity := 0.0
for i in fps:
velocity += acceleration * delta
position += velocity * delta
print(fps, "fps: position = ", position)
func try_half_before_and_after(fps: float, acceleration: float = 1):
var delta = 1.0 / fps
var position = 0.0
var velocity = 0.0
for i in fps:
velocity += acceleration * delta * 0.5
position += velocity * delta
velocity += acceleration * delta * 0.5
print(fps, "fps: position = ", position)
func try(fps_list, acceleration: float = 1):
print("\nAll before.")
for fps in fps_list:
try_before(fps, acceleration)
print("\n50/50 before and after.")
for fps in fps_list:
try_half_before_and_after(fps, acceleration)
You can just change values of fps_list or acceleration when calling my function try().
With this script, I get the following:
All before.
3.0fps: position = 3.33333333333333
10.0fps: position = 2.75
30.0fps: position = 2.58333333333333
60.0fps: position = 2.54166666666667
120.0fps: position = 2.52083333333333
50/50 before and after.
3.0fps: position = 2.5
10.0fps: position = 2.5
30.0fps: position = 2.5
60.0fps: position = 2.5
120.0fps: position = 2.5
For anybody interested, this difference is due to a numerical error. The problem is that you are trying to approximate a second order function with a first order method.
You are always only sampling at fixed points at which you calculating the velocity and applying it backward for the last frame. The problem is, when you are accelerating, the velocity in between the current and the last frame was not constant. So by applying only the current velocity to the whole last frame you are introducing an error. Depending on the sampling interval (i.e. size of delta) this error increases and you get different positions based on the frame-rate.
To fix this, the approach suggested by @ikanaut takes the midpoint of the velocity of the current and last step. This way it is always the same, independent of the frame-rate, assuming constant acceleration.
I donāt know what move_and_slide is doing under the hood, so it might already handle this correctly.
So much for the intuitive explanation. From a numerical standpoint, when you calculate the velocity first, you are doing the first order explicit/forward Euler integration. But since you are applying it on a second order function, you introduce a first order error. There are multiple numerical approaches to efficiently manage second order integration. The kick-drift-kick method suggested is called Leapfrog integration which is a variant of Verlet integration - if you want to go down this rabbit-hole
@pmoosi I have not taken the time to deeply understand your reply yet (I will do). But I want to share something because I tried measuring the impact of different approaches on bodies position at a given time.
-- capture positions at 4s --
- at 6 physics fps
----- with move_and_slide:
position.x = 76.6666564941406
position.x = 73.4722366333008 <--before and after
----- with move_and_collide:
position.x = 76.6666564941406
position.x = 73.4722366333008 <--before and after
- at 30 physics fps
----- with move_and_slide:
position.x = 79.3333511352539
position.x = 78.6721954345703 <--before and after
----- with move_and_collide:
position.x = 79.3333511352539
position.x = 78.6721954345703 <--before and after
- at 60 physics fps
----- with move_and_slide:
position.x = 79.6666259765625
position.x = 79.3348007202148 <--before and after
----- with move_and_collide:
position.x = 79.6666259765625
position.x = 79.3348007202148 <--before and after
- at 120 physics fps
----- with move_and_slide:
position.x = 79.8334197998047
position.x = 79.6668395996094 <--before and after
----- with move_and_collide:
position.x = 79.8334197998047
position.x = 79.6668395996094 <--before and after
The difference between @ikanaut suggestion (before and after) and adding only before is not obvious in this ārealā example by comparison.
How I got this ? The easier way for me to explain would be to share the project, but I just realize that there is no way to upload zip files here⦠so here are the scripts:
I didnāt look at your code in detail, but itās interesting that there is such a major difference between 6fps and the rest. With the ābefore and afterā approach it should basically be the same for all fps with some floating point precision differences.
In any case, if you have acceleration and you use the approach where you are calculating the velocity before, you will inevitably have different final positions based on the frame-rate. How big these differences are depends on variables such as the acceleration, the duration and the actual frame-rate. It might not be relevant in your cases, but itās still something to keep in mind.
Btw., it might help to āvisualizeā the problem by taking a very simple example (e.g., speed = acceleration = 1, frame-rate-1 = 1fps, frame-rate-2 = 2fps) and calculating the position at each time-step.