update movement

This commit is contained in:
Mitchell Marino 2026-02-02 15:21:36 -06:00
parent e4be421ef9
commit e09b7d2537
3 changed files with 8 additions and 145 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 34 B

1
assets/player.png Symbolic link
View File

@ -0,0 +1 @@
/home/mitchell/Pictures/player.png

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 34 B

View File

@ -6,15 +6,7 @@ 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_message::<MovementAction>() app.add_message::<MovementAction>()
.add_systems(Update, ((keyboard_input, gamepad_input), movement).chain()) .add_systems(Update, ((keyboard_input, gamepad_input), movement).chain());
.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(NarrowPhaseSystems::Last),
);
} }
} }
@ -51,8 +43,8 @@ impl CharacterControllerBundle {
character_controller: CharacterController, character_controller: CharacterController,
body: RigidBody::Kinematic, body: RigidBody::Kinematic,
collider, collider,
speed: MaxSpeed(100.), speed: MaxSpeed(300.),
acceleration: MaxAcceleration(1000.), acceleration: MaxAcceleration(5000.),
} }
} }
} }
@ -109,6 +101,7 @@ fn movement(
for (max_speed, max_acceleration, mut linear_velocity) in &mut controllers { for (max_speed, max_acceleration, mut linear_velocity) in &mut controllers {
while movement_reader.len() > 1 { while movement_reader.len() > 1 {
warn!("Extra movement message. Ignoring");
movement_reader.read(); movement_reader.read();
} }
@ -128,122 +121,3 @@ fn movement(
**linear_velocity += delta; **linear_velocity += delta;
} }
} }
/// 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: Collisions,
bodies: Query<&RigidBody>,
collider_rbs: Query<&ColliderOf, Without<Sensor>>,
mut character_controllers: Query<
(&mut Position, &mut LinearVelocity),
(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) =
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;
}
if deepest_penetration > 0.0 {
// 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.
// Avoid climbing up walls.
impulse.y = impulse.y.max(0.0);
linear_velocity.0 -= impulse;
}
}
}
}

View File

@ -23,12 +23,7 @@ fn main() {
.run(); .run();
} }
fn setup( fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
mut commands: Commands,
mut materials: ResMut<Assets<ColorMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
asset_server: Res<AssetServer>,
) {
let mut projection = OrthographicProjection::default_2d(); let mut projection = OrthographicProjection::default_2d();
projection.scaling_mode = ScalingMode::AutoMin { projection.scaling_mode = ScalingMode::AutoMin {
min_width: 1920., min_width: 1920.,
@ -38,10 +33,9 @@ fn setup(
// player // player
commands.spawn(( commands.spawn((
Mesh2d(meshes.add(Capsule2d::new(12.5, 20.0))), Sprite::from_image(asset_server.load("player.png")),
MeshMaterial2d(materials.add(Color::srgb(0.2, 0.7, 0.9))), Transform::from_xyz(0.0, 0.0, 0.0),
Transform::from_xyz(0.0, -100.0, 0.0), CharacterControllerBundle::new(Collider::capsule(15., 27.5)),
CharacterControllerBundle::new(Collider::capsule(12.5, 20.0)),
)); ));
// A cube to move around // A cube to move around
@ -55,11 +49,6 @@ fn setup(
RigidBody::Dynamic, RigidBody::Dynamic,
Collider::rectangle(30.0, 30.0), Collider::rectangle(30.0, 30.0),
)); ));
commands.spawn((
Sprite::from_image(asset_server.load("player.png")),
Transform::from_xyz(0., 0., 0.),
));
} }
fn debug_border(mut gizmos: Gizmos) { fn debug_border(mut gizmos: Gizmos) {