Physics engine - Part 6: Debugging, pausing, saving
In this part, we'll add an inspector to our examples, and see how we can easily step through problematic cases one frame at a time.
At the moment it’s getting less and less clear what’s happening in each system, and what the various component values will be at what point.
In this section, we’ll set up a much more convenient way to get a feel to what’s happening with our entities and components.
It’s certainly doable with just breakpoints and logging, and feel free to skip this section if it doesn’t interest you. It’s not required to complete this tutorial series.
Adding bevy_editor_pls
bevy_editor_pls
is a crate that adds a world inspector for bevy, and it’s pretty easy to set up for our example projects.
First we add it as a dev dependency:
[dev-dependencies]
bevy_editor_pls = { git = "https://github.com/jakobhellermann/bevy_editor_pls", branch = "main" }
Then we add the plugin to examples:
.add_plugins(DefaultPlugins)
.add_plugin(XPBDPlugin::default())
.add_plugin(bevy_editor_pls::EditorPlugin)
And with that, you’ll already get some debugging tools:
You can inspect and change the values of bevy’s built in components at run-time.
If you expand one of our own types, though, like Pos
or Rot
, you’ll just see this:
There are two things we need to do to get the inspector to work for our components. First, we need to make them derive Reflect
and set the reflect(component)
flag:
#[derive(Reflect, Debug, Default, Clone, Copy, From)]
#[reflect(Component)]
pub struct Pos(pub Vec2);
Repeat this process for all of the components.
The second thing we need to do, is register our component types. We’ll do this at the start of our plugin:
impl Plugin for XPBDPlugin {
fn build(&self, app: &mut App) {
app.register_type::<Pos>()
.register_type::<PrevPos>()
.register_type::<Mass>()
.register_type::<Restitution>()
.register_type::<Aabb>()
.register_type::<CircleCollider>()
.register_type::<Vel>()
.register_type::<PreSolveVel>()
Whenever we add new component types, they now need to be added to this list to work with the inspector.
And that should make our components inspectable and editable:
One more thing we might want to do, is make picking is working. You might have noticed this thing:
This is a feature that let’s you ctrl+click on an entity to open its inspector. This is very handy when you have lot’s of objects on screen at the same time.
This is because picking needs to be enabled for both the camera, and the objects that we want to be able to click. This can be set up automatically by changing the editor settings. First we add a factory function for editor settings and set the mentioned automatic behavior.
fn editor_settings() -> EditorSettings {
let mut settings = EditorSettings::default();
settings.auto_pickable = true;
settings.auto_pickable_camera = true;
settings
}
Then we insert it as a resource before we add the inspector plugin:
.insert_resource(editor_settings())
.add_plugin(bevy_editor_pls::EditorPlugin)
And that’s it, we can now ctrl+click on an object to see and edit its components in real time
Pausing
Now the other thing that would be really nice, was if we could pause the simulation and calmly inspect/edit entities and then resume or step through the simulation. That makes it much easier to see exactly what happens if we encounter some bug or weird behavior we want to make sense of.
Currently, we’re advancing our simulation automatically in run_criteria
by adding elapsed time to an accumulator and then advancing the simulation once enough time has been added:
state.accumulator += time.delta_seconds();
So we need some way of overriding this.
Let’s start simple stupid: We’ll add a new field, paused
to the LoopState
resource:
#[derive(Debug, Default)]
struct LoopState {
has_added_time: bool,
accumulator: f32,
substepping: bool,
current_substep: u32,
paused: bool,
}
And then we’ll just do an early return if we’re paused:
fn run_criteria(time: Res<Time>, mut state: ResMut<LoopState>) -> ShouldRun {
if state.paused {
return ShouldRun::No;
}
Now we just need some way of changing paused
from our example.
It’s part of the LoopState
resource, which is private, so let’s simply move it over to resources.rs
make it public and rename it so it makes sense when used by external code, make its fields crate-visible, and add public methods for pausing/unpausing:
#[derive(Debug, Default)]
pub struct XpbdLoop {
pub(crate) has_added_time: bool,
pub(crate) accumulator: f32,
pub(crate) substepping: bool,
pub(crate) current_substep: u32,
pub(crate) paused: bool,
}
impl XpbdLoop {
pub fn paused(&self) -> bool {
self.paused
}
pub fn pause(&mut self) {
self.paused = true;
}
pub fn resume(&mut self) {
self.paused = false;
}
}
And update lib.rs
accordingly:
fn run_criteria(time: Res<Time>, mut state: ResMut<XpbdLoop>) -> ShouldRun {
// ...
fn first_substep(state: Res<XpbdLoop>) -> ShouldRun {
// ...
fn last_substep(state: Res<XpbdLoop>) -> ShouldRun {
// ...
.init_resource::<XpbdLoop>()
Now, we can easily implement pausing in our examples.
For instance, to pause at startup, you could simply do:
.add_startup_system(pause_xpbd)
// ...
fn pause_xpbd(mut xpbd_loop: ResMut<XpbdLoop>) {
xpbd_loop.pause();
}
However, that will not stop new bodies from spawning, because that’s handled outside the physics loop. So we’ll remove the startup system and use bevy’s states for pausing/unpausing, which makes the code much cleaner:
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
enum AppState {
Running,
Paused,
}
// ...
.add_state(AppState::Paused)
.add_startup_system(startup)
.add_system_set(SystemSet::on_enter(AppState::Paused).with_system(pause_xpbd))
.add_system_set(SystemSet::on_exit(AppState::Paused).with_system(resume_xpbd))
.add_system(pause_button)
.add_system_set(
SystemSet::on_update(AppState::Running)
.with_run_criteria(FixedTimestep::step(1. / 2.))
.with_system(spawn_boxes),
)
/// ...
fn pause_button(mut app_state: ResMut<State<AppState>>, keys: Res<Input<KeyCode>>) {
if keys.just_pressed(KeyCode::P) {
let new_state = match app_state.current() {
AppState::Paused => AppState::Running,
AppState::Running => AppState::Paused,
};
app_state.set(new_state).unwrap();
}
}
And now it works.... kinda. The boxes are still spawning when paused. The reason is that SystemSet::with_run_criteria
overwrites the run criteria for only running in the given state. So let’s remove that line and use a local timer in the system instead:
SystemSet::on_update(AppState::Running).with_system(spawn_boxes)
// ...
fn spawn_boxes(
mut commands: Commands,
materials: Res<Materials>,
mut timer: Local<Option<Timer>>,
time: Res<Time>,
) {
if timer.is_none() {
*timer = Some(Timer::from_seconds(0.5, true));
}
let timer = timer.as_mut().unwrap();
timer.tick(time.delta());
if !timer.just_finished() {
return;
}
And now, pausing should finally work as expected. We can press P, and then ctrl+click bodies to open their inspectors, edit whatever we want, and then hit P again to unpause.
Stepping
Now that pausing is working, it’s a small step to add... stepping 🥁.
Lets add a new method to the XpbdLoop
resource:
#[derive(Debug, Default)]
pub struct XpbdLoop {
pub(crate) has_added_time: bool,
pub(crate) accumulator: f32,
pub(crate) substepping: bool,
pub(crate) current_substep: u32,
pub(crate) paused: bool,
pub(crate) queued_steps: u32, // <-- new
}
impl XpbdLoop {
pub fn step(&mut self) {
self.queued_steps += 1;
}
We can now modify our run criteria so it checks queued_steps
when paused:
fn run_criteria(time: Res<Time>, mut state: ResMut<XpbdLoop>) -> ShouldRun {
if state.paused && state.queued_steps == 0 {
return ShouldRun::No;
}
if !state.has_added_time {
state.has_added_time = true;
if state.paused {
state.accumulator += DELTA_TIME * state.queued_steps as f32;
} else {
state.accumulator += time.delta_seconds();
}
}
if state.substepping {
state.current_substep += 1;
if state.current_substep < NUM_SUBSTEPS {
return ShouldRun::YesAndCheckAgain;
} else {
// We finished a whole step
if state.paused {
if state.queued_steps > 0 {
state.queued_steps = state.queued_steps - 1;
}
} else {
state.accumulator -= DELTA_TIME;
}
state.current_substep = 0;
state.substepping = false;
}
}
if state.accumulator >= DELTA_TIME {
state.substepping = true;
state.current_substep = 0;
ShouldRun::YesAndCheckAgain
} else {
state.has_added_time = false;
ShouldRun::No
}
}
And now, we can simply add a new system to our example to do the stepping:
.add_system_set(SystemSet::on_update(AppState::Paused).with_system(step_button))
fn step_button(mut xpbd_loop: ResMut<XpbdLoop>, keys: Res<Input<KeyCode>>) {
if keys.just_pressed(KeyCode::Right) {
xpbd_loop.step();
}
}
Now you should be able to hit p to pause the simulation, ctrl+click an entity and inspect its values and then step one frame at a time by pressing the right arrow key.
Reducing example boiler-plate
Ok, our example is now much cooler, but it’s also a lot longer, and most of the code is not really relevant to what we’re actually demoing.
As we add more features, we are going to add many more examples, and they could all benefit from supporting pausing and stepping.
Instead of copying the common code around each time we create a new example, we’ll put all the common code in a plugin that we will re-use across examples. The exception is the basic
example. In my opinion, it’s nice to have at least one example that is completely self-contained.
Before we start making the plugin, there are a few low-hanging fruits we could fix.
pause_xpbd
and resume_xpbd
are generic and will likely be duplicated in almost every project using our physics engine that want to pause or resume simulations. So let’s move the to them to lib.rs
instead instead:
pub fn pause(mut xpbd_loop: ResMut<XpbdLoop>) {
xpbd_loop.pause();
}
pub fn resume(mut xpbd_loop: ResMut<XpbdLoop>) {
xpbd_loop.resume();
}
Also remove the _xpbd
suffix, as it’s implicit when we’re inside our crate:
And then update to use the library systems in box_pour.rs
instead:
.add_system_set(
SystemSet::on_enter(AppState::Paused).with_system(bevy_xpbd::pause),
)
.add_system_set(
SystemSet::on_exit(AppState::Paused).with_system(bevy_xpbd::resume),
)
Sharing code between examples is a bit clunky in rust. We will use the approach outlined in this stackoverflow post.
We start by creating a new crate for our example plugin. In the root of the crate, run:
cargo new --lib examples_common
Then add it as a dev-dependency in your root Cargo.toml
:
[dev-dependencies]
examples_common = { path = "examples_common" }
Add the following dependencies to examples_common/Cargo.toml
:
[dependencies]
bevy = "0.6"
bevy_editor_pls = { git = "https://github.com/jakobhellermann/bevy_editor_pls", branch = "main" }
bevy_xpbd = { path = ".." }
Open up examples_common/src/lib.rs
and replace its content with the regular plugin boilerplate:
use bevy::prelude::*;
use bevy_xpbd::*;
#[derive(Default)
pub struct XpbdExamplePlugin;
impl Plugin for XpbdExamplePlugin {
fn build(&self, app: &mut App) {}
}
Then let’s move all the code that’s not related to the box_pour
example into the new plugin:
use bevy::prelude::*;
use bevy_editor_pls::EditorSettings;
use bevy_xpbd::*;
#[derive(Default)]
pub struct XpbdExamplePlugin;
impl Plugin for XpbdExamplePlugin {
fn build(&self, app: &mut App) {
app.insert_resource(ClearColor(Color::rgb(0.8, 0.8, 0.9)))
.insert_resource(Msaa { samples: 4 })
.add_plugins(DefaultPlugins)
.add_plugin(XPBDPlugin::default())
.insert_resource(editor_settings())
.add_plugin(bevy_editor_pls::EditorPlugin)
.add_state(AppState::Running)
.add_startup_system(spawn_camera)
.add_system_set(
SystemSet::on_enter(AppState::Paused).with_system(bevy_xpbd::pause),
)
.add_system_set(
SystemSet::on_exit(AppState::Paused).with_system(bevy_xpbd::resume),
)
.add_system(pause_button)
.add_system_set(
SystemSet::on_update(AppState::Paused).with_system(step_button),
);
}
}
fn pause_button(mut app_state: ResMut<State<AppState>>, keys: Res<Input<KeyCode>>) {
if keys.just_pressed(KeyCode::P) {
let new_state = match app_state.current() {
AppState::Paused => AppState::Running,
AppState::Running => AppState::Paused,
};
app_state.set(new_state).unwrap();
}
}
fn step_button(mut xpbd_loop: ResMut<XpbdLoop>, keys: Res<Input<KeyCode>>) {
if keys.just_pressed(KeyCode::Right) {
xpbd_loop.step();
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum AppState {
Paused,
Running,
}
fn editor_settings() -> EditorSettings {
let mut settings = EditorSettings::default();
settings.auto_pickable = true;
settings.auto_pickable_camera = true;
settings
}
fn spawn_camera(mut commands: Commands) {
commands.spawn_bundle(OrthographicCameraBundle {
transform: Transform::from_translation(Vec3::new(0., 0., 100.)),
orthographic_projection: bevy::render::camera::OrthographicProjection {
scale: 0.01,
..Default::default()
},
..OrthographicCameraBundle::new_3d()
});
}
We make the AppState
public, so we can easily add new systems only to the Running
state.
box_pour.rs
. Then we update box_pour.rs
to use the new plugin:
use bevy::prelude::*;
use bevy_xpbd::*;
use examples_common::*;
use rand::random;
fn main() {
App::new()
.add_plugin(XpbdExamplePlugin::default())
.add_startup_system(startup)
.add_system_set(SystemSet::on_update(AppState::Running).with_system(spawn_boxes))
.add_system(despawn_boxes)
.run();
}
struct Materials {
blue: Handle<StandardMaterial>,
}
struct Meshes {
quad: Handle<Mesh>,
}
fn startup(
mut commands: Commands,
mut materials: ResMut<Assets<StandardMaterial>>
mut meshes: ResMut<Assets<Mesh>>,
) {
let blue = materials.add(StandardMaterial {
base_color: Color::rgb(0.4, 0.4, 0.6),
unlit: true,
..Default::default()
});
let quad = meshes.add(Mesh::from(shape::Quad::new(Vec2::ONE)));
let size = Vec2::new(10., 2.);
commands
.spawn_bundle(PbrBundle {
mesh: quad.clone(),
material: blue.clone(),
transform: Transform::from_scale(size.extend(1.)),
..Default::default()
})
.insert_bundle(StaticBoxBundle {
pos: Pos(Vec2::new(0., -3.)),
rot: Rot::from_degrees(0.),
collider: BoxCollider { size },
..Default::default()
});
commands.insert_resource(Meshes { quad });
commands.insert_resource(Materials { blue });
}
fn spawn_boxes(
mut commands: Commands,
materials: Res<Materials>,
mut timer: Local<Option<Timer>>,
time: Res<Time>,
meshes: Res<Meshes>,
) {
if timer.is_none() {
*timer = Some(Timer::from_seconds(0.5, true));
}
let timer = timer.as_mut().unwrap();
timer.tick(time.delta());
if !timer.just_finished() {
return;
}
let size = Vec2::splat(0.3);
let pos = Vec2::new(random::<f32>() - 0.5, random::<f32>() - 0.5) * 0.5 + Vec2::Y * 3.;
let rot = random::<Rot>();
commands
.spawn_bundle(PbrBundle {
mesh: meshes.quad.clone(),
material: materials.blue.clone(),
transform: Transform {
scale: size.extend(1.),
translation: pos.extend(0.),
rotation: rot.into(),
..Default::default()
},
..Default::default()
})
.insert_bundle(
DynamicBoxBundleBuilder::default()
.collider(size)
.pos(pos)
.rot(rot)
.vel(Vec2::new(random::<f32>() - 0.5, random::<f32>() - 0.5))
.ang_vel(random::<f32>() * 2. - 1.)
.build()
.unwrap(),
);
}
fn despawn_boxes(mut commands: Commands, query: Query<(Entity, &Pos)>) {
for (entity, pos) in query.iter() {
if pos.0.y < -20. {
commands.entity(entity).despawn();
}
}
}
Much better.
Now go ahead and do the same thing for the other examples as well (except simple.rs
).
Back to the future (adding rollback)
Now that the right arrow advances time, wouldn’t it be cool if the left arrow brought us back in time?
I’m, a huge fan of rollback networking, so making sure our physics engine supports some kind of rolling back is an obvious goal for me.
I’ve made a separate bevy crate that makes some of this a lot easier, bevy_snap
.
Go ahead and add it to our examples_common
dependencies:
bevy_snap = { git = "https://github.com/johanhelsing/bevy_snap", branch = "main" }
We will be adding all the save/load related functionality in a separate module, so create a new file, examples_common/src/saving.rs
, and add a public SavePlugin
to it:
use bevy::{prelude::*, reflect::TypeRegistry};
use bevy_snap::*;
use bevy_xpbd::*;
#[derive(Default)]
pub struct SavePlugin;
impl Plugin for SavePlugin {
fn build(&self, app: &mut App) {
todo!()
}
}
And export it in example_common/src/lib.rs
:
mod saving;
pub use saving::*;
bevy_snap works by taking and restoring snapshots of entity and resource state. However, not all types should be saved and restored, so in order to specify what types we’re after, we will define our own struct that implements bevy_snap::SnapType
, and add all the physics state components.
#[derive(Default)]
struct XpbdSnap;
impl SnapType for XpbdSnap {
fn add_types(registry: &mut TypeRegistry) {
registry.write().register::<Pos>();
registry.write().register::<Rot>();
registry.write().register::<PrevPos>();
registry.write().register::<Mass>();
registry.write().register::<Restitution>();
registry.write().register::<Aabb>();
registry.write().register::<CircleCollider>();
registry.write().register::<BoxCollider>();
registry.write().register::<Vel>();
registry.write().register::<PreSolveVel>();
registry.write().register::<Rot>();
registry.write().register::<PrevRot>();
registry.write().register::<AngVel>();
registry.write().register::<PreSolveAngVel>();
registry.write().register::<Inertia>();
}
}
Then, in our plugin, we use it as a type argument when we initialize bevy_snap
:
impl Plugin for SavePlugin {
fn build(&self, app: &mut App) {
app.add_plugin(SnapPlugin::<XpbdSnap>::default());
}
}
Now, we can generate new snapshots by calling commands.save::<XpbdSnap>();
, which will make the snapshot available as an event after the current bevy stage has finished. We’ll create a system that saves when we press S:
fn save_button(mut commands: Commands, keys: Res<Input<KeyCode>>) {
if keys.just_pressed(KeyCode::S) {
info!("Saving snapshot");
commands.save::<XpbdSnap>();
}
}
And one that stores the snapshots in a resource:
#[derive(Default)]
struct SaveSlot(WorldSnapshot<XpbdSnap>);
fn store_snapshot(
mut save_events: EventReader<SaveEvent<XpbdSnap>>,
mut save_slot: ResMut<SaveSlot>,
) {
for save_event in save_events.iter() {
info!("Writing snapshot to save slot resource");
save_slot.0 = save_event.snapshot.clone();
}
}
And finally one that restores snapshots when the L key is pressed:
fn load_button(mut commands: Commands, keys: Res<Input<KeyCode>>, save_slot: ResMut<SaveSlot>) {
if keys.just_pressed(KeyCode::L) {
info!("Loading snapshot");
commands.load::<XpbdSnap>(save_slot.0.clone());
}
}
The we’ll initialize the resource, and register all the new systems in our SavePlugin
:
impl Plugin for SavePlugin {
fn build(&self, app: &mut App) {
app.add_plugin(SnapPlugin::<XpbdSnap>::default())
.init_resource::<SaveSlot>()
.add_system(save_button)
.add_system(load_button)
.add_system(store_snapshot)
.add_system(auto_add_snap);
}
}
Now, let’s try adding it to the main XbdExamplePlugin
:
.add_plugin(bevy_editor_pls::EditorPlugin)
.add_plugin(SavePlugin::default())
And if we run the box_pour
example now, we can create new snapshots with S, and restore them with L.
That makes it much simpler to replay the same situation multiple times, so we can easily inspect different parts of it, or check if something is racy.
TODO:
Going forward, we could also add the following features:
- Continuously save stack of snapshots, pop and load with left arrow
- Save snapshots to disk
- Auto-save when things blow up (check for ridiculously high velocities) or panic