bevy_2d_box_physics 0.1.1

A 2D box-collision physics engine for use with the bevy engine
Documentation
use bevy::prelude::*;

/// A component describing what type of RigidBody we want to use (Static can't
/// move, Kinematic doesn't have gravity, but is movable and Dynamic is movable
/// and has gravity)
#[derive(Component, Debug)]
pub enum RigidBody {
    Static,
    Kinematic,
    Dynamic,
}

impl Default for RigidBody {
    fn default() -> Self {
        RigidBody::Static
    }
}

/// A component defining the velocity in the current frame
#[derive(Component, Default, Debug)]
pub struct Velocity(Vec2);

impl Velocity {
    /// Add the velocity corresponding to the vector v
    pub fn add_velocity(&mut self, v: &Vec2) {
        self.0 += *v;
    }

    /// Set the velocity to correspond the the vector v
    pub fn set_velocity(&mut self, v: &Vec2) {
        self.0 = *v;
    }

    /// Set the velocity in direction x
    pub fn set_x(&mut self, x: f32) {
        self.0.x = x;
    }

    /// Set the velocity in direction y
    pub fn set_y(&mut self, y: f32) {
        self.0.y = y;
    }

    /// The velocity in direction x
    pub fn x(&self) -> f32 {
        self.0.x
    }

    /// The velocity in direction y
    pub fn y(&self) -> f32 {
        self.0.y
    }

    /// Returns true if there is any velocity in direction x, and false if there
    /// is
    pub fn is_still_x(&self) -> bool {
        self.0.x == 0.0
    }

    /// Returns true if there is any velocity at all, and false if there is not
    pub fn is_still(&self) -> bool {
        self.0 == Vec2::ZERO
    }
}

/// A bundle for physics-movement, without collision included
#[derive(Bundle, Default)]
pub struct PhysicsBundle {
    pub rigidbody: RigidBody,
    pub velocity: Velocity,
}

/// A resource consisting of options for the physics-engine
#[derive(Resource)]
pub struct PhysicsConfiguration {
    pub paused: bool,
    pub show_collision_boxes: bool,
}

/// A resource defining the gravity direction and strength
#[derive(Resource)]
pub struct Gravity(pub Vec2);

pub fn apply_gravity(
    mut query: Query<(&mut Velocity, &RigidBody), With<RigidBody>>,
    time: Res<Time>,
    gravity: Res<Gravity>,
    physics_config: Res<PhysicsConfiguration>,
) {
    let g = gravity.0;
    let dt = time.delta_seconds();
    for (mut v, rigidbody) in query.iter_mut() {
        if let RigidBody::Dynamic = rigidbody {
            if !physics_config.paused {
                v.add_velocity(&(g * dt));
            }
        }
    }
}

/// Applies the velocity to the transform with the rigidbody
#[allow(clippy::type_complexity)]
pub fn move_rigidbodies(
    mut query: Query<(&mut Transform, &Velocity), (Changed<Velocity>, With<RigidBody>)>,
    time: Res<Time>,
    physics_config: Res<PhysicsConfiguration>,
) {
    if !physics_config.paused {
        let dt = time.delta_seconds();
        for (mut transform, v) in query.iter_mut() {
            transform.translation += v.0.extend(0.0) * dt;
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::collision::*;
    use crate::tests::create_physics_app;
    use bevy::utils::Duration;

    #[test]
    //#[ignore]
    fn gravity_works() {
        let mut app = create_physics_app();
        let mut time = Time::default();
        time.update();
        app.world.insert_resource(time);

        let dynamic_id = app
            .world
            .spawn_empty()
            .insert(PhysicsBundle {
                rigidbody: RigidBody::Dynamic,
                ..default()
            })
            .insert(Transform::from_xyz(0.0, 0.0, 0.0))
            .id();
        let static_id = app
            .world
            .spawn_empty()
            .insert(PhysicsBundle {
                rigidbody: RigidBody::Static,
                ..default()
            })
            .insert(Transform::from_xyz(0.0, 0.0, 0.0))
            .id();

        let mut time = app.world.resource_mut::<Time>();
        let last_update = time.last_update().unwrap();
        time.update_with_instant(last_update + Duration::from_secs_f32(1.0));

        app.update();
        // After updating gravity should have worked on the entity
        // with the dynamic rigidbody, but not the one with the static
        // rigidbody.
        assert!(
            app.world
                .get::<Transform>(dynamic_id)
                .unwrap()
                .translation
                .y
                < 0.0
        );
        assert_eq!(
            app.world.get::<Transform>(static_id).unwrap().translation.y,
            0.0
        );
    }

    #[test]
    fn velocity_works_with_collision() {
        let mut app = create_physics_app();
        let mut time = Time::default();
        time.update();
        app.world.insert_resource(time);

        let dynamic_id = app
            .world
            .spawn_empty()
            .insert(PhysicsBundle {
                rigidbody: RigidBody::Kinematic,
                ..default()
            })
            .insert(CollisionBundle {
                collision_box: CollisionBox(8.0, 8.0),
                collision_layers: CollisionLayers {
                    layer_id: 0,
                    collides_with_ids: vec![1],
                },
                ..default()
            })
            .insert(Transform::from_xyz(0.0, 0.0, 0.0))
            .id();

        app.world
            .spawn_empty()
            .insert(PhysicsBundle {
                rigidbody: RigidBody::Static,
                ..default()
            })
            .insert(CollisionBundle {
                collision_box: CollisionBox(8.0, 8.0),
                collision_layers: CollisionLayers {
                    layer_id: 1,
                    collides_with_ids: vec![0],
                },
                ..default()
            })
            .insert(Transform::from_xyz(20.0, 0.0, 0.0));

        // Set the delta time
        let mut time = app.world.resource_mut::<Time>();
        let last_update = time.last_update().unwrap();
        time.update_with_instant(last_update + Duration::from_secs_f32(1.1));

        // Limit velocity when collision on the right
        app.world
            .get_mut::<Velocity>(dynamic_id)
            .unwrap()
            .set_velocity(&Vec2::new(10.0, 0.0));
        app.update();
        assert_eq!(
            app.world
                .get::<Transform>(dynamic_id)
                .unwrap()
                .translation
                .x,
            4.0
        );

        // Check that the object stays there the next frame as well
        app.update();
        assert_eq!(
            app.world
                .get::<Transform>(dynamic_id)
                .unwrap()
                .translation
                .x,
            4.0
        );

        // Limit velocity when collision on the left
        app.world
            .get_mut::<Transform>(dynamic_id)
            .unwrap()
            .translation
            .x = 40.0;
        app.world
            .get_mut::<Velocity>(dynamic_id)
            .unwrap()
            .set_velocity(&Vec2::new(-10.0, 0.0));
        app.update();
        assert_eq!(
            app.world
                .get::<Transform>(dynamic_id)
                .unwrap()
                .translation
                .x,
            36.0
        );

        // Limit velocity when collision at bottom
        app.world
            .get_mut::<Transform>(dynamic_id)
            .unwrap()
            .translation = Vec3::new(10.0, 20.0, 0.0);
        app.world
            .get_mut::<Velocity>(dynamic_id)
            .unwrap()
            .set_velocity(&Vec2::new(0.0, -10.0));
        app.update();
        assert_eq!(
            app.world
                .get::<Transform>(dynamic_id)
                .unwrap()
                .translation
                .y,
            16.0
        );

        // Limit velocity when collision at top
        app.world
            .get_mut::<Transform>(dynamic_id)
            .unwrap()
            .translation
            .y = -20.0;
        app.world
            .get_mut::<Velocity>(dynamic_id)
            .unwrap()
            .set_velocity(&Vec2::new(0.0, 10.0));
        app.update();
        assert_eq!(
            app.world
                .get::<Transform>(dynamic_id)
                .unwrap()
                .translation
                .y,
            -16.0
        );
    }
}