When to use, and when to not use delta

Godot Version

v4.5.stable.mono.official [876b29033]

Question

I know this is basic question, but I’m finding straight up contradictory information about it, even on this forum.

For presentation I will use code from this tutorial (which is very nice btw, I highly recommend checking it out if you are beginner) GitHub - Una1n/Platformer2DMovementTutorial at End-Part2

What I have managed to find out:

  • Use _physics_process if you do anything with physics, use _process in every other scenario. Makes sense. But both of them provide delta.
  • You should use delta at all times, except when modifying velocity, because then it happens on it’s own. Okay, understandable.

Example code:

func handle_horizontal_movement(body: CharacterBody2D, direction: float) -> void:
	var velocity_change_speed: float = 0.0
	if body.is_on_floor():
		velocity_change_speed = ground_accel_speed if direction != 0 else ground_decel_speed
	else:
		velocity_change_speed = air_accel_speed if direction != 0 else air_decel_speed

	body.velocity.x = move_toward(body.velocity.x, direction * speed, velocity_change_speed)

Delta is not used, because we only modify velocity. Makes sense. Except I’ve found post with identical formula, and they said that actually in this case you should multiply velocity_change_speed by delta. This contradicts the previous rules.

Another example code:

func handle_gravity(body: CharacterBody2D, delta: float) -> void:
	if not body.is_on_floor():
		body.velocity.y += gravity * delta

	is_falling = body.velocity.y > 0 and not body.is_on_floor()

We again only modify velocity, but this time we do multiply by delta. The same codebase, the same operation, completely different rules.

Even when you look at automatically generated template, things are mixed up:

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()

Of course, I do not imply that everyone is wrong except me, I just want to know what are the true rules of how things work. Because it looks like a lot of people don’t understand it either, and post incorrect information as truth.

You multiply with delta whenever changing a variable by some amount each frame.
velocity += velocity_change_speed * delta needs delta if it’s in one of the _process functions.
velocity = new_velocity does not need delta because it’s just a value you’re setting the velocity to, you’re not changing it by a value.

2 Likes

I have actually noticed it not so long after posting this question. Is there no other catches included? Is it that “easy”?

As far as I know that’s it; there’s just one “oddity” I can think of: a lot of people try to do smooth camera motion with something like position = lerp(position, target_position, 0.5), which doesn’t work but position = lerp(position, target_position, delta) doesn’t work either. For this I think you actually need position = lerp(position, target_position, 1.0 - exp(delta)) for reasons I honestly don’t understand.

Usually though, yes, it is that easy

1 Like

So I will mark your answer as solution (unless someone proves you wrong, but it makes sense so far). It also means that multiplying velocity_change_speed by delta in my example is incorrect operation, because there is no +=, we are just applying new value every time.

In your example

	body.velocity.x = move_toward(body.velocity.x, direction * speed, velocity_change_speed)

you’re moving body.velocity.x towards a new value by velocity_change_speed. It’s a different syntax, but it’s still using the previous velocity to calculate the new value, so velocity_change_speed should be multiplied by delta.
Though I want to add, multiplying by delta in _physics_process() will only be relevant if you change the game’s physics ticks per second (either during development or at runtime). Otherwise it’s basically a constant of 1/60.

2 Likes

So the rule persists, but sometimes you need to check at function/method to see what it actually does.
Edit
Funnily enough I can’t find details about this function at official documentation page.

1 Like

Oh, yeah, seems the syntax is wrong in the example. I assumed it should be something like Vector2.move_toward().

This seems to be different one, since the one in example seems to return float, and take float, float, float. The one in documentation returns vector, and takes vector and float.

It is a different function, I just expected it to behave similar (just for floats instead of Vector2). The one in the example is this one:

2 Likes

This isn’t correct either. Generally you want to use move_toward, with position you are defining speed, but you can use move_toward on speed values such as velocity to define acceleration in pixels or meters per second, rather than lerp’s “percent of difference per-frame”, and lerp is always per-frame, using delta actually makes it’s frame dependence much worse.

position = position.move_toward(target_position, speed * delta)

You can learn more about lerp smoothing, and why it fails so often from this video

And if you do need a decaying value you can use a different function

func exp_decay(a, b, decay: float, delta: float):
    return b + (a - b) * exp(-decay * delta)

# still, the "decay" value is based on feels, not real units, but it's no longer per-frame
# 1 to 25 from slow to fast is reasonable.
position = exp_decay(position, target_position, 16, delta)
1 Like

If documentation says it’s delta, then I think it’s safe to assume you want delta there. Considering that people mix up those things so often it probably means that it doesn’t affect it much, but it’s better to always try to do the things the right way, even if difference is minimal. Well made game should feel better to play, even if those are just tiny details.

I think you don’t need to custom define the “exp decay” function.
Since the result is the same as the following code:

var decay = 16
position = lerp(position, target_position, 1 - exp(-decay * delta) )

The reason already in the video you pass above.

The function a = lerp(a, b, F) in fact is a = b + (a-b)(1-F)
So, let F = 1 - exp(-decay * delta)
Then the result of lerp function is totally the same as the exp_decay function.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.