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

@pmoosi , Thank you very much for your explanations !

This is very illuminating:

However, there are some details that I don’t completely understand:

  • What is the relation with integration ?
  • What is the relation with solving a differential equation ? How this can help us ?

This is how they (mathematically) relate. The velocity is the derivative (i.e., the rate of change) of the position and the acceleration of the velocity.
To get the position function based on the velocity function, you need to solve the integral. And this is exactly what you are numerically doing, in ‘real-time’, at fixed steps. If the velocity is constant, then the position is linear and you can simply add velocity * dlt every time step to get the exact solution (the Euler method). However, when your introduce constant acceleration you introduce an error. To get the exact solution you have to solve from the acceleration to the velocity to get the correct position function.
You don’t have to think of it like this in these terms (I usually don’t), but it can help make problems clearer. Numerical analysis is also a well studied field so there are a lot of solutions for problems (e.g., the second order integration), that you might not find if you don’t search for it with the right terms - although in times of llms this might not be that big a problem anymore.

1 Like

I made interesting observations !
@pmoosi, you gave me an idea: Using the equations of motion I learned at school. So I wrote the following script:

extends CharacterBody2D

var t = 0
var v_0 = Vector2.ZERO
var pos_0 = position

@onready var acceleration = Vector2.RIGHT * get_parent().get_meta("acceleration")

func _physics_process(delta: float) -> void:
	t += delta
	var new_position : Vector2 = (acceleration * t ** 2) / 2 + v_0 * t + pos_0
	velocity = new_position - position
	move_and_collide(velocity)

and an equivalent with move_and_slide:

extends CharacterBody2D

var t = 0
var v_0 = Vector2.ZERO
var pos_0 = position

@onready var acceleration = Vector2.RIGHT * get_parent().get_meta("acceleration")

func _physics_process(delta: float) -> void:
	t += delta
	var new_position : Vector2 = (acceleration * t ** 2) / 2 + v_0 * t + pos_0
	velocity = new_position - position
	velocity /= delta
	move_and_slide()

Then, I made measures of position (at different frame rates) with the following script (which is an improvement of the one I shared earlier: `delta` : when and when not to use it? - #19 by rom1dev):

extends Node2D

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

const END_TIME = 5
var t = 0
const fps_list = [6, 20, 30, 40, 60, 70, 100, 120]

func _ready() -> void:
	print("import matplotlib.pyplot as plt\n\n", "fps_list = ", fps_list,
"""

JTB_mvs = []
BNA_mvs = []
PHY_mvs = []

JTB_mvc = []
BNA_mvc = []
PHY_mvc = []

JTB_mvs_position_X = 0
BNA_mvs_position_X = 0
PHY_mvs_position_X = 0

JTB_mvc_position_X = 0
BNA_mvc_position_X = 0
PHY_mvc_position_X = 0

def add_to_lists():
	JTB_mvs.append(JTB_mvs_position_X)
	BNA_mvs.append(BNA_mvs_position_X)
	PHY_mvs.append(PHY_mvs_position_X)

	JTB_mvc.append(JTB_mvc_position_X)
	BNA_mvc.append(BNA_mvc_position_X)
	PHY_mvc.append(PHY_mvc_position_X)


"""
	)
	print("expected_position_X = ", 0.5 * $Sc/MvCollideOwnew.acceleration.x * END_TIME ** 2)
	print("# -- capture positions at ", END_TIME, "s -- ")
	print("end_time = ", END_TIME)
	Engine.physics_ticks_per_second = fps_list[counter]

func _physics_process(delta: float) -> void:
	t += delta
	if t >= END_TIME or Engine.get_physics_frames() - previous_try_frames == Engine.physics_ticks_per_second * END_TIME:
		get_child(0).name = "Sc"
		print("\n\n#time = ", t, "s impressicion: ", t - END_TIME)
		print("# ", "at ", Engine.physics_ticks_per_second, " physics fps")
		print("\n# ----- with move_and_slide:")
		print("JTB_mvs_position_X = ", $Sc/CharacterBody2D.position.x, " # just before")
		print("BNA_mvs_position_X = ", $Sc/CharacterBody2D2.position.x, " # before and after")
		print("PHY_mvs_position_X = ", $Sc/MvSlideOwnew.position.x, " # physic equation")
		print("\n# ----- with move_and_collide:")
		print("JTB_mvc_position_X = ", $Sc/MvCollide.position.x, " # just before")
		print("BNA_mvc_position_X = ", $Sc/MvCollide2.position.x, " # before and after")
		print("PHY_mvc_position_X = ", $Sc/MvCollideOwnew.position.x, " # physic equation")
		print("add_to_lists()")
		if counter >= fps_list.size() - 1:
			print(
"""

plt.xlabel("physics FPS")
plt.ylabel(f"position after {end_time}s")

plt.plot(fps_list, JTB_mvc, "yo", label="just before (move and collide)")
plt.plot(fps_list, BNA_mvc, "go",label="before and after (move and collide)")
plt.plot(fps_list, PHY_mvc, "ro", label="physic equation based (move and collide)")
plt.plot(fps_list, JTB_mvs, "y-", label="just before (move and slide)")
plt.plot(fps_list, BNA_mvs, "g-",label="before and after (move and slide)", linewidth=5.0)
plt.plot(fps_list, PHY_mvs, "r-", label="physic equation based (move and slide)")
plt.plot(fps_list, [expected_position_X] * len(fps_list), "b--")
plt.legend()
plt.show()

"""
			)
			get_tree().change_scene_to_packed(end_scene)
			return
		else:
			counter += 1
		Engine.physics_ticks_per_second = fps_list[counter]
		previous_try_frames = Engine.get_physics_frames()
		t = 0
		$Sc.queue_free()
		add_child(sc_scene.instantiate())

This also measures the positions of bodies with different scripts (I have shared earlier: `delta` : when and when not to use it? - #19 by rom1dev).
It prints a python script which I then use to visualize measures:


import matplotlib.pyplot as plt

fps_list = [6, 20, 30, 40, 60, 70, 100, 120]

JTB_mvs = []
BNA_mvs = []
PHY_mvs = []

JTB_mvc = []
BNA_mvc = []
PHY_mvc = []

JTB_mvs_position_X = 0
BNA_mvs_position_X = 0
PHY_mvs_position_X = 0

JTB_mvc_position_X = 0
BNA_mvc_position_X = 0
PHY_mvc_position_X = 0

def add_to_lists():
	JTB_mvs.append(JTB_mvs_position_X)
	BNA_mvs.append(BNA_mvs_position_X)
	PHY_mvs.append(PHY_mvs_position_X)

	JTB_mvc.append(JTB_mvc_position_X)
	BNA_mvc.append(BNA_mvc_position_X)
	PHY_mvc.append(PHY_mvc_position_X)



expected_position_X = 625.0
# -- capture positions at 5s -- 
end_time = 5


#time = 5.0s impressicion: 0.0
# at 6 physics fps

# ----- with move_and_slide:
JTB_mvs_position_X = 604.166564941406 # just before
BNA_mvs_position_X = 584.027893066406 # before and after
PHY_mvs_position_X = 584.027770996094 # physic equation

# ----- with move_and_collide:
JTB_mvc_position_X = 604.166564941406 # just before
BNA_mvc_position_X = 584.027893066406 # before and after
PHY_mvc_position_X = 584.027770996094 # physic equation
add_to_lists()


#time = 4.99999999999999s impressicion: -0.00000000000001
# at 20 physics fps

# ----- with move_and_slide:
JTB_mvs_position_X = 618.75 # just before
BNA_mvs_position_X = 612.5625 # before and after
PHY_mvs_position_X = 612.5625 # physic equation

# ----- with move_and_collide:
JTB_mvc_position_X = 618.75 # just before
BNA_mvc_position_X = 612.5625 # before and after
PHY_mvc_position_X = 612.5625 # physic equation
add_to_lists()


#time = 4.99999999999999s impressicion: -0.00000000000001
# at 30 physics fps

# ----- with move_and_slide:
JTB_mvs_position_X = 620.833557128906 # just before
BNA_mvs_position_X = 616.693969726562 # before and after
PHY_mvs_position_X = 616.694458007812 # physic equation

# ----- with move_and_collide:
JTB_mvc_position_X = 620.833557128906 # just before
BNA_mvc_position_X = 616.693969726562 # before and after
PHY_mvc_position_X = 616.694458007812 # physic equation
add_to_lists()


#time = 5.0s impressicion: 0.0
# at 40 physics fps

# ----- with move_and_slide:
JTB_mvs_position_X = 621.875 # just before
BNA_mvs_position_X = 618.765625 # before and after
PHY_mvs_position_X = 618.765625 # physic equation

# ----- with move_and_collide:
JTB_mvc_position_X = 621.875 # just before
BNA_mvc_position_X = 618.765625 # before and after
PHY_mvc_position_X = 618.765625 # physic equation
add_to_lists()


#time = 4.99999999999999s impressicion: -0.00000000000001
# at 60 physics fps

# ----- with move_and_slide:
JTB_mvs_position_X = 622.916259765625 # just before
BNA_mvs_position_X = 620.841247558594 # before and after
PHY_mvs_position_X = 620.840270996094 # physic equation

# ----- with move_and_collide:
JTB_mvc_position_X = 622.916259765625 # just before
BNA_mvc_position_X = 620.841247558594 # before and after
PHY_mvc_position_X = 620.840270996094 # physic equation
add_to_lists()


#time = 4.99999999999998s impressicion: -0.00000000000002
# at 70 physics fps

# ----- with move_and_slide:
JTB_mvs_position_X = 623.213500976562 # just before
BNA_mvs_position_X = 621.433898925781 # before and after
PHY_mvs_position_X = 621.433715820312 # physic equation

# ----- with move_and_collide:
JTB_mvc_position_X = 623.213500976562 # just before
BNA_mvc_position_X = 621.433898925781 # before and after
PHY_mvc_position_X = 621.433715820312 # physic equation
add_to_lists()


#time = 4.99999999999994s impressicion: -0.00000000000006
# at 100 physics fps

# ----- with move_and_slide:
JTB_mvs_position_X = 623.749938964844 # just before
BNA_mvs_position_X = 622.502502441406 # before and after
PHY_mvs_position_X = 622.502502441406 # physic equation

# ----- with move_and_collide:
JTB_mvc_position_X = 623.749938964844 # just before
BNA_mvc_position_X = 622.502502441406 # before and after
PHY_mvc_position_X = 622.502502441406 # physic equation
add_to_lists()


#time = 5.00000000000004s impressicion: 0.00000000000004
# at 120 physics fps

# ----- with move_and_slide:
JTB_mvs_position_X = 623.959228515625 # just before
BNA_mvs_position_X = 622.916625976562 # before and after
PHY_mvs_position_X = 622.918395996094 # physic equation

# ----- with move_and_collide:
JTB_mvc_position_X = 623.959228515625 # just before
BNA_mvc_position_X = 622.916625976562 # before and after
PHY_mvc_position_X = 622.918395996094 # physic equation
add_to_lists()


plt.xlabel("physics FPS")
plt.ylabel(f"position after {end_time}s")

plt.plot(fps_list, JTB_mvc, "yo", label="just before (move and collide)")
plt.plot(fps_list, BNA_mvc, "go",label="before and after (move and collide)")
plt.plot(fps_list, PHY_mvc, "ro", label="physic equation based (move and collide)")
plt.plot(fps_list, JTB_mvs, "y-", label="just before (move and slide)")
plt.plot(fps_list, BNA_mvs, "g-",label="before and after (move and slide)", linewidth=5.0)
plt.plot(fps_list, PHY_mvs, "r-", label="physic equation based (move and slide)")
plt.plot(fps_list, [expected_position_X] * len(fps_list), "b--")
plt.legend()
plt.show()

And the resulting graph (after executing this python script):

I made 3 interesting observations:

  1. my new idea (“physics equation based”) gives the exact same result as “before and after”.
  2. “just before” is always closer to the expected value.
  3. “just before” seems to be less frame rate dependent.

Is someone able to explain these observations (particularly 2 and 3 which are the most surprising to me) ?

I hope this post is clear enough… So if it is not, or if there are missing information, don’t hesitate to ask.

EDIT: I just fixed a mistake (the “(move and collide)” and “(move and slide)” labels in on the graph were inverted). I just re-uploaded a correct graph and correct python and GDScript code so what you see now is correct. That said, as move_and_slide and move_and_collide doesn’t seem to make any difference in this case, this fix is minor.

1 Like

Just so you know Godots default physics engine is non-deterministic.

Also looking at the implementation it seems to use some sort of delta calculation internally.

1 Like

Thank you. But as far I know, something is deterministic when the same inputs produce the same outputs.
Here, the problem is that we have frame rate dependent code.

Also, thank you for the link, this has been the occasion for me to learn a bit of C++ and how Godot works. I also checked the move_and_collide function as the problem is the same with move_and_slide and move_and_collide. That said, I still don’t have explanation for my surprising observations.

(float * float ** int) / int + float * float + float

There is also some integer/float mixing happening here that could be throwing some stuff off. Truncating values. Especially the divide by integer.

But i looked at your code and i cant really understand what you have done, other than some algorithm you wrote doesnt match to some expectation?

1 Like

Yes. You’re sticking things into a black box that hasn’t been taken into account in this thread. Two, in fact. move_and_slide() and move_and_collide(). So while the math is fun, it doesn’t prove or disprove anything, because you don’t know what is happening inside those boxes.

This brings up something that is worth discussion. The links @pmoosi provided were interesting, but potentially overcomplicated. I actually took a game development math class in college. You were required to complete physics, calc and differential equations before taking it. We spent the entire semester programming physics equations by cheating. (This was before 2010 and in C++, as @iknaut pointed out this is important.) Because at least back then, you couldn’t do all that complex math every frame, even when framerates were 30-60 fps. So you had to cheat. You approximated. You didn’t need as much precision in your decimals. So we took all the real world math and physics that we had learned, and learned how to dumb it down to only what the computer needs to fake physics.

While this is a very interesting academic discussion, your results show that it is an intellectual rabbit hole that has no bearing on using the Godot physics engine. This whole discussion is only relevant if you are working on the move_and_slide() and move_and_collide() functions themselves. Since you aren’t writing a physics engine from scratch, you’re overcomplicating your code for no reason as someone pointed out. (I think it was @gertkeno)

@iknaut 's advice is interesting, but it comes from 2004, and 21 years ago Godot didn’t exist - so it has no bearing on how Godot’s physics engine works. Also, as @gertkeno pointed out, _physics_process() tries to keep things tied to the framerate at 60fps. If you were to run all this code in _process() which is framerate independent, you might actually see that it is helpful. But, that’s an academic exercise.

The whole point of using _physics_process() and move_and_slide is so that we don’t have to worry about these things.

If you really want to have this discussion, you should be discussing the characterbody2d implementation of _move_and_slide_grounded() on line 112.

2 Likes

In your test the correct numerical and analytical approach give the exact same solution. Since both give mathematically the correct solution, this suggests that something is either off with your benchmark or move_and_slide/collide do something that changes the expected outcome.

After skimming your code, the problem is most likely the former. Specifically when you check when to end:

Engine.get_physics_frames() - previous_try_frames == Engine.physics_ticks_per_second * END_TIME

The parents _physics_process is called before the one of the child, so you are stopping one frame too early. This would explain why the position error is greater when you have a lower frame-rate. It also explains why ‘just before’ is closer: As we identified earlier, the approximated velocity is higher than the ‘real’ one (it’s the current one for the whole last frame), so it ‘makes up’ for stopping early.

As @dragonforge-dev mentions this is purely a ‘out of interest’ discussion (I thought this was clear - I started my first post with ‘For anybody interested’ :wink: ). Normally you can just assume that delta of physics_process is constant enough that it doesn’t matter. Although, one could argue that since ‘before and after’ has basically no overhead and give the exact solution, you can just use it without much downside (except one additional line of code).

2 Likes

Thank you @dragonforge-dev and @pmoosi !
I was a bit lost yesterday, but now I feel like I understand better than ever.


Thanks for mentioning this, it makes things clearer !
But let’s say I want my game to be able to run at 60 fps as well as 120 fps. If my physics_process is always only executed 60 times per seconds, players playing at 120 fps will not get the experience they expect… How to solve this problem ? By using what I like calling “before and after” ?


I understand better now, why you started your post like this… I was interested after all, that’s probably why I didn’t worry more about this expression.


For anybody interested (let me use this expression too :wink:),
I tried to execute my code one frame later and got the following graph:


So, the result is not surprising anymore !

Nice the you got the expected results.

As was mentioned before, _physics_process tries to run at a fixed frame-rate, independent of the actual frame-rate.This is by default 60, but can be changed in the settings (or via Engine.physics_ticks_per_second). You should always do physics related computation, such as collision checks/move_and_slide, during physics ticks. You can read about it here.

Because you update at a fixed frame-rate, everybody is getting approximately the same result, independent of their actual frame-rate. A problem is that, for users with higher fps, it might not be as smooth as expected (or it might even be jittery if the frame-rates don’t match). A solution to this is physics interpolation. That is, interpolating the position between physics updates at the actual frame-rate to get visually smooth movement.

The ‘before and after’ approach is to always have the same outcome, independent of the frame-rate. However, when you update the position during a physic tick, the frame-rate is always the same anyways (at least in theory). So the outcome is always the same (although it might not match the ‘correct’ solution as you tests show). This means that everybody is getting the same experience as you when you run it on your machine.
It is worth mentioning, that the physics ticks can vary in certain cases - e.g., when the CPU can’t keep up for some reason. By using the ‘before and after’ approach, you are on the safe side. I would not suggest to update an existing project, that already works nicely. But if you are starting a new one, there is almost no downside to using it.


Since this thread took a bit of a detour, here’s a summary to clarify things:

  • Do all movement updates that involve collision - or any other physics related computation - in _physics_process.
  • _physics_process always runs at the same rate. So usually everybody gets the same outcome.
  • To achieve visually smooth movement, you can apply physics interpolation.
  • You should always normalize you parameters with delta, as was mentioned in the first few answers. An exception is velocity (and only velocity) when using it with move_and_slide, since this is done internally.
  • To be safe from potential _physics_process fluctuation you can use ‘before and after’ (when accelerating) without much downside. You also get the same result as with the actual analytical/physical function, assuming no other impacts. But it’s usually not worth it to update your existing code-base for it.
4 Likes

When you are using projectiles, be they bullets or fireballs spells, delta becomes much more important because you no longer have move_and_slide or move_and_collide to move them.

1 Like

I will check how to use physics interpolation. But I think this thread has now reached its goal !

Thank you! I was planning to make a summary but as you already did, I don’t need to worry about that anymore.


Thank you to everyone who have participated in this thread ! :tada: