`delta` : when and when not to use it?

Godot Version

v4.4.1.stable.official [49a5bc7b6]

I already know the following

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)
1 Like

Thank you,
I took time to read your answer and think about that. Then, I made the following suppositions:

  • even if move_and_slide takes delta into account, we can’t just forget about delta when using move_and_slide.
  • Move move_and_slide is just multiplying velocity by delta before the translation.
  • with move_and_slide, we have to multiply accelerations (like gravity) by delta.

Are they all true ?

1 Like

Here’s a quick explanation of what multiplying by delta actually does:

  1. For the sake of this reply, 1 unit = 1 pixel in 2D or 1 meter in 3D.
  2. 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ā€.
  3. If this increment is velocity, than that implies that velocity = units per frame.
  4. 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.
  5. 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:

velocity.x = move_toward(velocity.x, move_speed, acceleration * delta)
velocity.y += gravity * delta
move_and_slide()

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:

velocity.x = move_toward(velocity.x, move_speed, acceleration * delta * 0.5)
velocity.y += gravity * delta * 0.5
move_and_slide()
velocity.x = move_toward(velocity.x, move_speed, acceleration * delta * 0.5)
velocity.y += gravity * delta * 0.5

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() before move_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.

3 Likes

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

1 Like

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.

My last reply was related to:

I was trying to explain what this schema does:

The dashed red square shows what happens between two calls to move_and_slide()

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.

1 Like

Delta time is the same before and after move_and_slide(). You could also calculate acceleration, save it to a variable, and then do something like:

var acceleration = calculate_acceleration() * delta
velocity += acceleration * 0.5
move_and_slide()
velocity += acceleration * 0.5

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.

3 Likes

I get the following (running the code in a ready function of a node 2d)

All before.
60fps
0.50833333333333
6fps
0.58333333333333

50/50 before and after.
60fps
0.5
6fps
0.5

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

It seems you are right, @ikanaut !

I would like to check if the result is the same with move_and_collide() move_and_slide()…

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 :wink:

Edit: Mixed up forward and backward Euler method

3 Likes

@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:

extends Node2D

var counter = 0
var previous_try_frames = 0
var sc_scene = preload("res://first/sc.tscn")

const END_TIME = 4
const fps_list = [6, 30, 60, 120]

func _ready() -> void:
	print(" -- capture positions at ", END_TIME, "s -- ")
	Engine.physics_ticks_per_second = fps_list[counter]

func _physics_process(delta: float) -> void:
	if Engine.get_physics_frames() - previous_try_frames == Engine.physics_ticks_per_second * END_TIME:
		get_child(0).name = "Sc"
		print("\n - ", "at ", Engine.physics_ticks_per_second, " physics fps")
		print(" ----- with move_and_slide:")
		print("position.x = ", $Sc/CharacterBody2D.position.x)
		print("position.x = ", $Sc/CharacterBody2D2.position.x, " <--before and after")
		print(" ----- with move_and_collide:")
		print("position.x = ", $Sc/MvCollide.position.x)
		print("position.x = ", $Sc/MvCollide2.position.x, " <--before and after")
		counter += 1
		if counter >= fps_list.size():
			print("\n -- END -- ")
			return
		Engine.physics_ticks_per_second = fps_list[counter]
		previous_try_frames = Engine.get_physics_frames()
		$Sc.queue_free()
		add_child(sc_scene.instantiate())

# name: CharacterBody2D
extends CharacterBody2D

@onready var acceleration = get_parent().get_meta("acceleration")

func _physics_process(delta: float) -> void:
	velocity.x += acceleration * delta
	move_and_slide()




# name: CharacterBody2D2
extends CharacterBody2D

@onready var acceleration = get_parent().get_meta("acceleration")

func _physics_process(delta: float) -> void:
	velocity.x += acceleration * delta * 0.5
	move_and_slide()
	velocity.x += acceleration * delta * 0.5




# name: MvCollide
extends CharacterBody2D

@onready var acceleration = get_parent().get_meta("acceleration")

func _physics_process(delta: float) -> void:
	velocity.x += acceleration * delta
	move_and_collide(velocity * delta)




# name: MvCollide2
extends CharacterBody2D

@onready var acceleration = get_parent().get_meta("acceleration")

func _physics_process(delta: float) -> void:
	velocity.x += acceleration * delta * 0.5
	move_and_collide(velocity * delta)
	velocity.x += acceleration * delta * 0.5

and the tree:

Sorry, I know it is not very clean code…

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.