Kinematic character controller (#11)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use avian3d::prelude::*;
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::{controls::Controls, physics_layers::GameLayer, player::PlayerMovement};
|
||||
use crate::{controller::PlayerMovement, controls::Controls, physics_layers::GameLayer};
|
||||
|
||||
#[derive(Component, Reflect, Debug)]
|
||||
pub struct CameraTarget;
|
||||
|
||||
539
src/controller.rs
Normal file
539
src/controller.rs
Normal file
@@ -0,0 +1,539 @@
|
||||
use avian3d::{math::*, prelude::*};
|
||||
use bevy::{ecs::query::Has, prelude::*};
|
||||
|
||||
use crate::player::PlayerRig;
|
||||
|
||||
pub struct CharacterControllerPlugin;
|
||||
|
||||
impl Plugin for CharacterControllerPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<PlayerMovement>();
|
||||
app.init_resource::<MovementSettings>();
|
||||
app.register_type::<MovementSettings>();
|
||||
app.register_type::<MovementDampingFactor>();
|
||||
app.register_type::<JumpImpulse>();
|
||||
app.register_type::<ControllerGravity>();
|
||||
app.register_type::<MovementAcceleration>();
|
||||
|
||||
app.add_event::<MovementAction>();
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
keyboard_input,
|
||||
gamepad_input,
|
||||
clear_movement_flag,
|
||||
brake_on_release,
|
||||
update_grounded,
|
||||
apply_gravity,
|
||||
movement,
|
||||
apply_movement_damping,
|
||||
)
|
||||
.chain(),
|
||||
);
|
||||
app.add_systems(
|
||||
// Run collision handling after collision detection.
|
||||
//
|
||||
// NOTE: The collision implementation here is very basic and a bit buggy.
|
||||
// A collide-and-slide algorithm would likely work better.
|
||||
PostProcessCollisions,
|
||||
kinematic_controller_collisions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An event sent for a movement input action.
|
||||
#[derive(Event)]
|
||||
pub enum MovementAction {
|
||||
Move(Vector2),
|
||||
Jump,
|
||||
}
|
||||
|
||||
/// A marker component indicating that an entity is using a character controller.
|
||||
#[derive(Component)]
|
||||
pub struct CharacterController;
|
||||
|
||||
/// A marker component indicating that an entity is on the ground.
|
||||
#[derive(Component)]
|
||||
#[component(storage = "SparseSet")]
|
||||
pub struct Grounded;
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub struct PlayerMovement {
|
||||
pub any_direction: bool,
|
||||
}
|
||||
|
||||
#[derive(Resource, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
struct MovementSettings {
|
||||
damping_normal: f32,
|
||||
damping_brake: f32,
|
||||
damping_brake_air: f32,
|
||||
}
|
||||
|
||||
// todo some duplicate with player.rs settings
|
||||
impl Default for MovementSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
damping_normal: 1.0,
|
||||
damping_brake: 30.0,
|
||||
damping_brake_air: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The acceleration used for character movement.
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct MovementAcceleration(Scalar);
|
||||
|
||||
/// The damping factor used for slowing down movement.
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct MovementDampingFactor(Scalar);
|
||||
|
||||
/// The strength of a jump.
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct JumpImpulse(Scalar);
|
||||
|
||||
/// The gravitational acceleration used for a character controller.
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct ControllerGravity(Vector);
|
||||
|
||||
/// The maximum angle a slope can have for a character controller
|
||||
/// to be able to climb and jump. If the slope is steeper than this angle,
|
||||
/// the character will slide down.
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct MaxSlopeAngle(Scalar);
|
||||
|
||||
/// A bundle that contains the components needed for a basic
|
||||
/// kinematic character controller.
|
||||
#[derive(Bundle)]
|
||||
pub struct CharacterControllerBundle {
|
||||
character_controller: CharacterController,
|
||||
rigid_body: RigidBody,
|
||||
collider: Collider,
|
||||
ground_caster: ShapeCaster,
|
||||
gravity: ControllerGravity,
|
||||
movement: MovementBundle,
|
||||
}
|
||||
|
||||
/// A bundle that contains components for character movement.
|
||||
#[derive(Bundle)]
|
||||
pub struct MovementBundle {
|
||||
acceleration: MovementAcceleration,
|
||||
damping: MovementDampingFactor,
|
||||
jump_impulse: JumpImpulse,
|
||||
max_slope_angle: MaxSlopeAngle,
|
||||
}
|
||||
|
||||
impl MovementBundle {
|
||||
pub const fn new(
|
||||
acceleration: Scalar,
|
||||
damping: Scalar,
|
||||
jump_impulse: Scalar,
|
||||
max_slope_angle: Scalar,
|
||||
) -> Self {
|
||||
Self {
|
||||
acceleration: MovementAcceleration(acceleration),
|
||||
damping: MovementDampingFactor(damping),
|
||||
jump_impulse: JumpImpulse(jump_impulse),
|
||||
max_slope_angle: MaxSlopeAngle(max_slope_angle),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MovementBundle {
|
||||
fn default() -> Self {
|
||||
Self::new(30.0, 0.9, 7.0, PI * 0.45)
|
||||
}
|
||||
}
|
||||
|
||||
impl CharacterControllerBundle {
|
||||
pub fn new(collider: Collider, gravity: Vector) -> Self {
|
||||
// Create shape caster as a slightly smaller version of collider
|
||||
let mut caster_shape = collider.clone();
|
||||
caster_shape.set_scale(Vector::ONE * 0.98, 10);
|
||||
|
||||
Self {
|
||||
character_controller: CharacterController,
|
||||
rigid_body: RigidBody::Kinematic,
|
||||
collider,
|
||||
ground_caster: ShapeCaster::new(
|
||||
caster_shape,
|
||||
Vector::ZERO,
|
||||
Quaternion::default(),
|
||||
Dir3::NEG_Y,
|
||||
)
|
||||
.with_max_distance(0.2),
|
||||
gravity: ControllerGravity(gravity),
|
||||
movement: MovementBundle::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_movement(
|
||||
mut self,
|
||||
acceleration: Scalar,
|
||||
damping: Scalar,
|
||||
jump_impulse: Scalar,
|
||||
max_slope_angle: Scalar,
|
||||
) -> Self {
|
||||
self.movement = MovementBundle::new(acceleration, damping, jump_impulse, max_slope_angle);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends [`MovementAction`] events based on keyboard input.
|
||||
fn keyboard_input(
|
||||
mut movement_event_writer: EventWriter<MovementAction>,
|
||||
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||
) {
|
||||
let up_binds = [KeyCode::KeyW, KeyCode::ArrowUp];
|
||||
let down_binds = [KeyCode::KeyS, KeyCode::ArrowDown];
|
||||
let left_binds = [KeyCode::KeyA, KeyCode::ArrowLeft];
|
||||
let right_binds = [KeyCode::KeyD, KeyCode::ArrowRight];
|
||||
|
||||
let up = keyboard_input.any_pressed(up_binds);
|
||||
let down = keyboard_input.any_pressed(down_binds);
|
||||
let left = keyboard_input.any_pressed(left_binds);
|
||||
let right = keyboard_input.any_pressed(right_binds);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends [`MovementAction`] events based on gamepad input.
|
||||
fn gamepad_input(
|
||||
mut movement_event_writer: EventWriter<MovementAction>,
|
||||
gamepads: Query<&Gamepad>,
|
||||
) {
|
||||
for gamepad in gamepads.iter() {
|
||||
if let (Some(x), Some(y)) = (
|
||||
gamepad.get(GamepadAxis::LeftStickX),
|
||||
gamepad.get(GamepadAxis::LeftStickY),
|
||||
) {
|
||||
let deadzone = 0.01;
|
||||
let dir = Vector2::new(x as Scalar, y as Scalar).clamp_length_max(1.0);
|
||||
|
||||
if dir.length_squared() > deadzone {
|
||||
movement_event_writer.send(MovementAction::Move(dir));
|
||||
}
|
||||
}
|
||||
|
||||
if gamepad.just_pressed(GamepadButton::South) {
|
||||
movement_event_writer.send(MovementAction::Jump);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply extra friction when no movement input is given
|
||||
/// In the original you stop instantly in this case
|
||||
fn brake_on_release(
|
||||
player_movement: Res<PlayerMovement>,
|
||||
movement_settings: Res<MovementSettings>,
|
||||
mut damping_q: Query<(Entity, &mut MovementDampingFactor)>,
|
||||
grounded_q: Query<&Grounded>,
|
||||
) {
|
||||
for (entity, mut damping) in &mut damping_q {
|
||||
let is_grounded = grounded_q.get(entity).is_ok();
|
||||
|
||||
if !player_movement.any_direction && is_grounded {
|
||||
damping.0 = movement_settings.damping_brake;
|
||||
} else if !player_movement.any_direction && !is_grounded {
|
||||
damping.0 = movement_settings.damping_brake_air;
|
||||
} else {
|
||||
damping.0 = movement_settings.damping_normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the [`Grounded`] status for character controllers.
|
||||
fn update_grounded(
|
||||
mut commands: Commands,
|
||||
mut query: Query<
|
||||
(Entity, &ShapeHits, &Rotation, Option<&MaxSlopeAngle>),
|
||||
With<CharacterController>,
|
||||
>,
|
||||
) {
|
||||
for (entity, hits, rotation, max_slope_angle) in &mut query {
|
||||
// The character is grounded if the shape caster has a hit with a normal
|
||||
// that isn't too steep.
|
||||
let is_grounded = hits.iter().any(|hit| {
|
||||
if let Some(angle) = max_slope_angle {
|
||||
(rotation * -hit.normal2).angle_between(Vector::Y).abs() <= angle.0
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
if is_grounded {
|
||||
commands.entity(entity).insert(Grounded);
|
||||
} else {
|
||||
commands.entity(entity).remove::<Grounded>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_movement_flag(
|
||||
mut player_movement: ResMut<PlayerMovement>,
|
||||
movement_event_reader: EventReader<MovementAction>,
|
||||
) {
|
||||
if movement_event_reader.is_empty() {
|
||||
player_movement.any_direction = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Responds to [`MovementAction`] events and moves character controllers accordingly.
|
||||
fn movement(
|
||||
time: Res<Time>,
|
||||
mut movement_event_reader: EventReader<MovementAction>,
|
||||
mut controllers: Query<(
|
||||
&MovementAcceleration,
|
||||
&JumpImpulse,
|
||||
&mut LinearVelocity,
|
||||
Has<Grounded>,
|
||||
)>,
|
||||
rig_transform_q: Option<Single<&GlobalTransform, With<PlayerRig>>>,
|
||||
mut player_movement: ResMut<PlayerMovement>,
|
||||
) {
|
||||
let delta_time = time.delta_secs();
|
||||
|
||||
for event in movement_event_reader.read() {
|
||||
for (movement_acceleration, jump_impulse, mut linear_velocity, is_grounded) in
|
||||
&mut controllers
|
||||
{
|
||||
match event {
|
||||
MovementAction::Move(direction) => {
|
||||
let mut direction = direction.extend(0.0).xzy();
|
||||
|
||||
if let Some(ref rig_transform) = rig_transform_q {
|
||||
direction = (rig_transform.forward() * direction.z)
|
||||
+ (rig_transform.right() * direction.x);
|
||||
}
|
||||
|
||||
linear_velocity.x -= direction.x * movement_acceleration.0 * delta_time;
|
||||
linear_velocity.z -= direction.z * movement_acceleration.0 * delta_time;
|
||||
|
||||
// Update movement flag
|
||||
let deadzone = 0.2;
|
||||
|
||||
// todo this is probably not necessary
|
||||
if player_movement.any_direction && direction.length_squared() < deadzone {
|
||||
player_movement.any_direction = false;
|
||||
}
|
||||
|
||||
if !player_movement.any_direction && direction.length_squared() > deadzone {
|
||||
player_movement.any_direction = true;
|
||||
}
|
||||
}
|
||||
MovementAction::Jump => {
|
||||
if is_grounded {
|
||||
linear_velocity.y = jump_impulse.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies [`ControllerGravity`] to character controllers.
|
||||
fn apply_gravity(
|
||||
time: Res<Time>,
|
||||
mut controllers: Query<(&ControllerGravity, &mut LinearVelocity)>,
|
||||
) {
|
||||
let delta_time = time.delta_secs();
|
||||
|
||||
for (gravity, mut linear_velocity) in &mut controllers {
|
||||
linear_velocity.0 += gravity.0 * delta_time;
|
||||
}
|
||||
}
|
||||
|
||||
/// Slows down movement in the XZ plane.
|
||||
fn apply_movement_damping(
|
||||
time: Res<Time>,
|
||||
mut query: Query<(&MovementDampingFactor, &mut LinearVelocity)>,
|
||||
) {
|
||||
let delta_time = time.delta_secs();
|
||||
|
||||
for (damping_factor, mut linear_velocity) in &mut query {
|
||||
// We could use `LinearDamping`, but we don't want to dampen movement along the Y axis
|
||||
linear_velocity.x *= 1.0 - damping_factor.0 * delta_time;
|
||||
linear_velocity.z *= 1.0 - damping_factor.0 * delta_time;
|
||||
}
|
||||
}
|
||||
|
||||
/// Kinematic bodies do not get pushed by collisions by default,
|
||||
/// so it needs to be done manually.
|
||||
///
|
||||
/// This system handles collision response for kinematic character controllers
|
||||
/// by pushing them along their contact normals by the current penetration depth,
|
||||
/// and applying velocity corrections in order to snap to slopes, slide along walls,
|
||||
/// and predict collisions using speculative contacts.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn kinematic_controller_collisions(
|
||||
collisions: Res<Collisions>,
|
||||
bodies: Query<&RigidBody>,
|
||||
collider_parents: Query<&ColliderParent, Without<Sensor>>,
|
||||
mut character_controllers: Query<
|
||||
(
|
||||
&mut Position,
|
||||
&Rotation,
|
||||
&mut LinearVelocity,
|
||||
Option<&MaxSlopeAngle>,
|
||||
),
|
||||
(With<RigidBody>, With<CharacterController>),
|
||||
>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
// Iterate through collisions and move the kinematic body to resolve penetration
|
||||
for contacts in collisions.iter() {
|
||||
// Get the rigid body entities of the colliders (colliders could be children)
|
||||
let Ok([collider_parent1, collider_parent2]) =
|
||||
collider_parents.get_many([contacts.entity1, contacts.entity2])
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Get the body of the character controller and whether it is the first
|
||||
// or second entity in the collision.
|
||||
let is_first: bool;
|
||||
|
||||
let character_rb: RigidBody;
|
||||
let is_other_dynamic: bool;
|
||||
|
||||
let (mut position, rotation, mut linear_velocity, max_slope_angle) =
|
||||
if let Ok(character) = character_controllers.get_mut(collider_parent1.get()) {
|
||||
is_first = true;
|
||||
character_rb = *bodies.get(collider_parent1.get()).unwrap();
|
||||
is_other_dynamic = bodies
|
||||
.get(collider_parent2.get())
|
||||
.is_ok_and(|rb| rb.is_dynamic());
|
||||
character
|
||||
} else if let Ok(character) = character_controllers.get_mut(collider_parent2.get()) {
|
||||
is_first = false;
|
||||
character_rb = *bodies.get(collider_parent2.get()).unwrap();
|
||||
is_other_dynamic = bodies
|
||||
.get(collider_parent1.get())
|
||||
.is_ok_and(|rb| rb.is_dynamic());
|
||||
character
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// This system only handles collision response for kinematic character controllers.
|
||||
if !character_rb.is_kinematic() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Iterate through contact manifolds and their contacts.
|
||||
// Each contact in a single manifold shares the same contact normal.
|
||||
for manifold in contacts.manifolds.iter() {
|
||||
let normal = if is_first {
|
||||
-manifold.global_normal1(rotation)
|
||||
} else {
|
||||
-manifold.global_normal2(rotation)
|
||||
};
|
||||
|
||||
let mut deepest_penetration: Scalar = Scalar::MIN;
|
||||
|
||||
// Solve each penetrating contact in the manifold.
|
||||
for contact in manifold.contacts.iter() {
|
||||
if contact.penetration > 0.0 {
|
||||
position.0 += normal * contact.penetration;
|
||||
}
|
||||
deepest_penetration = deepest_penetration.max(contact.penetration);
|
||||
}
|
||||
|
||||
// For now, this system only handles velocity corrections for collisions against static geometry.
|
||||
if is_other_dynamic {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine if the slope is climbable or if it's too steep to walk on.
|
||||
let slope_angle = normal.angle_between(Vector::Y);
|
||||
let climbable = max_slope_angle.is_some_and(|angle| slope_angle.abs() <= angle.0);
|
||||
|
||||
if deepest_penetration > 0.0 {
|
||||
// If the slope is climbable, snap the velocity so that the character
|
||||
// up and down the surface smoothly.
|
||||
if climbable {
|
||||
// Points in the normal's direction in the XZ plane.
|
||||
let normal_direction_xz =
|
||||
normal.reject_from_normalized(Vector::Y).normalize_or_zero();
|
||||
|
||||
// The movement speed along the direction above.
|
||||
let linear_velocity_xz = linear_velocity.dot(normal_direction_xz);
|
||||
|
||||
// Snap the Y speed based on the speed at which the character is moving
|
||||
// up or down the slope, and how steep the slope is.
|
||||
//
|
||||
// A 2D visualization of the slope, the contact normal, and the velocity components:
|
||||
//
|
||||
// ╱
|
||||
// normal ╱
|
||||
// * ╱
|
||||
// │ * ╱ velocity_x
|
||||
// │ * - - - - - -
|
||||
// │ * | velocity_y
|
||||
// │ * |
|
||||
// *───────────────────*
|
||||
|
||||
let max_y_speed = -linear_velocity_xz * slope_angle.tan();
|
||||
linear_velocity.y = linear_velocity.y.max(max_y_speed);
|
||||
} else {
|
||||
// The character is intersecting an unclimbable object, like a wall.
|
||||
// We want the character to slide along the surface, similarly to
|
||||
// a collide-and-slide algorithm.
|
||||
|
||||
// Don't apply an impulse if the character is moving away from the surface.
|
||||
if linear_velocity.dot(normal) > 0.0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Slide along the surface, rejecting the velocity along the contact normal.
|
||||
let impulse = linear_velocity.reject_from_normalized(normal);
|
||||
linear_velocity.0 = impulse;
|
||||
}
|
||||
} else {
|
||||
// The character is not yet intersecting the other object,
|
||||
// but the narrow phase detected a speculative collision.
|
||||
//
|
||||
// We need to push back the part of the velocity
|
||||
// that would cause penetration within the next frame.
|
||||
|
||||
let normal_speed = linear_velocity.dot(normal);
|
||||
|
||||
// Don't apply an impulse if the character is moving away from the surface.
|
||||
if normal_speed > 0.0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compute the impulse to apply.
|
||||
let impulse_magnitude =
|
||||
normal_speed - (deepest_penetration / time.delta_secs_f64().adjust_precision());
|
||||
let mut impulse = impulse_magnitude * normal;
|
||||
|
||||
// Apply the impulse differently depending on the slope angle.
|
||||
if climbable {
|
||||
// Avoid sliding down slopes.
|
||||
linear_velocity.y -= impulse.y.min(0.0);
|
||||
} else {
|
||||
// Avoid climbing up walls.
|
||||
impulse.y = impulse.y.max(0.0);
|
||||
linear_velocity.0 -= impulse;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ mod alien;
|
||||
mod billboards;
|
||||
mod camera;
|
||||
mod cash;
|
||||
mod controller;
|
||||
mod controls;
|
||||
mod cutscene;
|
||||
mod gates;
|
||||
@@ -26,9 +27,8 @@ use bevy::core_pipeline::tonemapping::Tonemapping;
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::view::ColorGrading;
|
||||
use bevy::scene::SceneInstanceReady;
|
||||
use bevy_tnua::prelude::TnuaControllerPlugin;
|
||||
use bevy_tnua_avian3d::TnuaAvian3dPlugin;
|
||||
use bevy_trenchbroom::prelude::*;
|
||||
use controller::CharacterControllerPlugin;
|
||||
use physics_layers::GameLayer;
|
||||
|
||||
#[derive(Resource, Reflect, Debug)]
|
||||
@@ -69,10 +69,7 @@ fn main() {
|
||||
// });
|
||||
|
||||
app.add_plugins(PhysicsPlugins::default());
|
||||
app.add_plugins((
|
||||
TnuaControllerPlugin::new(FixedUpdate),
|
||||
TnuaAvian3dPlugin::new(FixedUpdate),
|
||||
));
|
||||
app.add_plugins(CharacterControllerPlugin);
|
||||
|
||||
// app.add_plugins(PhysicsDebugPlugin::default());
|
||||
// app.add_plugins(bevy_inspector_egui::quick::WorldInspectorPlugin::default());
|
||||
|
||||
@@ -2,18 +2,21 @@ use crate::{
|
||||
alien::{ALIEN_ASSET_PATH, Animations},
|
||||
camera::{CameraArmRotation, CameraTarget},
|
||||
cash::{Cash, CashCollectEvent},
|
||||
controller::{CharacterControllerBundle, PlayerMovement},
|
||||
controls::Controls,
|
||||
heads_ui::HeadChanged,
|
||||
physics_layers::GameLayer,
|
||||
tb_entities::SpawnPoint,
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use avian3d::{
|
||||
math::{Scalar, Vector},
|
||||
prelude::*,
|
||||
};
|
||||
use bevy::{
|
||||
prelude::*,
|
||||
window::{CursorGrabMode, PrimaryWindow},
|
||||
};
|
||||
use bevy_tnua::{TnuaUserControlsSystemSet, prelude::*};
|
||||
use bevy_tnua_avian3d::TnuaAvian3dSensorShape;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Component, Default)]
|
||||
@@ -33,14 +36,8 @@ struct PlayerSpawned {
|
||||
spawned: bool,
|
||||
}
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub struct PlayerMovement {
|
||||
pub any_direction: bool,
|
||||
}
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.init_resource::<PlayerSpawned>();
|
||||
app.init_resource::<PlayerMovement>();
|
||||
app.add_systems(Startup, (initial_grab_cursor, cursor_recenter));
|
||||
app.add_systems(
|
||||
Update,
|
||||
@@ -51,10 +48,6 @@ pub fn plugin(app: &mut App) {
|
||||
setup_animations_marker_for_player,
|
||||
),
|
||||
);
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
apply_controls.in_set(TnuaUserControlsSystemSet),
|
||||
);
|
||||
|
||||
app.add_systems(Update, (rotate_view_keyboard, rotate_view_gamepad));
|
||||
|
||||
@@ -80,21 +73,28 @@ fn spawn(
|
||||
let mesh = asset_server
|
||||
.load(GltfAssetLabel::Scene(0).from_asset("models/heads/angry demonstrator.glb"));
|
||||
|
||||
let gravity = Vector::NEG_Y * 40.0;
|
||||
let collider = Collider::capsule(0.9, 1.2);
|
||||
let acceleration = 30.0;
|
||||
let damping = 0.95;
|
||||
let jump_impulse = 18.0;
|
||||
let max_slope_angle = (60.0 as Scalar).to_radians();
|
||||
|
||||
commands
|
||||
.spawn((
|
||||
Name::from("player"),
|
||||
Player,
|
||||
CameraTarget,
|
||||
transform,
|
||||
TransformInterpolation,
|
||||
TransformExtrapolation,
|
||||
Visibility::default(),
|
||||
RigidBody::Dynamic,
|
||||
Collider::capsule(1.2, 1.5),
|
||||
// LockedAxes::ROTATION_LOCKED, todo
|
||||
CollisionLayers::new(LayerMask(GameLayer::Player.to_bits()), LayerMask::ALL),
|
||||
LockedAxes::ROTATION_LOCKED,
|
||||
TnuaController::default(),
|
||||
TnuaAvian3dSensorShape(Collider::cylinder(0.8, 0.0)),
|
||||
CharacterControllerBundle::new(collider, gravity).with_movement(
|
||||
acceleration,
|
||||
damping,
|
||||
jump_impulse,
|
||||
max_slope_angle,
|
||||
),
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent
|
||||
@@ -102,7 +102,7 @@ fn spawn(
|
||||
Name::from("body rig"),
|
||||
PlayerRig,
|
||||
CameraArmRotation,
|
||||
Transform::from_translation(Vec3::new(0., -3., 0.))
|
||||
Transform::from_translation(Vec3::new(0., -1.45, 0.))
|
||||
.with_rotation(Quat::from_rotation_y(std::f32::consts::PI))
|
||||
.with_scale(Vec3::splat(1.4)),
|
||||
SceneRoot(
|
||||
@@ -187,57 +187,6 @@ fn initial_grab_cursor(mut primary_window: Query<&mut Window, With<PrimaryWindow
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_controls(
|
||||
controls: Res<Controls>,
|
||||
mut query: Query<&mut TnuaController>,
|
||||
player: Query<&Transform, With<PlayerRig>>,
|
||||
mut movement: ResMut<PlayerMovement>,
|
||||
) {
|
||||
let Ok(mut controller) = query.get_single_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(player) = player.get_single() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut direction = player.forward().as_vec3() * -controls.keyboard_state.move_dir.y;
|
||||
direction += player.right().as_vec3() * -controls.keyboard_state.move_dir.x;
|
||||
|
||||
if let Some(gamepad) = controls.gamepad_state {
|
||||
direction += player.forward().as_vec3() * -gamepad.move_dir.y;
|
||||
direction += player.right().as_vec3() * -gamepad.move_dir.x;
|
||||
}
|
||||
|
||||
if movement.any_direction != (direction != Vec3::ZERO) {
|
||||
movement.any_direction = direction != Vec3::ZERO;
|
||||
}
|
||||
|
||||
controller.basis(TnuaBuiltinWalk {
|
||||
// The `desired_velocity` determines how the character will move.
|
||||
desired_velocity: direction.normalize_or_zero() * 15.0,
|
||||
spring_strengh: 1000.,
|
||||
spring_dampening: 0.5,
|
||||
// The `float_height` must be greater (even if by little) from the distance between the
|
||||
// character's center and the lowest point of its collider.
|
||||
float_height: 3.0,
|
||||
// `TnuaBuiltinWalk` has many other fields for customizing the movement - but they have
|
||||
// sensible defaults. Refer to the `TnuaBuiltinWalk`'s documentation to learn what they do.
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Feed the jump action every frame as long as the player holds the jump button. If the player
|
||||
// stops holding the jump button, simply stop feeding the action.
|
||||
if controls.keyboard_state.jump || controls.gamepad_state.map(|gp| gp.jump).unwrap_or(false) {
|
||||
controller.action(TnuaBuiltinJump {
|
||||
// The height is the only mandatory field of the jump button.
|
||||
height: 4.0,
|
||||
// `TnuaBuiltinJump` also has customization fields with sensible defaults.
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_cash(
|
||||
mut commands: Commands,
|
||||
mut collision_event_reader: EventReader<CollisionStarted>,
|
||||
@@ -264,7 +213,7 @@ fn setup_animations_marker_for_player(
|
||||
mut commands: Commands,
|
||||
animation_handles: Query<Entity, Added<AnimationGraphHandle>>,
|
||||
parent_query: Query<&Parent>,
|
||||
player: Query<&Player>,
|
||||
player: Query<&PlayerRig>,
|
||||
) {
|
||||
for entity in animation_handles.iter() {
|
||||
for ancestor in parent_query.iter_ancestors(entity) {
|
||||
|
||||
@@ -43,19 +43,19 @@ impl SpawnPoint {
|
||||
|
||||
#[derive(SolidClass, Component, Reflect, Default)]
|
||||
#[reflect(Component)]
|
||||
#[geometry(GeometryProvider::new().trimesh_collider().render())]
|
||||
#[geometry(GeometryProvider::new().convex_collider().render())]
|
||||
pub struct Worldspawn;
|
||||
|
||||
#[derive(SolidClass, Component, Reflect, Default)]
|
||||
#[reflect(Component)]
|
||||
#[require(Transform)]
|
||||
#[geometry(GeometryProvider::new().trimesh_collider().render())]
|
||||
#[geometry(GeometryProvider::new().convex_collider().render())]
|
||||
pub struct Crates;
|
||||
|
||||
#[derive(SolidClass, Component, Reflect, Default)]
|
||||
#[reflect(Component)]
|
||||
#[require(Transform)]
|
||||
#[geometry(GeometryProvider::new().trimesh_collider().render())]
|
||||
#[geometry(GeometryProvider::new().convex_collider().render())]
|
||||
pub struct NamedEntity {
|
||||
pub name: String,
|
||||
}
|
||||
@@ -63,7 +63,7 @@ pub struct NamedEntity {
|
||||
#[derive(SolidClass, Component, Reflect, Default)]
|
||||
#[reflect(Component)]
|
||||
#[require(Transform, Target)]
|
||||
#[geometry(GeometryProvider::new().trimesh_collider().render())]
|
||||
#[geometry(GeometryProvider::new().convex_collider().render())]
|
||||
pub struct Platform;
|
||||
|
||||
#[derive(PointClass, Component, Reflect, Default)]
|
||||
@@ -76,7 +76,7 @@ pub struct PlatformTarget {
|
||||
#[derive(SolidClass, Component, Reflect, Default)]
|
||||
#[reflect(Component)]
|
||||
#[require(Transform, Target)]
|
||||
#[geometry(GeometryProvider::new().trimesh_collider().render())]
|
||||
#[geometry(GeometryProvider::new().convex_collider().render())]
|
||||
pub struct Movable {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user