use happy_feet as kinematic character controller (#42)
This commit is contained in:
775
Cargo.lock
generated
775
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@ bevy_debug_log = "0.6.0"
|
|||||||
bevy_common_assets = { version = "0.13.0", features = ["ron"] }
|
bevy_common_assets = { version = "0.13.0", features = ["ron"] }
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
ron = "0.8"
|
ron = "0.8"
|
||||||
|
happy_feet = { git = "https://github.com/rustunit/happy_feet.git", rev = "ecfecc6243862bc2bc64dcadfd0efd21c766ab5b" }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
vergen-gitcl = "1.0"
|
vergen-gitcl = "1.0"
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ fn on_trigger_missile(
|
|||||||
let asset = gltf_assets.get(&mesh).unwrap();
|
let asset = gltf_assets.get(&mesh).unwrap();
|
||||||
|
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
Name::new("projectile-missle"),
|
Name::new("projectile-missile"),
|
||||||
CurverProjectile {
|
CurverProjectile {
|
||||||
time: time.elapsed_secs(),
|
time: time.elapsed_secs(),
|
||||||
damage: head.damage,
|
damage: head.damage,
|
||||||
@@ -153,6 +153,7 @@ fn shot_collision(
|
|||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut collision_event_reader: EventReader<CollisionStarted>,
|
mut collision_event_reader: EventReader<CollisionStarted>,
|
||||||
query_shot: Query<&Transform, With<CurverProjectile>>,
|
query_shot: Query<&Transform, With<CurverProjectile>>,
|
||||||
|
sensors: Query<(), With<Sensor>>,
|
||||||
assets: Res<ShotAssets>,
|
assets: Res<ShotAssets>,
|
||||||
mut sprite_params: Sprite3dParams,
|
mut sprite_params: Sprite3dParams,
|
||||||
) {
|
) {
|
||||||
@@ -161,6 +162,10 @@ fn shot_collision(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sensors.contains(*e1) && sensors.contains(*e2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let shot_entity = if query_shot.contains(*e1) { *e1 } else { *e2 };
|
let shot_entity = if query_shot.contains(*e1) { *e1 } else { *e2 };
|
||||||
|
|
||||||
let Ok(shot_pos) = query_shot.get(shot_entity).map(|t| t.translation) else {
|
let Ok(shot_pos) = query_shot.get(shot_entity).map(|t| t.translation) else {
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ fn shot_collision(
|
|||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut collision_event_reader: EventReader<CollisionStarted>,
|
mut collision_event_reader: EventReader<CollisionStarted>,
|
||||||
query_shot: Query<(&GunProjectile, &Transform)>,
|
query_shot: Query<(&GunProjectile, &Transform)>,
|
||||||
|
sensors: Query<(), With<Sensor>>,
|
||||||
assets: Res<ShotAssets>,
|
assets: Res<ShotAssets>,
|
||||||
mut sprite_params: Sprite3dParams,
|
mut sprite_params: Sprite3dParams,
|
||||||
) {
|
) {
|
||||||
@@ -159,6 +160,10 @@ fn shot_collision(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sensors.contains(*e1) && sensors.contains(*e2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let shot_entity = if query_shot.contains(*e1) { *e1 } else { *e2 };
|
let shot_entity = if query_shot.contains(*e1) { *e1 } else { *e2 };
|
||||||
|
|
||||||
if let Ok(mut entity) = commands.get_entity(shot_entity) {
|
if let Ok(mut entity) = commands.get_entity(shot_entity) {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ fn on_trigger_missile(
|
|||||||
let asset = gltf_assets.get(&mesh).unwrap();
|
let asset = gltf_assets.get(&mesh).unwrap();
|
||||||
|
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
Name::new("projectile-missle"),
|
Name::new("projectile-missile"),
|
||||||
MissileProjectile {
|
MissileProjectile {
|
||||||
time: time.elapsed_secs(),
|
time: time.elapsed_secs(),
|
||||||
damage: head.damage,
|
damage: head.damage,
|
||||||
@@ -143,6 +143,7 @@ fn shot_collision(
|
|||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut collision_event_reader: EventReader<CollisionStarted>,
|
mut collision_event_reader: EventReader<CollisionStarted>,
|
||||||
query_shot: Query<(&MissileProjectile, &Transform)>,
|
query_shot: Query<(&MissileProjectile, &Transform)>,
|
||||||
|
sensors: Query<(), With<Sensor>>,
|
||||||
assets: Res<ShotAssets>,
|
assets: Res<ShotAssets>,
|
||||||
mut sprite_params: Sprite3dParams,
|
mut sprite_params: Sprite3dParams,
|
||||||
) {
|
) {
|
||||||
@@ -151,6 +152,10 @@ fn shot_collision(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sensors.contains(*e1) && sensors.contains(*e2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let shot_entity = if query_shot.contains(*e1) { *e1 } else { *e2 };
|
let shot_entity = if query_shot.contains(*e1) { *e1 } else { *e2 };
|
||||||
|
|
||||||
let Ok((shot_pos, damage)) = query_shot
|
let Ok((shot_pos, damage)) = query_shot
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ fn on_trigger_thrown(
|
|||||||
Mass(0.01),
|
Mass(0.01),
|
||||||
LinearVelocity(vel),
|
LinearVelocity(vel),
|
||||||
Visibility::default(),
|
Visibility::default(),
|
||||||
|
Sensor,
|
||||||
))
|
))
|
||||||
.with_child((
|
.with_child((
|
||||||
AutoRotation(Quat::from_rotation_x(0.4) * Quat::from_rotation_z(0.3)),
|
AutoRotation(Quat::from_rotation_x(0.4) * Quat::from_rotation_z(0.3)),
|
||||||
@@ -111,6 +112,7 @@ fn shot_collision(
|
|||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut collision_event_reader: EventReader<CollisionStarted>,
|
mut collision_event_reader: EventReader<CollisionStarted>,
|
||||||
query_shot: Query<(&ThrownProjectile, &Transform)>,
|
query_shot: Query<(&ThrownProjectile, &Transform)>,
|
||||||
|
sensors: Query<(), With<Sensor>>,
|
||||||
assets: Res<ShotAssets>,
|
assets: Res<ShotAssets>,
|
||||||
mut sprite_params: Sprite3dParams,
|
mut sprite_params: Sprite3dParams,
|
||||||
) {
|
) {
|
||||||
@@ -119,6 +121,10 @@ fn shot_collision(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sensors.contains(*e1) && sensors.contains(*e2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let shot_entity = if query_shot.contains(*e1) { *e1 } else { *e2 };
|
let shot_entity = if query_shot.contains(*e1) { *e1 } else { *e2 };
|
||||||
|
|
||||||
let Ok((shot_pos, animation, damage)) =
|
let Ok((shot_pos, animation, damage)) =
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ pub fn plugin(app: &mut App) {
|
|||||||
app.init_resource::<CameraState>();
|
app.init_resource::<CameraState>();
|
||||||
app.add_systems(OnEnter(GameState::Playing), startup);
|
app.add_systems(OnEnter(GameState::Playing), startup);
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
Update,
|
PreUpdate,
|
||||||
(update, update_ui, update_look_around, rotate_view).run_if(in_state(GameState::Playing)),
|
(update, update_ui, update_look_around, rotate_view).run_if(in_state(GameState::Playing)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,7 @@ fn update(
|
|||||||
(&MainCamera, &mut Transform, &CameraRotationInput),
|
(&MainCamera, &mut Transform, &CameraRotationInput),
|
||||||
(Without<CameraTarget>, Without<CameraArmRotation>),
|
(Without<CameraTarget>, Without<CameraArmRotation>),
|
||||||
>,
|
>,
|
||||||
target: Single<&GlobalTransform, (With<CameraTarget>, Without<CameraArmRotation>)>,
|
target_q: Single<&Transform, (With<CameraTarget>, Without<CameraArmRotation>)>,
|
||||||
arm_rotation: Single<&Transform, With<CameraArmRotation>>,
|
arm_rotation: Single<&Transform, With<CameraArmRotation>>,
|
||||||
spatial_query: SpatialQuery,
|
spatial_query: SpatialQuery,
|
||||||
cam_state: Res<CameraState>,
|
cam_state: Res<CameraState>,
|
||||||
@@ -119,7 +119,7 @@ fn update(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let target = target.translation();
|
let target = target_q.translation;
|
||||||
|
|
||||||
let arm_tf = arm_rotation;
|
let arm_tf = arm_rotation;
|
||||||
|
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
use avian3d::{math::*, prelude::*};
|
|
||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
use super::controller_common::{CharacterController, MaxSlopeAngle};
|
|
||||||
|
|
||||||
/// 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)]
|
|
||||||
pub fn kinematic_controller_collisions(
|
|
||||||
collisions: Collisions,
|
|
||||||
bodies: Query<&RigidBody>,
|
|
||||||
collider_rbs: Query<&ColliderOf, Without<Sensor>>,
|
|
||||||
mut character_controllers: Query<
|
|
||||||
(&mut Position, &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([&ColliderOf { body: rb1 }, &ColliderOf { body: rb2 }]) =
|
|
||||||
collider_rbs.get_many([contacts.collider1, contacts.collider2])
|
|
||||||
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, mut linear_velocity, max_slope_angle) =
|
|
||||||
if let Ok(character) = character_controllers.get_mut(rb1) {
|
|
||||||
is_first = true;
|
|
||||||
character_rb = *bodies.get(rb1).unwrap();
|
|
||||||
is_other_dynamic = bodies.get(rb2).is_ok_and(|rb| rb.is_dynamic());
|
|
||||||
character
|
|
||||||
} else if let Ok(character) = character_controllers.get_mut(rb2) {
|
|
||||||
is_first = false;
|
|
||||||
character_rb = *bodies.get(rb2).unwrap();
|
|
||||||
is_other_dynamic = bodies.get(rb1).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.normal
|
|
||||||
} else {
|
|
||||||
manifold.normal
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut deepest_penetration: Scalar = Scalar::MIN;
|
|
||||||
|
|
||||||
// Solve each penetrating contact in the manifold.
|
|
||||||
for contact in manifold.points.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();
|
|
||||||
if linear_velocity.y < 0.0 {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +1,51 @@
|
|||||||
use avian3d::{math::*, prelude::*};
|
use avian3d::{math::*, prelude::*};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use happy_feet::KinematicVelocity;
|
||||||
use crate::{
|
use happy_feet::ground::{Grounding, GroundingConfig};
|
||||||
GameState, control::collisions::kinematic_controller_collisions, player::PlayerBodyMesh,
|
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) {
|
pub fn plugin(app: &mut App) {
|
||||||
app.init_resource::<PlayerMovement>();
|
app.add_plugins(CharacterPlugin::default());
|
||||||
app.init_resource::<MovementSettings>();
|
|
||||||
|
|
||||||
app.register_type::<MovementSettings>();
|
app.register_type::<MovementSpeedFactor>();
|
||||||
app.register_type::<MovementDampingFactor>();
|
|
||||||
app.register_type::<JumpImpulse>();
|
app.init_resource::<PlayerMovement>();
|
||||||
app.register_type::<ControllerGravity>();
|
|
||||||
app.register_type::<MovementSpeed>();
|
|
||||||
|
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
// Run collision handling after collision detection.
|
PreUpdate,
|
||||||
//
|
reset_upon_switch
|
||||||
// NOTE: The collision implementation here is very basic and a bit buggy.
|
.run_if(in_state(GameState::Playing))
|
||||||
// A collide-and-slide algorithm would likely work better.
|
.before(ControllerSet::ApplyControlsRun)
|
||||||
PhysicsSchedule,
|
.before(ControllerSet::ApplyControlsFly),
|
||||||
kinematic_controller_collisions
|
)
|
||||||
.in_set(NarrowPhaseSet::Last)
|
.add_systems(
|
||||||
.run_if(in_state(GameState::Playing)),
|
FixedPreUpdate,
|
||||||
|
decelerate.run_if(in_state(GameState::Playing)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reset the pitch and velocity of the character if the controller was switched.
|
/// Reset the pitch and velocity of the character if the controller was switched.
|
||||||
pub fn reset_upon_switch(
|
pub fn reset_upon_switch(
|
||||||
|
mut c: Commands,
|
||||||
mut event_controller_switch: EventReader<ControllerSwitchEvent>,
|
mut event_controller_switch: EventReader<ControllerSwitchEvent>,
|
||||||
|
controller: Res<SelectedController>,
|
||||||
mut rig_transform_q: Option<Single<&mut Transform, With<PlayerBodyMesh>>>,
|
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() {
|
for _ in event_controller_switch.read() {
|
||||||
// Reset velocity
|
velocity.0 = Vec3::ZERO;
|
||||||
for mut linear_velocity in &mut controllers {
|
|
||||||
linear_velocity.0 = Vec3::ZERO;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset pitch but keep yaw the same
|
// Reset pitch but keep yaw the same
|
||||||
if let Some(ref mut rig_transform) = rig_transform_q {
|
if let Some(ref mut rig_transform) = rig_transform_q {
|
||||||
@@ -47,6 +53,55 @@ pub fn reset_upon_switch(
|
|||||||
let yaw = euler_rot.0;
|
let yaw = euler_rot.0;
|
||||||
rig_transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, 0.0, 0.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,
|
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)]
|
#[derive(Component, Reflect)]
|
||||||
#[reflect(Component)]
|
#[reflect(Component)]
|
||||||
pub struct MovementSpeedFactor(pub f32);
|
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
|
/// A bundle that contains the components needed for a basic
|
||||||
/// kinematic character controller.
|
/// kinematic character controller.
|
||||||
#[derive(Bundle)]
|
#[derive(Bundle)]
|
||||||
pub struct CharacterControllerBundle {
|
pub struct CharacterControllerBundle {
|
||||||
character_controller: CharacterController,
|
character_controller: Character,
|
||||||
rigid_body: RigidBody,
|
|
||||||
collider: Collider,
|
collider: Collider,
|
||||||
ground_caster: ShapeCaster,
|
move_input: MoveInput,
|
||||||
gravity: ControllerGravity,
|
movement_factor: MovementSpeedFactor,
|
||||||
movement: MovementBundle,
|
|
||||||
collision_events: CollisionEventsEnabled,
|
collision_events: CollisionEventsEnabled,
|
||||||
|
movement_config: MovementConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A bundle that contains components for character movement.
|
|
||||||
#[derive(Bundle)]
|
#[derive(Bundle)]
|
||||||
pub struct MovementBundle {
|
struct MovementConfig {
|
||||||
acceleration: MovementSpeed,
|
movement: CharacterMovement,
|
||||||
jump_impulse: JumpImpulse,
|
step: SteppingConfig,
|
||||||
max_slope_angle: MaxSlopeAngle,
|
ground: GroundingConfig,
|
||||||
factor: MovementSpeedFactor,
|
gravity: CharacterGravity,
|
||||||
|
friction: CharacterFriction,
|
||||||
|
drag: CharacterDrag,
|
||||||
|
settings: ControllerSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MovementBundle {
|
const RUNNING_MOVEMENT_CONFIG: MovementConfig = MovementConfig {
|
||||||
pub const fn new(acceleration: Scalar, jump_impulse: Scalar, max_slope_angle: Scalar) -> Self {
|
movement: CharacterMovement {
|
||||||
Self {
|
target_speed: 15.0,
|
||||||
acceleration: MovementSpeed(acceleration),
|
acceleration: 40.0,
|
||||||
jump_impulse: JumpImpulse(jump_impulse),
|
},
|
||||||
max_slope_angle: MaxSlopeAngle(max_slope_angle),
|
step: SteppingConfig {
|
||||||
factor: MovementSpeedFactor(1.0),
|
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 {
|
const FLYING_MOVEMENT_CONFIG: MovementConfig = MovementConfig {
|
||||||
fn default() -> Self {
|
movement: CharacterMovement {
|
||||||
Self::new(30.0, 18.0, (60.0 as Scalar).to_radians())
|
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 {
|
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
|
// Create shape caster as a slightly smaller version of collider
|
||||||
let mut caster_shape = collider.clone();
|
let mut caster_shape = collider.clone();
|
||||||
caster_shape.set_scale(Vector::ONE * 0.98, 10);
|
caster_shape.set_scale(Vector::ONE * 0.98, 10);
|
||||||
|
|
||||||
|
let config = match controls {
|
||||||
|
HeadControls::Plane => FLYING_MOVEMENT_CONFIG,
|
||||||
|
HeadControls::Walk => RUNNING_MOVEMENT_CONFIG,
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
character_controller: CharacterController,
|
character_controller: Character { up: Dir3::Y },
|
||||||
rigid_body: RigidBody::Kinematic,
|
|
||||||
collider,
|
collider,
|
||||||
ground_caster: ShapeCaster::new(
|
move_input: MoveInput::default(),
|
||||||
caster_shape,
|
movement_factor: MovementSpeedFactor(1.0),
|
||||||
Vector::ZERO,
|
|
||||||
Quaternion::default(),
|
|
||||||
Dir3::NEG_Y,
|
|
||||||
)
|
|
||||||
.with_max_distance(0.2),
|
|
||||||
gravity: ControllerGravity(gravity),
|
|
||||||
movement: MovementBundle::default(),
|
|
||||||
collision_events: CollisionEventsEnabled,
|
collision_events: CollisionEventsEnabled,
|
||||||
|
movement_config: config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
use std::f32::consts::PI;
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
use avian3d::prelude::*;
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use happy_feet::prelude::MoveInput;
|
||||||
|
|
||||||
|
use crate::GameState;
|
||||||
|
use crate::control::controller_common::MovementSpeedFactor;
|
||||||
use crate::player::PlayerBodyMesh;
|
use crate::player::PlayerBodyMesh;
|
||||||
|
|
||||||
use super::{
|
use super::{ControlState, ControllerSet};
|
||||||
ControlState, ControllerSet,
|
|
||||||
controller_common::{MovementDampingFactor, MovementSpeed},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct CharacterControllerPlugin;
|
pub struct CharacterControllerPlugin;
|
||||||
|
|
||||||
impl Plugin for CharacterControllerPlugin {
|
impl Plugin for CharacterControllerPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
Update,
|
PreUpdate,
|
||||||
(rotate_rig, movement, apply_movement_damping)
|
(rotate_rig, apply_controls)
|
||||||
.chain()
|
.chain()
|
||||||
.in_set(ControllerSet::ApplyControlsFly), // todo only in GameState::Playing?
|
.in_set(ControllerSet::ApplyControlsFly)
|
||||||
|
.run_if(in_state(GameState::Playing)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,36 +56,13 @@ fn rotate_rig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Responds to [`MovementAction`] events and moves character controllers accordingly.
|
fn apply_controls(
|
||||||
fn movement(
|
mut character: Query<(&mut MoveInput, &MovementSpeedFactor)>,
|
||||||
mut controllers: Query<(&MovementSpeed, &mut LinearVelocity)>,
|
|
||||||
rig_transform_q: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
|
rig_transform_q: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
|
||||||
time: Res<Time>,
|
|
||||||
) {
|
) {
|
||||||
let move_dir = Vec2::new(0.0, 70.) * time.delta_secs();
|
let (mut char_input, factor) = character.single_mut().unwrap();
|
||||||
|
|
||||||
for (movement_acceleration, mut linear_velocity) in &mut controllers {
|
if let Some(ref rig_transform) = rig_transform_q {
|
||||||
let mut direction = move_dir.extend(0.0).xzy();
|
char_input.set(-*rig_transform.forward() * factor.0);
|
||||||
|
|
||||||
if let Some(ref rig_transform) = rig_transform_q {
|
|
||||||
direction =
|
|
||||||
(rig_transform.forward() * direction.z) + (rig_transform.right() * direction.x);
|
|
||||||
}
|
|
||||||
|
|
||||||
linear_velocity.0 = -direction * movement_acceleration.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,26 @@
|
|||||||
use super::{ControlState, ControllerSet, Controls, controller_common::MovementSpeedFactor};
|
use super::{ControlState, ControllerSet, Controls};
|
||||||
|
use crate::control::controller_common::MovementSpeedFactor;
|
||||||
|
use crate::control::controls::ControllerSettings;
|
||||||
use crate::{GameState, abilities::TriggerStateRes, player::PlayerBodyMesh};
|
use crate::{GameState, abilities::TriggerStateRes, player::PlayerBodyMesh};
|
||||||
use avian3d::{math::*, prelude::*};
|
use bevy::prelude::*;
|
||||||
use bevy::{ecs::query::Has, prelude::*};
|
use happy_feet::prelude::{Grounding, KinematicVelocity, MoveInput};
|
||||||
|
|
||||||
use super::controller_common::{
|
use super::controller_common::PlayerMovement;
|
||||||
CharacterController, ControllerGravity, Grounded, JumpImpulse, MaxSlopeAngle, MovementSpeed,
|
|
||||||
PlayerMovement, reset_upon_switch,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct CharacterControllerPlugin;
|
pub struct CharacterControllerPlugin;
|
||||||
|
|
||||||
impl Plugin for CharacterControllerPlugin {
|
impl Plugin for CharacterControllerPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
Update,
|
PreUpdate,
|
||||||
(
|
(set_movement_flag, rotate_view, apply_controls)
|
||||||
reset_upon_switch,
|
|
||||||
set_movement_flag,
|
|
||||||
update_grounded,
|
|
||||||
apply_gravity,
|
|
||||||
rotate_view,
|
|
||||||
movement,
|
|
||||||
)
|
|
||||||
.chain()
|
.chain()
|
||||||
.in_set(ControllerSet::ApplyControlsRun)
|
.in_set(ControllerSet::ApplyControlsRun)
|
||||||
.run_if(in_state(GameState::Playing)), // todo check if we can make this less viral
|
.run_if(in_state(GameState::Playing)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 {
|
|
||||||
debug!("grounded");
|
|
||||||
commands.entity(entity).insert(Grounded);
|
|
||||||
} else {
|
|
||||||
debug!("not grounded");
|
|
||||||
commands.entity(entity).remove::<Grounded>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the movement flag, which is an indicator for the rig animation and the braking system.
|
/// Sets the movement flag, which is an indicator for the rig animation and the braking system.
|
||||||
fn set_movement_flag(
|
fn set_movement_flag(
|
||||||
mut player_movement: ResMut<PlayerMovement>,
|
mut player_movement: ResMut<PlayerMovement>,
|
||||||
@@ -97,70 +60,38 @@ fn rotate_view(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Responds to [`MovementAction`] events and moves character controllers accordingly.
|
fn apply_controls(
|
||||||
fn movement(
|
controls: Res<ControlState>,
|
||||||
controls: Res<Controls>,
|
mut character: Query<(
|
||||||
mut controllers: Query<(
|
&mut MoveInput,
|
||||||
&MovementSpeed,
|
&mut Grounding,
|
||||||
|
&mut KinematicVelocity,
|
||||||
|
&ControllerSettings,
|
||||||
&MovementSpeedFactor,
|
&MovementSpeedFactor,
|
||||||
&JumpImpulse,
|
|
||||||
&mut LinearVelocity,
|
|
||||||
Has<Grounded>,
|
|
||||||
)>,
|
)>,
|
||||||
rig_transform_q: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
|
rig_transform_q: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
|
||||||
mut jump_used: Local<bool>,
|
|
||||||
) {
|
) {
|
||||||
let mut direction = controls.keyboard_state.move_dir;
|
let Ok((mut move_input, mut grounding, mut velocity, settings, move_factor)) =
|
||||||
|
character.single_mut()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let mut jump_requested = controls.keyboard_state.jump;
|
let mut direction = -controls.move_dir.extend(0.0).xzy();
|
||||||
|
|
||||||
if let Some(gamepad) = controls.gamepad_state {
|
if let Some(ref rig_transform) = rig_transform_q {
|
||||||
direction += gamepad.move_dir;
|
direction = (rig_transform.forward() * direction.z) + (rig_transform.right() * direction.x);
|
||||||
|
|
||||||
jump_requested |= gamepad.jump;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ground_normal = *grounding.normal().unwrap_or(Dir3::Y);
|
||||||
|
|
||||||
|
let y_projection = direction.project_onto(ground_normal);
|
||||||
|
direction -= y_projection;
|
||||||
direction = direction.normalize_or_zero();
|
direction = direction.normalize_or_zero();
|
||||||
|
|
||||||
for (movement_speed, factor, jump_impulse, mut linear_velocity, is_grounded) in &mut controllers
|
move_input.set(direction * move_factor.0);
|
||||||
{
|
|
||||||
let mut direction = direction.extend(0.0).xzy();
|
|
||||||
|
|
||||||
if let Some(ref rig_transform) = rig_transform_q {
|
if controls.jump && grounding.is_grounded() {
|
||||||
direction =
|
happy_feet::movement::jump(settings.jump_force, &mut velocity, &mut grounding, Dir3::Y)
|
||||||
(rig_transform.forward() * direction.z) + (rig_transform.right() * direction.x);
|
|
||||||
}
|
|
||||||
|
|
||||||
linear_velocity.x = -direction.x * movement_speed.0 * factor.0;
|
|
||||||
linear_velocity.z = -direction.z * movement_speed.0 * factor.0;
|
|
||||||
|
|
||||||
if is_grounded && jump_requested && !*jump_used {
|
|
||||||
linear_velocity.y = jump_impulse.0;
|
|
||||||
debug!("jump");
|
|
||||||
*jump_used = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !controls.keyboard_state.jump
|
|
||||||
&& !controls
|
|
||||||
.gamepad_state
|
|
||||||
.map(|state| state.jump)
|
|
||||||
.unwrap_or_default()
|
|
||||||
{
|
|
||||||
*jump_used = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Applies [`ControllerGravity`] to character controllers.
|
|
||||||
fn apply_gravity(
|
|
||||||
time: Res<Time>,
|
|
||||||
mut controllers: Query<(&ControllerGravity, &mut LinearVelocity, Option<&Grounded>)>,
|
|
||||||
) {
|
|
||||||
let delta_time = time.delta_secs();
|
|
||||||
|
|
||||||
for (gravity, mut linear_velocity, grounded) in &mut controllers {
|
|
||||||
if grounded.is_none() {
|
|
||||||
linear_velocity.0 += gravity.0 * delta_time;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::control::ControllerSet;
|
||||||
use crate::{
|
use crate::{
|
||||||
GameState,
|
GameState,
|
||||||
abilities::{TriggerCashHeal, TriggerState},
|
abilities::{TriggerCashHeal, TriggerState},
|
||||||
@@ -13,13 +14,15 @@ use bevy::{
|
|||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{ControlState, ControllerSet, Controls};
|
use super::{ControlState, Controls};
|
||||||
|
|
||||||
pub fn plugin(app: &mut App) {
|
pub fn plugin(app: &mut App) {
|
||||||
app.init_resource::<Controls>();
|
app.init_resource::<Controls>();
|
||||||
|
|
||||||
|
app.register_type::<ControllerSettings>();
|
||||||
|
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
Update,
|
PreUpdate,
|
||||||
(
|
(
|
||||||
gamepad_controls,
|
gamepad_controls,
|
||||||
keyboard_controls,
|
keyboard_controls,
|
||||||
@@ -34,6 +37,13 @@ pub fn plugin(app: &mut App) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Clone, PartialEq, Reflect)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct ControllerSettings {
|
||||||
|
pub deceleration_factor: f32,
|
||||||
|
pub jump_force: f32,
|
||||||
|
}
|
||||||
|
|
||||||
fn combine_controls(controls: Res<Controls>, mut combined_controls: ResMut<ControlState>) {
|
fn combine_controls(controls: Res<Controls>, mut combined_controls: ResMut<ControlState>) {
|
||||||
let keyboard = controls.keyboard_state;
|
let keyboard = controls.keyboard_state;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use crate::{
|
|||||||
heads_database::{HeadControls, HeadsDatabase},
|
heads_database::{HeadControls, HeadsDatabase},
|
||||||
};
|
};
|
||||||
|
|
||||||
mod collisions;
|
|
||||||
pub mod controller_common;
|
pub mod controller_common;
|
||||||
pub mod controller_flying;
|
pub mod controller_flying;
|
||||||
pub mod controller_running;
|
pub mod controller_running;
|
||||||
@@ -54,7 +53,7 @@ pub fn plugin(app: &mut App) {
|
|||||||
app.add_event::<ControllerSwitchEvent>();
|
app.add_event::<ControllerSwitchEvent>();
|
||||||
|
|
||||||
app.configure_sets(
|
app.configure_sets(
|
||||||
Update,
|
PreUpdate,
|
||||||
(
|
(
|
||||||
ControllerSet::CollectInputs,
|
ControllerSet::CollectInputs,
|
||||||
ControllerSet::ApplyControlsFly.run_if(resource_equals(SelectedController(
|
ControllerSet::ApplyControlsFly.run_if(resource_equals(SelectedController(
|
||||||
|
|||||||
@@ -88,15 +88,24 @@ fn on_head_drop(
|
|||||||
commands
|
commands
|
||||||
.spawn((
|
.spawn((
|
||||||
Name::new("headdrop"),
|
Name::new("headdrop"),
|
||||||
HeadDrop(drop.head_id),
|
|
||||||
Transform::from_translation(drop.pos),
|
Transform::from_translation(drop.pos),
|
||||||
Visibility::default(),
|
Visibility::default(),
|
||||||
Collider::sphere(1.5),
|
Collider::sphere(1.5),
|
||||||
LockedAxes::ROTATION_LOCKED,
|
LockedAxes::ROTATION_LOCKED,
|
||||||
RigidBody::Dynamic,
|
RigidBody::Dynamic,
|
||||||
CollisionLayers::new(LayerMask(GameLayer::Collectibles.to_bits()), LayerMask::ALL),
|
CollisionLayers::new(
|
||||||
|
GameLayer::CollectiblePhysics,
|
||||||
|
LayerMask::ALL & !GameLayer::Player.to_bits(),
|
||||||
|
),
|
||||||
CollisionEventsEnabled,
|
CollisionEventsEnabled,
|
||||||
Restitution::new(0.6),
|
Restitution::new(0.6),
|
||||||
|
children![(
|
||||||
|
Collider::sphere(1.5),
|
||||||
|
CollisionLayers::new(GameLayer::CollectibleSensors, GameLayer::Player),
|
||||||
|
Sensor,
|
||||||
|
CollisionEventsEnabled,
|
||||||
|
HeadDrop(drop.head_id),
|
||||||
|
)],
|
||||||
))
|
))
|
||||||
.insert_if(
|
.insert_if(
|
||||||
ExternalImpulse::new(spawn_dir * 180.).with_persistence(false),
|
ExternalImpulse::new(spawn_dir * 180.).with_persistence(false),
|
||||||
@@ -115,7 +124,7 @@ fn collect_head(
|
|||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut collision_event_reader: EventReader<CollisionStarted>,
|
mut collision_event_reader: EventReader<CollisionStarted>,
|
||||||
query_player: Query<&Player>,
|
query_player: Query<&Player>,
|
||||||
query_collectable: Query<&HeadDrop>,
|
query_collectable: Query<(&HeadDrop, &ChildOf)>,
|
||||||
query_secret: Query<&SecretHeadMarker>,
|
query_secret: Query<&SecretHeadMarker>,
|
||||||
) {
|
) {
|
||||||
for CollisionStarted(e1, e2) in collision_event_reader.read() {
|
for CollisionStarted(e1, e2) in collision_event_reader.read() {
|
||||||
@@ -127,7 +136,7 @@ fn collect_head(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let key = query_collectable.get(collectable).unwrap();
|
let (key, child_of) = query_collectable.get(collectable).unwrap();
|
||||||
|
|
||||||
let is_secret = query_secret.contains(collectable);
|
let is_secret = query_secret.contains(collectable);
|
||||||
|
|
||||||
@@ -137,6 +146,6 @@ fn collect_head(
|
|||||||
commands.trigger(PlaySound::HeadCollect);
|
commands.trigger(PlaySound::HeadCollect);
|
||||||
}
|
}
|
||||||
commands.trigger(HeadCollected(key.0));
|
commands.trigger(HeadCollected(key.0));
|
||||||
commands.entity(collectable).despawn();
|
commands.entity(child_of.parent()).despawn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::abilities::HeadAbility;
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Default, Reflect, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Default, Reflect, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum HeadControls {
|
pub enum HeadControls {
|
||||||
#[default]
|
#[default]
|
||||||
Walk,
|
Walk,
|
||||||
|
|||||||
28
src/keys.rs
28
src/keys.rs
@@ -31,21 +31,29 @@ fn on_spawn_key(trigger: Trigger<KeySpawn>, mut commands: Commands, assets: Res<
|
|||||||
|
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
Name::new("key"),
|
Name::new("key"),
|
||||||
Key(id.clone()),
|
|
||||||
Transform::from_translation(*position),
|
Transform::from_translation(*position),
|
||||||
Visibility::default(),
|
Visibility::default(),
|
||||||
Collider::sphere(1.5),
|
Collider::sphere(1.5),
|
||||||
ExternalImpulse::new(spawn_dir * 180.).with_persistence(false),
|
ExternalImpulse::new(spawn_dir * 180.).with_persistence(false),
|
||||||
LockedAxes::ROTATION_LOCKED,
|
LockedAxes::ROTATION_LOCKED,
|
||||||
RigidBody::Dynamic,
|
RigidBody::Dynamic,
|
||||||
CollisionLayers::new(LayerMask(GameLayer::Collectibles.to_bits()), LayerMask::ALL),
|
CollisionLayers::new(GameLayer::CollectiblePhysics, GameLayer::Level),
|
||||||
CollisionEventsEnabled,
|
CollisionEventsEnabled,
|
||||||
Restitution::new(0.6),
|
Restitution::new(0.6),
|
||||||
Children::spawn(Spawn((
|
children![
|
||||||
Billboard,
|
(
|
||||||
SquishAnimation(2.6),
|
Billboard,
|
||||||
SceneRoot(assets.mesh_key.clone()),
|
SquishAnimation(2.6),
|
||||||
))),
|
SceneRoot(assets.mesh_key.clone()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Collider::sphere(1.5),
|
||||||
|
CollisionLayers::new(GameLayer::CollectibleSensors, GameLayer::Player),
|
||||||
|
Sensor,
|
||||||
|
CollisionEventsEnabled,
|
||||||
|
Key(id.clone()),
|
||||||
|
)
|
||||||
|
],
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +61,7 @@ fn collect_key(
|
|||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut collision_event_reader: EventReader<CollisionStarted>,
|
mut collision_event_reader: EventReader<CollisionStarted>,
|
||||||
query_player: Query<&Player>,
|
query_player: Query<&Player>,
|
||||||
query_collectable: Query<&Key>,
|
query_collectable: Query<(&Key, &ChildOf)>,
|
||||||
) {
|
) {
|
||||||
for CollisionStarted(e1, e2) in collision_event_reader.read() {
|
for CollisionStarted(e1, e2) in collision_event_reader.read() {
|
||||||
let collectable = if query_player.contains(*e1) && query_collectable.contains(*e2) {
|
let collectable = if query_player.contains(*e1) && query_collectable.contains(*e2) {
|
||||||
@@ -64,10 +72,10 @@ fn collect_key(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let key = query_collectable.get(collectable).unwrap();
|
let (key, child_of) = query_collectable.get(collectable).unwrap();
|
||||||
|
|
||||||
commands.trigger(PlaySound::KeyCollect);
|
commands.trigger(PlaySound::KeyCollect);
|
||||||
commands.trigger(KeyCollected(key.0.clone()));
|
commands.trigger(KeyCollected(key.0.clone()));
|
||||||
commands.entity(collectable).despawn();
|
commands.entity(child_of.parent()).despawn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ enum GameState {
|
|||||||
fn main() {
|
fn main() {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
|
|
||||||
app.register_type::<DebugVisuals>();
|
app.register_type::<DebugVisuals>()
|
||||||
|
.register_type::<TransformInterpolation>();
|
||||||
app.insert_resource(DebugVisuals {
|
app.insert_resource(DebugVisuals {
|
||||||
unlit: false,
|
unlit: false,
|
||||||
tonemapping: Tonemapping::None,
|
tonemapping: Tonemapping::None,
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ pub enum GameLayer {
|
|||||||
Player,
|
Player,
|
||||||
Npc,
|
Npc,
|
||||||
Projectile,
|
Projectile,
|
||||||
Collectibles,
|
CollectiblePhysics,
|
||||||
|
CollectibleSensors,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ struct ActivePlatform {
|
|||||||
pub fn plugin(app: &mut App) {
|
pub fn plugin(app: &mut App) {
|
||||||
app.register_type::<ActivePlatform>();
|
app.register_type::<ActivePlatform>();
|
||||||
app.add_systems(OnEnter(GameState::Playing), init);
|
app.add_systems(OnEnter(GameState::Playing), init);
|
||||||
app.add_systems(Update, move_active.run_if(in_state(GameState::Playing)));
|
app.add_systems(
|
||||||
|
FixedUpdate,
|
||||||
|
move_active.run_if(in_state(GameState::Playing)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(
|
fn init(
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use crate::{
|
|||||||
sounds::PlaySound,
|
sounds::PlaySound,
|
||||||
tb_entities::SpawnPoint,
|
tb_entities::SpawnPoint,
|
||||||
};
|
};
|
||||||
use avian3d::{math::Vector, prelude::*};
|
use avian3d::prelude::*;
|
||||||
use bevy::{
|
use bevy::{
|
||||||
input::common_conditions::input_just_pressed,
|
input::common_conditions::input_just_pressed,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
@@ -61,7 +61,6 @@ fn spawn(
|
|||||||
|
|
||||||
let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.));
|
let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.));
|
||||||
|
|
||||||
let gravity = Vector::NEG_Y * 40.0;
|
|
||||||
let collider = Collider::capsule(0.9, 1.2);
|
let collider = Collider::capsule(0.9, 1.2);
|
||||||
|
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
@@ -80,8 +79,11 @@ fn spawn(
|
|||||||
transform,
|
transform,
|
||||||
Visibility::default(),
|
Visibility::default(),
|
||||||
// LockedAxes::ROTATION_LOCKED, todo
|
// LockedAxes::ROTATION_LOCKED, todo
|
||||||
CollisionLayers::new(LayerMask(GameLayer::Player.to_bits()), LayerMask::ALL),
|
CollisionLayers::new(
|
||||||
CharacterControllerBundle::new(collider, gravity),
|
GameLayer::Player,
|
||||||
|
LayerMask::ALL & !GameLayer::CollectiblePhysics.to_bits(),
|
||||||
|
),
|
||||||
|
CharacterControllerBundle::new(collider, heads_db.head_stats(0).controls),
|
||||||
children![(
|
children![(
|
||||||
Name::new("player-rig"),
|
Name::new("player-rig"),
|
||||||
PlayerBodyMesh,
|
PlayerBodyMesh,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use bevy::{
|
|||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
use bevy_trenchbroom::prelude::*;
|
use bevy_trenchbroom::prelude::*;
|
||||||
|
use happy_feet::prelude::PhysicsMover;
|
||||||
|
|
||||||
use crate::cash::Cash;
|
use crate::cash::Cash;
|
||||||
use crate::loading_assets::GameAssets;
|
use crate::loading_assets::GameAssets;
|
||||||
@@ -65,6 +66,7 @@ pub struct NamedEntity {
|
|||||||
#[reflect(QuakeClass, Component)]
|
#[reflect(QuakeClass, Component)]
|
||||||
#[base(Transform, Target)]
|
#[base(Transform, Target)]
|
||||||
#[spawn_hooks(SpawnHooks::new().convex_collider())]
|
#[spawn_hooks(SpawnHooks::new().convex_collider())]
|
||||||
|
#[require(PhysicsMover = PhysicsMover, TransformInterpolation)]
|
||||||
pub struct Platform;
|
pub struct Platform;
|
||||||
|
|
||||||
#[derive(PointClass, Component, Reflect, Default)]
|
#[derive(PointClass, Component, Reflect, Default)]
|
||||||
@@ -172,7 +174,7 @@ impl CashSpawn {
|
|||||||
SceneRoot(mesh),
|
SceneRoot(mesh),
|
||||||
Cash,
|
Cash,
|
||||||
Collider::cuboid(2., 3.0, 2.),
|
Collider::cuboid(2., 3.0, 2.),
|
||||||
CollisionLayers::new(LayerMask(GameLayer::Collectibles.to_bits()), LayerMask::ALL),
|
CollisionLayers::new(GameLayer::CollectibleSensors, LayerMask::ALL),
|
||||||
CollisionEventsEnabled,
|
CollisionEventsEnabled,
|
||||||
Sensor,
|
Sensor,
|
||||||
));
|
));
|
||||||
|
|||||||
10
src/water.rs
10
src/water.rs
@@ -1,7 +1,5 @@
|
|||||||
use crate::{
|
use crate::control::controller_common::MovementSpeedFactor;
|
||||||
GameState, control::controller_common::MovementSpeedFactor, global_observer, player::Player,
|
use crate::{GameState, global_observer, player::Player, tb_entities::Water};
|
||||||
tb_entities::Water,
|
|
||||||
};
|
|
||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
@@ -35,7 +33,9 @@ fn setup(mut commands: Commands, query: Query<(Entity, &Children), With<Water>>)
|
|||||||
ColliderConstructorHierarchy::new(ColliderConstructor::ConvexHullFromMesh),
|
ColliderConstructorHierarchy::new(ColliderConstructor::ConvexHullFromMesh),
|
||||||
));
|
));
|
||||||
|
|
||||||
commands.entity(child).insert(WaterSensor);
|
// TODO: Figure out why water requires a `Sensor` or else the character will stand *on* it
|
||||||
|
// rather than *in* it
|
||||||
|
commands.entity(child).insert((WaterSensor, Sensor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user