Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FPS Character example #476

Merged
merged 8 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ name: Rust

on:
push:
branches: [ master ]
branches: [master]
pull_request:
branches: [ master ]
branches: [master]

env:
CARGO_TERM_COLOR: always
RUST_CACHE_KEY: rust-cache-20240617

jobs:
check-fmt:
Expand All @@ -29,6 +30,8 @@ jobs:
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ env.RUST_CACHE_KEY }}
- run: sudo apt update && sudo apt-get install pkg-config libx11-dev libasound2-dev libudev-dev
- name: Clippy for bevy_rapier2d
run: cargo clippy --verbose -p bevy_rapier2d
Expand All @@ -53,6 +56,8 @@ jobs:
components: clippy
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ env.RUST_CACHE_KEY }}
- name: Clippy bevy_rapier2d
run: cd bevy_rapier2d && cargo clippy --verbose --features wasm-bindgen,bevy/webgl2 --target wasm32-unknown-unknown
- name: Clippy bevy_rapier3d
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and new features. Please have a look at the

- Derive `Debug` for `LockedAxes`.
- Expose `is_sliding_down_slope` to both `MoveShapeOutput` and `KinematicCharacterControllerOutput`.
- Added a First Person Shooter `character_controller` example for `bevy_rapier3d`.

### Fix

Expand Down
217 changes: 217 additions & 0 deletions bevy_rapier3d/examples/character_controller3.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
use bevy::{
input::{mouse::MouseMotion, InputSystem},
prelude::*,
};
use bevy_rapier3d::{control::KinematicCharacterController, prelude::*};

const MOUSE_SENSITIVITY: f32 = 0.3;
const GROUND_TIMER: f32 = 0.5;
const MOVEMENT_SPEED: f32 = 8.0;
const JUMP_SPEED: f32 = 20.0;
const GRAVITY: f32 = -9.81;

fn main() {
App::new()
.insert_resource(ClearColor(Color::srgb(
0xF9 as f32 / 255.0,
0xF9 as f32 / 255.0,
0xFF as f32 / 255.0,
)))
.init_resource::<MovementInput>()
.init_resource::<LookInput>()
.add_plugins((
DefaultPlugins,
RapierPhysicsPlugin::<NoUserData>::default(),
RapierDebugRenderPlugin::default(),
))
.add_systems(Startup, (setup_player, setup_map))
.add_systems(PreUpdate, handle_input.after(InputSystem))
.add_systems(Update, player_look)
.add_systems(FixedUpdate, player_movement)
.run();
}

pub fn setup_player(mut commands: Commands) {
commands
.spawn((
SpatialBundle {
transform: Transform::from_xyz(0.0, 5.0, 0.0),
..default()
},
Collider::round_cylinder(0.9, 0.3, 0.2),
KinematicCharacterController {
custom_mass: Some(5.0),
up: Vec3::Y,
offset: CharacterLength::Absolute(0.01),
slide: true,
autostep: Some(CharacterAutostep {
max_height: CharacterLength::Relative(0.3),
min_width: CharacterLength::Relative(0.5),
include_dynamic_bodies: false,
}),
// Don’t allow climbing slopes larger than 45 degrees.
max_slope_climb_angle: 45.0_f32.to_radians(),
// Automatically slide down on slopes smaller than 30 degrees.
Vrixyz marked this conversation as resolved.
Show resolved Hide resolved
min_slope_slide_angle: 30.0_f32.to_radians(),
apply_impulse_to_dynamic_bodies: true,
snap_to_ground: None,
..default()
},
))
.with_children(|b| {
// FPS Camera
b.spawn(Camera3dBundle {
transform: Transform::from_xyz(0.0, 0.2, -0.1),
..Default::default()
});
});
}

fn setup_map(mut commands: Commands) {
/*
* Ground
*/
let ground_size = 50.0;
let ground_height = 0.1;

commands.spawn((
TransformBundle::from(Transform::from_xyz(0.0, -ground_height, 0.0)),
Collider::cuboid(ground_size, ground_height, ground_size),
));
/*
* Stairs
*/
let stair_len = 30;
let stair_step = 0.2;
for i in 1..=stair_len {
let step = i as f32;
let collider = Collider::cuboid(1.0, step * stair_step, 1.0);
commands.spawn((
TransformBundle::from(Transform::from_xyz(
40.0,
step * stair_step,
step * 2.0 - 20.0,
)),
collider.clone(),
));
commands.spawn((
TransformBundle::from(Transform::from_xyz(
-40.0,
step * stair_step,
step * -2.0 + 20.0,
)),
collider.clone(),
));
commands.spawn((
TransformBundle::from(Transform::from_xyz(
step * 2.0 - 20.0,
step * stair_step,
40.0,
)),
collider.clone(),
));
commands.spawn((
TransformBundle::from(Transform::from_xyz(
step * -2.0 + 20.0,
step * stair_step,
-40.0,
)),
collider.clone(),
));
}
}

/// Keyboard input vector
#[derive(Default, Resource, Deref, DerefMut)]
struct MovementInput(Vec3);

/// Mouse input vector
#[derive(Default, Resource, Deref, DerefMut)]
struct LookInput(Vec2);

fn handle_input(
keyboard: Res<ButtonInput<KeyCode>>,
mut movement: ResMut<MovementInput>,
mut look: ResMut<LookInput>,
mut mouse_events: EventReader<MouseMotion>,
) {
if keyboard.pressed(KeyCode::KeyW) {
movement.z -= 1.0;
}
if keyboard.pressed(KeyCode::KeyS) {
movement.z += 1.0
}
if keyboard.pressed(KeyCode::KeyA) {
movement.x -= 1.0;
}
if keyboard.pressed(KeyCode::KeyD) {
movement.x += 1.0
}
**movement = movement.normalize_or_zero();
if keyboard.pressed(KeyCode::ShiftLeft) {
**movement *= 2.0;
}
if keyboard.pressed(KeyCode::Space) {
movement.y = 1.0;
}

for event in mouse_events.read() {
look.x -= event.delta.x * MOUSE_SENSITIVITY;
look.y -= event.delta.y * MOUSE_SENSITIVITY;
look.y = look.y.clamp(-89.9, 89.9); // Limit pitch
}
}

fn player_movement(
time: Res<Time>,
mut input: ResMut<MovementInput>,
mut player: Query<(
&mut Transform,
&mut KinematicCharacterController,
Option<&KinematicCharacterControllerOutput>,
)>,
mut vertical_movement: Local<f32>,
mut grounded_timer: Local<f32>,
) {
let Ok((transform, mut controller, output)) = player.get_single_mut() else {
return;
};
let delta_time = time.delta_seconds();
// Retrieve input
let mut movement = Vec3::new(input.x, 0.0, input.z) * MOVEMENT_SPEED;
let jump_speed = input.y * JUMP_SPEED;
// Clear input
**input = Vec3::ZERO;
// Check physics ground check
if output.map(|o| o.grounded).unwrap_or(false) {
*grounded_timer = GROUND_TIMER;
*vertical_movement = 0.0;
}
// If we are grounded we can jump
if *grounded_timer > 0.0 {
*grounded_timer -= delta_time;
// If we jump we clear the grounded tolerance
if jump_speed > 0.0 {
*vertical_movement = jump_speed;
*grounded_timer = 0.0;
}
}
movement.y = *vertical_movement;
*vertical_movement += GRAVITY * delta_time * controller.custom_mass.unwrap_or(1.0);
controller.translation = Some(transform.rotation * (movement * delta_time));
}

fn player_look(
mut player: Query<&mut Transform, (With<KinematicCharacterController>, Without<Camera>)>,
mut camera: Query<&mut Transform, With<Camera>>,
input: Res<LookInput>,
) {
let Ok(mut transform) = player.get_single_mut() else {
return;
};
transform.rotation = Quat::from_axis_angle(Vec3::Y, input.x.to_radians());
let Ok(mut transform) = camera.get_single_mut() else {
return;
};
transform.rotation = Quat::from_axis_angle(Vec3::X, input.y.to_radians());
}