From e142b2fbb4ac95c352981ee225ef7af645e1cf56 Mon Sep 17 00:00:00 2001 From: UndefinedBHVR Date: Wed, 25 Dec 2024 17:52:31 -0600 Subject: [PATCH 1/2] Collide and slide 3d v2 implementation. --- crates/avian3d/Cargo.toml | 4 + .../examples/collide_and_slide_3d/main.rs | 70 ++ .../examples/collide_and_slide_3d/plugin.rs | 602 ++++++++++++++++++ 3 files changed, 676 insertions(+) create mode 100644 crates/avian3d/examples/collide_and_slide_3d/main.rs create mode 100644 crates/avian3d/examples/collide_and_slide_3d/plugin.rs diff --git a/crates/avian3d/Cargo.toml b/crates/avian3d/Cargo.toml index 3685afa2..51698e48 100644 --- a/crates/avian3d/Cargo.toml +++ b/crates/avian3d/Cargo.toml @@ -100,6 +100,10 @@ required-features = ["3d", "default-collider", "bevy_scene"] name = "kinematic_character_3d" required-features = ["3d", "default-collider", "bevy_scene"] +[[example]] +name = "collide_and_slide_3d" +required-features = ["3d", "default-collider", "bevy_scene"] + [[example]] name = "cast_ray_predicate" required-features = ["3d", "default-collider"] diff --git a/crates/avian3d/examples/collide_and_slide_3d/main.rs b/crates/avian3d/examples/collide_and_slide_3d/main.rs new file mode 100644 index 00000000..0dbfe453 --- /dev/null +++ b/crates/avian3d/examples/collide_and_slide_3d/main.rs @@ -0,0 +1,70 @@ +//! A kinematic character controller implementation based on collide-and-slide +//! with multi-pass collision detection and response. + +mod plugin; + +use avian3d::prelude::*; +use bevy::prelude::*; +use examples_common_3d::ExampleCommonPlugin; +use plugin::*; + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins, + ExampleCommonPlugin, + PhysicsPlugins::default(), + CharacterControllerPlugin, + )) + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + assets: Res, +) { + // Player with new character controller bundle + commands.spawn(( + Mesh3d(meshes.add(Capsule3d::new(0.4, 1.0))), + MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))), + Transform::from_xyz(0.0, 1.5, 0.0), + CharacterControllerBundle::default(), + )); + + // A cube to move around + commands.spawn(( + RigidBody::Dynamic, + Collider::cuboid(1.0, 1.0, 1.0), + Mesh3d(meshes.add(Cuboid::default())), + MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))), + Transform::from_xyz(3.0, 2.0, 3.0), + )); + + // Environment (see the `collider_constructors` example for creating colliders from scenes) + commands.spawn(( + SceneRoot(assets.load("character_controller_demo.glb#Scene0")), + Transform::from_rotation(Quat::from_rotation_y(-std::f32::consts::PI * 0.5)), + ColliderConstructorHierarchy::new(ColliderConstructor::ConvexHullFromMesh), + RigidBody::Static, + )); + + // Light + commands.spawn(( + PointLight { + intensity: 2_000_000.0, + range: 50.0, + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(0.0, 15.0, 0.0), + )); + + // Camera + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-7.0, 9.5, 15.0).looking_at(Vec3::ZERO, Vec3::Y), + )); +} diff --git a/crates/avian3d/examples/collide_and_slide_3d/plugin.rs b/crates/avian3d/examples/collide_and_slide_3d/plugin.rs new file mode 100644 index 00000000..2922b629 --- /dev/null +++ b/crates/avian3d/examples/collide_and_slide_3d/plugin.rs @@ -0,0 +1,602 @@ +use avian3d::{math::*, prelude::*}; +use bevy::prelude::*; + +pub struct CharacterControllerPlugin; + +impl Plugin for CharacterControllerPlugin { + fn build(&self, app: &mut App) { + app.add_event::() + .add_systems( + Update, + (keyboard_input, gamepad_input, handle_movement_actions).chain(), + ) + .add_systems( + PostUpdate, + ( + ground_detection_system, + gravity_system, + process_movement_passes, + update_kinematic_character_controller, + ) + .chain(), + ); + } +} + +#[derive(Event)] +pub enum MovementAction { + Move(Vector2), + Jump, +} + +#[derive(Component)] +pub struct KinematicCharacterController { + pub prev_velocity: Vector, + pub velocity: Vector, + pub collider: Collider, +} + +impl Default for KinematicCharacterController { + fn default() -> Self { + Self { + prev_velocity: Vector::ZERO, + velocity: Vector::ZERO, + collider: Collider::capsule(0.4, 0.8), + } + } +} + +#[derive(Component)] +pub struct KCCGrounded { + pub grounded: bool, + pub prev_grounded: bool, +} + +impl Default for KCCGrounded { + fn default() -> Self { + Self { + grounded: true, + prev_grounded: false, + } + } +} + +#[derive(Component)] +pub struct KCCGravity { + pub terminal_velocity: Scalar, + pub acceleration_factor: Scalar, + pub current_velocity: Vector, + pub direction: Vector, +} + +impl Default for KCCGravity { + fn default() -> Self { + Self { + terminal_velocity: 53.0, + acceleration_factor: 9.81 * 2.0, + current_velocity: Vector::ZERO, + direction: Vector::NEG_Y, + } + } +} + +#[derive(Component)] +pub struct KCCSlope { + pub max_slope_angle: Scalar, +} + +impl Default for KCCSlope { + fn default() -> Self { + Self { + max_slope_angle: (35.0 as Scalar).to_radians(), + } + } +} + +#[derive(Component)] +pub struct MovementSettings { + pub speed: Scalar, + pub jump_force: Scalar, +} + +impl Default for MovementSettings { + fn default() -> Self { + Self { + speed: 10.0, + jump_force: 7.0, + } + } +} + +#[derive(Component)] +pub struct KCCCoyoteTime { + pub timer: Timer, + pub can_jump: bool, +} + +impl Default for KCCCoyoteTime { + fn default() -> Self { + Self { + timer: Timer::from_seconds(0.1, TimerMode::Once), + can_jump: false, + } + } +} + +#[derive(Bundle)] +pub struct CharacterControllerBundle { + controller: KinematicCharacterController, + rigid_body: RigidBody, + grounded: KCCGrounded, + gravity: KCCGravity, + slope: KCCSlope, + movement: MovementSettings, // Add this field + coyote_time: KCCCoyoteTime, +} + +impl Default for CharacterControllerBundle { + fn default() -> Self { + Self { + controller: KinematicCharacterController::default(), + rigid_body: RigidBody::Kinematic, + grounded: KCCGrounded::default(), + gravity: KCCGravity::default(), + slope: KCCSlope::default(), + movement: MovementSettings::default(), // Add this field + coyote_time: KCCCoyoteTime::default(), + } + } +} + +// Movement constants +const MAX_BUMPS: u32 = 4; +const MIN_MOVEMENT: Scalar = 0.0001; +const COLLISION_EPSILON: Scalar = 0.01; +const DEPENETRATION_EPSILON: Scalar = 0.01; + +fn keyboard_input( + mut movement_event_writer: EventWriter, + keyboard_input: Res>, +) { + let up = keyboard_input.any_pressed([KeyCode::KeyW, KeyCode::ArrowUp]); + let down = keyboard_input.any_pressed([KeyCode::KeyS, KeyCode::ArrowDown]); + let left = keyboard_input.any_pressed([KeyCode::KeyA, KeyCode::ArrowLeft]); + let right = keyboard_input.any_pressed([KeyCode::KeyD, KeyCode::ArrowRight]); + + let horizontal = right as i8 - left as i8; + let vertical = up as i8 - down as i8; + let direction = Vector2::new(horizontal as Scalar, vertical as Scalar).clamp_length_max(1.0); + + if direction != Vector2::ZERO { + movement_event_writer.send(MovementAction::Move(direction)); + } + + if keyboard_input.just_pressed(KeyCode::Space) { + movement_event_writer.send(MovementAction::Jump); + } +} + +fn gamepad_input( + mut movement_event_writer: EventWriter, + gamepads: Query<&Gamepad>, +) { + for gamepad in gamepads.iter() { + if let (Some(x), Some(y)) = ( + gamepad.get(GamepadAxis::LeftStickX), + gamepad.get(GamepadAxis::LeftStickY), + ) { + movement_event_writer.send(MovementAction::Move( + Vector2::new(x as Scalar, y as Scalar).clamp_length_max(1.0), + )); + } + + if gamepad.just_pressed(GamepadButton::South) { + movement_event_writer.send(MovementAction::Jump); + } + } +} + +/// Processes incoming movement events and updates character controller states. +/// +/// # Parameters +/// * `time` - Time resource for delta time calculations +/// * `movement_events` - Event reader for movement input events +/// * `query` - Query for character components needed for movement processing +fn handle_movement_actions( + time: Res