Invalid behavior of `TimestepMode::Interpolated`
I have a problem with TimestepMode::Interpolated, I analyze a code of this mode since few days and finally I think: it can not work correct with user defined systems.
Why?
Interpolated mode tries to keep selected frame rate (delta time) of physics pipeline steps like another engines tries to do this in thing which named FixedUpdate. Bevy also have their own FixedUpdate and it is a place where I tried to place things from my user defined systems, but this causes skipping Rapier's physics steps for my data which works by jitter.
Expected behavior
User defined systems should execute once for every Rapier's physics step. Rapier should do they work on end of every subupdate from Bevy's FixedUpdate instead of created own implementation of that. Or executing user defined systems in their own subupdate loop. In my opinion, first option is better.
A gameloop should looks like this:
Actual behavior
User defined systems executes independently than Rapier's physics step. Code of step_simulation system invoke whole pipeline work in itself, without any calls of user defined systems etc. And this causes changing a data between user defined systems without propagating this in physics.
A gameloop looks now:
Reproduction
Registring:
// I register this in FixedUpdate, because I want to keep selected physics delta time, like another engines do that.
app.add_systems(FixedUpdate, move_characters);
System code:
fn move_characters(mut query: Query<&mut KinematicCharacterController>) {
for (mut controller) in &mut query {
// Property of controller should always be `None`, because it was cleared after any pipeline step.
// But now it can be executed few times before next pipeline step will be executed.
controller.translation = Some(Vec3::new(0.0, 0.0, 1.0));
}
}
Environment:
- bevy v0.12.1
- bevy_rapier v0.23.0
Research steps
I tried to eliminate the same problem like @liquidev, because I work with him.
@liquidev's idea of invalid interpolation
He started from this idea, and even he make a pull request #463 which try to fix that, but this is not patching our problem and in my opinion (he do not must agree with that) his changes are wrong, but I plan to explain why in that pull request.
Idea of unstable delta time
Bevy's ECS scheduler currently have a little problem with delta time stability. This is described in https://github.com/bevyengine/bevy/issues/4669. Even @liquidev create chart of this, and how it is related with Rapier's lerp problem (dz is variation in character's position in z axis):
Initially I think about this like a big problem with lerp, but after consideration I think ustabilities like that should not very influence Because things which is unrelated with physics looks fine on different FPS, and generally so many published games on any engines have unstable frame time, but looks fine.
Generally from what I understanded and noticed, this relation exists, because this unstability causes an aditionals Rapier's subupdate and Bevy's FixedUpdate subupdate, which start a whole problem which I explain in this issue.
Ok, we moved this to graph from expected behavior and now this works almost correctly. Almost because this still have a bit problems with this lerp from #463.
Notes
Simple game loop:
let fixed_time = n;
let mut previous_time = get_system_time();
let mut overstep = 0.0;
loop {
let current_time = get_system_time();
let elapsed_time = current_time - previous_time;
previous_time = current_time;
overstep += elapsed_time;
while overstep >= fixed_time {
fixed_update();
overstep -= fixed_time;
}
pre_update();
update();
post_update();
render();
}
For n = 6 and frametime = 10 this will result:
| Frame id | Initial overstep | Overstep | Number of FixedUpdates | Number of Updates |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 1 |
| 1 | 0 | 10 | 1 | 1 |
| 2 | 4 | 14 | 2 | 1 |
| 3 | 2 | 12 | 2 | 1 |
| 4 | 0 | 10 | 1 | 1 |
For n = 4 and frametime = 10 this will result:
| Frame id | Initial overstep | Overstep | Number of FixedUpdates | Number of Updates |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 1 |
| 1 | 0 | 10 | 2 | 1 |
| 2 | 2 | 12 | 3 | 1 |
| 3 | 0 | 10 | 2 | 1 |
| 4 | 2 | 12 | 3 | 1 |
With this definition of fixed update function:
fn fixed_update() {
position += 1;
}
Rendered data will be looks:
n = 6 and frametime = 10
| Frame id | Position | Position delta |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 1 | 1 |
| 2 | 3 | 2 |
| 3 | 5 | 2 |
| 4 | 6 | 1 |
n = 4 and frametime = 10
| Frame id | Position | Position delta |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 2 | 2 |
| 2 | 5 | 3 |
| 3 | 7 | 2 |
| 4 | 10 | 3 |
Little update which is works sooo nice, but still it have little problems due to unstable real time in Bevy. https://github.com/liquidev/bevy_rapier/commit/8dfab9c465580297601236bed58daf7c0de77ab5