Skip to content

Game and physics ticks are out of sync #3575

@Doliman100

Description

@Doliman100

Describe the bug

The anti-gravity from the physics tutorial works only if game and physics tick interval match. It seems the physics tick interval is hardcoded to 0.015 s. It matches the default game tick interval and the tutorial sample works fine. But if run a game or a server with -tickrate 33 argument, the game tick interval changes it to 0.03(03) s. There are two problems.
The first one is that during one game tick, two physics ticks occur.
The second one is that sometimes three physics ticks can occur during one game tick. This happens because these ticks are not synchronized. This problem was mentioned in this issue Facepunch/garrysmod-issues#3199.

It seems that physics ticks always start with some game tick. Usually it's about 1 s, but the exact number of ticks is varying. For tick rate 33 this number was 35 in single-player and 34 on all servers I tried. Here is a table of measured sync game tick for different -tickrate argument. SP means single-player, MP means multi-player server.

-tickrate SP MP
30 31 31
32 34 34
33 35 34
66 68 67

The physics ticks may resync on some condition. I was in single-player for 33 hours and the initial sync 35 was actual for all the time. But once I visited a server at 10 hours uptime and 18 hours uptime, for the first time the sync tick was 34 and for the second time it was 2031386. I don't know what causes this.
Here are values captured using x64dbg in 8 seconds after joining single-player. The seconds are converted to game and physics ticks number. The values weren't rounded by me, they are actually the exact integers after the division.

float curtime()
 8.7878789901733398 / 0.030303031206130981 = 290
double IVP_Environment::current_time
 7.7272729575634 / 0.030303031206130981 = 255
double IVP_Environment::time_of_next_psi
 7.7399998269975185 / 0.014999999664723873 = 516
double IVP_Environment::time_of_last_psi
 7.724999827332795 / 0.014999999664723873 = 515

The sync game tick is GameTicksOffset = 290 - 255 = 35. These values were captured inside event tick(). Capturing it in a global space produces 34 as a result of ticks subtraction which is wrong.

My workaround to get the physics tick offset is to set a known velocity U and measure a distance traveled by a prop in the next game tick. Since we know an equation of a distance traveled per one physics tick, we can calculate the number of physics ticks passed since the previous game tick PhysicsTickOffset = (P2 - P1) / (U * PhysicsTickInterval). This is possible, because E:pos() returns an interpolated position between m_world_last and m_world_next. Pos = m_world_last * (1 - F) + m_world_next * F. This offset may be used as an input of other chips, for example to defeat gravity.

The following offset was measured at the same tick as values captured using x64dbg mentioned above.

GameTicksOffset = 291
PhysicsTicksOffset = 1.1717441443247032

Substituting these values in the equation (GameTicksOffset - X) * GameTickInterval / PhysicsTickInterval - PhysicsTicksOffset allows find the sync game tick X. This equation is a number of physics ticks between the previous physics tick and X. As you can see, the value X = 34 gives an exact integer, but the previous and the next have much less precision, so 34 is the actual sync game tick. Currently I implemented finding X using bruteforce through values in a range from -200 to 1000. It would be useful to find a way to find X in any range without substituting each value individually.

-64 1.0430813063067035e-05
35 0
134 0.99998956918693693

We can calculate number of physics ticks between it and the Y game ticks using the following equation: (Y - X) * GameTickInterval / PhysicsTickInterval. Here we don't need to add floating point values, so the precision is higher. You can use this equation in your own chips and use found offset as an input to predict how much physics ticks will occur until the next time you could apply force.

We need a way to calculate how much ticks will occur between the current and the next game ticks statically. Without movement measurement. It also shouldn't depend on any particular entity, but may depend on the world entity if that would be useful, since it always exists.

How to reproduce the bug

Run the game with tick rate 33: .\gmod.exe -tickrate 33, join single-player and run this sample chip.
test_ticks_sync.txt

Use TestType = 0 to see the current behavior of the code from the physics tutorial. You will see the prop moving up. This happens because of the first problem mentioned. The script assumes that the game and physics tick intervals match and the prop distance traveled per one game tick is calculated as Pos = (Velocity * GameTickInterval) * GameTickInterval, but as described above, two or three physics ticks may occur during one game tick, so the real equation is Pos = (Velocity * PhysicsTickInterval * N) * PhysicsTickInterval. Since gravity is applied each physics tick, the Velocity * PhysicsTickInterval * N part turns into an arithmetic progression Gravity * PhysicsTickInterval * N * (1 + N) / 2. To defeat it, we have to apply the following velocity AntiGravity = -Gravity * PhysicsTickInterval * (1 + N) / 2, because Pos = (AntiGravity * N + Gravity * PhysicsTickInterval * N * (1 + N) / 2) * PhysicsTickInterval = 0.
Set TestType = 1 to see behavior with the described arithmetic progression solution, which fixes the first problem. The object stays on place, but every 1.5 seconds it drops down by -0.135 in. This is caused by the second problem, when three physics ticks occur during one game tick.
TestType = 2 is an implementation of my workaround described above. The object will be moved at the world origin, then the offset will be measured and used to defeat gravity. You will see the prop standing in place, without moving anywhere. It will very slowly falls due to damping force, but it doesn't matter compared to the original problems. It will also jitter around the target position, but this is due to the prop is actually moving between the game ticks. This is like throwing a ball up and catching it when it returns to your hand each second.
TestType = 3 is an alternative implementation without prop core access. Take note the prop has to be at [0, 0, 0] position, because the applied velocity is 2^(-24) to prevent drag and damping forces and get pure values. The traveled distance will be around 1.79350332e-06. If the initial prop position will be [0 0 1], then the destination position will be rounded to 1.0000018, so the important movement info will be lost. If pos will be [0 0 32], then the movement will be lost completely since FP32 32 + 1.79350332e-06 = 32.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions