use crate::prelude::*;
#[cfg(feature = "default-collider")]
use approx::assert_relative_eq;
use bevy::{
ecs::schedule::ScheduleBuildSettings, prelude::*, time::TimeUpdateStrategy, utils::Instant,
};
#[cfg(feature = "enhanced-determinism")]
use insta::assert_debug_snapshot;
use std::time::Duration;
#[cfg(feature = "enhanced-determinism")]
macro_rules! setup_insta {
($($expr:expr),*) => {
let _insta_settings_guard = {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path(format!("{manifest_dir}/snapshots"));
settings.bind_to_scope()
};
};
}
fn create_app() -> App {
let mut app = App::new();
app.add_plugins((MinimalPlugins, TransformPlugin, PhysicsPlugins::default()));
#[cfg(feature = "async-collider")]
{
app.add_plugins((
bevy::asset::AssetPlugin::default(),
bevy::scene::ScenePlugin,
))
.init_resource::<Assets<Mesh>>();
}
app.insert_resource(TimeUpdateStrategy::ManualInstant(Instant::now()));
app
}
fn tick_60_fps(app: &mut App) {
let mut update_strategy = app.world.resource_mut::<TimeUpdateStrategy>();
let TimeUpdateStrategy::ManualInstant(prev_time) = *update_strategy else {
unimplemented!()
};
*update_strategy =
TimeUpdateStrategy::ManualInstant(prev_time + Duration::from_secs_f64(1. / 60.));
app.update();
}
#[cfg(all(feature = "3d", feature = "default-collider"))]
fn setup_cubes_simulation(mut commands: Commands) {
let mut next_id = 0;
let floor_size = Vector::new(80.0, 1.0, 80.0);
commands.spawn((
RigidBody::Static,
Position(Vector::NEG_Y),
Collider::cuboid(floor_size.x, floor_size.y, floor_size.z),
));
let radius = 1.0;
let count_x = 4;
let count_y = 4;
let count_z = 4;
for y in 0..count_y {
for x in 0..count_x {
for z in 0..count_z {
let pos = Vector::new(
(x as Scalar - count_x as Scalar * 0.5) * 2.1 * radius,
10.0 * radius * y as Scalar,
(z as Scalar - count_z as Scalar * 0.5) * 2.1 * radius,
);
commands.spawn((
SpatialBundle::default(),
RigidBody::Dynamic,
Position(pos + Vector::Y * 5.0),
Collider::cuboid(radius * 2.0, radius * 2.0, radius * 2.0),
Id(next_id),
));
next_id += 1;
}
}
}
}
#[test]
fn it_loads_plugin_without_errors() -> Result<(), Box<dyn std::error::Error>> {
let mut app = create_app();
for _ in 0..500 {
tick_60_fps(&mut app);
}
Ok(())
}
#[test]
#[cfg(feature = "default-collider")]
fn body_with_velocity_moves_on_first_frame() {
let mut app = create_app();
app.insert_resource(Gravity::ZERO);
app.add_systems(Startup, |mut commands: Commands| {
commands.spawn((
SpatialBundle::default(),
RigidBody::Dynamic,
LinearVelocity(Vector::X),
#[cfg(feature = "2d")]
MassPropertiesBundle::new_computed(&Collider::circle(0.5), 1.0),
#[cfg(feature = "3d")]
MassPropertiesBundle::new_computed(&Collider::sphere(0.5), 1.0),
));
});
tick_60_fps(&mut app);
let mut app_query = app.world.query::<(&Position, &RigidBody)>();
let (pos, _body) = app_query.single(&app.world);
assert!(pos.x > 0.0);
}
#[test]
#[cfg(feature = "default-collider")]
fn body_with_velocity_moves() {
let mut app = create_app();
app.insert_resource(Gravity::ZERO);
app.add_systems(Startup, |mut commands: Commands| {
commands.spawn((
SpatialBundle::default(),
RigidBody::Dynamic,
LinearVelocity(Vector::X),
#[cfg(feature = "2d")]
MassPropertiesBundle::new_computed(&Collider::circle(0.5), 1.0),
#[cfg(feature = "3d")]
MassPropertiesBundle::new_computed(&Collider::sphere(0.5), 1.0),
));
});
const UPDATES: usize = 500;
for _ in 0..UPDATES {
tick_60_fps(&mut app);
}
let mut app_query = app.world.query::<(&Transform, &RigidBody)>();
let (transform, _body) = app_query.single(&app.world);
assert_relative_eq!(transform.translation.y, 0.);
assert_relative_eq!(transform.translation.z, 0.);
assert_relative_eq!(
transform.translation.x,
1. * UPDATES as f32 * 1. / 60.,
epsilon = 0.03 );
#[cfg(feature = "enhanced-determinism")]
{
setup_insta!();
assert_debug_snapshot!(transform);
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)]
struct Id(usize);
#[cfg(all(feature = "3d", feature = "enhanced-determinism"))]
#[test]
fn cubes_simulation_is_deterministic_across_machines() {
setup_insta!();
let mut app = create_app();
app.add_systems(Startup, setup_cubes_simulation);
const SECONDS: usize = 10;
const UPDATES: usize = 60 * SECONDS;
for _ in 0..UPDATES {
tick_60_fps(&mut app);
}
let mut app_query = app.world.query::<(&Id, &Transform)>();
let mut bodies: Vec<(&Id, &Transform)> = app_query.iter(&app.world).collect();
bodies.sort_by_key(|b| b.0);
assert_debug_snapshot!(bodies);
}
#[cfg(all(feature = "3d", feature = "default-collider"))]
#[test]
fn cubes_simulation_is_locally_deterministic() {
use itertools::Itertools;
fn run_cubes() -> Vec<(Id, Transform)> {
let mut app = create_app();
app.add_systems(Startup, setup_cubes_simulation);
const SECONDS: usize = 5;
const UPDATES: usize = 60 * SECONDS;
for _ in 0..UPDATES {
tick_60_fps(&mut app);
}
let mut app_query = app.world.query::<(&Id, &Transform)>();
let mut bodies: Vec<(Id, Transform)> = app_query
.iter(&app.world)
.map(|(id, transform)| (*id, *transform))
.collect();
bodies.sort_by_key(|b| b.0);
bodies
}
for (a, b) in (0..4).map(|_| run_cubes()).tuple_windows() {
assert_eq!(a, b);
}
}
#[test]
fn no_ambiguity_errors() {
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
struct DeterministicSchedule;
let mut app = App::new();
app.add_plugins((MinimalPlugins, PhysicsPlugins::new(DeterministicSchedule)));
#[cfg(feature = "async-collider")]
{
app.add_plugins((
bevy::asset::AssetPlugin::default(),
bevy::scene::ScenePlugin,
))
.init_resource::<Assets<Mesh>>();
}
app.edit_schedule(DeterministicSchedule, |s| {
s.set_build_settings(ScheduleBuildSettings {
ambiguity_detection: LogLevel::Error,
..default()
});
})
.add_systems(Update, |w: &mut World| {
w.run_schedule(DeterministicSchedule);
});
app.update();
}