Skip to content

Commit f850272

Browse files
FPS Character example (#476)
Co-authored-by: Thierry Berger <contact@thierryberger.com>
1 parent b77d846 commit f850272

File tree

3 files changed

+225
-2
lines changed

3 files changed

+225
-2
lines changed

.github/workflows/main.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ name: Rust
22

33
on:
44
push:
5-
branches: [ master ]
5+
branches: [master]
66
pull_request:
7-
branches: [ master ]
7+
branches: [master]
88

99
env:
1010
CARGO_TERM_COLOR: always
11+
RUST_CACHE_KEY: rust-cache-20240617
1112

1213
jobs:
1314
check-fmt:
@@ -29,6 +30,8 @@ jobs:
2930
with:
3031
components: clippy
3132
- uses: Swatinem/rust-cache@v2
33+
with:
34+
prefix-key: ${{ env.RUST_CACHE_KEY }}
3235
- run: sudo apt update && sudo apt-get install pkg-config libx11-dev libasound2-dev libudev-dev
3336
- name: Clippy for bevy_rapier2d
3437
run: cargo clippy --verbose -p bevy_rapier2d
@@ -53,6 +56,8 @@ jobs:
5356
components: clippy
5457
targets: wasm32-unknown-unknown
5558
- uses: Swatinem/rust-cache@v2
59+
with:
60+
prefix-key: ${{ env.RUST_CACHE_KEY }}
5661
- name: Clippy bevy_rapier2d
5762
run: cd bevy_rapier2d && cargo clippy --verbose --features wasm-bindgen,bevy/webgl2 --target wasm32-unknown-unknown
5863
- name: Clippy bevy_rapier3d

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and new features. Please have a look at the
1818

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

2223
### Fix
2324

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
use bevy::{
2+
input::{mouse::MouseMotion, InputSystem},
3+
prelude::*,
4+
};
5+
use bevy_rapier3d::{control::KinematicCharacterController, prelude::*};
6+
7+
const MOUSE_SENSITIVITY: f32 = 0.3;
8+
const GROUND_TIMER: f32 = 0.5;
9+
const MOVEMENT_SPEED: f32 = 8.0;
10+
const JUMP_SPEED: f32 = 20.0;
11+
const GRAVITY: f32 = -9.81;
12+
13+
fn main() {
14+
App::new()
15+
.insert_resource(ClearColor(Color::srgb(
16+
0xF9 as f32 / 255.0,
17+
0xF9 as f32 / 255.0,
18+
0xFF as f32 / 255.0,
19+
)))
20+
.init_resource::<MovementInput>()
21+
.init_resource::<LookInput>()
22+
.add_plugins((
23+
DefaultPlugins,
24+
RapierPhysicsPlugin::<NoUserData>::default(),
25+
RapierDebugRenderPlugin::default(),
26+
))
27+
.add_systems(Startup, (setup_player, setup_map))
28+
.add_systems(PreUpdate, handle_input.after(InputSystem))
29+
.add_systems(Update, player_look)
30+
.add_systems(FixedUpdate, player_movement)
31+
.run();
32+
}
33+
34+
pub fn setup_player(mut commands: Commands) {
35+
commands
36+
.spawn((
37+
SpatialBundle {
38+
transform: Transform::from_xyz(0.0, 5.0, 0.0),
39+
..default()
40+
},
41+
Collider::round_cylinder(0.9, 0.3, 0.2),
42+
KinematicCharacterController {
43+
custom_mass: Some(5.0),
44+
up: Vec3::Y,
45+
offset: CharacterLength::Absolute(0.01),
46+
slide: true,
47+
autostep: Some(CharacterAutostep {
48+
max_height: CharacterLength::Relative(0.3),
49+
min_width: CharacterLength::Relative(0.5),
50+
include_dynamic_bodies: false,
51+
}),
52+
// Don’t allow climbing slopes larger than 45 degrees.
53+
max_slope_climb_angle: 45.0_f32.to_radians(),
54+
// Automatically slide down on slopes smaller than 30 degrees.
55+
min_slope_slide_angle: 30.0_f32.to_radians(),
56+
apply_impulse_to_dynamic_bodies: true,
57+
snap_to_ground: None,
58+
..default()
59+
},
60+
))
61+
.with_children(|b| {
62+
// FPS Camera
63+
b.spawn(Camera3dBundle {
64+
transform: Transform::from_xyz(0.0, 0.2, -0.1),
65+
..Default::default()
66+
});
67+
});
68+
}
69+
70+
fn setup_map(mut commands: Commands) {
71+
/*
72+
* Ground
73+
*/
74+
let ground_size = 50.0;
75+
let ground_height = 0.1;
76+
77+
commands.spawn((
78+
TransformBundle::from(Transform::from_xyz(0.0, -ground_height, 0.0)),
79+
Collider::cuboid(ground_size, ground_height, ground_size),
80+
));
81+
/*
82+
* Stairs
83+
*/
84+
let stair_len = 30;
85+
let stair_step = 0.2;
86+
for i in 1..=stair_len {
87+
let step = i as f32;
88+
let collider = Collider::cuboid(1.0, step * stair_step, 1.0);
89+
commands.spawn((
90+
TransformBundle::from(Transform::from_xyz(
91+
40.0,
92+
step * stair_step,
93+
step * 2.0 - 20.0,
94+
)),
95+
collider.clone(),
96+
));
97+
commands.spawn((
98+
TransformBundle::from(Transform::from_xyz(
99+
-40.0,
100+
step * stair_step,
101+
step * -2.0 + 20.0,
102+
)),
103+
collider.clone(),
104+
));
105+
commands.spawn((
106+
TransformBundle::from(Transform::from_xyz(
107+
step * 2.0 - 20.0,
108+
step * stair_step,
109+
40.0,
110+
)),
111+
collider.clone(),
112+
));
113+
commands.spawn((
114+
TransformBundle::from(Transform::from_xyz(
115+
step * -2.0 + 20.0,
116+
step * stair_step,
117+
-40.0,
118+
)),
119+
collider.clone(),
120+
));
121+
}
122+
}
123+
124+
/// Keyboard input vector
125+
#[derive(Default, Resource, Deref, DerefMut)]
126+
struct MovementInput(Vec3);
127+
128+
/// Mouse input vector
129+
#[derive(Default, Resource, Deref, DerefMut)]
130+
struct LookInput(Vec2);
131+
132+
fn handle_input(
133+
keyboard: Res<ButtonInput<KeyCode>>,
134+
mut movement: ResMut<MovementInput>,
135+
mut look: ResMut<LookInput>,
136+
mut mouse_events: EventReader<MouseMotion>,
137+
) {
138+
if keyboard.pressed(KeyCode::KeyW) {
139+
movement.z -= 1.0;
140+
}
141+
if keyboard.pressed(KeyCode::KeyS) {
142+
movement.z += 1.0
143+
}
144+
if keyboard.pressed(KeyCode::KeyA) {
145+
movement.x -= 1.0;
146+
}
147+
if keyboard.pressed(KeyCode::KeyD) {
148+
movement.x += 1.0
149+
}
150+
**movement = movement.normalize_or_zero();
151+
if keyboard.pressed(KeyCode::ShiftLeft) {
152+
**movement *= 2.0;
153+
}
154+
if keyboard.pressed(KeyCode::Space) {
155+
movement.y = 1.0;
156+
}
157+
158+
for event in mouse_events.read() {
159+
look.x -= event.delta.x * MOUSE_SENSITIVITY;
160+
look.y -= event.delta.y * MOUSE_SENSITIVITY;
161+
look.y = look.y.clamp(-89.9, 89.9); // Limit pitch
162+
}
163+
}
164+
165+
fn player_movement(
166+
time: Res<Time>,
167+
mut input: ResMut<MovementInput>,
168+
mut player: Query<(
169+
&mut Transform,
170+
&mut KinematicCharacterController,
171+
Option<&KinematicCharacterControllerOutput>,
172+
)>,
173+
mut vertical_movement: Local<f32>,
174+
mut grounded_timer: Local<f32>,
175+
) {
176+
let Ok((transform, mut controller, output)) = player.get_single_mut() else {
177+
return;
178+
};
179+
let delta_time = time.delta_seconds();
180+
// Retrieve input
181+
let mut movement = Vec3::new(input.x, 0.0, input.z) * MOVEMENT_SPEED;
182+
let jump_speed = input.y * JUMP_SPEED;
183+
// Clear input
184+
**input = Vec3::ZERO;
185+
// Check physics ground check
186+
if output.map(|o| o.grounded).unwrap_or(false) {
187+
*grounded_timer = GROUND_TIMER;
188+
*vertical_movement = 0.0;
189+
}
190+
// If we are grounded we can jump
191+
if *grounded_timer > 0.0 {
192+
*grounded_timer -= delta_time;
193+
// If we jump we clear the grounded tolerance
194+
if jump_speed > 0.0 {
195+
*vertical_movement = jump_speed;
196+
*grounded_timer = 0.0;
197+
}
198+
}
199+
movement.y = *vertical_movement;
200+
*vertical_movement += GRAVITY * delta_time * controller.custom_mass.unwrap_or(1.0);
201+
controller.translation = Some(transform.rotation * (movement * delta_time));
202+
}
203+
204+
fn player_look(
205+
mut player: Query<&mut Transform, (With<KinematicCharacterController>, Without<Camera>)>,
206+
mut camera: Query<&mut Transform, With<Camera>>,
207+
input: Res<LookInput>,
208+
) {
209+
let Ok(mut transform) = player.get_single_mut() else {
210+
return;
211+
};
212+
transform.rotation = Quat::from_axis_angle(Vec3::Y, input.x.to_radians());
213+
let Ok(mut transform) = camera.get_single_mut() else {
214+
return;
215+
};
216+
transform.rotation = Quat::from_axis_angle(Vec3::X, input.y.to_radians());
217+
}

0 commit comments

Comments
 (0)