use happy_feet as kinematic character controller (#42)

This commit is contained in:
extrawurst
2025-06-17 21:27:20 +02:00
committed by GitHub
parent 580419e823
commit a2ea917c1e
22 changed files with 727 additions and 786 deletions

View File

@@ -1,45 +1,51 @@
use avian3d::{math::*, prelude::*};
use bevy::prelude::*;
use crate::{
GameState, control::collisions::kinematic_controller_collisions, player::PlayerBodyMesh,
use happy_feet::KinematicVelocity;
use happy_feet::ground::{Grounding, GroundingConfig};
use happy_feet::prelude::{
Character, CharacterDrag, CharacterFriction, CharacterGravity, CharacterMovement,
CharacterPlugin, MoveInput, SteppingBehaviour, SteppingConfig,
};
use super::ControllerSwitchEvent;
use crate::GameState;
use crate::control::SelectedController;
use crate::control::controls::ControllerSettings;
use crate::heads_database::HeadControls;
use crate::player::PlayerBodyMesh;
use super::{ControllerSet, ControllerSwitchEvent};
pub fn plugin(app: &mut App) {
app.init_resource::<PlayerMovement>();
app.init_resource::<MovementSettings>();
app.add_plugins(CharacterPlugin::default());
app.register_type::<MovementSettings>();
app.register_type::<MovementDampingFactor>();
app.register_type::<JumpImpulse>();
app.register_type::<ControllerGravity>();
app.register_type::<MovementSpeed>();
app.register_type::<MovementSpeedFactor>();
app.init_resource::<PlayerMovement>();
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.
PhysicsSchedule,
kinematic_controller_collisions
.in_set(NarrowPhaseSet::Last)
.run_if(in_state(GameState::Playing)),
PreUpdate,
reset_upon_switch
.run_if(in_state(GameState::Playing))
.before(ControllerSet::ApplyControlsRun)
.before(ControllerSet::ApplyControlsFly),
)
.add_systems(
FixedPreUpdate,
decelerate.run_if(in_state(GameState::Playing)),
);
}
/// Reset the pitch and velocity of the character if the controller was switched.
pub fn reset_upon_switch(
mut c: Commands,
mut event_controller_switch: EventReader<ControllerSwitchEvent>,
controller: Res<SelectedController>,
mut rig_transform_q: Option<Single<&mut Transform, With<PlayerBodyMesh>>>,
mut controllers: Query<&mut LinearVelocity>,
mut velocity: Single<&mut KinematicVelocity, With<Character>>,
character: Single<Entity, With<Character>>,
) {
for _ in event_controller_switch.read() {
// Reset velocity
for mut linear_velocity in &mut controllers {
linear_velocity.0 = Vec3::ZERO;
}
velocity.0 = Vec3::ZERO;
// Reset pitch but keep yaw the same
if let Some(ref mut rig_transform) = rig_transform_q {
@@ -47,6 +53,55 @@ pub fn reset_upon_switch(
let yaw = euler_rot.0;
rig_transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, 0.0, 0.0);
}
match controller.0 {
ControllerSet::ApplyControlsFly => {
c.entity(*character).insert(FLYING_MOVEMENT_CONFIG);
}
ControllerSet::ApplyControlsRun => {
c.entity(*character).insert(RUNNING_MOVEMENT_CONFIG);
}
_ => unreachable!(),
}
}
}
/// Decelerates the player in the directions of "undesired velocity"; velocity that is not aligned
/// with the movement input direction. This makes it quicker to reverse direction, and prevents
/// sliding around, even with low friction, without slowing down the player globally like high
/// friction or drag would.
fn decelerate(
mut character: Query<(
&mut KinematicVelocity,
&MoveInput,
Option<&Grounding>,
&ControllerSettings,
)>,
) {
for (mut velocity, input, grounding, settings) in &mut character {
let direction = input.value.normalize();
let ground_normal = grounding
.and_then(|it| it.normal())
.unwrap_or(Dir3::Y)
.as_vec3();
let velocity_within_90_degrees = direction.dot(velocity.0) > 0.0;
let desired_velocity = if direction != Vec3::ZERO && velocity_within_90_degrees {
// project velocity onto direction to extract the component directly aligned with direction
velocity.0.project_onto(direction)
} else {
// if velocity isn't within 90 degrees of direction then the projection would be in the
// exact opposite direction of `direction`; so just zero it
Vec3::ZERO
};
let undesired_velocity = velocity.0 - desired_velocity;
let vertical_undesired_velocity = undesired_velocity.project_onto(ground_normal);
// only select the velocity along the ground plane; that way the character can't decelerate
// while falling or jumping, but will decelerate along slopes properly
let undesired_velocity = undesired_velocity - vertical_undesired_velocity;
let deceleration =
Vec3::ZERO.move_towards(undesired_velocity, settings.deceleration_factor);
velocity.0 -= deceleration;
}
}
@@ -56,124 +111,97 @@ pub struct PlayerMovement {
pub shooting: bool,
}
/// 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, Reflect)]
#[reflect(Resource)]
pub struct MovementSettings {
pub damping_normal: f32,
pub damping_brake: f32,
pub 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,
}
}
}
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct MovementSpeedFactor(pub f32);
/// The speed used for character movement.
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct MovementSpeed(pub Scalar);
/// The damping factor used for slowing down movement.
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct MovementDampingFactor(pub Scalar);
/// The strength of a jump.
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct JumpImpulse(pub Scalar);
/// The gravitational acceleration used for a character controller.
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct ControllerGravity(pub 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(pub 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,
character_controller: Character,
collider: Collider,
ground_caster: ShapeCaster,
gravity: ControllerGravity,
movement: MovementBundle,
move_input: MoveInput,
movement_factor: MovementSpeedFactor,
collision_events: CollisionEventsEnabled,
movement_config: MovementConfig,
}
/// A bundle that contains components for character movement.
#[derive(Bundle)]
pub struct MovementBundle {
acceleration: MovementSpeed,
jump_impulse: JumpImpulse,
max_slope_angle: MaxSlopeAngle,
factor: MovementSpeedFactor,
struct MovementConfig {
movement: CharacterMovement,
step: SteppingConfig,
ground: GroundingConfig,
gravity: CharacterGravity,
friction: CharacterFriction,
drag: CharacterDrag,
settings: ControllerSettings,
}
impl MovementBundle {
pub const fn new(acceleration: Scalar, jump_impulse: Scalar, max_slope_angle: Scalar) -> Self {
Self {
acceleration: MovementSpeed(acceleration),
jump_impulse: JumpImpulse(jump_impulse),
max_slope_angle: MaxSlopeAngle(max_slope_angle),
factor: MovementSpeedFactor(1.0),
}
}
}
const RUNNING_MOVEMENT_CONFIG: MovementConfig = MovementConfig {
movement: CharacterMovement {
target_speed: 15.0,
acceleration: 40.0,
},
step: SteppingConfig {
max_height: 0.25,
behaviour: SteppingBehaviour::Grounded,
},
ground: GroundingConfig {
max_angle: PI / 4.0,
max_distance: 0.2,
snap_to_surface: true,
},
gravity: CharacterGravity(vec3(0.0, -60.0, 0.0)),
friction: CharacterFriction(10.0),
drag: CharacterDrag(0.0),
settings: ControllerSettings {
jump_force: 25.0,
deceleration_factor: 1.0,
},
};
impl Default for MovementBundle {
fn default() -> Self {
Self::new(30.0, 18.0, (60.0 as Scalar).to_radians())
}
}
const FLYING_MOVEMENT_CONFIG: MovementConfig = MovementConfig {
movement: CharacterMovement {
target_speed: 20.0,
acceleration: 50.0,
},
step: SteppingConfig {
max_height: 0.25,
behaviour: SteppingBehaviour::Never,
},
ground: GroundingConfig {
max_angle: 0.0,
max_distance: -1.0,
snap_to_surface: false,
},
gravity: CharacterGravity(Vec3::ZERO),
friction: CharacterFriction(0.0),
drag: CharacterDrag(1.0),
settings: ControllerSettings {
jump_force: 0.0,
deceleration_factor: 0.0,
},
};
impl CharacterControllerBundle {
pub fn new(collider: Collider, gravity: Vector) -> Self {
pub fn new(collider: Collider, controls: HeadControls) -> 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);
let config = match controls {
HeadControls::Plane => FLYING_MOVEMENT_CONFIG,
HeadControls::Walk => RUNNING_MOVEMENT_CONFIG,
};
Self {
character_controller: CharacterController,
rigid_body: RigidBody::Kinematic,
character_controller: Character { up: Dir3::Y },
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(),
move_input: MoveInput::default(),
movement_factor: MovementSpeedFactor(1.0),
collision_events: CollisionEventsEnabled,
movement_config: config,
}
}
}