use bevy::prelude::*;
use nalgebra::SVector;
use symtropy_physics::{body::BodyHandle, CollisionEvent, PhysicsCallback, PhysicsWorld};
#[derive(Resource)]
pub struct BevyPhysics<const D: usize> {
pub world: PhysicsWorld<D>,
}
impl<const D: usize> Default for BevyPhysics<D> {
fn default() -> Self {
Self {
world: PhysicsWorld::new(SVector::zeros()),
}
}
}
impl<const D: usize> BevyPhysics<D> {
pub fn with_gravity(gravity: [f64; D]) -> Self {
Self {
world: PhysicsWorld::new(SVector::from(gravity)),
}
}
}
#[derive(Component)]
pub struct PhysicsBody {
pub handle: BodyHandle,
pub visual_radius: f32,
}
impl PhysicsBody {
pub fn new(handle: BodyHandle, visual_radius: f32) -> Self {
Self { handle, visual_radius }
}
}
#[derive(Resource, Default)]
pub struct NoCouplingResource;
impl<const D: usize> PhysicsCallback<D> for NoCouplingResource {
fn modulate_force(&self, _: BodyHandle, force: &SVector<f64, D>) -> SVector<f64, D> {
*force
}
fn modulate_impulse(&self, impulse: f64, _: &SVector<f64, D>) -> f64 {
impulse
}
fn friction_multiplier(&self, _: &SVector<f64, D>, _: BodyHandle) -> f64 {
1.0
}
fn on_collision(&mut self, _: &CollisionEvent<D>) {}
fn record_dissipation(&mut self, _: f64) {}
}
pub fn step_physics<const D: usize, C: PhysicsCallback<D> + Resource>(
mut physics: ResMut<BevyPhysics<D>>,
mut cb: ResMut<C>,
time: Res<Time<Fixed>>,
) {
physics
.world
.step_with_callback(time.delta_secs_f64(), &mut *cb);
}
pub fn sync_transforms<const D: usize>(
physics: Res<BevyPhysics<D>>,
mut query: Query<(&PhysicsBody, &mut Transform)>,
) {
for (body_comp, mut transform) in &mut query {
if let Some(body) = physics.world.body(body_comp.handle) {
let pos = body.position();
if D >= 1 {
transform.translation.x = pos.coord(0) as f32;
}
if D >= 2 {
transform.translation.y = pos.coord(1) as f32;
}
if D >= 3 {
transform.translation.z = pos.coord(2) as f32;
}
}
}
}
pub struct BevyPhysicsCorePlugin<const D: usize> {
pub gravity: SVector<f64, D>,
}
impl<const D: usize> Default for BevyPhysicsCorePlugin<D> {
fn default() -> Self {
Self {
gravity: SVector::zeros(),
}
}
}
impl<const D: usize> BevyPhysicsCorePlugin<D> {
pub fn with_gravity(gravity: [f64; D]) -> Self {
Self {
gravity: SVector::from(gravity),
}
}
}
pub struct BevyPhysicsPlugin<const D: usize> {
pub gravity: SVector<f64, D>,
}
impl<const D: usize> Default for BevyPhysicsPlugin<D> {
fn default() -> Self {
Self {
gravity: SVector::zeros(),
}
}
}
impl<const D: usize> BevyPhysicsPlugin<D> {
pub fn with_gravity(gravity: [f64; D]) -> Self {
Self {
gravity: SVector::from(gravity),
}
}
}
macro_rules! impl_core_plugin {
($d:literal) => {
impl Plugin for BevyPhysicsCorePlugin<$d> {
fn build(&self, app: &mut App) {
app.insert_resource(BevyPhysics::<$d> {
world: PhysicsWorld::new(self.gravity),
});
app.add_systems(FixedUpdate, sync_transforms::<$d>);
}
}
};
}
macro_rules! impl_full_plugin {
($d:literal) => {
impl Plugin for BevyPhysicsPlugin<$d> {
fn build(&self, app: &mut App) {
app.insert_resource(BevyPhysics::<$d> {
world: PhysicsWorld::new(self.gravity),
});
app.insert_resource(NoCouplingResource);
app.add_systems(
FixedUpdate,
(
step_physics::<$d, NoCouplingResource>,
sync_transforms::<$d>,
)
.chain(),
);
}
}
};
}
impl_core_plugin!(2);
impl_core_plugin!(3);
impl_core_plugin!(4);
impl_full_plugin!(2);
impl_full_plugin!(3);
impl_full_plugin!(4);
#[cfg(test)]
mod tests {
use super::*;
use symtropy_math::Point;
#[test]
fn bevy_physics_default_has_zero_gravity_2d() {
let p = BevyPhysics::<2>::default();
let g = p.world.gravity;
assert_eq!(g[0], 0.0);
assert_eq!(g[1], 0.0);
}
#[test]
fn bevy_physics_with_gravity_stores_gravity() {
let p = BevyPhysics::<3>::with_gravity([0.0, -9.81, 0.0]);
let g = p.world.gravity;
assert!((g[1] - (-9.81)).abs() < 1e-9);
}
#[test]
fn physics_body_new_stores_handle_and_radius() {
let mut world = PhysicsWorld::<2>::new(SVector::zeros());
let h = world.add_sphere(Point::new([0.0, 0.0]), 1.0, 1.0);
let b = PhysicsBody::new(h, 0.5);
assert_eq!(b.handle, h);
assert!((b.visual_radius - 0.5).abs() < 1e-9);
}
#[test]
fn no_coupling_modulate_force_is_identity() {
let cb = NoCouplingResource;
let mut world = PhysicsWorld::<2>::new(SVector::zeros());
let h = world.add_sphere(Point::new([0.0, 0.0]), 1.0, 1.0);
let f_in = SVector::from([3.14, -2.71]);
let f_out = <NoCouplingResource as PhysicsCallback<2>>::modulate_force(&cb, h, &f_in);
assert_eq!(f_in, f_out);
}
#[test]
fn no_coupling_friction_multiplier_is_one() {
let cb = NoCouplingResource;
let mut world = PhysicsWorld::<3>::new(SVector::zeros());
let h = world.add_sphere(Point::new([0.0, 0.0, 0.0]), 1.0, 1.0);
let point = SVector::from([0.0, 0.0, 0.0]);
let mu = <NoCouplingResource as PhysicsCallback<3>>::friction_multiplier(&cb, &point, h);
assert!((mu - 1.0).abs() < 1e-9);
}
#[test]
fn core_plugin_2d_registers_resource() {
let mut app = App::new();
BevyPhysicsCorePlugin::<2>::default().build(&mut app);
assert!(app.world().contains_resource::<BevyPhysics<2>>());
}
#[test]
fn full_plugin_2d_registers_both_resources() {
let mut app = App::new();
BevyPhysicsPlugin::<2>::default().build(&mut app);
assert!(app.world().contains_resource::<BevyPhysics<2>>());
assert!(app.world().contains_resource::<NoCouplingResource>());
}
#[test]
fn full_plugin_3d_registers_both_resources() {
let mut app = App::new();
BevyPhysicsPlugin::<3>::default().build(&mut app);
assert!(app.world().contains_resource::<BevyPhysics<3>>());
assert!(app.world().contains_resource::<NoCouplingResource>());
}
#[test]
fn full_plugin_4d_registers_both_resources() {
let mut app = App::new();
BevyPhysicsPlugin::<4>::default().build(&mut app);
assert!(app.world().contains_resource::<BevyPhysics<4>>());
assert!(app.world().contains_resource::<NoCouplingResource>());
}
#[test]
fn plugin_with_gravity_resource_accessible() {
let mut app = App::new();
BevyPhysicsPlugin::<2>::with_gravity([0.0, -9.81]).build(&mut app);
let res = app.world().resource::<BevyPhysics<2>>();
let g = res.world.gravity;
assert!((g[1] - (-9.81)).abs() < 1e-9);
}
#[test]
fn manual_step_with_no_coupling_gravity_pulls_body_down() {
let mut p = BevyPhysics::<2>::with_gravity([0.0, -9.81]);
let h = p.world.add_sphere(Point::new([0.0, 10.0]), 1.0, 1.0);
let y0 = p.world.body(h).unwrap().position().coord(1);
let mut cb = NoCouplingResource;
for _ in 0..10 {
p.world.step_with_callback(1.0 / 60.0, &mut cb);
}
let y1 = p.world.body(h).unwrap().position().coord(1);
assert!(y1 < y0, "gravity should pull body down: y0={y0}, y1={y1}");
}
#[test]
fn can_add_multiple_bodies() {
let mut p = BevyPhysics::<3>::default();
let h1 = p.world.add_sphere(Point::new([0.0, 0.0, 0.0]), 1.0, 1.0);
let h2 = p.world.add_sphere(Point::new([5.0, 0.0, 0.0]), 1.0, 1.0);
assert!(p.world.body(h1).is_some());
assert!(p.world.body(h2).is_some());
}
}