diff --git a/assets/surf_playground.blend b/assets/surf_playground.blend new file mode 100644 index 0000000..b4788c4 Binary files /dev/null and b/assets/surf_playground.blend differ diff --git a/assets/surf_playground.glb b/assets/surf_playground.glb new file mode 100644 index 0000000..f1c1a76 Binary files /dev/null and b/assets/surf_playground.glb differ diff --git a/examples/surf.rs b/examples/surf.rs new file mode 100644 index 0000000..1352cc9 --- /dev/null +++ b/examples/surf.rs @@ -0,0 +1,219 @@ +use std::f32::consts::TAU; + +use bevy::{ + gltf::{GltfMesh, GltfNode}, + gltf::Gltf, + math::Vec3Swizzles, + prelude::*, + window::CursorGrabMode, +}; +use bevy_rapier3d::prelude::*; + +use bevy_fps_controller::controller::*; + +const SPAWN_POINT: Vec3 = Vec3::new(0.0, 1.0, 0.0); + +fn main() { + App::new() + .insert_resource(AmbientLight { + color: Color::WHITE, + brightness: 0.5, + }) + .insert_resource(ClearColor(Color::hex("D4F5F5").unwrap())) + .insert_resource(RapierConfiguration::default()) + .add_plugins(DefaultPlugins) + .add_plugins(RapierPhysicsPlugin::::default()) + // .add_plugins(RapierDebugRenderPlugin::default()) + .add_plugins(FpsControllerPlugin) + .add_systems(Startup, setup) + .add_systems(Update, (manage_cursor, scene_colliders, display_text, respawn)) + .run(); +} + +fn setup( + mut commands: Commands, + mut window: Query<&mut Window>, + assets: Res, +) { + let mut window = window.single_mut(); + window.title = String::from("Minimal FPS Controller Example"); + // commands.spawn(Window { title: "Minimal FPS Controller Example".to_string(), ..default() }); + + commands.spawn(DirectionalLightBundle { + directional_light: DirectionalLight { + illuminance: 6000.0, + shadows_enabled: true, + ..default() + }, + transform: Transform::from_xyz(4.0, 7.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }); + + // Note that we have two entities for the player + // One is a "logical" player that handles the physics computation and collision + // The other is a "render" player that is what is displayed to the user + // This distinction is useful for later on if you want to add multiplayer, + // where often time these two ideas are not exactly synced up + let logical_entity = commands + .spawn(( + Collider::capsule(Vec3::Y * 0.5, Vec3::Y * 1.5, 0.5), + Friction { + coefficient: 0.0, + combine_rule: CoefficientCombineRule::Min, + }, + Restitution { + coefficient: 0.0, + combine_rule: CoefficientCombineRule::Min, + }, + ActiveEvents::COLLISION_EVENTS, + Velocity::zero(), + RigidBody::Dynamic, + Sleeping::disabled(), + LockedAxes::ROTATION_LOCKED, + AdditionalMassProperties::Mass(1.0), + GravityScale(0.0), + Ccd { enabled: true }, // Prevent clipping when going fast + TransformBundle::from_transform(Transform::from_translation(SPAWN_POINT)), + LogicalPlayer, + FpsControllerInput { + pitch: -TAU / 12.0, + yaw: TAU * 5.0 / 8.0, + ..default() + }, + FpsController { + air_acceleration: 80.0, + ..default() + }, + )) + .insert(CameraConfig { + height_offset: 0.0, + radius_scale: 0.75, + }) + .id(); + + commands.spawn(( + Camera3dBundle { + projection: Projection::Perspective(PerspectiveProjection { + fov: TAU / 5.0, + ..default() + }), + ..default() + }, + RenderPlayer { logical_entity }, + )); + + commands.insert_resource(MainScene { + handle: assets.load("surf_playground.glb"), + is_loaded: false, + }); + + commands.spawn(TextBundle::from_section( + "", + TextStyle { + font: assets.load("fira_mono.ttf"), + font_size: 24.0, + color: Color::BLACK, + }, + ).with_style(Style { + position_type: PositionType::Absolute, + top: Val::Px(5.0), + left: Val::Px(5.0), + ..default() + })); +} + +fn respawn( + mut query: Query<(&mut Transform, &mut Velocity)>, +) { + for (mut transform, mut velocity) in &mut query { + if transform.translation.y > -50.0 { + continue; + } + + velocity.linvel = Vec3::ZERO; + transform.translation = SPAWN_POINT; + } +} + +#[derive(Resource)] +struct MainScene { + handle: Handle, + is_loaded: bool, +} + +fn scene_colliders( + mut commands: Commands, + mut main_scene: ResMut, + gltf_assets: Res>, + gltf_mesh_assets: Res>, + gltf_node_assets: Res>, + mesh_assets: Res>, +) { + if main_scene.is_loaded { + return; + } + + let gltf = gltf_assets.get(&main_scene.handle); + + if let Some(gltf) = gltf { + let scene = gltf.scenes.first().unwrap().clone(); + commands.spawn(SceneBundle { + scene, + ..default() + }); + for node in &gltf.nodes { + let node = gltf_node_assets.get(node).unwrap(); + if let Some(gltf_mesh) = node.mesh.clone() { + let gltf_mesh = gltf_mesh_assets.get(&gltf_mesh).unwrap(); + for mesh_primitive in &gltf_mesh.primitives { + let mesh = mesh_assets.get(&mesh_primitive.mesh).unwrap(); + commands.spawn(( + Collider::from_bevy_mesh(mesh, &ComputedColliderShape::TriMesh).unwrap(), + RigidBody::Fixed, + TransformBundle::from_transform(node.transform), + )); + } + } + } + main_scene.is_loaded = true; + } +} + +fn manage_cursor( + btn: Res>, + key: Res>, + mut window_query: Query<&mut Window>, + mut controller_query: Query<&mut FpsController>, +) { + let mut window = window_query.single_mut(); + if btn.just_pressed(MouseButton::Left) { + window.cursor.grab_mode = CursorGrabMode::Locked; + window.cursor.visible = false; + for mut controller in &mut controller_query { + controller.enable_input = true; + } + } + if key.just_pressed(KeyCode::Escape) { + window.cursor.grab_mode = CursorGrabMode::None; + window.cursor.visible = true; + for mut controller in &mut controller_query { + controller.enable_input = false; + } + } +} + +fn display_text( + mut controller_query: Query<(&Transform, &Velocity)>, + mut text_query: Query<&mut Text>, +) { + for (transform, velocity) in &mut controller_query { + for mut text in &mut text_query { + text.sections[0].value = format!( + "vel: {:.2}, {:.2}, {:.2}\npos: {:.2}, {:.2}, {:.2}\nspd: {:.2}", + velocity.linvel.x, velocity.linvel.y, velocity.linvel.z, + transform.translation.x, transform.translation.y, transform.translation.z, + velocity.linvel.xz().length() + ); + } + } +} diff --git a/src/controller.rs b/src/controller.rs index b55d89a..873968e 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -125,6 +125,7 @@ pub struct FpsController { pub key_jump: KeyCode, pub key_fly: KeyCode, pub key_crouch: KeyCode, + pub surf_allowed: bool, } impl Default for FpsController { @@ -171,6 +172,7 @@ impl Default for FpsController { key_fly: KeyCode::F, key_crouch: KeyCode::ControlLeft, sensitivity: 0.001, + surf_allowed: true, } } } @@ -306,7 +308,8 @@ pub fn fps_controller_move( wish_speed = f32::min(wish_speed, max_speed); if let Some((toi, toi_details)) = toi_details_unwrap(ground_cast) { - let has_traction = Vec3::dot(toi_details.normal1, Vec3::Y) > controller.traction_normal_cutoff; + let traction = Vec3::dot(toi_details.normal1, Vec3::Y); + let has_traction = traction > controller.traction_normal_cutoff; // Only apply friction after at least one tick, allows b-hopping without losing speed if controller.ground_tick >= 1 && has_traction { @@ -331,8 +334,9 @@ pub fn fps_controller_move( controller.acceleration, velocity.linvel, dt, + controller.max_air_speed, ); - if !has_traction { + if !has_traction { add.y -= controller.gravity * dt; } velocity.linvel += add; @@ -358,6 +362,7 @@ pub fn fps_controller_move( controller.air_acceleration, velocity.linvel, dt, + controller.max_air_speed, ); add.y = -controller.gravity * dt; velocity.linvel += add; @@ -467,7 +472,7 @@ fn overhang_component(entity: Entity, transform: &Transform, physics_context: &R None } -fn acceleration(wish_direction: Vec3, wish_speed: f32, acceleration: f32, velocity: Vec3, dt: f32) -> Vec3 { +fn acceleration(wish_direction: Vec3, wish_speed: f32, acceleration: f32, velocity: Vec3, dt: f32, max_speed: f32) -> Vec3 { let velocity_projection = Vec3::dot(velocity, wish_direction); let add_speed = wish_speed - velocity_projection; if add_speed <= 0.0 { @@ -475,7 +480,11 @@ fn acceleration(wish_direction: Vec3, wish_speed: f32, acceleration: f32, veloci } let acceleration_speed = f32::min(acceleration * wish_speed * dt, add_speed); - wish_direction * acceleration_speed + let result = wish_direction * acceleration_speed; + if result.length() > max_speed { + return Vec3::ZERO; + } + result } fn get_pressed(key_input: &Res>, key: KeyCode) -> f32 {