Skip to content

Commit 1ee1c33

Browse files
authored
Rework structure and improve examples (#3)
- Extract general easing logic from `TransformInterpolationPlugin` into new `TransformEasingPlugin` - Add more system sets to `TransformEasingSet` and improve scheduling - Rename easing systems to include `_lerp`/`_slerp` - Significantly improve docs for interpolation and components - Rework crate-level docs - Impove `interpolation` example - Add `extrapolation` example
1 parent f246b9b commit 1ee1c33

File tree

5 files changed

+866
-330
lines changed

5 files changed

+866
-330
lines changed

README.md

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Next, add the `TransformInterpolationPlugin`:
6464

6565
```rust
6666
use bevy::prelude::*;
67-
use bevy_transform_interpolation::*;
67+
use bevy_transform_interpolation::prelude::*;
6868

6969
fn main() {
7070
App::new()
@@ -79,29 +79,29 @@ You can choose to interpolate transform, rotation, or scale individually, or use
7979

8080
```rust
8181
use bevy::prelude::*;
82-
use bevy_transform_interpolation::*;
82+
use bevy_transform_interpolation::prelude::*;
8383

8484
fn setup(mut commands: Commands) {
8585
// Only interpolate translation.
86-
commands.spawn((TransformBundle::default(), TranslationInterpolation));
86+
commands.spawn((Transform::default(), TranslationInterpolation));
8787

8888
// Only interpolate rotation.
89-
commands.spawn((TransformBundle::default(), RotationInterpolation));
89+
commands.spawn((Transform::default(), RotationInterpolation));
9090

9191
// Only interpolate scale.
92-
commands.spawn((TransformBundle::default(), ScaleInterpolation));
92+
commands.spawn((Transform::default(), ScaleInterpolation));
9393

9494
// Interpolate translation and rotation, but not scale.
9595
commands.spawn((
96-
TransformBundle::default(),
96+
Transform::default(),
9797
TranslationInterpolation,
9898
RotationInterpolation,
9999
));
100100

101101
// Interpolate the entire transform: translation, rotation, and scale.
102102
// The components can be added individually, or using the `TransformInterpolation` component.
103103
commands.spawn((
104-
TransformBundle::default(),
104+
Transform::default(),
105105
TransformInterpolation,
106106
));
107107
}
@@ -112,7 +112,7 @@ by configuring the `TransformInterpolationPlugin`:
112112

113113
```rust
114114
use bevy::prelude::*;
115-
use bevy_transform_interpolation::*;
115+
use bevy_transform_interpolation::prelude::*;
116116

117117
fn main() {
118118
App::new()
@@ -165,17 +165,6 @@ Note that the core easing logic and components are intentionally not tied to int
165165
A physics engine could implement **transform extrapolation** using velocity and the same easing functionality,
166166
supplying its own `TranslationExtrapolation` and `RotationExtrapolation` components.
167167

168-
## Caveats
169-
170-
- In cases where the previous or current gameplay transform are already stored separately from `Transform`,
171-
storing them in the easing states as well may be redundant. Although it *is* still useful for allowing
172-
`Transform` to be modified directly and for wider compatibility with the ecosystem.
173-
- Transform extrapolation is currently not supported as a built-in feature, as it typically requires a velocity
174-
for the prediction of the next state. However, it could be supported by external libraries such as physics engines
175-
in a similar way to `src/interpolation.rs`, and simply updating the `start` and `end` states differently.
176-
- Large angular velocities may cause visual artifacts, as the interpolation follows the shortest path.
177-
A physics engine could handle this properly.
178-
179168
## License
180169

181170
`bevy_transform_interpolation` is free and open source. All code in this repository is dual-licensed under either:

examples/extrapolation.rs

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
//! Transform extrapolation is not a built-in feature in `bevy_transform_interpolation`, because it requires velocity.
2+
//! However, it can be implemented in a relatively straightforward way on top of `TransformEasingPlugin`.
3+
//!
4+
//! This example showcases how `Transform` extrapolation can be used to make movement
5+
//! appear smooth at fixed timesteps, and how it compares to `Transform` interpolation.
6+
//!
7+
//! Unlike `Transform` interpolation, which eases between the previous and current positions,
8+
//! `Transform` extrapolation predicts future positions based on velocity. This makes movement
9+
//! feel more responsive than interpolation, but it also produces jumpy results when the prediction is wrong,
10+
//! such as when the velocity of an object suddenly changes.
11+
12+
use bevy::{
13+
color::palettes::{
14+
css::WHITE,
15+
tailwind::{CYAN_400, LIME_400, RED_400},
16+
},
17+
prelude::*,
18+
};
19+
use bevy_transform_interpolation::{
20+
prelude::*, RotationEasingState, TransformEasingSet, TranslationEasingState,
21+
};
22+
23+
const MOVEMENT_SPEED: f32 = 250.0;
24+
const ROTATION_SPEED: f32 = 2.0;
25+
26+
fn main() {
27+
let mut app = App::new();
28+
29+
// Add the `TransformInterpolationPlugin` and `TransformExtrapolationPlugin` to the app to enable
30+
// transform interpolation and extrapolation.
31+
app.add_plugins((
32+
DefaultPlugins,
33+
TransformInterpolationPlugin::default(),
34+
// This is a custom plugin! See the implementation below.
35+
TransformExtrapolationPlugin,
36+
));
37+
38+
// Set the fixed timestep to just 5 Hz for demonstration purposes.
39+
app.insert_resource(Time::<Fixed>::from_hz(5.0));
40+
41+
// Setup the scene and UI, and update text in `Update`.
42+
app.add_systems(Startup, (setup, setup_text))
43+
.add_systems(Update, (change_timestep, update_timestep_text));
44+
45+
// Move entities in `FixedUpdate`. The movement should appear smooth for interpolated/extrapolated entities.
46+
app.add_systems(
47+
FixedUpdate,
48+
(flip_movement_direction.before(movement), movement, rotate),
49+
);
50+
51+
// Run the app.
52+
app.run();
53+
}
54+
55+
/// The linear velocity of an entity indicating its movement speed and direction.
56+
#[derive(Component, Deref, DerefMut)]
57+
struct LinearVelocity(Vec2);
58+
59+
/// The angular velocity of an entity indicating its rotation speed.
60+
#[derive(Component, Deref, DerefMut)]
61+
struct AngularVelocity(f32);
62+
63+
#[derive(Debug, Default)]
64+
pub struct TransformExtrapolationPlugin;
65+
66+
impl Plugin for TransformExtrapolationPlugin {
67+
fn build(&self, app: &mut App) {
68+
// Reset the transform to the start of the extrapolation at the beginning of the fixed timestep
69+
// to match the true position from the end of the previous fixed tick.
70+
app.add_systems(
71+
FixedFirst,
72+
reset_extrapolation.before(TransformEasingSet::Reset),
73+
);
74+
75+
// Update the start and end state of the extrapolation at the end of the fixed timestep.
76+
app.add_systems(
77+
FixedLast,
78+
update_easing_states.in_set(TransformEasingSet::UpdateEnd),
79+
);
80+
}
81+
82+
fn finish(&self, app: &mut App) {
83+
// Add the `TransformEasingPlugin` if it hasn't been added yet.
84+
// It performs the actual easing based on the start and end states set by the extrapolation.
85+
if !app.is_plugin_added::<TransformEasingPlugin>() {
86+
app.add_plugins(TransformEasingPlugin);
87+
}
88+
}
89+
}
90+
91+
/// Enables `Transform` extrapolation for an entity.
92+
///
93+
/// Only extrapolates the translation and rotation components of the transform
94+
/// based on the `LinearVelocity` and `AngularVelocity` components.
95+
#[derive(Component)]
96+
#[require(TranslationEasingState, RotationEasingState)]
97+
struct TransformExtrapolation;
98+
99+
/// Resets the transform to the start of the extrapolation at the beginning of the fixed timestep
100+
/// to match the true position from the end of the previous fixed tick.
101+
fn reset_extrapolation(
102+
mut query: Query<
103+
(
104+
&mut Transform,
105+
&TranslationEasingState,
106+
&RotationEasingState,
107+
),
108+
With<TransformExtrapolation>,
109+
>,
110+
) {
111+
for (mut transform, translation_easing, rotation_easing) in &mut query {
112+
if let Some(start) = translation_easing.start {
113+
transform.translation = start;
114+
}
115+
if let Some(start) = rotation_easing.start {
116+
transform.rotation = start;
117+
}
118+
}
119+
}
120+
121+
/// Updates the start and end states of the extrapolation for the next fixed timestep.
122+
fn update_easing_states(
123+
mut query: Query<
124+
(
125+
&Transform,
126+
&mut TranslationEasingState,
127+
&mut RotationEasingState,
128+
&LinearVelocity,
129+
&AngularVelocity,
130+
),
131+
With<TransformExtrapolation>,
132+
>,
133+
time: Res<Time>,
134+
) {
135+
let delta_secs = time.delta_secs();
136+
137+
for (transform, mut translation_easing, mut rotation_easing, lin_vel, ang_vel) in &mut query {
138+
translation_easing.start = Some(transform.translation);
139+
rotation_easing.start = Some(transform.rotation);
140+
141+
// Extrapolate the next state based on the current state and velocities.
142+
let next_translation = transform.translation + lin_vel.extend(0.0) * delta_secs;
143+
let next_rotation = transform.rotation * Quat::from_rotation_z(ang_vel.0 * delta_secs);
144+
145+
// In 3D, with a `Vec3` angular velocity, the next rotation could be computed like this:
146+
//
147+
// let scaled_axis = ang_vel.0 * delta_secs;
148+
// let next_rotation = transform.rotation * Quat::from_scaled_axis(scaled_axis);
149+
150+
translation_easing.end = Some(next_translation);
151+
rotation_easing.end = Some(next_rotation);
152+
}
153+
}
154+
155+
// The rest of the code is scene setup, and largely the same as in the `interpolation.rs` example.
156+
157+
fn setup(
158+
mut commands: Commands,
159+
mut materials: ResMut<Assets<ColorMaterial>>,
160+
mut meshes: ResMut<Assets<Mesh>>,
161+
) {
162+
// Spawn a camera.
163+
commands.spawn(Camera2d);
164+
165+
let mesh = meshes.add(Rectangle::from_length(60.0));
166+
167+
// This entity uses transform interpolation.
168+
commands.spawn((
169+
Name::new("Interpolation"),
170+
Mesh2d(mesh.clone()),
171+
MeshMaterial2d(materials.add(Color::from(CYAN_400)).clone()),
172+
Transform::from_xyz(-500.0, 120.0, 0.0),
173+
TransformInterpolation,
174+
LinearVelocity(Vec2::new(MOVEMENT_SPEED, 0.0)),
175+
AngularVelocity(ROTATION_SPEED),
176+
));
177+
178+
// This entity uses transform extrapolation.
179+
commands.spawn((
180+
Name::new("Extrapolation"),
181+
Mesh2d(mesh.clone()),
182+
MeshMaterial2d(materials.add(Color::from(LIME_400)).clone()),
183+
Transform::from_xyz(-500.0, 00.0, 0.0),
184+
TransformExtrapolation,
185+
LinearVelocity(Vec2::new(MOVEMENT_SPEED, 0.0)),
186+
AngularVelocity(ROTATION_SPEED),
187+
));
188+
189+
// This entity is simulated in `FixedUpdate` without any smoothing.
190+
commands.spawn((
191+
Name::new("No Interpolation"),
192+
Mesh2d(mesh.clone()),
193+
MeshMaterial2d(materials.add(Color::from(RED_400)).clone()),
194+
Transform::from_xyz(-500.0, -120.0, 0.0),
195+
LinearVelocity(Vec2::new(MOVEMENT_SPEED, 0.0)),
196+
AngularVelocity(ROTATION_SPEED),
197+
));
198+
}
199+
200+
/// Flips the movement directions of objects when they reach the left or right side of the screen.
201+
fn flip_movement_direction(mut query: Query<(&Transform, &mut LinearVelocity)>) {
202+
for (transform, mut lin_vel) in &mut query {
203+
if transform.translation.x > 500.0 && lin_vel.0.x > 0.0 {
204+
lin_vel.0 = Vec2::new(-MOVEMENT_SPEED, 0.0);
205+
} else if transform.translation.x < -500.0 && lin_vel.0.x < 0.0 {
206+
lin_vel.0 = Vec2::new(MOVEMENT_SPEED, 0.0);
207+
}
208+
}
209+
}
210+
211+
/// Changes the timestep of the simulation when the up or down arrow keys are pressed.
212+
fn change_timestep(mut time: ResMut<Time<Fixed>>, keyboard_input: Res<ButtonInput<KeyCode>>) {
213+
if keyboard_input.pressed(KeyCode::ArrowUp) {
214+
let new_timestep = (time.delta_secs_f64() * 0.9).max(1.0 / 255.0);
215+
time.set_timestep_seconds(new_timestep);
216+
}
217+
if keyboard_input.pressed(KeyCode::ArrowDown) {
218+
let new_timestep = (time.delta_secs_f64() * 1.1).min(1.0);
219+
time.set_timestep_seconds(new_timestep);
220+
}
221+
}
222+
223+
/// Moves entities based on their `LinearVelocity`.
224+
fn movement(mut query: Query<(&mut Transform, &LinearVelocity)>, time: Res<Time>) {
225+
let delta_secs = time.delta_secs();
226+
227+
for (mut transform, lin_vel) in &mut query {
228+
transform.translation += lin_vel.extend(0.0) * delta_secs;
229+
}
230+
}
231+
232+
/// Rotates entities based on their `AngularVelocity`.
233+
fn rotate(mut query: Query<(&mut Transform, &AngularVelocity)>, time: Res<Time>) {
234+
let delta_secs = time.delta_secs();
235+
236+
for (mut transform, ang_vel) in &mut query {
237+
transform.rotate_local_z(ang_vel.0 * delta_secs);
238+
}
239+
}
240+
241+
#[derive(Component)]
242+
struct TimestepText;
243+
244+
fn setup_text(mut commands: Commands) {
245+
let font = TextFont {
246+
font_size: 20.0,
247+
..default()
248+
};
249+
250+
commands
251+
.spawn((
252+
Text::new("Fixed Hz: "),
253+
TextColor::from(WHITE),
254+
font.clone(),
255+
Node {
256+
position_type: PositionType::Absolute,
257+
top: Val::Px(10.0),
258+
left: Val::Px(10.0),
259+
..default()
260+
},
261+
))
262+
.with_child((TimestepText, TextSpan::default()));
263+
264+
commands.spawn((
265+
Text::new("Change Timestep With Up/Down Arrow"),
266+
TextColor::from(WHITE),
267+
font.clone(),
268+
Node {
269+
position_type: PositionType::Absolute,
270+
top: Val::Px(10.0),
271+
right: Val::Px(10.0),
272+
..default()
273+
},
274+
));
275+
276+
commands.spawn((
277+
Text::new("Interpolation"),
278+
TextColor::from(CYAN_400),
279+
font.clone(),
280+
Node {
281+
position_type: PositionType::Absolute,
282+
top: Val::Px(50.0),
283+
left: Val::Px(10.0),
284+
..default()
285+
},
286+
));
287+
288+
commands.spawn((
289+
Text::new("Extrapolation"),
290+
TextColor::from(LIME_400),
291+
font.clone(),
292+
Node {
293+
position_type: PositionType::Absolute,
294+
top: Val::Px(75.0),
295+
left: Val::Px(10.0),
296+
..default()
297+
},
298+
));
299+
300+
commands.spawn((
301+
Text::new("No Interpolation"),
302+
TextColor::from(RED_400),
303+
font.clone(),
304+
Node {
305+
position_type: PositionType::Absolute,
306+
top: Val::Px(100.0),
307+
left: Val::Px(10.0),
308+
..default()
309+
},
310+
));
311+
}
312+
313+
fn update_timestep_text(
314+
mut text: Single<&mut TextSpan, With<TimestepText>>,
315+
time: Res<Time<Fixed>>,
316+
) {
317+
let timestep = time.timestep().as_secs_f32().recip();
318+
text.0 = format!("{timestep:.2}");
319+
}

0 commit comments

Comments
 (0)