bevy_xpbd
bevy_xpbd copied to clipboard
Manifold with no contacts
Kinematic controller example, even though is mentioned to be basic, produces an interesting result. I've slightly modified the code, basically there needs to be a character with rectangle collider and another static rectangle collider with reachable corner. To reproduce, jump diagonally into static block trying to touch corner to corner until stuck.
The logs I've added printed following:
2024-07-16T21:36:24.627873Z INFO kinematic_character_2d: Velocity: LinearVelocity(Vec2(188.1912, 325.01816))
2024-07-16T21:36:24.628731Z INFO kinematic_character_2d::plugin: manifold ContactManifold { contacts: [], normal1: Vec2(1.0, 0.0), normal2: Vec2(-1.0, 0.0), index: 0 }
2024-07-16T21:36:24.629385Z INFO kinematic_character_2d::plugin: -192.30272, -3.4028235e38, 0.016666667, Vec2(-1.0, -0.0)
2024-07-16T21:36:24.629871Z INFO kinematic_character_2d::plugin: Applying impulse Vec2(-inf, 0.0)
2024-07-16T21:36:24.644551Z INFO kinematic_character_2d: Velocity: LinearVelocity(Vec2(inf, 300.01794))
As you can see, a manifold is found, but without any contacts. Collision code assumes that at least one contact exists so it can override default value of deepest_penetration, this assumption fails so generated impulse to compensate becomes infinite. So my question is, is it ok for manifold to have no contact points? If yes, how to interpet such manifold.
main.rs
mod plugin;
use avian2d::{math::*, prelude::*};
use bevy::{
prelude::*,
render::{render_asset::RenderAssetUsages, render_resource::PrimitiveTopology},
sprite::MaterialMesh2dBundle,
};
use examples_common_2d::ExampleCommonPlugin;
use plugin::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
ExampleCommonPlugin,
PhysicsPlugins::default().with_length_unit(20.0),
PhysicsDebugPlugin::default(),
CharacterControllerPlugin,
))
.insert_resource(ClearColor(Color::srgb(0.05, 0.05, 0.1)))
.insert_resource(Gravity(Vector::NEG_Y * 1000.0))
.add_systems(Startup, setup)
.add_systems(Update, print_velocity)
.run();
}
fn print_velocity(velocities: Query<(&RigidBody, &LinearVelocity)>) {
for (rb, v) in velocities.iter() {
if !rb.is_kinematic() {
continue;
}
info!("Velocity: {:?}", v);
}
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
// Player
commands.spawn((
MaterialMesh2dBundle {
mesh: meshes.add(Capsule2d::new(12.5, 20.0)).into(),
material: materials.add(Color::srgb(0.2, 0.7, 0.9)),
transform: Transform::from_xyz(0.0, -100.0, 0.0),
..default()
},
CharacterControllerBundle::new(Collider::rectangle(25., 40.0), Vector::NEG_Y * 1500.0)
.with_movement(1250.0, 0.92, 400.0, (30.0 as Scalar).to_radians()),
));
// A cube to move around
commands.spawn((
SpriteBundle {
sprite: Sprite {
color: Color::srgb(0.0, 0.4, 0.7),
custom_size: Some(Vec2::new(30.0, 30.0)),
..default()
},
transform: Transform::from_xyz(50.0, -70.0, 0.0),
..default()
},
RigidBody::Static,
Collider::rectangle(30.0, 30.0),
));
// Platforms
commands.spawn((
SpriteBundle {
sprite: Sprite {
color: Color::srgb(0.7, 0.7, 0.8),
custom_size: Some(Vec2::new(1100.0, 50.0)),
..default()
},
transform: Transform::from_xyz(0.0, -175.0, 0.0),
..default()
},
RigidBody::Static,
Collider::rectangle(1100.0, 50.0),
));
// Camera
commands.spawn(Camera2dBundle::default());
}
plugin.rs
use avian2d::{math::*, prelude::*};
use bevy::{ecs::query::Has, prelude::*};
pub struct CharacterControllerPlugin;
impl Plugin for CharacterControllerPlugin {
fn build(&self, app: &mut App) {
app.add_event::<MovementAction>()
.add_systems(
Update,
(
keyboard_input,
gamepad_input,
update_grounded,
apply_gravity,
movement,
apply_movement_damping,
)
.chain(),
)
.add_systems(
PostProcessCollisions,
kinematic_controller_collisions,
);
}
}
#[derive(Event)]
pub enum MovementAction {
Move(Scalar),
Jump,
}
#[derive(Component)]
pub struct CharacterController;
#[derive(Component)]
#[component(storage = "SparseSet")]
pub struct Grounded;
#[derive(Component)]
pub struct MovementAcceleration(Scalar);
#[derive(Component)]
pub struct MovementDampingFactor(Scalar);
#[derive(Component)]
pub struct JumpImpulse(Scalar);
#[derive(Component)]
pub struct ControllerGravity(Vector);
#[derive(Component)]
pub struct MaxSlopeAngle(Scalar);
#[derive(Bundle)]
pub struct CharacterControllerBundle {
character_controller: CharacterController,
rigid_body: RigidBody,
collider: Collider,
ground_caster: ShapeCaster,
gravity: ControllerGravity,
movement: MovementBundle,
}
#[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 {
let mut caster_shape = collider.clone();
caster_shape.set_scale(Vector::ONE * 0.99, 10);
Self {
character_controller: CharacterController,
rigid_body: RigidBody::Kinematic,
collider,
ground_caster: ShapeCaster::new(caster_shape, Vector::ZERO, 0.0, Dir2::NEG_Y)
.with_max_time_of_impact(10.0),
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
}
}
fn keyboard_input(
mut movement_event_writer: EventWriter<MovementAction>,
keyboard_input: Res<ButtonInput<KeyCode>>,
) {
let left = keyboard_input.any_pressed([KeyCode::KeyA, KeyCode::ArrowLeft]);
let right = keyboard_input.any_pressed([KeyCode::KeyD, KeyCode::ArrowRight]);
let horizontal = right as i8 - left as i8;
let direction = horizontal as Scalar;
if direction != 0.0 {
movement_event_writer.send(MovementAction::Move(direction));
}
if keyboard_input.just_pressed(KeyCode::Space) {
movement_event_writer.send(MovementAction::Jump);
}
}
fn gamepad_input(
mut movement_event_writer: EventWriter<MovementAction>,
gamepads: Res<Gamepads>,
axes: Res<Axis<GamepadAxis>>,
buttons: Res<ButtonInput<GamepadButton>>,
) {
for gamepad in gamepads.iter() {
let axis_lx = GamepadAxis {
gamepad,
axis_type: GamepadAxisType::LeftStickX,
};
if let Some(x) = axes.get(axis_lx) {
movement_event_writer.send(MovementAction::Move(x as Scalar));
}
let jump_button = GamepadButton {
gamepad,
button_type: GamepadButtonType::South,
};
if buttons.just_pressed(jump_button) {
movement_event_writer.send(MovementAction::Jump);
}
}
}
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 {
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 movement(
time: Res<Time>,
mut movement_event_reader: EventReader<MovementAction>,
mut controllers: Query<(
&MovementAcceleration,
&JumpImpulse,
&mut LinearVelocity,
Has<Grounded>,
)>,
) {
let delta_time = time.delta_seconds_f64().adjust_precision();
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) => {
linear_velocity.x += *direction * movement_acceleration.0 * delta_time;
}
MovementAction::Jump => {
if is_grounded {
linear_velocity.y = jump_impulse.0;
}
}
}
}
}
}
fn apply_gravity(
time: Res<Time>,
mut controllers: Query<(&ControllerGravity, &mut LinearVelocity)>,
) {
let delta_time = time.delta_seconds_f64().adjust_precision();
for (gravity, mut linear_velocity) in &mut controllers {
linear_velocity.0 += gravity.0 * delta_time;
}
}
fn apply_movement_damping(mut query: Query<(&MovementDampingFactor, &mut LinearVelocity)>) {
for (damping_factor, mut linear_velocity) in &mut query {
linear_velocity.x *= damping_factor.0;
}
}
#[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>,
) {
for contacts in collisions.iter() {
let Ok([collider_parent1, collider_parent2]) =
collider_parents.get_many([contacts.entity1, contacts.entity2])
else {
continue;
};
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;
};
if !character_rb.is_kinematic() {
continue;
}
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;
info!("manifold {:?}", manifold);
for contact in manifold.contacts.iter() {
if contact.penetration > 0.0 {
position.0 += normal * contact.penetration;
}
deepest_penetration = deepest_penetration.max(contact.penetration);
}
if is_other_dynamic {
continue;
}
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 climbable {
let normal_direction_x =
normal.reject_from_normalized(Vector::Y).normalize_or_zero();
let linear_velocity_x = linear_velocity.dot(normal_direction_x);
let max_y_speed = -linear_velocity_x * slope_angle.tan();
linear_velocity.y = linear_velocity.y.max(max_y_speed);
} else {
if linear_velocity.dot(normal) > 0.0 {
continue;
}
let impulse = linear_velocity.reject_from_normalized(normal);
linear_velocity.0 = impulse;
}
} else {
let normal_speed = linear_velocity.dot(normal);
if normal_speed > 0.0 {
continue;
}
let impulse_magnitude = normal_speed
- (deepest_penetration / time.delta_seconds_f64().adjust_precision());
let mut impulse = impulse_magnitude * normal;
info!("{:?}, {:?}, {:?}, {:?}", normal_speed, deepest_penetration, time.delta_seconds_f64(), normal);
if climbable {
linear_velocity.y -= impulse.y.min(0.0);
} else {
impulse.y = impulse.y.max(0.0);
info!("Applying impulse {:?}", impulse);
linear_velocity.0 -= impulse;
}
}
}
}
}