By popular demand…
@chonkdb
The technical purpose of a game (engine) is to incessantly render frames and display them for your viewing pleasure. It runs in an infinite while loop and just pumps out frames as fast as possible (or desired)
Suppose we’re vsynced at 60 fps, and we have a blank screen. The engine will display a blank frame in 16 millisecond intervals. Let’s call this interval the “frame time”:
|-- frame time --|
0 16 32
|----------------|----------------|-------->
^ ^ ^
start display display
frame 0 frame 1
Now if we have something to render, the computer will need to do some work inside the frame time to produce the pixels that will be displayed when the next vsync interrupt arrives. Let’s suppose the rendering work takes up 6 milliseconds of the frame time:
0 16 32
|RRRRRR----------|RRRRRR----------|RRRRRR-->
^ ^ ^
start display display
frame 0 frame 1
After the rendering work has been finished, the computer will wait for another 10 ms until it displays the result of its work, i.e - it’ll be idle until the next vsync interrupt.
Let’s now suppose that we want to render the results of a physics simulation that runs at 240 ticks per second and that we want to run the simulation precisely synchronized with real time. And let’s suppose that the engine internal time to run the simulation is 1 ms and your callback function takes another 1 ms, resulting in 2 ms of work to run one tick of the simulation:
0 16 32
|Ph--Ph--Ph--Ph--|Ph--Ph--Ph--Ph--|Ph--Ph-->
|RRRRRR----------|RRRRRR----------|RRRRRR-->
^ ^ ^
start display display
frame 0 frame 1
Doing it like this, we have two problems. First, we’ll need to interrupt whatever the rendering code is doing at the time, just to run our simulation tick, and second the rendering code wouldn’t know what to render because the simulation state would change in the midst of the rendering job.
So the solution is to first run the simulation, and then render it, pretending that simulation step time (delta) advances in regular intervals as if it was executed in real time with desired ticks per second. In other words, the execution of the simulation code is not synchronized with the time the simulation simulates:
0 16 32
|PhPhPhPhRRRRRR--|PhPhPhPhRRRRRR--|PhPhPh-->
^ ^ ^
start display display
frame 0 frame 1
As you can see, in the first 8 ms of the frame time, we execute the whole 16 ms worth of simulation, with uniform deltas as we were running every 4 ms. Then we render the results of that simulation. And then we wait for the leftover 2 ms for the vsync interrupt to arrive and display our results on the screen.
Note that your _physics_process() gets called at each h in the diagram. With the delta value that looks like it’s corresponding to the h in the previous diagram. So if you print the real time at each tick, it will apparently move in small increments for 4 ticks and then one big increment as the frame is completed. Rinse and repeat. Just like you’ve seen in your tests.
To verify this, your can alter your test code a bit. In each physics tick, measure the difference between the real time gotten from Time.get_ticks_usec() and the time accumulated via delta:
var tick := 0
var frame := 0
var real_time: float
var time_according_to_delta: float
func _physics_process(delta):
if tick == 0:
time_according_to_delta = Time.get_ticks_usec() - 1_000_000 / 60.0
real_time = Time.get_ticks_usec()
print("tick %d: delta-real = %d ms"%[tick, (time_according_to_delta - real_time) / 1_000.0])
time_according_to_delta += delta * 1_000_000
tick += 1
func _process(delta):
print("-> display frame %d"%frame)
frame += 1
if frame > 30:
set_process(false)
set_physics_process(false)
The results will be something like:
-> display frame 23
tick 94: delta-real = -5 ms
tick 95: delta-real = -1 ms
tick 96: delta-real = 2 ms
tick 97: delta-real = 6 ms
-> display frame 24
tick 98: delta-real = -4 ms
tick 99: delta-real = 0 ms
tick 100: delta-real = 3 ms
tick 101: delta-real = 7 ms
-> display frame 25
tick 102: delta-real = -4 ms
tick 103: delta-real = 0 ms
tick 104: delta-real = 3 ms
tick 105: delta-real = 7 ms
-> display frame 26
We can see that the time accumulated through delta consistently dances around the real time within the frame span.
If we falsely assume that delta represents the real time interval, we’ll equally falsely conclude that there’s something wrong with Time.get_tick_usec(). But in fact, the opposite is true - delta is not tied with real time. It’s just the in-simulation time.
If you wish we can visit the key parts in Godot’s source code to verify that this is indeed what’s happening.