Skip to content

Commit 986c2bb

Browse files
authored
Support custom integration and using LinearVelocity for KCCs (#899)
# Objective Currently, for KCC movement, users must keep `LinearVelocity` at zero and manage their own externally stored velocity. Otherwise, you will end up applying the velocity twice, once by the move-and-slide algorithm, and once by the built-in position integration. This is inconvenient, and also causes potentially incorrect collision response for dynamic bodies that bump into the kinematic body, as the contact won't consider relative velocity properly. Not good! It would be good if users could use `LinearVelocity` even for KCCs, and simply change the way it is applied. ## Solution Add `CustomVelocityIntegration` and `CustomPositionIntegration` components for disabling Avian's built-in integration logic. This allows users to implement their own movement driven by `LinearVelocity` and move-and-slide. ## Testing Ran `move_and_slide_2d` and `move_and_slide_3d`. --- ## Showcase Add the `CustomPositionIntegration` component to disable Avian's own position updates based on velocity: ```rust commands.spawn(( Character, RigidBody::Kinematic, Collider::from(shape), // We want to control position updates manually using move and slide. CustomPositionIntegration, )); ``` Movement systems can then simply modify `LinearVelocity`. In a way, this also makes many systems generic over dynamic and kinematic character controllers. ```rust fn character_movement( mut query: Query<&mut LinearVelocity, With<Character>>, input: Res<ButtonInput<KeyCode>>, ) { for mut lin_vel in &mut query { // Determine movement velocity from input let mut delta_vel = Vec2::ZERO; if input.pressed(KeyCode::KeyW) { delta_vel += Vec2::Y } if input.pressed(KeyCode::KeyS) { delta_vel += Vec2::NEG_Y } if input.pressed(KeyCode::KeyA) { delta_vel += Vec2::NEG_X } if input.pressed(KeyCode::KeyD) { delta_vel += Vec2::X } delta_vel = delta_vel.normalize_or_zero(); delta_vel *= 100.0; if input.pressed(KeyCode::ShiftLeft) { delta_vel *= 2.0; } lin_vel.0 += delta_vel.adjust_precision(); } } ``` You can then just run `move_and_slide` in some system, using `LinearVelocity` as the input velocity, and write the output velocity to `LinearVelocity`.
1 parent d72c48b commit 986c2bb

File tree

6 files changed

+279
-200
lines changed

6 files changed

+279
-200
lines changed

crates/avian2d/examples/move_and_slide_2d.rs

Lines changed: 112 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,17 @@ fn main() {
2020
.with_length_unit(50.0),
2121
))
2222
.add_systems(Startup, setup)
23-
.add_systems(FixedUpdate, move_player)
23+
.add_systems(FixedUpdate, (player_movement, run_move_and_slide).chain())
2424
.add_systems(Update, update_debug_text)
2525
.run();
2626
}
2727

28-
#[derive(Component, Default)]
29-
struct Player {
30-
/// The velocity of the player.
31-
///
32-
/// We cannot use [`LinearVelocity`] directly, because we control the transform manually,
33-
/// and don't want to also apply simulation velocity on top.
34-
velocity: Vec2,
35-
/// The entities touched during the last `move_and_slide` call. Stored for debug printing.
36-
touched: EntityHashSet,
37-
}
28+
#[derive(Component)]
29+
struct Player;
30+
31+
/// The entities touched during the last `move_and_slide` call. Stored for debug printing.
32+
#[derive(Component, Default, Deref, DerefMut)]
33+
struct TouchedEntities(EntityHashSet);
3834

3935
#[derive(Component)]
4036
struct DebugText;
@@ -47,13 +43,16 @@ fn setup(
4743
// Character
4844
let shape = Circle::new(30.0);
4945
commands.spawn((
46+
Player,
5047
Mesh2d(meshes.add(shape)),
5148
MeshMaterial2d(materials.add(Color::from(tailwind::SKY_400.with_alpha(0.6)))),
5249
Collider::from(shape),
5350
RigidBody::Kinematic,
54-
Player::default(),
5551
TransformInterpolation,
56-
// Not needed for meve and slide to work, but we add it for debug printing
52+
// We want to control position updates manually using move and slide.
53+
CustomPositionIntegration,
54+
// Store touched and colliding entities for debug printing.
55+
TouchedEntities::default(),
5756
CollidingEntities::default(),
5857
));
5958

@@ -186,99 +185,121 @@ fn setup(
186185
));
187186
}
188187

189-
fn move_player(
190-
player: Single<(Entity, &mut Transform, &mut Player, &Collider), Without<Camera>>,
191-
move_and_slide: MoveAndSlide,
188+
/// System to handle player movement and friction.
189+
///
190+
/// This only updates velocity. The actual movement is handled by the `run_move_and_slide` system.
191+
fn player_movement(
192+
mut query: Query<&mut LinearVelocity, With<Player>>,
192193
time: Res<Time>,
193194
input: Res<ButtonInput<KeyCode>>,
194-
mut gizmos: Gizmos,
195195
) {
196-
let (entity, mut transform, mut player, collider) = player.into_inner();
197-
let mut movement_velocity = Vec2::ZERO;
198-
if input.pressed(KeyCode::KeyW) {
199-
movement_velocity += Vec2::Y
200-
}
201-
if input.pressed(KeyCode::KeyS) {
202-
movement_velocity += Vec2::NEG_Y
203-
}
204-
if input.pressed(KeyCode::KeyA) {
205-
movement_velocity += Vec2::NEG_X
206-
}
207-
if input.pressed(KeyCode::KeyD) {
208-
movement_velocity += Vec2::X
209-
}
210-
movement_velocity = movement_velocity.normalize_or_zero();
211-
movement_velocity *= 100.0;
212-
if input.pressed(KeyCode::ShiftLeft) {
213-
movement_velocity *= 2.0;
214-
}
196+
for mut lin_vel in &mut query {
197+
// Determine movement velocity from input
198+
let mut movement_velocity = Vec2::ZERO;
199+
if input.pressed(KeyCode::KeyW) {
200+
movement_velocity += Vec2::Y
201+
}
202+
if input.pressed(KeyCode::KeyS) {
203+
movement_velocity += Vec2::NEG_Y
204+
}
205+
if input.pressed(KeyCode::KeyA) {
206+
movement_velocity += Vec2::NEG_X
207+
}
208+
if input.pressed(KeyCode::KeyD) {
209+
movement_velocity += Vec2::X
210+
}
211+
movement_velocity = movement_velocity.normalize_or_zero();
212+
movement_velocity *= 100.0;
213+
if input.pressed(KeyCode::ShiftLeft) {
214+
movement_velocity *= 2.0;
215+
}
215216

216-
// Add velocity from last frame to preserve momentum
217-
movement_velocity += player.velocity;
217+
// Add to current velocity
218+
lin_vel.0 += movement_velocity.adjust_precision();
218219

219-
let current_speed = movement_velocity.length();
220-
if current_speed > 0.0 {
221-
// Apply friction
222-
movement_velocity = movement_velocity / current_speed
223-
* (current_speed - current_speed * 20.0 * time.delta_secs()).max(0.0)
220+
let current_speed = lin_vel.length();
221+
if current_speed > 0.0 {
222+
// Apply friction
223+
lin_vel.0 = lin_vel.0 / current_speed
224+
* (current_speed - current_speed * 20.0 * time.delta_secs().adjust_precision())
225+
.max(0.0)
226+
}
224227
}
228+
}
225229

226-
player.touched.clear();
230+
/// System to run the move and slide algorithm, updating the player's transform and velocity.
231+
///
232+
/// This replaces Avian's default "position integration" that moves kinematic bodies based on their
233+
/// velocity without any collision handling.
234+
fn run_move_and_slide(
235+
mut query: Query<
236+
(
237+
Entity,
238+
&mut Transform,
239+
&mut LinearVelocity,
240+
&mut TouchedEntities,
241+
&Collider,
242+
),
243+
With<Player>,
244+
>,
245+
move_and_slide: MoveAndSlide,
246+
time: Res<Time>,
247+
mut gizmos: Gizmos,
248+
) {
249+
for (entity, mut transform, mut lin_vel, mut touched, collider) in &mut query {
250+
touched.clear();
227251

228-
// Perform move and slide
229-
let MoveAndSlideOutput {
230-
position,
231-
projected_velocity: velocity,
232-
} = move_and_slide.move_and_slide(
233-
collider,
234-
transform.translation.xy().adjust_precision(),
235-
transform
236-
.rotation
237-
.to_euler(EulerRot::XYZ)
238-
.2
239-
.adjust_precision(),
240-
movement_velocity.adjust_precision(),
241-
time.delta(),
242-
&MoveAndSlideConfig::default(),
243-
&SpatialQueryFilter::from_excluded_entities([entity]),
244-
|hit| {
245-
// For each collision, draw debug gizmos
246-
if hit.intersects() {
247-
gizmos.circle_2d(
248-
Isometry2d::from_translation(transform.translation.xy()),
249-
33.0,
250-
tailwind::RED_600,
251-
);
252-
} else {
253-
gizmos.arrow_2d(
254-
hit.point.f32(),
255-
(hit.point
256-
+ hit.normal.adjust_precision() * hit.collision_distance
257-
/ time.delta_secs().adjust_precision())
258-
.f32(),
259-
tailwind::EMERALD_400,
260-
);
261-
}
262-
player.touched.insert(hit.entity);
263-
true
264-
},
265-
);
252+
// Perform move and slide
253+
let MoveAndSlideOutput {
254+
position,
255+
projected_velocity,
256+
} = move_and_slide.move_and_slide(
257+
collider,
258+
transform.translation.xy().adjust_precision(),
259+
transform
260+
.rotation
261+
.to_euler(EulerRot::XYZ)
262+
.2
263+
.adjust_precision(),
264+
lin_vel.0,
265+
time.delta(),
266+
&MoveAndSlideConfig::default(),
267+
&SpatialQueryFilter::from_excluded_entities([entity]),
268+
|hit| {
269+
// For each collision, draw debug gizmos
270+
if hit.intersects() {
271+
gizmos.circle_2d(transform.translation.xy(), 33.0, tailwind::RED_600);
272+
} else {
273+
gizmos.arrow_2d(
274+
hit.point.f32(),
275+
(hit.point
276+
+ hit.normal.adjust_precision() * hit.collision_distance
277+
/ time.delta_secs().adjust_precision())
278+
.f32(),
279+
tailwind::EMERALD_400,
280+
);
281+
}
282+
touched.insert(hit.entity);
283+
true
284+
},
285+
);
266286

267-
// Update transform and stored velocity
268-
transform.translation = position.extend(0.0).f32();
269-
player.velocity = velocity.f32();
287+
// Update transform and velocity
288+
transform.translation = position.extend(0.0).f32();
289+
lin_vel.0 = projected_velocity;
290+
}
270291
}
271292

272293
fn update_debug_text(
273294
mut text: Single<&mut Text, With<DebugText>>,
274-
player: Single<(&Player, &CollidingEntities), With<Player>>,
295+
player: Single<(&LinearVelocity, &TouchedEntities, &CollidingEntities), With<Player>>,
275296
names: Query<NameOrEntity>,
276297
) {
277-
let (player, colliding_entities) = player.into_inner();
298+
let (lin_vel, touched, colliding_entities) = player.into_inner();
278299
***text = format!(
279300
"velocity: [{:.3}, {:.3}]\n{} intersections (goal is 0): {:#?}\n{} touched: {:#?}",
280-
player.velocity.x,
281-
player.velocity.y,
301+
lin_vel.x,
302+
lin_vel.y,
282303
colliding_entities.len(),
283304
names
284305
.iter_many(colliding_entities.iter())
@@ -287,9 +308,9 @@ fn update_debug_text(
287308
.map(|n| format!("{} ({})", name.entity, n))
288309
.unwrap_or_else(|| format!("{}", name.entity)))
289310
.collect::<Vec<_>>(),
290-
player.touched.len(),
311+
touched.len(),
291312
names
292-
.iter_many(player.touched.iter())
313+
.iter_many(touched.iter())
293314
.map(|name| name
294315
.name
295316
.map(|n| format!("{} ({})", name.entity, n))

0 commit comments

Comments
 (0)