Controllers
A controller is a higher-level tool computing the motion of a body from what your application asks for, rather than leaving it entirely to the forces of the simulation. Rapier provides:
- The character controller, moving a kinematic body along the obstacles of the scene.
- The vehicle controller, driving a car-like body on ray-casted wheels.
- The PID controller, steering a dynamic body toward a target pose at the velocity level.
A controller is a higher-level tool computing the motion of a body from what your application asks for, rather than leaving it entirely to the forces of the simulation. Rapier provides:
- The character controller, moving a kinematic body along the obstacles of the scene.
- The vehicle controller, driving a car-like body on ray-casted wheels.
- The PID controller, steering a dynamic body toward a target pose at the velocity level.
A controller is a higher-level tool computing the motion of a body from what your application asks for, rather than
leaving it entirely to the forces of the simulation. Rapier provides the following controllers, exposed by bevy_rapier
as components:
- The character controller, moving a kinematic body along the obstacles of the scene.
- The vehicle controller, driving a car-like body on ray-casted
wheels (only available with
bevy_rapier3d). - The PID controller, steering a dynamic body toward a target pose at the velocity level.
A controller is a higher-level tool computing the motion of a body from what your application asks for, rather than
leaving it entirely to the forces of the simulation. Rapier provides the following controllers, each being an object
owned by your application: it is allocated by its New function (e.g. r3NewPidController) and must be freed by its
Free function (e.g. r3FreePidController):
- The character controller, moving a kinematic body along the obstacles of the scene.
- The vehicle controller, driving a car-like body on ray-casted wheels (only available in 3D).
- The PID controller, steering a dynamic body toward a target pose at the velocity level.
A controller is a higher-level tool computing the motion of a body from what your application asks for, rather than
leaving it entirely to the forces of the simulation. Rapier provides the following controllers, each being an ordinary
Python object owned by your application (it doesn’t belong to the PhysicsWorld, and is given the parts of the world
it needs when it is used):
- The character controller, moving a kinematic body along the obstacles of the scene.
- The vehicle controller, driving a car-like body on ray-casted wheels.
- The PID controller, steering a dynamic body toward a target pose at the velocity level.
Character controller
Most games involve bodies behaving in ways that defy the laws of physics: floating platforms, elevators, playable characters, etc. This is why kinematic bodies exist: they offer a total control over the body’s trajectory since they are completely immune to forces or impulses (like gravity, contacts, joints).
But this control comes at a price: it is up to the user to take any obstacle into account by running custom collision-detection operations manually and update the trajectory accordingly. This can be very difficult. Detecting obstacles usually rely on ray-casting or shape-casting, used to adjust the trajectory based on the potential contact normals. Often, multiple ray or shape-casts are needed, and the trajectory adjustment code isn’t straightforward.
The Kinematic Character Controller (which we will abbreviate to character controller) is a higher-level tool that will emit the proper ray-casts and shape-casts to adjust the user-defined trajectory based on obstacles. The well-known move-and-slide operation is the main feature of a character controller.
Despite its name, a character controller can also be used for moving objects that are not characters. For example, a character controller may be used to move a platform. In the rest of this guide, we will use the word character to designate whatever you would like to move using the character controller.
Rapier provides a built-in general-purpose character controller implementation. It allows you to easily:
- Stop at obstacles.
- Slide on slopes that are not to steep.
- Climb stairs automatically.
- Walk over small obstacles.
- Interact with moving platforms.
Despite the fact that this built-in character controller is designed to be generic enough to serve as a good starting
point for many common use-cases, character-control (especially for the player’s character itself) is often very
game-specific. Therefore the builtin character controller may not work perfectly out-of-the-box for all game
types.
Setup and usage
The character controller implementation is exposed as the KinematicCharacterController structure. This structure only
contains information about the character controller’s behavior. It does not contain any collider-specific or
rigid-body-specific information like handles, velocities, positions, etc. Therefore, the same instance of KinematicCharacterController
can be used to control multiple rigid-bodies/colliders if they rely on the same set of parameters.
The KinematicCharacterController exposes only two methods:
move_shapeis responsible for calculating the possible movement of a character based on the desired movement, obstacles, and character controller options.solve_character_collision_impulsesis detailed in the collisions section.
- Example 2D
- Example 3D
// The translation we would like to apply if there were no obstacles.
let desired_translation = Vector::new(1.0, -2.0);
// Create the character controller, here with the default configuration.
let character_controller = KinematicCharacterController::default();
// Init the query pipeline.
let filter = QueryFilter::default()
// Make sure the character we are trying to move isn’t considered an obstacle.
.exclude_rigid_body(rigid_body_handle);
let query_pipeline = world.query_pipeline_with_filter(filter);
// Calculate the possible movement.
let corrected_movement = character_controller.move_shape(
dt, // The timestep length (can be set to SimulationSettings::dt).
&query_pipeline, // The query pipeline.
character_shape, // The character’s shape.
character_pos, // The character’s initial position.
desired_translation,
|_| {}, // We don’t care about events in this example.
);
// TODO: apply the `corrected_movement.translation` to the rigid-body or collider based on the rules described below.
// The translation we would like to apply if there were no obstacles.
let desired_translation = Vector::new(1.0, -2.0, 3.0);
// Create the character controller, here with the default configuration.
let character_controller = KinematicCharacterController::default();
// Init the query pipeline.
let filter = QueryFilter::default()
// Make sure the character we are trying to move isn’t considered an obstacle.
.exclude_rigid_body(rigid_body_handle);
let query_pipeline = world.query_pipeline_with_filter(filter);
// Calculate the possible movement.
let corrected_movement = character_controller.move_shape(
dt, // The timestep length (can be set to SimulationSettings::dt).
&query_pipeline, // The query pipeline.
character_shape, // The character’s shape.
character_pos, // The character’s initial position.
desired_translation,
|_| {}, // We don’t care about events in this example.
);
// TODO: apply the `corrected_movement.translation` to the rigid-body or collider based on the rules described below.
The recommended way to update the character’s position depends on its representation:
- A collider not attached to any rigid-body: set the collider’s position directly to the corrected movement added to its current position.
- A velocity-based kinematic rigid-body: set its velocity to the computed movement divided by the timestep length.
- A position-based kinematic rigid-body: set its next kinematic position to the corrected movement added to its current position.
The character controller is used through the KinematicCharacterController component. This
component contains the character controller’s behavior settings, as well as the translation to apply to the character.
The KinematicCharacterController component must be added to the same entity as a Transform component. If the field
KinematicCharacterController::custom_shape isn’t set, then the entity it is attached to must also contain a Collider component.
That collider can optionally be attached to a rigid-body. At each frame, the KinematicCharacterController::translation
field can be set to the desired translation for that character.
During the next physics update step, that translation will be resolved against obstacles, and the resulting movement will be automatically applied to the entity’s transform, or the transform of the entity containing the rigid-body the collider to move is attached to.
The applied character motion, and the information of whether the character
is touching the ground at its final position, can be read with the KinematicCharacterControllerOutput component
(inserted automatically to the same entity as the KinematicCharacterController component).
- Example 2D
- Example 3D
fn setup_physics(mut commands: Commands) {
commands
.spawn(RigidBody::KinematicPositionBased)
.insert(Collider::ball(0.5))
.insert(KinematicCharacterController::default());
}
fn update_system(mut controllers: Query<&mut KinematicCharacterController>) {
for mut controller in controllers.iter_mut() {
controller.translation = Some(Vec2::new(1.0, -0.5));
}
}
fn read_result_system(controllers: Query<(Entity, &KinematicCharacterControllerOutput)>) {
for (entity, output) in controllers.iter() {
println!(
"Entity {:?} moved by {:?} and touches the ground: {:?}",
entity, output.effective_translation, output.grounded
);
}
}
fn setup_physics(mut commands: Commands) {
commands
.spawn(RigidBody::KinematicPositionBased)
.insert(Collider::ball(0.5))
.insert(Transform::default())
.insert(KinematicCharacterController {
..KinematicCharacterController::default()
});
}
fn update_system(time: Res<Time>, mut controllers: Query<&mut KinematicCharacterController>) {
for mut controller in controllers.iter_mut() {
controller.translation = Some(Vec3::new(1.0, -5.0, -1.0) * time.delta_secs());
}
}
fn read_result_system(controllers: Query<(Entity, &KinematicCharacterControllerOutput)>) {
for (entity, output) in controllers.iter() {
println!(
"Entity {:?} moved by {:?} and touches the ground: {:?}",
entity, output.effective_translation, output.grounded
);
}
}
The character controller can also be used without the KinematicCharacterController component, by calling
RapierContextMut::move_shape (from the WriteRapierContext system parameter) with the desired translation, the shape
of the character (a Collider can be given directly), and its current position. This calculates the possible movement
right away, based on the positions of the colliders at the end of the last timestep, but doesn't apply it: it is up to
you to apply the resulting MoveShapeOutput::effective_translation to the character. The behavior of the controller is
configured by the MoveShapeOptions argument (which has the same settings as the KinematicCharacterController
component), and its obstacles by the QueryFilter argument detailed in the
scene query filters section. The collisions are given to the last argument, a
closure called on each obstacle hit along the path:
- Example 2D
- Example 3D
/// Marks a character moved without the `KinematicCharacterController` component.
#[derive(Component)]
struct ManualCharacter;
fn move_character_manually(
mut context: WriteRapierContext,
mut characters: Query<(Entity, &Collider, &mut Transform), With<ManualCharacter>>,
) -> Result {
let mut context = context.single_mut()?;
for (entity, collider, mut transform) in characters.iter_mut() {
// The translation we would like to apply if there were no obstacles.
let desired_translation = Vec2::new(1.0, -0.5);
// Configure the controller like with the `KinematicCharacterController` component.
let options = MoveShapeOptions {
snap_to_ground: Some(CharacterLength::Absolute(0.5)),
..default()
};
// Make sure the character we are trying to move isn’t considered an obstacle.
let filter = QueryFilter::default().exclude_collider(entity);
// Calculate the possible movement.
let output = context.move_shape(
desired_translation,
collider, // The character’s shape.
transform.translation.truncate(), // The character’s initial position.
transform.rotation.to_euler(EulerRot::ZYX).0, // The character’s rotation.
1.0, // The character’s mass, for the impulses applied to dynamic bodies.
&options,
filter,
|collision| println!("The character hit the entity {:?}.", collision.entity),
);
// The movement isn’t applied automatically.
transform.translation += output.effective_translation.extend(0.0);
}
Ok(())
}
/// Marks a character moved without the `KinematicCharacterController` component.
#[derive(Component)]
struct ManualCharacter;
fn move_character_manually(
time: Res<Time>,
mut context: WriteRapierContext,
mut characters: Query<(Entity, &Collider, &mut Transform), With<ManualCharacter>>,
) -> Result {
let mut context = context.single_mut()?;
for (entity, collider, mut transform) in characters.iter_mut() {
// The translation we would like to apply if there were no obstacles.
let desired_translation = Vec3::new(1.0, -5.0, -1.0) * time.delta_secs();
// Configure the controller like with the `KinematicCharacterController` component.
let options = MoveShapeOptions {
snap_to_ground: Some(CharacterLength::Absolute(0.5)),
..default()
};
// Make sure the character we are trying to move isn’t considered an obstacle.
let filter = QueryFilter::default().exclude_collider(entity);
// Calculate the possible movement.
let output = context.move_shape(
desired_translation,
collider, // The character’s shape.
transform.translation, // The character’s initial position.
transform.rotation, // The character’s rotation.
1.0, // The character’s mass, for the impulses applied to dynamic bodies.
&options,
filter,
|collision| println!("The character hit the entity {:?}.", collision.entity),
);
// The movement isn’t applied automatically.
transform.translation += output.effective_translation;
}
Ok(())
}
Unlike the KinematicCharacterController component, move_shape doesn't know which collider is the character. If the
character is a collider (or is attached to a rigid-body) present in the physics scene, the filter must exclude that
collider (and that rigid-body) from the set of obstacles (with QueryFilter::exclude_collider and
QueryFilter::exclude_rigid_body) to prevent the character from colliding with itself.
A new character controller can be created and removed by the physics World:
// The gap the controller will leave between the character and its environment.
let offset = 0.01;
// Create the controller.
let characterController = world.createCharacterController(offset);
// Remove the controller once we are done with it.
world.removeCharacterController(characterController);
Note that the character controller does not store a reference to the rigid-body and collider it controls. Therefore, the
same instance of the CharacterController class can be used to control different colliders. This can be useful if
you want to apply the same kind of character control settings to multiple characters.
The created character controller can then be used to control the movement of a collider taking into account obstacles on its path. This is done in two steps:
- Given a desired translation, compute the actual translation that we can apply to the collider based on the obstacles.
- Read the result and apply it to the rigid-body or collider (if it isn’t attached to a rigid-body) by setting its position, kinematic velocity, or next kinematic position, depending on the situation.
let characterController = world.createCharacterController(offset);
characterController.computeColliderMovement(
collider, // The collider we would like to move.
desiredTranslation, // The movement we would like to apply if there wasn’t any obstacle.
);
// Read the result.
let correctedMovement = characterController.computedMovement();
// TODO: apply this corrected movement by following the rules described below.
The recommended way to update the character’s position depends on its representation:
- A collider not attached to any rigid-body: set the collider’s position directly (with
collider.setTranslation) to the corrected movement added to its current position. - A velocity-based kinematic rigid-body: set its velocity (with
rigidBody.setLinvel) to the computed movement divided by the timestep length. - A position-based kinematic rigid-body: set its next kinematic position (with
rigidBody.setNextKinematicTranslation) to the corrected movement added to its current position.
The character controller implementation is exposed as the R3KinematicCharacterController object, created with its
default configuration by r3NewKinematicCharacterController, and freed by r3FreeKinematicCharacterController. This
object only contains information about the character controller’s behavior (as well as the collisions recorded by its
last movement calculation, see the collisions section). It does not contain
any collider-specific or rigid-body-specific information like handles, velocities, positions, etc. Therefore, the same
R3KinematicCharacterController can be used to control multiple rigid-bodies/colliders if they rely on the same set
of parameters. Its behavior is configured by its setters (detailed in the next sections), and it is used through the
following functions:
r3KinematicCharacterController_MoveShapeis responsible for calculating the possible movement of a character based on the desired movement, obstacles, and character controller options. It is given the world containing the obstacles, theR3QueryOptionsselecting these obstacles (see the filtering section), the timestep length, the shape of the character, and its current pose. The character’s shape is anR3SharedShape, e.g., created withr3BallSharedShapeor cloned from a collider withr3Collider_CloneShape(and freed withr3FreeSharedShapeonce it is no longer needed).r3KinematicCharacterController_Collisionsandr3KinematicCharacterController_SolveCharacterCollisionImpulsesare detailed in the collisions section.
- Example 2D
- Example 3D
// The translation we would like to apply if there were no obstacles.
R2Vector desired_translation = r2Vector(1.0, -2.0);
// Create the character controller, here with the default configuration.
R2KinematicCharacterController *character_controller = r2NewKinematicCharacterController();
// Init the query options.
R2QueryOptions options = r2DefaultQueryOptions();
// Make sure the character we are trying to move isn't considered an obstacle.
options.filter.exclude_rigid_body = rigid_body_handle;
// Calculate the possible movement.
R2CharacterMovement corrected_movement = r2KinematicCharacterController_MoveShape(
world, // The world containing the obstacles.
&options, // The query options (NULL for the default ones).
character_controller, // The character controller.
dt, // The timestep length (can be set to r2TimeStep(world)).
character_shape, // The character's shape.
character_pos, // The character's initial position.
desired_translation);
// TODO: apply the `corrected_movement.translation` to the rigid-body or collider based on the rules described below.
// Free the character controller once it is no longer needed.
r2FreeKinematicCharacterController(character_controller);
// The translation we would like to apply if there were no obstacles.
R3Vector desired_translation = r3Vector(1.0, -2.0, 3.0);
// Create the character controller, here with the default configuration.
R3KinematicCharacterController *character_controller = r3NewKinematicCharacterController();
// Init the query options.
R3QueryOptions options = r3DefaultQueryOptions();
// Make sure the character we are trying to move isn't considered an obstacle.
options.filter.exclude_rigid_body = rigid_body_handle;
// Calculate the possible movement.
R3CharacterMovement corrected_movement = r3KinematicCharacterController_MoveShape(
world, // The world containing the obstacles.
&options, // The query options (NULL for the default ones).
character_controller, // The character controller.
dt, // The timestep length (can be set to r3TimeStep(world)).
character_shape, // The character's shape.
character_pos, // The character's initial position.
desired_translation);
// TODO: apply the `corrected_movement.translation` to the rigid-body or collider based on the rules described below.
// Free the character controller once it is no longer needed.
r3FreeKinematicCharacterController(character_controller);
The obstacles are taken at the positions they had at the end of the last r3Step (or r3DetectCollisions). The
returned R3CharacterMovement contains the corrected movement (its translation field), as well as whether the
character touches the ground at its final position (its grounded field), and whether it is sliding down a slope that
is too steep to climb (its is_sliding_down_slope field). The corrected movement isn’t applied automatically: the
recommended way to update the character’s position depends on its representation:
- A collider not attached to any rigid-body: set the collider’s position directly (with
r3Collider_SetTranslation) to the corrected movement added to its current position. - A velocity-based kinematic rigid-body: set its velocity (with
r3RigidBody_SetLinvel) to the computed movement divided by the timestep length. - A position-based kinematic rigid-body: set its next kinematic position (with
r3RigidBody_SetNextKinematicTranslation) to the corrected movement added to its current position.
The character controller implementation is exposed as the KinematicCharacterController class. This class only
contains information about the character controller’s behavior. It does not contain any collider-specific or
rigid-body-specific information like handles, velocities, positions, etc. Therefore, the same instance of
KinematicCharacterController can be used to control multiple rigid-bodies/colliders if they rely on the same set of
parameters. Its settings (detailed in the next sections) are properties that can also be given as keyword arguments
to its constructor (e.g. KinematicCharacterController(slide=False, max_slope_climb_angle=0.5)). The
KinematicCharacterController exposes only two methods:
move_shapeis responsible for calculating the possible movement of a character based on the desired movement, obstacles, and character controller options. It is given the timestep length, the rigid-body set and the collider set (both unused and kept for backward compatibility: they can beNone, or must be the sets of the query pipeline), the query pipeline of the world containing the obstacles (e.g.world.query_pipeline), the shape of the character (aSharedShape, e.g. theshapeof itsCollider), its current pose, the desired translation, and optionally theQueryFilterselecting the obstacles (see the filtering section) and a callback receiving the collisions (see the collisions section).solve_character_collision_impulsesis detailed in the collisions section.
# The translation we would like to apply if there were no obstacles.
desired_translation = (1.0, -2.0, 3.0)
# Create the character controller, here with the default configuration.
character_controller = rp.KinematicCharacterController()
# Make sure the character we are trying to move isn’t considered an obstacle.
query_filter = rp.QueryFilter().exclude_rigid_body(rigid_body_handle)
# Calculate the possible movement.
corrected_movement = character_controller.move_shape(
dt, # The timestep length (can be set to world.integration_parameters.dt).
None, # The rigid-body set, unused: the one of the query pipeline is used.
None, # The collider set, unused: the one of the query pipeline is used.
world.query_pipeline, # The query pipeline containing the obstacles.
character_shape, # The character’s shape.
character_pos, # The character’s initial position.
desired_translation,
query_filter, # The obstacles to consider.
)
# TODO: apply the `corrected_movement.translation` to the rigid-body or collider based on the rules described below.
The obstacles are taken at the positions they had at the end of the last PhysicsWorld.step (or the last
PhysicsWorld.update_query_pipeline). The returned EffectiveCharacterMovement contains the corrected movement (its
translation property), as well as whether the character touches the ground at its final position (its grounded
property), and whether it is sliding down a slope that is too steep to climb (its is_sliding_down_slope property).
The corrected movement isn’t applied automatically: the recommended way to update the character’s position depends on
its representation:
- A collider not attached to any rigid-body: set the collider’s position directly (with its
translationproperty) to the corrected movement added to its current position. - A velocity-based kinematic rigid-body: set its velocity (with its
linvelproperty) to the computed movement divided by the timestep length. - A position-based kinematic rigid-body: set its next kinematic position (with
RigidBody.set_next_kinematic_translation) to the corrected movement added to its current position.
The character’s shape may be any shape supported by Rapier. However, it is recommended to either use a cuboid, a ball, or a capsule since they involve less computations and less numerical approximations.
The built-in character controller does not support rotational movement. It only supports translations.
Character offset
For performance and numerical stability reasons, the character controller will attempt to preserve a small gap between
the character shape and the environment. This small gap is named offset and acts as a small margin around the character
shape. A good value for this offset is something sufficiently small to make the gap unnoticeable, but sufficiently large
to avoid numerical issues (if the character seems to get stuck inexplicably, try increasing the offset).
// The character offset is set to 0.01.
character_controller.offset = CharacterLength::Absolute(0.01);
// The character offset is set to 0.01 multiplied by the shape’s height.
character_controller.offset = CharacterLength::Relative(0.01);
/* Configure the character controller when the collider is created. */
commands
.spawn(Collider::ball(0.5))
.insert(KinematicCharacterController {
// The character offset is set to 0.01.
offset: CharacterLength::Absolute(0.01),
..default()
});
commands
.spawn(Collider::ball(0.5))
.insert(KinematicCharacterController {
// The character offset is set to 0.01 multiplied by the collider’s height.
offset: CharacterLength::Relative(0.01),
..default()
});
// Here the character controller is initialized with an offset of 0.01.
let offset = 0.01;
let characterController = world.createCharacterController(0.01);
The offset is set with r3KinematicCharacterController_SetOffset. Like every length of the character controller, it
is given as an R3CharacterLength: its value is either an absolute length (if its relative field is 0), or a
factor multiplied by the height of the character’s shape (if its relative field is 1). The current offset is
given by r3KinematicCharacterController_Offset.
// The character offset is set to 0.01.
r2KinematicCharacterController_SetOffset(character_controller,
(R2CharacterLength){.value = 0.01, .relative = 0});
// The character offset is set to 0.01 multiplied by the shape's height.
r2KinematicCharacterController_SetOffset(character_controller,
(R2CharacterLength){.value = 0.01, .relative = 1});
If the character still gets stuck when sliding against surfaces, the small distance by which its motion is pushed
along the normals of the obstacles hit can also be increased with r3KinematicCharacterController_SetNormalNudgeFactor
(too large values cause bumps when sliding on a flat ground).
The offset is set with the offset property. Like every length of the character controller, it is given as a
CharacterLength: either an absolute length (created by CharacterLength.absolute), or a factor multiplied by the
size of the character’s shape (created by CharacterLength.relative). A plain float is also accepted, and
interpreted as an absolute length.
# The character offset is set to 0.01.
character_controller.offset = rp.CharacterLength.absolute(0.01)
# The character offset is set to 0.01 multiplied by the shape’s height.
character_controller.offset = rp.CharacterLength.relative(0.01)
It is not recommended to change the offset after the creation of the character controller.
Up vector
The up vector instructs the character controller of what direction should be considered vertical. The horizontal plane is the plane orthogonal to this up vector. There are two equivalent ways to evaluate the slope of the floor: by taking the angle between the floor and the horizontal plane (in 2D), or by taking the angle between the up-vector and the normal of the floor (in 2D and 3D). By default, the up vector is the positive y axis, but it can be modified to be any (unit) vector that suits the application.
// Set the up-vector to the positive X axis.
character_controller.up = Vector::X;
- Example 2D
- Example 3D
/* Character controller with the positive X axis as the up vector. */
commands
.spawn(Collider::ball(0.5))
.insert(KinematicCharacterController {
up: Vec2::X,
..default()
});
/* Modify the character controller’s up vector inside of a system. */
fn modify_character_controller_up(
mut character_controllers: Query<&mut KinematicCharacterController>,
) {
for mut character_controller in character_controllers.iter_mut() {
character_controller.up = Vec2::X;
}
}
/* Character controller with the positive X axis as the up vector. */
commands
.spawn(RigidBody::KinematicPositionBased)
.insert(Collider::ball(0.5))
.insert(Transform::default().with_translation(Vec3::Z * -10f32))
.insert(KinematicCharacterController {
up: Vec3::X,
..default()
});
/* Modify the character controller’s up vector inside of a system. */
fn modify_character_controller_up(
mut character_controllers: Query<&mut KinematicCharacterController>,
) {
for mut character_controller in character_controllers.iter_mut() {
character_controller.up = Vec3::Y;
}
}
- Example 2D
- Example 3D
let characterController = world.createCharacterController(0.01);
// Change the character controller’s up vector to the positive X axis.
characterController.setUp({ x: 1.0, y: 0.0 });
let characterController = world.createCharacterController(0.01);
// Change the character controller’s up vector to the positive Z axis.
characterController.setUp({ x: 0.0, y: 0.0, z: 1.0 });
The up vector is set with r3KinematicCharacterController_SetUp (it is normalized automatically), and read with
r3KinematicCharacterController_Up:
// Set the up-vector to the positive X axis.
r2KinematicCharacterController_SetUp(character_controller, r2Vector(1.0, 0.0));
The up vector is set with the up property (it isn’t normalized automatically, so it must be given as a unit vector):
# Set the up-vector to the positive X axis.
character_controller.up = (1.0, 0.0, 0.0)
Slopes
If sliding is enabled, the character can automatically climb slopes if they are not too steep, or slide down slopes if they are too steep. Sliding is configured by the following parameters:
- The max slope climb angle: if the angle between the slope to climb and the horizontal floor is larger than this value, then the character won’t be able to slide up this slope.
- The min slope slide angle: if the angle between the slope and the horizontal floor is smaller than this value, then the vertical component of the character’s movement won’t result in any sliding.
As always in Rapier, angles are specified in radians.
// Don’t allow climbing slopes larger than 45 degrees.
character_controller.max_slope_climb_angle = 45_f32.to_radians();
// Automatically slide down on slopes smaller than 30 degrees.
character_controller.min_slope_slide_angle = 30_f32.to_radians();
/* Configure the character controller when the collider is created. */
commands
.spawn(Collider::ball(0.5))
.insert(KinematicCharacterController {
// Don’t allow climbing slopes larger than 45 degrees.
max_slope_climb_angle: 45_f32.to_radians(),
// Automatically slide down on slopes smaller than 30 degrees.
min_slope_slide_angle: 30_f32.to_radians(),
..default()
});
/* Configure the slopes inside of a system. */
fn modify_character_controller_slopes(
mut character_controllers: Query<&mut KinematicCharacterController>,
) {
for mut character_controller in character_controllers.iter_mut() {
// Don’t allow climbing slopes larger than 45 degrees.
character_controller.max_slope_climb_angle = 45_f32.to_radians();
// Automatically slide down on slopes smaller than 30 degrees.
character_controller.min_slope_slide_angle = 30_f32.to_radians();
}
}
let characterController = world.createCharacterController(0.01);
// Don’t allow climbing slopes larger than 45 degrees.
characterController.setMaxSlopeClimbAngle(45 * Math.PI / 180);
// Automatically slide down on slopes smaller than 30 degrees.
characterController.setMinSlopeSlideAngle(30 * Math.PI / 180);
Both angles are set at once with r3KinematicCharacterController_SetSlopes. Sliding itself is enabled by default, and
can be disabled with r3KinematicCharacterController_SetSlide. The current slope angles and sliding setting can be read
with r3KinematicCharacterController_Settings.
r2KinematicCharacterController_SetSlopes(
character_controller,
// Don't allow climbing slopes larger than 45 degrees.
45.0 * R2_PI / 180.0,
// Automatically slide down on slopes smaller than 30 degrees.
30.0 * R2_PI / 180.0);
The angles are set with the max_slope_climb_angle and min_slope_slide_angle properties. Sliding itself is enabled
by default, and can be disabled by setting the slide property to False.
# Don’t allow climbing slopes larger than 45 degrees.
character_controller.max_slope_climb_angle = math.radians(45.0)
# Automatically slide down on slopes smaller than 30 degrees.
character_controller.min_slope_slide_angle = math.radians(30.0)
Stairs and small obstacles
If enabled, the autostep setting allows the character to climb stairs automatically and walk over small obstacles. Autostepping requires the following parameters:
- The maximum height the character can step over. If the vertical movement needed to step over this obstacle is larger than this value, then the character will be stopped by the obstacle.
- The minimum (horizontal) width available on top of the obstacle. If, after the character is teleported on top of the obstacle, it cannot move forward by a distance larger than this minimum width, then the character will just be stopped by the obstacle (without being moved to the top of the obstacle).
- Whether or not autostepping is enabled for dynamic bodies. If it is not enabled for dynamic bodies, the character won’t attempt to automatically step over small dynamic bodies. Disabling this can be useful if we want the character to push these small objects (see collisions) instead of just stepping over them.
The following depicts (top) one configuration where all the autostepping conditions are satisfied, and, (bottom) two configurations where these conditions are not all satisfied (left: because the width of the step is too small, right: because the height of the step is too large):
Autostepping will only activate if the character is touching the floor right before the obstacle. This prevents the player from being teleported on to of a platform while it is in the air.
// Set autostep to None to disable it.
character_controller.autostep = None;
// Autostep if the step height is smaller than 0.5, and its width larger than 0.2.
character_controller.autostep = Some(CharacterAutostep {
max_height: CharacterLength::Absolute(0.5),
min_width: CharacterLength::Absolute(0.2),
include_dynamic_bodies: true,
});
// Autostep if the step height is smaller than 0.3 multiplied by the character’s height,
// and its width larger than 0.5 multiplied by the character’s width (i.e. half the character’s
// width).
character_controller.autostep = Some(CharacterAutostep {
max_height: CharacterLength::Relative(0.3),
min_width: CharacterLength::Relative(0.5),
include_dynamic_bodies: true,
});
/* Configure the character controller when the collider is created. */
// Autostep if the step height is smaller than 0.5, and its width larger than 0.2.
commands
.spawn(Collider::ball(0.5))
.insert(KinematicCharacterController {
autostep: Some(CharacterAutostep {
max_height: CharacterLength::Absolute(0.5),
min_width: CharacterLength::Absolute(0.2),
include_dynamic_bodies: true,
}),
..default()
});
// Autostep if the step height is smaller than 0.3 multiplied by the character’s height,
// and its width larger than 0.5 multiplied by the character’s width (i.e. half the character’s
// width).
commands
.spawn(Collider::ball(0.5))
.insert(KinematicCharacterController {
autostep: Some(CharacterAutostep {
max_height: CharacterLength::Relative(0.3),
min_width: CharacterLength::Relative(0.5),
include_dynamic_bodies: true,
}),
..default()
});
/* Configure autostep inside of a system. */
fn modify_character_controller_autostep(
mut character_controllers: Query<&mut KinematicCharacterController>,
) {
for mut character_controller in character_controllers.iter_mut() {
character_controller.autostep = Some(CharacterAutostep {
max_height: CharacterLength::Absolute(0.5),
min_width: CharacterLength::Absolute(0.2),
include_dynamic_bodies: true,
});
}
}
let characterController = world.createCharacterController(0.01);
// Autostep if the step height is smaller than 0.5, its width is larger than 0.2,
// and allow stepping on dynamic bodies.
characterController.enableAutostep(0.5, 0.2, true);
// Disable autostep.
characterController.disableAutostep();
Autostepping is configured with r3KinematicCharacterController_SetAutostep, which takes whether it is enabled, the
maximum height and minimum width (both as R3CharacterLength, see the
character offset), and whether it is enabled for dynamic bodies. The
current settings are given by r3KinematicCharacterController_Autostep as an R3CharacterAutostep structure:
// Set `enabled` (the second argument) to 0 to disable autostep (the other arguments are then ignored).
r2KinematicCharacterController_SetAutostep(character_controller, 0, (R2CharacterLength){0},
(R2CharacterLength){0}, 0);
// Autostep if the step height is smaller than 0.5, and its width larger than 0.2.
r2KinematicCharacterController_SetAutostep(
character_controller, 1,
(R2CharacterLength){.value = 0.5, .relative = 0}, // The maximum height.
(R2CharacterLength){.value = 0.2, .relative = 0}, // The minimum width.
1); // Include dynamic bodies.
// Autostep if the step height is smaller than 0.3 multiplied by the character's height,
// and its width larger than 0.5 multiplied by the character's width (i.e. half the character's
// width).
r2KinematicCharacterController_SetAutostep(
character_controller, 1,
(R2CharacterLength){.value = 0.3, .relative = 1}, // The maximum height.
(R2CharacterLength){.value = 0.5, .relative = 1}, // The minimum width.
1); // Include dynamic bodies.
Autostepping is configured with the autostep property: either None (the default) to disable it, or a
CharacterAutostep created from the maximum height and minimum width (both as CharacterLength, see the
character offset), and whether it is enabled for dynamic bodies:
# Set autostep to None to disable it.
character_controller.autostep = None
# Autostep if the step height is smaller than 0.5, and its width larger than 0.2.
character_controller.autostep = rp.CharacterAutostep(
max_height=rp.CharacterLength.absolute(0.5),
min_width=rp.CharacterLength.absolute(0.2),
include_dynamic_bodies=True,
)
# Autostep if the step height is smaller than 0.3 multiplied by the character’s height,
# and its width larger than 0.5 multiplied by the character’s width (i.e. half the character’s
# width).
character_controller.autostep = rp.CharacterAutostep(
max_height=rp.CharacterLength.relative(0.3),
min_width=rp.CharacterLength.relative(0.5),
include_dynamic_bodies=True,
)
Snap-to-ground
If enabled, snap-to-ground will force the character to stick to the ground if the following conditions are met simultaneously:
- At the start of the movement, the character touches the ground.
- The movement has a slight downward component.
- At the end of the desired movement, the character would be separated from the ground by a distance smaller than the distance provided by the snap-to-ground parameter.
If these conditions are met, the character is automatically teleported down to the ground at the end of its motion. Typical usages of snap-to-ground include going downstairs or remaining in contact with the floor when moving downhill.
// Set snap-to-ground to None to disable it.
character_controller.snap_to_ground = None;
// Snap to the ground if the vertical distance to the ground is smaller than 0.5.
character_controller.snap_to_ground = Some(CharacterLength::Absolute(0.5));
// Snap to the ground if the vertical distance to the ground is smaller than 0.2 times the character’s height.
character_controller.snap_to_ground = Some(CharacterLength::Relative(0.2));
/* Configure the character controller when the collider is created. */
// Snap to the ground if the vertical distance to the ground is smaller than 0.5.
commands
.spawn(Collider::ball(0.5))
.insert(KinematicCharacterController {
snap_to_ground: Some(CharacterLength::Absolute(0.5)),
..default()
});
// Snap to the ground if the vertical distance to the ground is smaller than 0.2 times the character’s height
commands
.spawn(Collider::ball(0.5))
.insert(KinematicCharacterController {
snap_to_ground: Some(CharacterLength::Relative(0.2)),
..default()
});
/* Configure snap-to-ground inside of a system. */
fn modify_character_controller_snap_to_ground(
mut character_controllers: Query<&mut KinematicCharacterController>,
) {
for mut character_controller in character_controllers.iter_mut() {
character_controller.snap_to_ground = Some(CharacterLength::Absolute(0.5));
}
}
let characterController = world.createCharacterController(0.01);
// Snap to the ground if the vertical distance to the ground is smaller than 0.5.
characterController.enableSnapToGround(0.5);
// Disable snap-to-ground.
characterController.disableSnapToGround();
Snap-to-ground is configured with r3KinematicCharacterController_SetSnapToGround, which takes whether it is enabled,
and the snapping distance as an R3CharacterLength (see the character offset):
// Set `enabled` (the second argument) to 0 to disable snap-to-ground (the distance is then ignored).
r2KinematicCharacterController_SetSnapToGround(character_controller, 0, (R2CharacterLength){0});
// Snap to the ground if the vertical distance to the ground is smaller than 0.5.
r2KinematicCharacterController_SetSnapToGround(character_controller, 1,
(R2CharacterLength){.value = 0.5, .relative = 0});
// Snap to the ground if the vertical distance to the ground is smaller than 0.2 times the character's height.
r2KinematicCharacterController_SetSnapToGround(character_controller, 1,
(R2CharacterLength){.value = 0.2, .relative = 1});
Snap-to-ground is configured with the snap_to_ground property: either None to disable it, or the snapping
distance as a CharacterLength (see the character offset). It is
enabled by default, with a distance of CharacterLength.relative(0.2):
# Set snap-to-ground to None to disable it.
character_controller.snap_to_ground = None
# Snap to the ground if the vertical distance to the ground is smaller than 0.5.
character_controller.snap_to_ground = rp.CharacterLength.absolute(0.5)
# Snap to the ground if the vertical distance to the ground is smaller than 0.2 times the character’s height.
character_controller.snap_to_ground = rp.CharacterLength.relative(0.2)
Filtering
It is possible to let the character controller ignore some obstacles. This is achieved by configuring the
filter argument of the KinematicCharacterController::move_shape method. This QueryFilter structure is detailed
in the scene query filters section.
If the character-controller is used to move a collider (and the rigid-body it may be attached to) that is present
in the physics scene, the filters must be used to exclude that collider (and that rigid-body) from the set of
obstacles (with QueryFilter::exclude_collider and QueryFilter::exclude_rigid_body) to prevent the character from
colliding with itself.
It is possible to let the character controller ignore some obstacles. This is achieved by configuring the following
fields of the KinematicCharacterController component:
filter_flags: to exclude whole families of obstacles (e.g. all the colliders attached to dynamic rigid-bodies).filter_groups: to filter based on the colliders collision groups.exclude_colliders: the set of entities of colliders to ignore.exclude_rigid_bodies: the set of entities of rigid-bodies whose attached colliders must all be ignored.filter_predicate: an arbitrary closure, wrapped into aControllerFilterPredicate, to filter-out colliders based on user-defined rules. It is given the entity of each collider as well as its Rapier collider (rapier::geometry::Collider). Since it is stored in a component, this closure can't borrow any system parameter.
/* Configure the character controller filters when the collider is created. */
commands
.spawn(Collider::ball(0.5))
.insert(KinematicCharacterController {
// Ignore all the sensors and all the colliders attached to dynamic rigid-bodies.
filter_flags: QueryFilterFlags::EXCLUDE_SENSORS | QueryFilterFlags::EXCLUDE_DYNAMIC,
// The character is part of the group 1 and only interacts with the group 2.
filter_groups: Some(CollisionGroups::new(Group::GROUP_1, Group::GROUP_2)),
// Ignore the collider attached to the `platform` entity.
exclude_colliders: [platform].into_iter().collect(),
// Ignore the colliders with a ball shape.
filter_predicate: Some(ControllerFilterPredicate::new(|_entity, collider| {
collider.shape().as_ball().is_none()
})),
..default()
});
When the obstacles to ignore depend on the state of your game (e.g. to let the characters walk through the doors that
are open), it is simpler to insert the ControllerIgnored marker component on these colliders from your own systems.
A collider with this component (or attached to a rigid-body with this component) is ignored by every character
controller, as well as by the wheels of the vehicle controllers,
until the component is removed:
/// A door the characters can only walk through while it is open.
#[derive(Component)]
struct Door {
open: bool,
}
/* Hide the open doors from every controller inside of a system. */
fn update_doors(mut commands: Commands, doors: Query<(Entity, &Door), Changed<Door>>) {
for (entity, door) in doors.iter() {
if door.open {
commands.entity(entity).insert(ControllerIgnored);
} else {
commands.entity(entity).remove::<ControllerIgnored>();
}
}
}
The collider moved by the character controller (and the rigid-body it may be attached to) is always excluded automatically from the set of obstacles: there is no need to exclude it manually.
It is possible to let the character controller ignore some obstacles. This can be achieved by setting the
optional arguments of the KinematicCharacterController.computeColliderMovement method:
filterFlags: to exclude whole families of obstacles (e.g. all the colliders attached to dynamic rigid-bodies).filterGroups: filter based on the colliders collision groups.filterPredicate: an arbitrary closure to filter-out colliders based on user-defined rules.
It is possible to let the character controller ignore some obstacles. This is achieved by configuring the options
argument (an R3QueryOptions, initialized by r3DefaultQueryOptions) of r3KinematicCharacterController_MoveShape.
This structure is detailed in the scene query filters section:
- The
flagsfield of itsfilterallows you to exclude whole families of obstacles (e.g. all the colliders attached to dynamic rigid-bodies withR3_QUERY_EXCLUDE_DYNAMIC). - The
use_groupsandgroupsfields of itsfilterallow you to filter based on the colliders collision groups. - The
exclude_colliderandexclude_rigid_bodyfields of itsfilterexclude one collider, and all the colliders attached to one rigid-body. - Its
predicatefield is an optional callback to filter-out colliders based on user-defined rules. It is given theuserDatafield of the options, a read-only access to the world (anR3ReadContext), and the handle of each collider, and returns a nonzero value to keep that collider as an obstacle.
If the character-controller is used to move a collider (and the rigid-body it may be attached to) that is present
in the physics scene, the filters must be used to exclude that collider (and that rigid-body) from the set of
obstacles (with the exclude_collider and exclude_rigid_body fields of the filter) to prevent the character from
colliding with itself.
It is possible to let the character controller ignore some obstacles. This is achieved by configuring the
filter argument of the KinematicCharacterController.move_shape method. This QueryFilter class is detailed in the
scene query filters section:
- Its
flags(aQueryFilterFlags, also set by constructors likeQueryFilter.exclude_dynamic()) allow you to exclude whole families of obstacles (e.g. all the colliders attached to dynamic rigid-bodies). - Its
groupsmethod allows you to filter based on the colliders collision groups. - Its
exclude_colliderandexclude_rigid_bodymethods exclude one collider, and all the colliders attached to one rigid-body. - Its
predicatemethod sets an optional callable to filter-out colliders based on user-defined rules. It is given theColliderHandleand theColliderof each potential obstacle, and returnsTrueto keep it as an obstacle.
If the character-controller is used to move a collider (and the rigid-body it may be attached to) that is present
in the physics scene, the filters must be used to exclude that collider (and that rigid-body) from the set of
obstacles (with QueryFilter.exclude_collider and QueryFilter.exclude_rigid_body) to prevent the character from
colliding with itself.
Collisions
As the character moves along its path, it will hit grounds and obstacles before sliding or stepping on them. Knowing what collider was hit on this path, and where the hit took place, can be valuable to apply various logic (custom forces, sound effects, etc.) This is why a set of character collision events are collected during the calculation of its trajectory.
The character collision events are given in chronological order. For example, if, during the resolution of the character motion, the character hits an obstacle A, then slides against it, and then hits another obstacle B. The collision with A will be reported first, and the collision with B will be reported second.
let character_controller = KinematicCharacterController::default();
// Use a closure to handle or collect the collisions while
// the character is being moved.
character_controller.move_shape(
dt,
&query_pipeline,
character_shape,
character_pos,
desired_translation,
|collision| { /* Handle or collect the collision in this closure. */ },
);
The character collisions are stored in the KinematicCharacterControllerOutput::collisions field
after each update of the character controller:
/* Read the character controller collisions stored in the character controller’s output. */
fn read_character_controller_collisions(
character_controller_outputs: Query<&KinematicCharacterControllerOutput>,
) {
for output in character_controller_outputs.iter() {
for collision in &output.collisions {
// Do something with that collision information.
println!(
"The character hit the entity {:?} after moving by {}.",
collision.entity, collision.translation_applied
);
}
}
}
The hit field of each collision has the same form as the result of a
shape-casting: its first witness point and normal are on the obstacle, in
world-space, whereas its second witness point and normal are on the character, in its local-space.
let characterController = world.createCharacterController(0.01);
characterController.computeColliderMovement(collider, desiredMovementVector);
// After the collider movement calculation is done, we can read the
// collision events.
for (let i = 0; i < characterController.numComputedCollisions(); i++) {
let collision = characterController.computedCollision(i);
// Do something with that collision information.
}
The character collisions are recorded by the character controller during each call to
r3KinematicCharacterController_MoveShape, and remain available until its next call. They are copied into a buffer of
R3CharacterCollision with r3KinematicCharacterController_Collisions (call it once with a NULL buffer and a zero
capacity to get the number of collisions, then a second time to copy them):
R2KinematicCharacterController *character_controller = r2NewKinematicCharacterController();
// The collisions are recorded by the controller while the character is being moved.
r2KinematicCharacterController_MoveShape(world, &options, character_controller, dt,
character_shape, character_pos,
desired_translation);
// Read them after the movement (they remain available until the next movement
// calculation of this controller).
size_t num_collisions = r2KinematicCharacterController_Collisions(character_controller, NULL, 0);
R2CharacterCollision *collisions = malloc(num_collisions * sizeof(R2CharacterCollision));
r2KinematicCharacterController_Collisions(character_controller, collisions, num_collisions);
for (size_t k = 0; k < num_collisions; k++) {
R2CharacterCollision collision = collisions[k];
/* Handle the collision with the collider `collision.collider`. */
(void)collision;
}
free(collisions);
Each collision gives the handle of the collider hit, the pose of the character at the time of the hit
(character_pos), as well as the parts of the desired translation already applied (translation_applied) and
remaining (translation_remaining) at that time. Its hit field has the same form as the result of a
shape-casting: its first witness point and normal are on the obstacle, in
world-space, whereas its second witness point and normal are on the character, in its local-space.
The character collisions are given to the optional events_callback argument of
KinematicCharacterController.move_shape: a callable invoked with each CharacterCollision while the movement is
being calculated (an exception raised by that callable is re-raised by move_shape):
character_controller = rp.KinematicCharacterController()
def on_collision(collision):
# Handle or collect the collision in this callback.
pass
# Give a callback to handle or collect the collisions while
# the character is being moved.
character_controller.move_shape(
dt,
None,
None,
world.query_pipeline,
character_shape,
character_pos,
desired_translation,
query_filter,
events_callback=on_collision,
)
Each collision gives the handle of the collider hit, the pose of the character at the time of the hit
(character_pos), as well as the parts of the desired translation already applied (translation_applied) and
remaining (translation_remaining) at that time. Its hit property is a ShapeCastHit, i.e., it has the same form
as the result of a shape-casting: its first witness point and normal are on the
obstacle, in world-space, whereas its second witness point and normal are on the character, in its local-space.
Unless dynamic bodies are filtered-out by the character controller’s filters, they may be hit during the resolution of the character movement. If that happens, these dynamic bodies will generally not react to (i.e. not be pushed by) the character because the character controller’s offset prevents actual contacts from happening.
In these situations forces need to be applied manually to this rigid-bodies. The character controller can apply these forces for you if needed:
// First, collect all the collisions.
let mut collisions = vec![];
character_controller.move_shape(
dt,
&query_pipeline,
character_shape,
character_pos,
desired_translation,
|collision| collisions.push(collision),
);
// Then, let the character controller solve (and apply) the collision impulses
// to the dynamic rigid-bodies hit along its path.
// Note that we need to init a QueryPipelineMut here (because the impulse
// application will modify rigid-bodies.
let mut query_pipeline_mut = world.broad_phase.as_query_pipeline_mut(
world.narrow_phase.query_dispatcher(),
&mut world.bodies,
&mut world.colliders,
filter,
);
character_controller.solve_character_collision_impulses(
dt,
&mut query_pipeline_mut,
character_shape,
character_mass,
&collisions,
);
/* Configure the character controller when the collider is created. */
commands
.spawn(Collider::ball(0.5))
.insert(KinematicCharacterController {
// Enable the automatic application of impulses to the dynamic bodies
// hit by the character along its path.
apply_impulse_to_dynamic_bodies: true,
..default()
});
/* Configure dynamic impulses inside of a system. */
fn modify_character_controller_impulses(
mut character_controllers: Query<&mut KinematicCharacterController>,
) {
for mut character_controller in character_controllers.iter_mut() {
// Enable the automatic application of impulses to the dynamic bodies
// hit by the character along its path.
character_controller.apply_impulse_to_dynamic_bodies = true;
}
}
The mass of the character taken into account for computing these impulses is the mass of the rigid-body it is
attached to, unless KinematicCharacterController::custom_mass is set.
let characterController = world.createCharacterController(0.01);
// Enable the automatic application of impulses to the dynamic bodies
// hit by the character along its path.
characterController.setApplyImpulsesToDynamicBodies(true);
// First, calculate the movement, which records all the collisions.
r2KinematicCharacterController_MoveShape(world, &options, character_controller, dt,
character_shape, character_pos,
desired_translation);
// Then, let the character controller solve (and apply) the collision impulses
// to the dynamic rigid-bodies hit along its path. Note that this must be given the
// same shape, timestep length, and query options as the movement calculation.
r2KinematicCharacterController_SolveCharacterCollisionImpulses(
character_controller, character_shape, dt, character_mass, &options);
The impulses are computed from the collisions recorded by the last r3KinematicCharacterController_MoveShape call
of the character controller, and applied to the rigid-bodies of the world given to that call. The mass of the
character is given explicitly: it can be, for example, the mass of the rigid-body it is attached to (as given by
r3RigidBody_Mass). The R3QueryOptions should be the same as the ones given to the movement calculation. Unlike
r3KinematicCharacterController_MoveShape, this calls the predicate of the options, if any, once for every collider
of the world before applying the impulses, while the world is locked for writing: it may only use the Read
functions of its R3ReadContext.
# First, collect all the collisions.
collisions = []
character_controller.move_shape(
dt,
None,
None,
world.query_pipeline,
character_shape,
character_pos,
desired_translation,
query_filter,
events_callback=collisions.append,
)
# Then, let the character controller solve (and apply) the collision impulses
# to the dynamic rigid-bodies hit along its path.
character_controller.solve_character_collision_impulses(
dt,
world.rigid_bodies,
world.colliders,
world.query_pipeline,
character_shape,
None, # Unused: each collision stores the pose of the character when it happened.
character_mass,
collisions,
query_filter,
)
The impulses are computed from the collisions collected during the move_shape call (each of them stores the pose
of the character when it happened, so the character_pos argument is unused and can be None), and applied to the
rigid-bodies of the given rigid-body set (which, like the collider set, must be the one of the query pipeline). The
shape and filter given to solve_character_collision_impulses should be the same as the ones given to move_shape.
Unlike move_shape, this calls the predicate of the filter, if any, once for every collider before applying the
impulses, while the sets are locked: it must not modify them. The mass of the character is given explicitly: it can
be, for example, the mass of the rigid-body it is attached to (as given by its RigidBody.mass property).
Gravity
Since you are responsible for providing the movement vector to the character controller at each frame, it is up to you to emulate gravity by adding a downward component to that movement vector.
Vehicle controller
Simulating a car with rigid-bodies and joints, for example using one rigid-body per wheel attached to the chassis by a joint, is
possible but can be difficult to control for games requiring non-realistic vehicles. This is why Rapier provides a
vehicle controller (the current implementation was ported from the btRaycastVehicle of Bullet). The vehicle is a
single rigid-body modeling its chassis, and its wheels are only represented by ray-casts pushing that body along a
spring-like suspension.
The vehicle controller is only available in 3D, i.e., with bevy_rapier3d.
The vehicle controller is only available in 3D, i.e., with the r3 functions.
Setup
The chassis is created like any other dynamic rigid-body, and the wheels are added to the controller afterwards. Each
wheel is given its position on the chassis, the direction of its suspension (the direction of its ray-cast), its axle,
the rest length of its suspension, and its radius. The WheelTuningR3WheelTuning
// The chassis is an ordinary dynamic rigid-body.
let hw = 0.3;
let hh = 0.15;
let (chassis_handle, _) = world.insert(
RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 1.0, 0.0)),
ColliderBuilder::cuboid(hw * 2.0, hh, hw).density(100.0),
);
// The tuning shared by the wheels: the suspension and the grip.
let tuning = WheelTuning {
suspension_stiffness: 100.0,
suspension_damping: 10.0,
..WheelTuning::default()
};
let mut vehicle = DynamicRayCastVehicleController::new(chassis_handle);
let wheel_positions = [
Vector::new(hw * 1.5, -hh, hw),
Vector::new(hw * 1.5, -hh, -hw),
Vector::new(-hw * 1.5, -hh, hw),
Vector::new(-hw * 1.5, -hh, -hw),
];
for position in wheel_positions {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
vehicle.add_wheel(position, -Vector::Y, Vector::Z, hh, hh / 4.0, &tuning);
}
// The chassis is an ordinary dynamic rigid-body.
let hw = 0.3;
let hh = 0.15;
let chassis = world.createRigidBody(
RAPIER.RigidBodyDesc.dynamic().setTranslation(0.0, 1.0, 0.0),
);
world.createCollider(RAPIER.ColliderDesc.cuboid(hw * 2.0, hh, hw).setDensity(100.0), chassis);
let vehicle = world.createVehicleController(chassis);
let wheelPositions = [
{ x: hw * 1.5, y: -hh, z: hw }, { x: hw * 1.5, y: -hh, z: -hw },
{ x: -hw * 1.5, y: -hh, z: hw }, { x: -hw * 1.5, y: -hh, z: -hw },
];
for (let position of wheelPositions) {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
vehicle.addWheel(position, { x: 0.0, y: -1.0, z: 0.0 }, { x: 0.0, y: 0.0, z: 1.0 }, hh, hh / 4.0);
}
// The tuning of each wheel: its suspension and its grip.
for (let i = 0; i < vehicle.numWheels(); ++i) {
vehicle.setWheelSuspensionStiffness(i, 100.0);
vehicle.setWheelSuspensionCompression(i, 10.0);
vehicle.setWheelSuspensionRelaxation(i, 10.0);
}
The vehicle controller is the RayCastVehicleController component, added to the entity of the
chassis' dynamic rigid-body. Its wheels are VehicleWheel values stored in its wheels field, and can be added,
removed, or modified at any time:
// The tuning shared by the wheels: the suspension and the grip.
let tuning = WheelTuning {
suspension_stiffness: 100.0,
suspension_damping: 10.0,
..WheelTuning::default()
};
let hw = 0.3;
let hh = 0.15;
let wheel_positions = [
Vec3::new(hw * 1.5, -hh, hw),
Vec3::new(hw * 1.5, -hh, -hw),
Vec3::new(-hw * 1.5, -hh, hw),
Vec3::new(-hw * 1.5, -hh, -hw),
];
let wheels = wheel_positions
.into_iter()
.map(|position| {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
VehicleWheel::new(position, -Vec3::Y, Vec3::Z, hh, hh / 4.0, tuning)
})
.collect();
// The chassis is an ordinary dynamic rigid-body, with the vehicle controller attached to it.
commands.spawn((
Transform::from_xyz(0.0, 1.0, 0.0),
RigidBody::Dynamic,
Collider::cuboid(hw * 2.0, hh, hw),
ColliderMassProperties::Density(100.0),
RayCastVehicleController::new(wheels),
));
The vehicle controller is the R3DynamicRayCastVehicleController object, created by
r3NewDynamicRayCastVehicleController from the handle of the chassis' dynamic rigid-body. It remembers the world of
that rigid-body, so it must be freed with r3FreeDynamicRayCastVehicleController before that world is freed. The
wheels are added with r3DynamicRayCastVehicleController_AddWheel, which returns the index of the new wheel (starting
from zero, in insertion order), and the R3WheelTuning given to each wheel is initialized with r3DefaultWheelTuning:
// The chassis is an ordinary dynamic rigid-body.
const R3Real hw = 0.3;
const R3Real hh = 0.15;
R3RigidBodyDesc chassis_body = r3DynamicRigidBodyDesc();
chassis_body.position.translation = r3Vector(0.0, 1.0, 0.0);
R3RigidBodyHandle chassis_handle = r3InsertRigidBody(world, &chassis_body);
R3ColliderDesc chassis_collider = r3CuboidColliderDesc(r3Vector(hw * 2.0, hh, hw));
chassis_collider.density = 100.0;
r3InsertCollider(chassis_handle, &chassis_collider);
// The tuning shared by the wheels: the suspension and the grip.
R3WheelTuning tuning = r3DefaultWheelTuning();
tuning.suspension_stiffness = 100.0;
tuning.suspension_damping = 10.0;
// The controller must be freed (with r3FreeDynamicRayCastVehicleController) before its world.
R3DynamicRayCastVehicleController *vehicle = r3NewDynamicRayCastVehicleController(chassis_handle);
const R3Vector wheel_positions[4] = {
{hw * 1.5, -hh, hw},
{hw * 1.5, -hh, -hw},
{-hw * 1.5, -hh, hw},
{-hw * 1.5, -hh, -hw},
};
for (size_t i = 0; i < 4; i++) {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
r3DynamicRayCastVehicleController_AddWheel(vehicle, wheel_positions[i], r3Vector(0.0, -1.0, 0.0),
r3Vector(0.0, 0.0, 1.0), hh, hh / 4.0, &tuning);
}
By default, the vehicle moves forward along the local x axis of the chassis, and its local y axis points
upward. Other axes can be selected with r3DynamicRayCastVehicleController_SetAxes.
The vehicle controller is the DynamicRayCastVehicleController class, created from the handle of the chassis'
dynamic rigid-body. The wheels are added with its add_wheel method, which returns the index of the new wheel
(starting from zero, in insertion order). The WheelTuning given to each wheel is created with keyword arguments
overriding its default values (as given by WheelTuning.default()):
# The chassis is an ordinary dynamic rigid-body.
hw = 0.3
hh = 0.15
chassis_handle = world.add_body(
rp.RigidBody.dynamic(translation=(0.0, 1.0, 0.0)),
colliders=[rp.Collider.cuboid(hw * 2.0, hh, hw).density(100.0)],
)
# The tuning shared by the wheels: the suspension and the grip. The
# parameters that aren't given keep their default values.
tuning = rp.WheelTuning(suspension_stiffness=100.0, suspension_damping=10.0)
vehicle = rp.DynamicRayCastVehicleController(chassis_handle)
wheel_positions = [
(hw * 1.5, -hh, hw),
(hw * 1.5, -hh, -hw),
(-hw * 1.5, -hh, hw),
(-hw * 1.5, -hh, -hw),
]
for position in wheel_positions:
# The position of the wheel, the direction its suspension pushes along, its axle, the
# rest length of its suspension, and its radius; all in the local frame of the chassis.
vehicle.add_wheel(position, (0.0, -1.0, 0.0), (0.0, 0.0, 1.0), hh, hh / 4.0, tuning)
By default, the vehicle moves forward along the local x axis of the chassis, and its local y axis points
upward. Other axes can be selected with the index_forward_axis and index_up_axis properties of the controller
(0, 1, and 2 standing for the x, y, and z axes).
Driving the vehicle
A vehicle is driven by giving each of its wheels an engine force, a brake force, and a steering angle. The controller is then updated before each timestep, which is when the ray-casts are made and when the resulting suspension and friction forces are applied to the chassis. It is strongly recommended to exclude the chassis from the ray-casts, otherwise the ray might hit it and be misinterpreted as being the floor.
for _ in 0..200 {
// The vehicle is driven by setting the engine force, the brake, and the steering angle of
// its wheels. Here the two front wheels are the driving and steering ones.
let wheels = vehicle.wheels_mut();
wheels[0].engine_force = 30.0;
wheels[0].steering = 0.2;
wheels[1].engine_force = 30.0;
wheels[1].steering = 0.2;
// The wheels are ray-casted against the scene: the chassis itself, as well as every other
// dynamic body, is generally excluded from these ray-casts.
let queries = world.broad_phase.as_query_pipeline_mut(
world.narrow_phase.query_dispatcher(),
&mut world.bodies,
&mut world.colliders,
QueryFilter::exclude_dynamic().exclude_rigid_body(chassis_handle),
);
vehicle.update_vehicle(world.integration_parameters.dt, queries);
world.step();
}
println!("Vehicle speed: {}", vehicle.current_vehicle_speed);
for (let k = 0; k < 200; ++k) {
// The vehicle is driven by setting the engine force, the brake, and the steering angle of
// its wheels. Here the two front wheels are the driving and steering ones.
vehicle.setWheelEngineForce(0, 30.0);
vehicle.setWheelSteering(0, 0.2);
vehicle.setWheelEngineForce(1, 30.0);
vehicle.setWheelSteering(1, 0.2);
// The wheels are ray-casted against the scene: the dynamic bodies, including the chassis
// itself, are generally excluded from these ray-casts.
vehicle.updateVehicle(world.integrationParameters.dt, RAPIER.QueryFilterFlags.EXCLUDE_DYNAMIC);
world.step();
}
console.log("Vehicle speed:", vehicle.currentVehicleSpeed());
The controller is updated automatically by the plugin before each simulation step, and the
colliders attached to the chassis are always excluded from the ray-casts. Other colliders can be excluded with the
filter_flags, filter_groups, exclude_colliders, exclude_rigid_bodies, and filter_predicate fields of
the RayCastVehicleController (similar to the ones of the
character controller), or by inserting the ControllerIgnored component on them
(or on their rigid-body). So driving the vehicle is just a matter of modifying
the engine force, brake, and steering of its wheels:
fn drive_vehicle(mut vehicles: Query<&mut RayCastVehicleController>) {
for mut vehicle in vehicles.iter_mut() {
// The vehicle is driven by setting the engine force, the brake, and the steering angle of
// its wheels. Here the two front wheels are the driving and steering ones.
for wheel in &mut vehicle.wheels[0..2] {
wheel.engine_force = 30.0;
wheel.steering = 0.2;
}
// The colliders of the chassis are always ignored by the ray-casts of the wheels. Other
// colliders, here the ones attached to dynamic rigid-bodies, can be ignored too.
vehicle.filter_flags =
QueryFilterFlags::EXCLUDE_SENSORS | QueryFilterFlags::EXCLUDE_DYNAMIC;
// The results of the last update are written back into the component.
println!("Vehicle speed: {}", vehicle.current_vehicle_speed);
for wheel in &vehicle.wheels {
println!(
"Wheel in contact: {}, suspension length: {}",
wheel.state.is_in_contact, wheel.state.suspension_length
);
}
}
}
The forces applied by the vehicle controller are computed once per update of the plugin, using the length of the last simulation timestep. So it is best used with a fixed timestep.
The engine force, brake, and steering angle of a wheel are set with r3DynamicRayCastVehicleController_SetWheelControls
(from the index of the wheel), and the controller is updated with r3DynamicRayCastVehicleController_UpdateVehicle
before each r3Step. The colliders attached to the chassis are always excluded from the ray-casts, and the other
obstacles can be filtered with the R3QueryOptions given to the update (see the
scene query filters). Note that its predicate callback, if any, is called once
for every collider of the world before the update, while the world is locked for writing: it may only use the Read
functions of its R3ReadContext.
// The wheels are ray-casted against the scene: the chassis itself is always excluded, and
// every other dynamic body is generally excluded from these ray-casts too.
R3QueryOptions options = r3DefaultQueryOptions();
options.filter.flags = R3_QUERY_EXCLUDE_DYNAMIC;
for (int i = 0; i < 200; i++) {
// The vehicle is driven by setting the steering angle, the engine force, and the brake of
// its wheels. Here the two front wheels (indices 0 and 1) are the driving and steering ones.
r3DynamicRayCastVehicleController_SetWheelControls(vehicle, 0, 0.2, 30.0, 0.0);
r3DynamicRayCastVehicleController_SetWheelControls(vehicle, 1, 0.2, 30.0, 0.0);
r3DynamicRayCastVehicleController_UpdateVehicle(vehicle, r3TimeStep(world), &options);
r3Step(world, NULL, NULL);
}
printf("Vehicle speed: %f\n", (double)r3DynamicRayCastVehicleController_CurrentVehicleSpeed(vehicle));
The engine force, brake, and steering angle of a wheel are set with the apply_engine_force, set_brake, and
set_steering methods of the controller (from the index of the wheel), and the controller is updated with its
update_vehicle method before each PhysicsWorld.step. The colliders attached to the chassis are always excluded
from the ray-casts, and the other obstacles can be filtered with the optional QueryFilter given to update_vehicle
(see the scene query filters). The predicate of that filter, if any, is called
once for every collider before the update, while the sets are locked: it must not modify them.
for _ in range(200):
# The vehicle is driven by setting the engine force, the brake, and the steering angle of
# its wheels (from their indices). Here the two front wheels are the driving and steering ones.
vehicle.apply_engine_force(0, 30.0)
vehicle.set_steering(0, 0.2)
vehicle.apply_engine_force(1, 30.0)
vehicle.set_steering(1, 0.2)
# The wheels are ray-casted against the scene: the chassis itself is always excluded from
# these ray-casts, and every other dynamic body is generally excluded too.
vehicle.update_vehicle(
world.integration_parameters.dt,
world.rigid_bodies,
world.colliders,
world.query_pipeline,
rp.QueryFilter.exclude_dynamic(),
)
world.step()
print("Vehicle speed:", vehicle.current_vehicle_speed)
The state of each wheel after an update (whether it touches the floor, the compression of its suspension, its rotation angle, etc.) is readable from the controller. This is what the rendering of the wheels can be based on since there is no actual per-wheel rigid-bodies to read their state from.
The state of each wheel is written back by the plugin into its VehicleWheel::state field (a
VehicleWheelState), and the speed of the vehicle into RayCastVehicleController::current_vehicle_speed. The
VehicleWheel::local_transform method computes the transform of a wheel relative to the chassis (including its
suspension length, steering, and rotation), which can be given directly to the child entity rendering that wheel.
The state of the wheels is copied into a buffer of R3WheelState by r3DynamicRayCastVehicleController_Wheels: the
world-space center of each wheel, the world-space directions of its suspension and axle, its rotation angle, the length
and force of its suspension, and its contact with the ground (whether it touches it, the collider touched, and the
world-space contact point and normal). The speed of the vehicle along its forward axis is given by
r3DynamicRayCastVehicleController_CurrentVehicleSpeed.
// The wheels are given in the order they were added to the controller.
R3WheelState wheels[4];
size_t num_wheels = r3DynamicRayCastVehicleController_Wheels(vehicle, wheels, 4);
for (size_t i = 0; i < num_wheels; i++) {
// The world-space center of the wheel, its current suspension length, rotation angle, etc.
printf("Wheel %zu: center (%f, %f, %f), suspension length %f, rotation %f, in contact: %u\n", i,
(double)wheels[i].center.x, (double)wheels[i].center.y, (double)wheels[i].center.z,
(double)wheels[i].suspension_length, (double)wheels[i].rotation,
(unsigned)wheels[i].is_in_contact);
}
The state of the wheels is given by the wheels method of the controller (or by its wheel method for a single
wheel) as a list of Wheel objects. These are copies: modifying them doesn’t affect the vehicle. Each wheel gives its
world-space center, the world-space directions of its suspension and axle, its rotation angle, the force of its
suspension (wheel_suspension_force), and its raycast_info (a RayCastInfo): the length of its suspension
(suspension_length) and its contact with the ground (is_in_contact, the collider touched ground_object, and the
world-space contact point contact_point_ws and normal contact_normal_ws). The speed of the vehicle along its
forward axis is given by the current_vehicle_speed property of the controller (in meters per second, or with the
current_speed_km_hour method in kilometers per hour).
# The wheels are copies of the state of the wheels after the last update.
for wheel in vehicle.wheels():
contact = wheel.raycast_info
print(
"Wheel center:", wheel.center, # World-space center of the wheel.
"axle:", wheel.axle, # World-space direction of its axle.
"rotation:", wheel.rotation, # Rotation angle around its axle.
"suspension length:", contact.suspension_length,
"touches the ground:", contact.is_in_contact,
"ground collider:", contact.ground_object, # None if it doesn’t touch the ground.
)
Vehicle controller
Simulating a car with rigid-bodies and joints, for example using one rigid-body per wheel attached to the chassis by a joint, is
possible but can be difficult to control for games requiring non-realistic vehicles. This is why Rapier provides a
vehicle controller (the current implementation was ported from the btRaycastVehicle of Bullet). The vehicle is a
single rigid-body modeling its chassis, and its wheels are only represented by ray-casts pushing that body along a
spring-like suspension.
The vehicle controller is only available in 3D, i.e., with bevy_rapier3d.
The vehicle controller is only available in 3D, i.e., with the r3 functions.
Setup
The chassis is created like any other dynamic rigid-body, and the wheels are added to the controller afterwards. Each
wheel is given its position on the chassis, the direction of its suspension (the direction of its ray-cast), its axle,
the rest length of its suspension, and its radius. The WheelTuningR3WheelTuning
// The chassis is an ordinary dynamic rigid-body.
let hw = 0.3;
let hh = 0.15;
let (chassis_handle, _) = world.insert(
RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 1.0, 0.0)),
ColliderBuilder::cuboid(hw * 2.0, hh, hw).density(100.0),
);
// The tuning shared by the wheels: the suspension and the grip.
let tuning = WheelTuning {
suspension_stiffness: 100.0,
suspension_damping: 10.0,
..WheelTuning::default()
};
let mut vehicle = DynamicRayCastVehicleController::new(chassis_handle);
let wheel_positions = [
Vector::new(hw * 1.5, -hh, hw),
Vector::new(hw * 1.5, -hh, -hw),
Vector::new(-hw * 1.5, -hh, hw),
Vector::new(-hw * 1.5, -hh, -hw),
];
for position in wheel_positions {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
vehicle.add_wheel(position, -Vector::Y, Vector::Z, hh, hh / 4.0, &tuning);
}
// The chassis is an ordinary dynamic rigid-body.
let hw = 0.3;
let hh = 0.15;
let chassis = world.createRigidBody(
RAPIER.RigidBodyDesc.dynamic().setTranslation(0.0, 1.0, 0.0),
);
world.createCollider(RAPIER.ColliderDesc.cuboid(hw * 2.0, hh, hw).setDensity(100.0), chassis);
let vehicle = world.createVehicleController(chassis);
let wheelPositions = [
{ x: hw * 1.5, y: -hh, z: hw }, { x: hw * 1.5, y: -hh, z: -hw },
{ x: -hw * 1.5, y: -hh, z: hw }, { x: -hw * 1.5, y: -hh, z: -hw },
];
for (let position of wheelPositions) {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
vehicle.addWheel(position, { x: 0.0, y: -1.0, z: 0.0 }, { x: 0.0, y: 0.0, z: 1.0 }, hh, hh / 4.0);
}
// The tuning of each wheel: its suspension and its grip.
for (let i = 0; i < vehicle.numWheels(); ++i) {
vehicle.setWheelSuspensionStiffness(i, 100.0);
vehicle.setWheelSuspensionCompression(i, 10.0);
vehicle.setWheelSuspensionRelaxation(i, 10.0);
}
The vehicle controller is the RayCastVehicleController component, added to the entity of the
chassis' dynamic rigid-body. Its wheels are VehicleWheel values stored in its wheels field, and can be added,
removed, or modified at any time:
// The tuning shared by the wheels: the suspension and the grip.
let tuning = WheelTuning {
suspension_stiffness: 100.0,
suspension_damping: 10.0,
..WheelTuning::default()
};
let hw = 0.3;
let hh = 0.15;
let wheel_positions = [
Vec3::new(hw * 1.5, -hh, hw),
Vec3::new(hw * 1.5, -hh, -hw),
Vec3::new(-hw * 1.5, -hh, hw),
Vec3::new(-hw * 1.5, -hh, -hw),
];
let wheels = wheel_positions
.into_iter()
.map(|position| {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
VehicleWheel::new(position, -Vec3::Y, Vec3::Z, hh, hh / 4.0, tuning)
})
.collect();
// The chassis is an ordinary dynamic rigid-body, with the vehicle controller attached to it.
commands.spawn((
Transform::from_xyz(0.0, 1.0, 0.0),
RigidBody::Dynamic,
Collider::cuboid(hw * 2.0, hh, hw),
ColliderMassProperties::Density(100.0),
RayCastVehicleController::new(wheels),
));
The vehicle controller is the R3DynamicRayCastVehicleController object, created by
r3NewDynamicRayCastVehicleController from the handle of the chassis' dynamic rigid-body. It remembers the world of
that rigid-body, so it must be freed with r3FreeDynamicRayCastVehicleController before that world is freed. The
wheels are added with r3DynamicRayCastVehicleController_AddWheel, which returns the index of the new wheel (starting
from zero, in insertion order), and the R3WheelTuning given to each wheel is initialized with r3DefaultWheelTuning:
// The chassis is an ordinary dynamic rigid-body.
const R3Real hw = 0.3;
const R3Real hh = 0.15;
R3RigidBodyDesc chassis_body = r3DynamicRigidBodyDesc();
chassis_body.position.translation = r3Vector(0.0, 1.0, 0.0);
R3RigidBodyHandle chassis_handle = r3InsertRigidBody(world, &chassis_body);
R3ColliderDesc chassis_collider = r3CuboidColliderDesc(r3Vector(hw * 2.0, hh, hw));
chassis_collider.density = 100.0;
r3InsertCollider(chassis_handle, &chassis_collider);
// The tuning shared by the wheels: the suspension and the grip.
R3WheelTuning tuning = r3DefaultWheelTuning();
tuning.suspension_stiffness = 100.0;
tuning.suspension_damping = 10.0;
// The controller must be freed (with r3FreeDynamicRayCastVehicleController) before its world.
R3DynamicRayCastVehicleController *vehicle = r3NewDynamicRayCastVehicleController(chassis_handle);
const R3Vector wheel_positions[4] = {
{hw * 1.5, -hh, hw},
{hw * 1.5, -hh, -hw},
{-hw * 1.5, -hh, hw},
{-hw * 1.5, -hh, -hw},
};
for (size_t i = 0; i < 4; i++) {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
r3DynamicRayCastVehicleController_AddWheel(vehicle, wheel_positions[i], r3Vector(0.0, -1.0, 0.0),
r3Vector(0.0, 0.0, 1.0), hh, hh / 4.0, &tuning);
}
By default, the vehicle moves forward along the local x axis of the chassis, and its local y axis points
upward. Other axes can be selected with r3DynamicRayCastVehicleController_SetAxes.
The vehicle controller is the DynamicRayCastVehicleController class, created from the handle of the chassis'
dynamic rigid-body. The wheels are added with its add_wheel method, which returns the index of the new wheel
(starting from zero, in insertion order). The WheelTuning given to each wheel is created with keyword arguments
overriding its default values (as given by WheelTuning.default()):
# The chassis is an ordinary dynamic rigid-body.
hw = 0.3
hh = 0.15
chassis_handle = world.add_body(
rp.RigidBody.dynamic(translation=(0.0, 1.0, 0.0)),
colliders=[rp.Collider.cuboid(hw * 2.0, hh, hw).density(100.0)],
)
# The tuning shared by the wheels: the suspension and the grip. The
# parameters that aren't given keep their default values.
tuning = rp.WheelTuning(suspension_stiffness=100.0, suspension_damping=10.0)
vehicle = rp.DynamicRayCastVehicleController(chassis_handle)
wheel_positions = [
(hw * 1.5, -hh, hw),
(hw * 1.5, -hh, -hw),
(-hw * 1.5, -hh, hw),
(-hw * 1.5, -hh, -hw),
]
for position in wheel_positions:
# The position of the wheel, the direction its suspension pushes along, its axle, the
# rest length of its suspension, and its radius; all in the local frame of the chassis.
vehicle.add_wheel(position, (0.0, -1.0, 0.0), (0.0, 0.0, 1.0), hh, hh / 4.0, tuning)
By default, the vehicle moves forward along the local x axis of the chassis, and its local y axis points
upward. Other axes can be selected with the index_forward_axis and index_up_axis properties of the controller
(0, 1, and 2 standing for the x, y, and z axes).
Driving the vehicle
A vehicle is driven by giving each of its wheels an engine force, a brake force, and a steering angle. The controller is then updated before each timestep, which is when the ray-casts are made and when the resulting suspension and friction forces are applied to the chassis. It is strongly recommended to exclude the chassis from the ray-casts, otherwise the ray might hit it and be misinterpreted as being the floor.
for _ in 0..200 {
// The vehicle is driven by setting the engine force, the brake, and the steering angle of
// its wheels. Here the two front wheels are the driving and steering ones.
let wheels = vehicle.wheels_mut();
wheels[0].engine_force = 30.0;
wheels[0].steering = 0.2;
wheels[1].engine_force = 30.0;
wheels[1].steering = 0.2;
// The wheels are ray-casted against the scene: the chassis itself, as well as every other
// dynamic body, is generally excluded from these ray-casts.
let queries = world.broad_phase.as_query_pipeline_mut(
world.narrow_phase.query_dispatcher(),
&mut world.bodies,
&mut world.colliders,
QueryFilter::exclude_dynamic().exclude_rigid_body(chassis_handle),
);
vehicle.update_vehicle(world.integration_parameters.dt, queries);
world.step();
}
println!("Vehicle speed: {}", vehicle.current_vehicle_speed);
for (let k = 0; k < 200; ++k) {
// The vehicle is driven by setting the engine force, the brake, and the steering angle of
// its wheels. Here the two front wheels are the driving and steering ones.
vehicle.setWheelEngineForce(0, 30.0);
vehicle.setWheelSteering(0, 0.2);
vehicle.setWheelEngineForce(1, 30.0);
vehicle.setWheelSteering(1, 0.2);
// The wheels are ray-casted against the scene: the dynamic bodies, including the chassis
// itself, are generally excluded from these ray-casts.
vehicle.updateVehicle(world.integrationParameters.dt, RAPIER.QueryFilterFlags.EXCLUDE_DYNAMIC);
world.step();
}
console.log("Vehicle speed:", vehicle.currentVehicleSpeed());
The controller is updated automatically by the plugin before each simulation step, and the
colliders attached to the chassis are always excluded from the ray-casts. Other colliders can be excluded with the
filter_flags, filter_groups, exclude_colliders, exclude_rigid_bodies, and filter_predicate fields of
the RayCastVehicleController (similar to the ones of the
character controller), or by inserting the ControllerIgnored component on them
(or on their rigid-body). So driving the vehicle is just a matter of modifying
the engine force, brake, and steering of its wheels:
fn drive_vehicle(mut vehicles: Query<&mut RayCastVehicleController>) {
for mut vehicle in vehicles.iter_mut() {
// The vehicle is driven by setting the engine force, the brake, and the steering angle of
// its wheels. Here the two front wheels are the driving and steering ones.
for wheel in &mut vehicle.wheels[0..2] {
wheel.engine_force = 30.0;
wheel.steering = 0.2;
}
// The colliders of the chassis are always ignored by the ray-casts of the wheels. Other
// colliders, here the ones attached to dynamic rigid-bodies, can be ignored too.
vehicle.filter_flags =
QueryFilterFlags::EXCLUDE_SENSORS | QueryFilterFlags::EXCLUDE_DYNAMIC;
// The results of the last update are written back into the component.
println!("Vehicle speed: {}", vehicle.current_vehicle_speed);
for wheel in &vehicle.wheels {
println!(
"Wheel in contact: {}, suspension length: {}",
wheel.state.is_in_contact, wheel.state.suspension_length
);
}
}
}
The forces applied by the vehicle controller are computed once per update of the plugin, using the length of the last simulation timestep. So it is best used with a fixed timestep.
The engine force, brake, and steering angle of a wheel are set with r3DynamicRayCastVehicleController_SetWheelControls
(from the index of the wheel), and the controller is updated with r3DynamicRayCastVehicleController_UpdateVehicle
before each r3Step. The colliders attached to the chassis are always excluded from the ray-casts, and the other
obstacles can be filtered with the R3QueryOptions given to the update (see the
scene query filters). Note that its predicate callback, if any, is called once
for every collider of the world before the update, while the world is locked for writing: it may only use the Read
functions of its R3ReadContext.
// The wheels are ray-casted against the scene: the chassis itself is always excluded, and
// every other dynamic body is generally excluded from these ray-casts too.
R3QueryOptions options = r3DefaultQueryOptions();
options.filter.flags = R3_QUERY_EXCLUDE_DYNAMIC;
for (int i = 0; i < 200; i++) {
// The vehicle is driven by setting the steering angle, the engine force, and the brake of
// its wheels. Here the two front wheels (indices 0 and 1) are the driving and steering ones.
r3DynamicRayCastVehicleController_SetWheelControls(vehicle, 0, 0.2, 30.0, 0.0);
r3DynamicRayCastVehicleController_SetWheelControls(vehicle, 1, 0.2, 30.0, 0.0);
r3DynamicRayCastVehicleController_UpdateVehicle(vehicle, r3TimeStep(world), &options);
r3Step(world, NULL, NULL);
}
printf("Vehicle speed: %f\n", (double)r3DynamicRayCastVehicleController_CurrentVehicleSpeed(vehicle));
The engine force, brake, and steering angle of a wheel are set with the apply_engine_force, set_brake, and
set_steering methods of the controller (from the index of the wheel), and the controller is updated with its
update_vehicle method before each PhysicsWorld.step. The colliders attached to the chassis are always excluded
from the ray-casts, and the other obstacles can be filtered with the optional QueryFilter given to update_vehicle
(see the scene query filters). The predicate of that filter, if any, is called
once for every collider before the update, while the sets are locked: it must not modify them.
for _ in range(200):
# The vehicle is driven by setting the engine force, the brake, and the steering angle of
# its wheels (from their indices). Here the two front wheels are the driving and steering ones.
vehicle.apply_engine_force(0, 30.0)
vehicle.set_steering(0, 0.2)
vehicle.apply_engine_force(1, 30.0)
vehicle.set_steering(1, 0.2)
# The wheels are ray-casted against the scene: the chassis itself is always excluded from
# these ray-casts, and every other dynamic body is generally excluded too.
vehicle.update_vehicle(
world.integration_parameters.dt,
world.rigid_bodies,
world.colliders,
world.query_pipeline,
rp.QueryFilter.exclude_dynamic(),
)
world.step()
print("Vehicle speed:", vehicle.current_vehicle_speed)
The state of each wheel after an update (whether it touches the floor, the compression of its suspension, its rotation angle, etc.) is readable from the controller. This is what the rendering of the wheels can be based on since there is no actual per-wheel rigid-bodies to read their state from.
The state of each wheel is written back by the plugin into its VehicleWheel::state field (a
VehicleWheelState), and the speed of the vehicle into RayCastVehicleController::current_vehicle_speed. The
VehicleWheel::local_transform method computes the transform of a wheel relative to the chassis (including its
suspension length, steering, and rotation), which can be given directly to the child entity rendering that wheel.
The state of the wheels is copied into a buffer of R3WheelState by r3DynamicRayCastVehicleController_Wheels: the
world-space center of each wheel, the world-space directions of its suspension and axle, its rotation angle, the length
and force of its suspension, and its contact with the ground (whether it touches it, the collider touched, and the
world-space contact point and normal). The speed of the vehicle along its forward axis is given by
r3DynamicRayCastVehicleController_CurrentVehicleSpeed.
// The wheels are given in the order they were added to the controller.
R3WheelState wheels[4];
size_t num_wheels = r3DynamicRayCastVehicleController_Wheels(vehicle, wheels, 4);
for (size_t i = 0; i < num_wheels; i++) {
// The world-space center of the wheel, its current suspension length, rotation angle, etc.
printf("Wheel %zu: center (%f, %f, %f), suspension length %f, rotation %f, in contact: %u\n", i,
(double)wheels[i].center.x, (double)wheels[i].center.y, (double)wheels[i].center.z,
(double)wheels[i].suspension_length, (double)wheels[i].rotation,
(unsigned)wheels[i].is_in_contact);
}
The state of the wheels is given by the wheels method of the controller (or by its wheel method for a single
wheel) as a list of Wheel objects. These are copies: modifying them doesn’t affect the vehicle. Each wheel gives its
world-space center, the world-space directions of its suspension and axle, its rotation angle, the force of its
suspension (wheel_suspension_force), and its raycast_info (a RayCastInfo): the length of its suspension
(suspension_length) and its contact with the ground (is_in_contact, the collider touched ground_object, and the
world-space contact point contact_point_ws and normal contact_normal_ws). The speed of the vehicle along its
forward axis is given by the current_vehicle_speed property of the controller (in meters per second, or with the
current_speed_km_hour method in kilometers per hour).
# The wheels are copies of the state of the wheels after the last update.
for wheel in vehicle.wheels():
contact = wheel.raycast_info
print(
"Wheel center:", wheel.center, # World-space center of the wheel.
"axle:", wheel.axle, # World-space direction of its axle.
"rotation:", wheel.rotation, # Rotation angle around its axle.
"suspension length:", contact.suspension_length,
"touches the ground:", contact.is_in_contact,
"ground collider:", contact.ground_object, # None if it doesn’t touch the ground.
)
Vehicle controller
Simulating a car with rigid-bodies and joints, for example using one rigid-body per wheel attached to the chassis by a joint, is
possible but can be difficult to control for games requiring non-realistic vehicles. This is why Rapier provides a
vehicle controller (the current implementation was ported from the btRaycastVehicle of Bullet). The vehicle is a
single rigid-body modeling its chassis, and its wheels are only represented by ray-casts pushing that body along a
spring-like suspension.
The vehicle controller is only available in 3D, i.e., with bevy_rapier3d.
The vehicle controller is only available in 3D, i.e., with the r3 functions.
Setup
The chassis is created like any other dynamic rigid-body, and the wheels are added to the controller afterwards. Each
wheel is given its position on the chassis, the direction of its suspension (the direction of its ray-cast), its axle,
the rest length of its suspension, and its radius. The WheelTuningR3WheelTuning
// The chassis is an ordinary dynamic rigid-body.
let hw = 0.3;
let hh = 0.15;
let (chassis_handle, _) = world.insert(
RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 1.0, 0.0)),
ColliderBuilder::cuboid(hw * 2.0, hh, hw).density(100.0),
);
// The tuning shared by the wheels: the suspension and the grip.
let tuning = WheelTuning {
suspension_stiffness: 100.0,
suspension_damping: 10.0,
..WheelTuning::default()
};
let mut vehicle = DynamicRayCastVehicleController::new(chassis_handle);
let wheel_positions = [
Vector::new(hw * 1.5, -hh, hw),
Vector::new(hw * 1.5, -hh, -hw),
Vector::new(-hw * 1.5, -hh, hw),
Vector::new(-hw * 1.5, -hh, -hw),
];
for position in wheel_positions {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
vehicle.add_wheel(position, -Vector::Y, Vector::Z, hh, hh / 4.0, &tuning);
}
// The chassis is an ordinary dynamic rigid-body.
let hw = 0.3;
let hh = 0.15;
let chassis = world.createRigidBody(
RAPIER.RigidBodyDesc.dynamic().setTranslation(0.0, 1.0, 0.0),
);
world.createCollider(RAPIER.ColliderDesc.cuboid(hw * 2.0, hh, hw).setDensity(100.0), chassis);
let vehicle = world.createVehicleController(chassis);
let wheelPositions = [
{ x: hw * 1.5, y: -hh, z: hw }, { x: hw * 1.5, y: -hh, z: -hw },
{ x: -hw * 1.5, y: -hh, z: hw }, { x: -hw * 1.5, y: -hh, z: -hw },
];
for (let position of wheelPositions) {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
vehicle.addWheel(position, { x: 0.0, y: -1.0, z: 0.0 }, { x: 0.0, y: 0.0, z: 1.0 }, hh, hh / 4.0);
}
// The tuning of each wheel: its suspension and its grip.
for (let i = 0; i < vehicle.numWheels(); ++i) {
vehicle.setWheelSuspensionStiffness(i, 100.0);
vehicle.setWheelSuspensionCompression(i, 10.0);
vehicle.setWheelSuspensionRelaxation(i, 10.0);
}
The vehicle controller is the RayCastVehicleController component, added to the entity of the
chassis' dynamic rigid-body. Its wheels are VehicleWheel values stored in its wheels field, and can be added,
removed, or modified at any time:
// The tuning shared by the wheels: the suspension and the grip.
let tuning = WheelTuning {
suspension_stiffness: 100.0,
suspension_damping: 10.0,
..WheelTuning::default()
};
let hw = 0.3;
let hh = 0.15;
let wheel_positions = [
Vec3::new(hw * 1.5, -hh, hw),
Vec3::new(hw * 1.5, -hh, -hw),
Vec3::new(-hw * 1.5, -hh, hw),
Vec3::new(-hw * 1.5, -hh, -hw),
];
let wheels = wheel_positions
.into_iter()
.map(|position| {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
VehicleWheel::new(position, -Vec3::Y, Vec3::Z, hh, hh / 4.0, tuning)
})
.collect();
// The chassis is an ordinary dynamic rigid-body, with the vehicle controller attached to it.
commands.spawn((
Transform::from_xyz(0.0, 1.0, 0.0),
RigidBody::Dynamic,
Collider::cuboid(hw * 2.0, hh, hw),
ColliderMassProperties::Density(100.0),
RayCastVehicleController::new(wheels),
));
The vehicle controller is the R3DynamicRayCastVehicleController object, created by
r3NewDynamicRayCastVehicleController from the handle of the chassis' dynamic rigid-body. It remembers the world of
that rigid-body, so it must be freed with r3FreeDynamicRayCastVehicleController before that world is freed. The
wheels are added with r3DynamicRayCastVehicleController_AddWheel, which returns the index of the new wheel (starting
from zero, in insertion order), and the R3WheelTuning given to each wheel is initialized with r3DefaultWheelTuning:
// The chassis is an ordinary dynamic rigid-body.
const R3Real hw = 0.3;
const R3Real hh = 0.15;
R3RigidBodyDesc chassis_body = r3DynamicRigidBodyDesc();
chassis_body.position.translation = r3Vector(0.0, 1.0, 0.0);
R3RigidBodyHandle chassis_handle = r3InsertRigidBody(world, &chassis_body);
R3ColliderDesc chassis_collider = r3CuboidColliderDesc(r3Vector(hw * 2.0, hh, hw));
chassis_collider.density = 100.0;
r3InsertCollider(chassis_handle, &chassis_collider);
// The tuning shared by the wheels: the suspension and the grip.
R3WheelTuning tuning = r3DefaultWheelTuning();
tuning.suspension_stiffness = 100.0;
tuning.suspension_damping = 10.0;
// The controller must be freed (with r3FreeDynamicRayCastVehicleController) before its world.
R3DynamicRayCastVehicleController *vehicle = r3NewDynamicRayCastVehicleController(chassis_handle);
const R3Vector wheel_positions[4] = {
{hw * 1.5, -hh, hw},
{hw * 1.5, -hh, -hw},
{-hw * 1.5, -hh, hw},
{-hw * 1.5, -hh, -hw},
};
for (size_t i = 0; i < 4; i++) {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
r3DynamicRayCastVehicleController_AddWheel(vehicle, wheel_positions[i], r3Vector(0.0, -1.0, 0.0),
r3Vector(0.0, 0.0, 1.0), hh, hh / 4.0, &tuning);
}
By default, the vehicle moves forward along the local x axis of the chassis, and its local y axis points
upward. Other axes can be selected with r3DynamicRayCastVehicleController_SetAxes.
The vehicle controller is the DynamicRayCastVehicleController class, created from the handle of the chassis'
dynamic rigid-body. The wheels are added with its add_wheel method, which returns the index of the new wheel
(starting from zero, in insertion order). The WheelTuning given to each wheel is created with keyword arguments
overriding its default values (as given by WheelTuning.default()):
# The chassis is an ordinary dynamic rigid-body.
hw = 0.3
hh = 0.15
chassis_handle = world.add_body(
rp.RigidBody.dynamic(translation=(0.0, 1.0, 0.0)),
colliders=[rp.Collider.cuboid(hw * 2.0, hh, hw).density(100.0)],
)
# The tuning shared by the wheels: the suspension and the grip. The
# parameters that aren't given keep their default values.
tuning = rp.WheelTuning(suspension_stiffness=100.0, suspension_damping=10.0)
vehicle = rp.DynamicRayCastVehicleController(chassis_handle)
wheel_positions = [
(hw * 1.5, -hh, hw),
(hw * 1.5, -hh, -hw),
(-hw * 1.5, -hh, hw),
(-hw * 1.5, -hh, -hw),
]
for position in wheel_positions:
# The position of the wheel, the direction its suspension pushes along, its axle, the
# rest length of its suspension, and its radius; all in the local frame of the chassis.
vehicle.add_wheel(position, (0.0, -1.0, 0.0), (0.0, 0.0, 1.0), hh, hh / 4.0, tuning)
By default, the vehicle moves forward along the local x axis of the chassis, and its local y axis points
upward. Other axes can be selected with the index_forward_axis and index_up_axis properties of the controller
(0, 1, and 2 standing for the x, y, and z axes).
Driving the vehicle
A vehicle is driven by giving each of its wheels an engine force, a brake force, and a steering angle. The controller is then updated before each timestep, which is when the ray-casts are made and when the resulting suspension and friction forces are applied to the chassis. It is strongly recommended to exclude the chassis from the ray-casts, otherwise the ray might hit it and be misinterpreted as being the floor.
for _ in 0..200 {
// The vehicle is driven by setting the engine force, the brake, and the steering angle of
// its wheels. Here the two front wheels are the driving and steering ones.
let wheels = vehicle.wheels_mut();
wheels[0].engine_force = 30.0;
wheels[0].steering = 0.2;
wheels[1].engine_force = 30.0;
wheels[1].steering = 0.2;
// The wheels are ray-casted against the scene: the chassis itself, as well as every other
// dynamic body, is generally excluded from these ray-casts.
let queries = world.broad_phase.as_query_pipeline_mut(
world.narrow_phase.query_dispatcher(),
&mut world.bodies,
&mut world.colliders,
QueryFilter::exclude_dynamic().exclude_rigid_body(chassis_handle),
);
vehicle.update_vehicle(world.integration_parameters.dt, queries);
world.step();
}
println!("Vehicle speed: {}", vehicle.current_vehicle_speed);
for (let k = 0; k < 200; ++k) {
// The vehicle is driven by setting the engine force, the brake, and the steering angle of
// its wheels. Here the two front wheels are the driving and steering ones.
vehicle.setWheelEngineForce(0, 30.0);
vehicle.setWheelSteering(0, 0.2);
vehicle.setWheelEngineForce(1, 30.0);
vehicle.setWheelSteering(1, 0.2);
// The wheels are ray-casted against the scene: the dynamic bodies, including the chassis
// itself, are generally excluded from these ray-casts.
vehicle.updateVehicle(world.integrationParameters.dt, RAPIER.QueryFilterFlags.EXCLUDE_DYNAMIC);
world.step();
}
console.log("Vehicle speed:", vehicle.currentVehicleSpeed());
The controller is updated automatically by the plugin before each simulation step, and the
colliders attached to the chassis are always excluded from the ray-casts. Other colliders can be excluded with the
filter_flags, filter_groups, exclude_colliders, exclude_rigid_bodies, and filter_predicate fields of
the RayCastVehicleController (similar to the ones of the
character controller), or by inserting the ControllerIgnored component on them
(or on their rigid-body). So driving the vehicle is just a matter of modifying
the engine force, brake, and steering of its wheels:
fn drive_vehicle(mut vehicles: Query<&mut RayCastVehicleController>) {
for mut vehicle in vehicles.iter_mut() {
// The vehicle is driven by setting the engine force, the brake, and the steering angle of
// its wheels. Here the two front wheels are the driving and steering ones.
for wheel in &mut vehicle.wheels[0..2] {
wheel.engine_force = 30.0;
wheel.steering = 0.2;
}
// The colliders of the chassis are always ignored by the ray-casts of the wheels. Other
// colliders, here the ones attached to dynamic rigid-bodies, can be ignored too.
vehicle.filter_flags =
QueryFilterFlags::EXCLUDE_SENSORS | QueryFilterFlags::EXCLUDE_DYNAMIC;
// The results of the last update are written back into the component.
println!("Vehicle speed: {}", vehicle.current_vehicle_speed);
for wheel in &vehicle.wheels {
println!(
"Wheel in contact: {}, suspension length: {}",
wheel.state.is_in_contact, wheel.state.suspension_length
);
}
}
}
The forces applied by the vehicle controller are computed once per update of the plugin, using the length of the last simulation timestep. So it is best used with a fixed timestep.
The engine force, brake, and steering angle of a wheel are set with r3DynamicRayCastVehicleController_SetWheelControls
(from the index of the wheel), and the controller is updated with r3DynamicRayCastVehicleController_UpdateVehicle
before each r3Step. The colliders attached to the chassis are always excluded from the ray-casts, and the other
obstacles can be filtered with the R3QueryOptions given to the update (see the
scene query filters). Note that its predicate callback, if any, is called once
for every collider of the world before the update, while the world is locked for writing: it may only use the Read
functions of its R3ReadContext.
// The wheels are ray-casted against the scene: the chassis itself is always excluded, and
// every other dynamic body is generally excluded from these ray-casts too.
R3QueryOptions options = r3DefaultQueryOptions();
options.filter.flags = R3_QUERY_EXCLUDE_DYNAMIC;
for (int i = 0; i < 200; i++) {
// The vehicle is driven by setting the steering angle, the engine force, and the brake of
// its wheels. Here the two front wheels (indices 0 and 1) are the driving and steering ones.
r3DynamicRayCastVehicleController_SetWheelControls(vehicle, 0, 0.2, 30.0, 0.0);
r3DynamicRayCastVehicleController_SetWheelControls(vehicle, 1, 0.2, 30.0, 0.0);
r3DynamicRayCastVehicleController_UpdateVehicle(vehicle, r3TimeStep(world), &options);
r3Step(world, NULL, NULL);
}
printf("Vehicle speed: %f\n", (double)r3DynamicRayCastVehicleController_CurrentVehicleSpeed(vehicle));
The engine force, brake, and steering angle of a wheel are set with the apply_engine_force, set_brake, and
set_steering methods of the controller (from the index of the wheel), and the controller is updated with its
update_vehicle method before each PhysicsWorld.step. The colliders attached to the chassis are always excluded
from the ray-casts, and the other obstacles can be filtered with the optional QueryFilter given to update_vehicle
(see the scene query filters). The predicate of that filter, if any, is called
once for every collider before the update, while the sets are locked: it must not modify them.
for _ in range(200):
# The vehicle is driven by setting the engine force, the brake, and the steering angle of
# its wheels (from their indices). Here the two front wheels are the driving and steering ones.
vehicle.apply_engine_force(0, 30.0)
vehicle.set_steering(0, 0.2)
vehicle.apply_engine_force(1, 30.0)
vehicle.set_steering(1, 0.2)
# The wheels are ray-casted against the scene: the chassis itself is always excluded from
# these ray-casts, and every other dynamic body is generally excluded too.
vehicle.update_vehicle(
world.integration_parameters.dt,
world.rigid_bodies,
world.colliders,
world.query_pipeline,
rp.QueryFilter.exclude_dynamic(),
)
world.step()
print("Vehicle speed:", vehicle.current_vehicle_speed)
The state of each wheel after an update (whether it touches the floor, the compression of its suspension, its rotation angle, etc.) is readable from the controller. This is what the rendering of the wheels can be based on since there is no actual per-wheel rigid-bodies to read their state from.
The state of each wheel is written back by the plugin into its VehicleWheel::state field (a
VehicleWheelState), and the speed of the vehicle into RayCastVehicleController::current_vehicle_speed. The
VehicleWheel::local_transform method computes the transform of a wheel relative to the chassis (including its
suspension length, steering, and rotation), which can be given directly to the child entity rendering that wheel.
The state of the wheels is copied into a buffer of R3WheelState by r3DynamicRayCastVehicleController_Wheels: the
world-space center of each wheel, the world-space directions of its suspension and axle, its rotation angle, the length
and force of its suspension, and its contact with the ground (whether it touches it, the collider touched, and the
world-space contact point and normal). The speed of the vehicle along its forward axis is given by
r3DynamicRayCastVehicleController_CurrentVehicleSpeed.
// The wheels are given in the order they were added to the controller.
R3WheelState wheels[4];
size_t num_wheels = r3DynamicRayCastVehicleController_Wheels(vehicle, wheels, 4);
for (size_t i = 0; i < num_wheels; i++) {
// The world-space center of the wheel, its current suspension length, rotation angle, etc.
printf("Wheel %zu: center (%f, %f, %f), suspension length %f, rotation %f, in contact: %u\n", i,
(double)wheels[i].center.x, (double)wheels[i].center.y, (double)wheels[i].center.z,
(double)wheels[i].suspension_length, (double)wheels[i].rotation,
(unsigned)wheels[i].is_in_contact);
}
The state of the wheels is given by the wheels method of the controller (or by its wheel method for a single
wheel) as a list of Wheel objects. These are copies: modifying them doesn’t affect the vehicle. Each wheel gives its
world-space center, the world-space directions of its suspension and axle, its rotation angle, the force of its
suspension (wheel_suspension_force), and its raycast_info (a RayCastInfo): the length of its suspension
(suspension_length) and its contact with the ground (is_in_contact, the collider touched ground_object, and the
world-space contact point contact_point_ws and normal contact_normal_ws). The speed of the vehicle along its
forward axis is given by the current_vehicle_speed property of the controller (in meters per second, or with the
current_speed_km_hour method in kilometers per hour).
# The wheels are copies of the state of the wheels after the last update.
for wheel in vehicle.wheels():
contact = wheel.raycast_info
print(
"Wheel center:", wheel.center, # World-space center of the wheel.
"axle:", wheel.axle, # World-space direction of its axle.
"rotation:", wheel.rotation, # Rotation angle around its axle.
"suspension length:", contact.suspension_length,
"touches the ground:", contact.is_in_contact,
"ground collider:", contact.ground_object, # None if it doesn’t touch the ground.
)
Vehicle controller
Simulating a car with rigid-bodies and joints, for example using one rigid-body per wheel attached to the chassis by a joint, is
possible but can be difficult to control for games requiring non-realistic vehicles. This is why Rapier provides a
vehicle controller (the current implementation was ported from the btRaycastVehicle of Bullet). The vehicle is a
single rigid-body modeling its chassis, and its wheels are only represented by ray-casts pushing that body along a
spring-like suspension.
The vehicle controller is only available in 3D, i.e., with bevy_rapier3d.
The vehicle controller is only available in 3D, i.e., with the r3 functions.
Setup
The chassis is created like any other dynamic rigid-body, and the wheels are added to the controller afterwards. Each
wheel is given its position on the chassis, the direction of its suspension (the direction of its ray-cast), its axle,
the rest length of its suspension, and its radius. The WheelTuningR3WheelTuning
// The chassis is an ordinary dynamic rigid-body.
let hw = 0.3;
let hh = 0.15;
let (chassis_handle, _) = world.insert(
RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 1.0, 0.0)),
ColliderBuilder::cuboid(hw * 2.0, hh, hw).density(100.0),
);
// The tuning shared by the wheels: the suspension and the grip.
let tuning = WheelTuning {
suspension_stiffness: 100.0,
suspension_damping: 10.0,
..WheelTuning::default()
};
let mut vehicle = DynamicRayCastVehicleController::new(chassis_handle);
let wheel_positions = [
Vector::new(hw * 1.5, -hh, hw),
Vector::new(hw * 1.5, -hh, -hw),
Vector::new(-hw * 1.5, -hh, hw),
Vector::new(-hw * 1.5, -hh, -hw),
];
for position in wheel_positions {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
vehicle.add_wheel(position, -Vector::Y, Vector::Z, hh, hh / 4.0, &tuning);
}
// The chassis is an ordinary dynamic rigid-body.
let hw = 0.3;
let hh = 0.15;
let chassis = world.createRigidBody(
RAPIER.RigidBodyDesc.dynamic().setTranslation(0.0, 1.0, 0.0),
);
world.createCollider(RAPIER.ColliderDesc.cuboid(hw * 2.0, hh, hw).setDensity(100.0), chassis);
let vehicle = world.createVehicleController(chassis);
let wheelPositions = [
{ x: hw * 1.5, y: -hh, z: hw }, { x: hw * 1.5, y: -hh, z: -hw },
{ x: -hw * 1.5, y: -hh, z: hw }, { x: -hw * 1.5, y: -hh, z: -hw },
];
for (let position of wheelPositions) {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
vehicle.addWheel(position, { x: 0.0, y: -1.0, z: 0.0 }, { x: 0.0, y: 0.0, z: 1.0 }, hh, hh / 4.0);
}
// The tuning of each wheel: its suspension and its grip.
for (let i = 0; i < vehicle.numWheels(); ++i) {
vehicle.setWheelSuspensionStiffness(i, 100.0);
vehicle.setWheelSuspensionCompression(i, 10.0);
vehicle.setWheelSuspensionRelaxation(i, 10.0);
}
The vehicle controller is the RayCastVehicleController component, added to the entity of the
chassis' dynamic rigid-body. Its wheels are VehicleWheel values stored in its wheels field, and can be added,
removed, or modified at any time:
// The tuning shared by the wheels: the suspension and the grip.
let tuning = WheelTuning {
suspension_stiffness: 100.0,
suspension_damping: 10.0,
..WheelTuning::default()
};
let hw = 0.3;
let hh = 0.15;
let wheel_positions = [
Vec3::new(hw * 1.5, -hh, hw),
Vec3::new(hw * 1.5, -hh, -hw),
Vec3::new(-hw * 1.5, -hh, hw),
Vec3::new(-hw * 1.5, -hh, -hw),
];
let wheels = wheel_positions
.into_iter()
.map(|position| {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
VehicleWheel::new(position, -Vec3::Y, Vec3::Z, hh, hh / 4.0, tuning)
})
.collect();
// The chassis is an ordinary dynamic rigid-body, with the vehicle controller attached to it.
commands.spawn((
Transform::from_xyz(0.0, 1.0, 0.0),
RigidBody::Dynamic,
Collider::cuboid(hw * 2.0, hh, hw),
ColliderMassProperties::Density(100.0),
RayCastVehicleController::new(wheels),
));
The vehicle controller is the R3DynamicRayCastVehicleController object, created by
r3NewDynamicRayCastVehicleController from the handle of the chassis' dynamic rigid-body. It remembers the world of
that rigid-body, so it must be freed with r3FreeDynamicRayCastVehicleController before that world is freed. The
wheels are added with r3DynamicRayCastVehicleController_AddWheel, which returns the index of the new wheel (starting
from zero, in insertion order), and the R3WheelTuning given to each wheel is initialized with r3DefaultWheelTuning:
// The chassis is an ordinary dynamic rigid-body.
const R3Real hw = 0.3;
const R3Real hh = 0.15;
R3RigidBodyDesc chassis_body = r3DynamicRigidBodyDesc();
chassis_body.position.translation = r3Vector(0.0, 1.0, 0.0);
R3RigidBodyHandle chassis_handle = r3InsertRigidBody(world, &chassis_body);
R3ColliderDesc chassis_collider = r3CuboidColliderDesc(r3Vector(hw * 2.0, hh, hw));
chassis_collider.density = 100.0;
r3InsertCollider(chassis_handle, &chassis_collider);
// The tuning shared by the wheels: the suspension and the grip.
R3WheelTuning tuning = r3DefaultWheelTuning();
tuning.suspension_stiffness = 100.0;
tuning.suspension_damping = 10.0;
// The controller must be freed (with r3FreeDynamicRayCastVehicleController) before its world.
R3DynamicRayCastVehicleController *vehicle = r3NewDynamicRayCastVehicleController(chassis_handle);
const R3Vector wheel_positions[4] = {
{hw * 1.5, -hh, hw},
{hw * 1.5, -hh, -hw},
{-hw * 1.5, -hh, hw},
{-hw * 1.5, -hh, -hw},
};
for (size_t i = 0; i < 4; i++) {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
r3DynamicRayCastVehicleController_AddWheel(vehicle, wheel_positions[i], r3Vector(0.0, -1.0, 0.0),
r3Vector(0.0, 0.0, 1.0), hh, hh / 4.0, &tuning);
}
By default, the vehicle moves forward along the local x axis of the chassis, and its local y axis points
upward. Other axes can be selected with r3DynamicRayCastVehicleController_SetAxes.
The vehicle controller is the DynamicRayCastVehicleController class, created from the handle of the chassis'
dynamic rigid-body. The wheels are added with its add_wheel method, which returns the index of the new wheel
(starting from zero, in insertion order). The WheelTuning given to each wheel is created with keyword arguments
overriding its default values (as given by WheelTuning.default()):
# The chassis is an ordinary dynamic rigid-body.
hw = 0.3
hh = 0.15
chassis_handle = world.add_body(
rp.RigidBody.dynamic(translation=(0.0, 1.0, 0.0)),
colliders=[rp.Collider.cuboid(hw * 2.0, hh, hw).density(100.0)],
)
# The tuning shared by the wheels: the suspension and the grip. The
# parameters that aren't given keep their default values.
tuning = rp.WheelTuning(suspension_stiffness=100.0, suspension_damping=10.0)
vehicle = rp.DynamicRayCastVehicleController(chassis_handle)
wheel_positions = [
(hw * 1.5, -hh, hw),
(hw * 1.5, -hh, -hw),
(-hw * 1.5, -hh, hw),
(-hw * 1.5, -hh, -hw),
]
for position in wheel_positions:
# The position of the wheel, the direction its suspension pushes along, its axle, the
# rest length of its suspension, and its radius; all in the local frame of the chassis.
vehicle.add_wheel(position, (0.0, -1.0, 0.0), (0.0, 0.0, 1.0), hh, hh / 4.0, tuning)
By default, the vehicle moves forward along the local x axis of the chassis, and its local y axis points
upward. Other axes can be selected with the index_forward_axis and index_up_axis properties of the controller
(0, 1, and 2 standing for the x, y, and z axes).
Driving the vehicle
A vehicle is driven by giving each of its wheels an engine force, a brake force, and a steering angle. The controller is then updated before each timestep, which is when the ray-casts are made and when the resulting suspension and friction forces are applied to the chassis. It is strongly recommended to exclude the chassis from the ray-casts, otherwise the ray might hit it and be misinterpreted as being the floor.
for _ in 0..200 {
// The vehicle is driven by setting the engine force, the brake, and the steering angle of
// its wheels. Here the two front wheels are the driving and steering ones.
let wheels = vehicle.wheels_mut();
wheels[0].engine_force = 30.0;
wheels[0].steering = 0.2;
wheels[1].engine_force = 30.0;
wheels[1].steering = 0.2;
// The wheels are ray-casted against the scene: the chassis itself, as well as every other
// dynamic body, is generally excluded from these ray-casts.
let queries = world.broad_phase.as_query_pipeline_mut(
world.narrow_phase.query_dispatcher(),
&mut world.bodies,
&mut world.colliders,
QueryFilter::exclude_dynamic().exclude_rigid_body(chassis_handle),
);
vehicle.update_vehicle(world.integration_parameters.dt, queries);
world.step();
}
println!("Vehicle speed: {}", vehicle.current_vehicle_speed);
for (let k = 0; k < 200; ++k) {
// The vehicle is driven by setting the engine force, the brake, and the steering angle of
// its wheels. Here the two front wheels are the driving and steering ones.
vehicle.setWheelEngineForce(0, 30.0);
vehicle.setWheelSteering(0, 0.2);
vehicle.setWheelEngineForce(1, 30.0);
vehicle.setWheelSteering(1, 0.2);
// The wheels are ray-casted against the scene: the dynamic bodies, including the chassis
// itself, are generally excluded from these ray-casts.
vehicle.updateVehicle(world.integrationParameters.dt, RAPIER.QueryFilterFlags.EXCLUDE_DYNAMIC);
world.step();
}
console.log("Vehicle speed:", vehicle.currentVehicleSpeed());
The controller is updated automatically by the plugin before each simulation step, and the
colliders attached to the chassis are always excluded from the ray-casts. Other colliders can be excluded with the
filter_flags, filter_groups, exclude_colliders, exclude_rigid_bodies, and filter_predicate fields of
the RayCastVehicleController (similar to the ones of the
character controller), or by inserting the ControllerIgnored component on them
(or on their rigid-body). So driving the vehicle is just a matter of modifying
the engine force, brake, and steering of its wheels:
fn drive_vehicle(mut vehicles: Query<&mut RayCastVehicleController>) {
for mut vehicle in vehicles.iter_mut() {
// The vehicle is driven by setting the engine force, the brake, and the steering angle of
// its wheels. Here the two front wheels are the driving and steering ones.
for wheel in &mut vehicle.wheels[0..2] {
wheel.engine_force = 30.0;
wheel.steering = 0.2;
}
// The colliders of the chassis are always ignored by the ray-casts of the wheels. Other
// colliders, here the ones attached to dynamic rigid-bodies, can be ignored too.
vehicle.filter_flags =
QueryFilterFlags::EXCLUDE_SENSORS | QueryFilterFlags::EXCLUDE_DYNAMIC;
// The results of the last update are written back into the component.
println!("Vehicle speed: {}", vehicle.current_vehicle_speed);
for wheel in &vehicle.wheels {
println!(
"Wheel in contact: {}, suspension length: {}",
wheel.state.is_in_contact, wheel.state.suspension_length
);
}
}
}
The forces applied by the vehicle controller are computed once per update of the plugin, using the length of the last simulation timestep. So it is best used with a fixed timestep.
The engine force, brake, and steering angle of a wheel are set with r3DynamicRayCastVehicleController_SetWheelControls
(from the index of the wheel), and the controller is updated with r3DynamicRayCastVehicleController_UpdateVehicle
before each r3Step. The colliders attached to the chassis are always excluded from the ray-casts, and the other
obstacles can be filtered with the R3QueryOptions given to the update (see the
scene query filters). Note that its predicate callback, if any, is called once
for every collider of the world before the update, while the world is locked for writing: it may only use the Read
functions of its R3ReadContext.
// The wheels are ray-casted against the scene: the chassis itself is always excluded, and
// every other dynamic body is generally excluded from these ray-casts too.
R3QueryOptions options = r3DefaultQueryOptions();
options.filter.flags = R3_QUERY_EXCLUDE_DYNAMIC;
for (int i = 0; i < 200; i++) {
// The vehicle is driven by setting the steering angle, the engine force, and the brake of
// its wheels. Here the two front wheels (indices 0 and 1) are the driving and steering ones.
r3DynamicRayCastVehicleController_SetWheelControls(vehicle, 0, 0.2, 30.0, 0.0);
r3DynamicRayCastVehicleController_SetWheelControls(vehicle, 1, 0.2, 30.0, 0.0);
r3DynamicRayCastVehicleController_UpdateVehicle(vehicle, r3TimeStep(world), &options);
r3Step(world, NULL, NULL);
}
printf("Vehicle speed: %f\n", (double)r3DynamicRayCastVehicleController_CurrentVehicleSpeed(vehicle));
The engine force, brake, and steering angle of a wheel are set with the apply_engine_force, set_brake, and
set_steering methods of the controller (from the index of the wheel), and the controller is updated with its
update_vehicle method before each PhysicsWorld.step. The colliders attached to the chassis are always excluded
from the ray-casts, and the other obstacles can be filtered with the optional QueryFilter given to update_vehicle
(see the scene query filters). The predicate of that filter, if any, is called
once for every collider before the update, while the sets are locked: it must not modify them.
for _ in range(200):
# The vehicle is driven by setting the engine force, the brake, and the steering angle of
# its wheels (from their indices). Here the two front wheels are the driving and steering ones.
vehicle.apply_engine_force(0, 30.0)
vehicle.set_steering(0, 0.2)
vehicle.apply_engine_force(1, 30.0)
vehicle.set_steering(1, 0.2)
# The wheels are ray-casted against the scene: the chassis itself is always excluded from
# these ray-casts, and every other dynamic body is generally excluded too.
vehicle.update_vehicle(
world.integration_parameters.dt,
world.rigid_bodies,
world.colliders,
world.query_pipeline,
rp.QueryFilter.exclude_dynamic(),
)
world.step()
print("Vehicle speed:", vehicle.current_vehicle_speed)
The state of each wheel after an update (whether it touches the floor, the compression of its suspension, its rotation angle, etc.) is readable from the controller. This is what the rendering of the wheels can be based on since there is no actual per-wheel rigid-bodies to read their state from.
The state of each wheel is written back by the plugin into its VehicleWheel::state field (a
VehicleWheelState), and the speed of the vehicle into RayCastVehicleController::current_vehicle_speed. The
VehicleWheel::local_transform method computes the transform of a wheel relative to the chassis (including its
suspension length, steering, and rotation), which can be given directly to the child entity rendering that wheel.
The state of the wheels is copied into a buffer of R3WheelState by r3DynamicRayCastVehicleController_Wheels: the
world-space center of each wheel, the world-space directions of its suspension and axle, its rotation angle, the length
and force of its suspension, and its contact with the ground (whether it touches it, the collider touched, and the
world-space contact point and normal). The speed of the vehicle along its forward axis is given by
r3DynamicRayCastVehicleController_CurrentVehicleSpeed.
// The wheels are given in the order they were added to the controller.
R3WheelState wheels[4];
size_t num_wheels = r3DynamicRayCastVehicleController_Wheels(vehicle, wheels, 4);
for (size_t i = 0; i < num_wheels; i++) {
// The world-space center of the wheel, its current suspension length, rotation angle, etc.
printf("Wheel %zu: center (%f, %f, %f), suspension length %f, rotation %f, in contact: %u\n", i,
(double)wheels[i].center.x, (double)wheels[i].center.y, (double)wheels[i].center.z,
(double)wheels[i].suspension_length, (double)wheels[i].rotation,
(unsigned)wheels[i].is_in_contact);
}
The state of the wheels is given by the wheels method of the controller (or by its wheel method for a single
wheel) as a list of Wheel objects. These are copies: modifying them doesn’t affect the vehicle. Each wheel gives its
world-space center, the world-space directions of its suspension and axle, its rotation angle, the force of its
suspension (wheel_suspension_force), and its raycast_info (a RayCastInfo): the length of its suspension
(suspension_length) and its contact with the ground (is_in_contact, the collider touched ground_object, and the
world-space contact point contact_point_ws and normal contact_normal_ws). The speed of the vehicle along its
forward axis is given by the current_vehicle_speed property of the controller (in meters per second, or with the
current_speed_km_hour method in kilometers per hour).
# The wheels are copies of the state of the wheels after the last update.
for wheel in vehicle.wheels():
contact = wheel.raycast_info
print(
"Wheel center:", wheel.center, # World-space center of the wheel.
"axle:", wheel.axle, # World-space direction of its axle.
"rotation:", wheel.rotation, # Rotation angle around its axle.
"suspension length:", contact.suspension_length,
"touches the ground:", contact.is_in_contact,
"ground collider:", contact.ground_object, # None if it doesn’t touch the ground.
)
Vehicle controller
Simulating a car with rigid-bodies and joints, for example using one rigid-body per wheel attached to the chassis by a joint, is
possible but can be difficult to control for games requiring non-realistic vehicles. This is why Rapier provides a
vehicle controller (the current implementation was ported from the btRaycastVehicle of Bullet). The vehicle is a
single rigid-body modeling its chassis, and its wheels are only represented by ray-casts pushing that body along a
spring-like suspension.
The vehicle controller is only available in 3D, i.e., with bevy_rapier3d.
The vehicle controller is only available in 3D, i.e., with the r3 functions.
Setup
The chassis is created like any other dynamic rigid-body, and the wheels are added to the controller afterwards. Each
wheel is given its position on the chassis, the direction of its suspension (the direction of its ray-cast), its axle,
the rest length of its suspension, and its radius. The WheelTuningR3WheelTuning
// The chassis is an ordinary dynamic rigid-body.
let hw = 0.3;
let hh = 0.15;
let (chassis_handle, _) = world.insert(
RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 1.0, 0.0)),
ColliderBuilder::cuboid(hw * 2.0, hh, hw).density(100.0),
);
// The tuning shared by the wheels: the suspension and the grip.
let tuning = WheelTuning {
suspension_stiffness: 100.0,
suspension_damping: 10.0,
..WheelTuning::default()
};
let mut vehicle = DynamicRayCastVehicleController::new(chassis_handle);
let wheel_positions = [
Vector::new(hw * 1.5, -hh, hw),
Vector::new(hw * 1.5, -hh, -hw),
Vector::new(-hw * 1.5, -hh, hw),
Vector::new(-hw * 1.5, -hh, -hw),
];
for position in wheel_positions {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
vehicle.add_wheel(position, -Vector::Y, Vector::Z, hh, hh / 4.0, &tuning);
}
// The chassis is an ordinary dynamic rigid-body.
let hw = 0.3;
let hh = 0.15;
let chassis = world.createRigidBody(
RAPIER.RigidBodyDesc.dynamic().setTranslation(0.0, 1.0, 0.0),
);
world.createCollider(RAPIER.ColliderDesc.cuboid(hw * 2.0, hh, hw).setDensity(100.0), chassis);
let vehicle = world.createVehicleController(chassis);
let wheelPositions = [
{ x: hw * 1.5, y: -hh, z: hw }, { x: hw * 1.5, y: -hh, z: -hw },
{ x: -hw * 1.5, y: -hh, z: hw }, { x: -hw * 1.5, y: -hh, z: -hw },
];
for (let position of wheelPositions) {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
vehicle.addWheel(position, { x: 0.0, y: -1.0, z: 0.0 }, { x: 0.0, y: 0.0, z: 1.0 }, hh, hh / 4.0);
}
// The tuning of each wheel: its suspension and its grip.
for (let i = 0; i < vehicle.numWheels(); ++i) {
vehicle.setWheelSuspensionStiffness(i, 100.0);
vehicle.setWheelSuspensionCompression(i, 10.0);
vehicle.setWheelSuspensionRelaxation(i, 10.0);
}
The vehicle controller is the RayCastVehicleController component, added to the entity of the
chassis' dynamic rigid-body. Its wheels are VehicleWheel values stored in its wheels field, and can be added,
removed, or modified at any time:
// The tuning shared by the wheels: the suspension and the grip.
let tuning = WheelTuning {
suspension_stiffness: 100.0,
suspension_damping: 10.0,
..WheelTuning::default()
};
let hw = 0.3;
let hh = 0.15;
let wheel_positions = [
Vec3::new(hw * 1.5, -hh, hw),
Vec3::new(hw * 1.5, -hh, -hw),
Vec3::new(-hw * 1.5, -hh, hw),
Vec3::new(-hw * 1.5, -hh, -hw),
];
let wheels = wheel_positions
.into_iter()
.map(|position| {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
VehicleWheel::new(position, -Vec3::Y, Vec3::Z, hh, hh / 4.0, tuning)
})
.collect();
// The chassis is an ordinary dynamic rigid-body, with the vehicle controller attached to it.
commands.spawn((
Transform::from_xyz(0.0, 1.0, 0.0),
RigidBody::Dynamic,
Collider::cuboid(hw * 2.0, hh, hw),
ColliderMassProperties::Density(100.0),
RayCastVehicleController::new(wheels),
));
The vehicle controller is the R3DynamicRayCastVehicleController object, created by
r3NewDynamicRayCastVehicleController from the handle of the chassis' dynamic rigid-body. It remembers the world of
that rigid-body, so it must be freed with r3FreeDynamicRayCastVehicleController before that world is freed. The
wheels are added with r3DynamicRayCastVehicleController_AddWheel, which returns the index of the new wheel (starting
from zero, in insertion order), and the R3WheelTuning given to each wheel is initialized with r3DefaultWheelTuning:
// The chassis is an ordinary dynamic rigid-body.
const R3Real hw = 0.3;
const R3Real hh = 0.15;
R3RigidBodyDesc chassis_body = r3DynamicRigidBodyDesc();
chassis_body.position.translation = r3Vector(0.0, 1.0, 0.0);
R3RigidBodyHandle chassis_handle = r3InsertRigidBody(world, &chassis_body);
R3ColliderDesc chassis_collider = r3CuboidColliderDesc(r3Vector(hw * 2.0, hh, hw));
chassis_collider.density = 100.0;
r3InsertCollider(chassis_handle, &chassis_collider);
// The tuning shared by the wheels: the suspension and the grip.
R3WheelTuning tuning = r3DefaultWheelTuning();
tuning.suspension_stiffness = 100.0;
tuning.suspension_damping = 10.0;
// The controller must be freed (with r3FreeDynamicRayCastVehicleController) before its world.
R3DynamicRayCastVehicleController *vehicle = r3NewDynamicRayCastVehicleController(chassis_handle);
const R3Vector wheel_positions[4] = {
{hw * 1.5, -hh, hw},
{hw * 1.5, -hh, -hw},
{-hw * 1.5, -hh, hw},
{-hw * 1.5, -hh, -hw},
};
for (size_t i = 0; i < 4; i++) {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
r3DynamicRayCastVehicleController_AddWheel(vehicle, wheel_positions[i], r3Vector(0.0, -1.0, 0.0),
r3Vector(0.0, 0.0, 1.0), hh, hh / 4.0, &tuning);
}
By default, the vehicle moves forward along the local x axis of the chassis, and its local y axis points
upward. Other axes can be selected with r3DynamicRayCastVehicleController_SetAxes.
The vehicle controller is the DynamicRayCastVehicleController class, created from the handle of the chassis'
dynamic rigid-body. The wheels are added with its add_wheel method, which returns the index of the new wheel
(starting from zero, in insertion order). The WheelTuning given to each wheel is created with keyword arguments
overriding its default values (as given by WheelTuning.default()):
# The chassis is an ordinary dynamic rigid-body.
hw = 0.3
hh = 0.15
chassis_handle = world.add_body(
rp.RigidBody.dynamic(translation=(0.0, 1.0, 0.0)),
colliders=[rp.Collider.cuboid(hw * 2.0, hh, hw).density(100.0)],
)
# The tuning shared by the wheels: the suspension and the grip. The
# parameters that aren't given keep their default values.
tuning = rp.WheelTuning(suspension_stiffness=100.0, suspension_damping=10.0)
vehicle = rp.DynamicRayCastVehicleController(chassis_handle)
wheel_positions = [
(hw * 1.5, -hh, hw),
(hw * 1.5, -hh, -hw),
(-hw * 1.5, -hh, hw),
(-hw * 1.5, -hh, -hw),
]
for position in wheel_positions:
# The position of the wheel, the direction its suspension pushes along, its axle, the
# rest length of its suspension, and its radius; all in the local frame of the chassis.
vehicle.add_wheel(position, (0.0, -1.0, 0.0), (0.0, 0.0, 1.0), hh, hh / 4.0, tuning)
By default, the vehicle moves forward along the local x axis of the chassis, and its local y axis points
upward. Other axes can be selected with the index_forward_axis and index_up_axis properties of the controller
(0, 1, and 2 standing for the x, y, and z axes).
Driving the vehicle
A vehicle is driven by giving each of its wheels an engine force, a brake force, and a steering angle. The controller is then updated before each timestep, which is when the ray-casts are made and when the resulting suspension and friction forces are applied to the chassis. It is strongly recommended to exclude the chassis from the ray-casts, otherwise the ray might hit it and be misinterpreted as being the floor.
for _ in 0..200 {
// The vehicle is driven by setting the engine force, the brake, and the steering angle of
// its wheels. Here the two front wheels are the driving and steering ones.
let wheels = vehicle.wheels_mut();
wheels[0].engine_force = 30.0;
wheels[0].steering = 0.2;
wheels[1].engine_force = 30.0;
wheels[1].steering = 0.2;
// The wheels are ray-casted against the scene: the chassis itself, as well as every other
// dynamic body, is generally excluded from these ray-casts.
let queries = world.broad_phase.as_query_pipeline_mut(
world.narrow_phase.query_dispatcher(),
&mut world.bodies,
&mut world.colliders,
QueryFilter::exclude_dynamic().exclude_rigid_body(chassis_handle),
);
vehicle.update_vehicle(world.integration_parameters.dt, queries);
world.step();
}
println!("Vehicle speed: {}", vehicle.current_vehicle_speed);
for (let k = 0; k < 200; ++k) {
// The vehicle is driven by setting the engine force, the brake, and the steering angle of
// its wheels. Here the two front wheels are the driving and steering ones.
vehicle.setWheelEngineForce(0, 30.0);
vehicle.setWheelSteering(0, 0.2);
vehicle.setWheelEngineForce(1, 30.0);
vehicle.setWheelSteering(1, 0.2);
// The wheels are ray-casted against the scene: the dynamic bodies, including the chassis
// itself, are generally excluded from these ray-casts.
vehicle.updateVehicle(world.integrationParameters.dt, RAPIER.QueryFilterFlags.EXCLUDE_DYNAMIC);
world.step();
}
console.log("Vehicle speed:", vehicle.currentVehicleSpeed());
The controller is updated automatically by the plugin before each simulation step, and the
colliders attached to the chassis are always excluded from the ray-casts. Other colliders can be excluded with the
filter_flags, filter_groups, exclude_colliders, exclude_rigid_bodies, and filter_predicate fields of
the RayCastVehicleController (similar to the ones of the
character controller), or by inserting the ControllerIgnored component on them
(or on their rigid-body). So driving the vehicle is just a matter of modifying
the engine force, brake, and steering of its wheels:
fn drive_vehicle(mut vehicles: Query<&mut RayCastVehicleController>) {
for mut vehicle in vehicles.iter_mut() {
// The vehicle is driven by setting the engine force, the brake, and the steering angle of
// its wheels. Here the two front wheels are the driving and steering ones.
for wheel in &mut vehicle.wheels[0..2] {
wheel.engine_force = 30.0;
wheel.steering = 0.2;
}
// The colliders of the chassis are always ignored by the ray-casts of the wheels. Other
// colliders, here the ones attached to dynamic rigid-bodies, can be ignored too.
vehicle.filter_flags =
QueryFilterFlags::EXCLUDE_SENSORS | QueryFilterFlags::EXCLUDE_DYNAMIC;
// The results of the last update are written back into the component.
println!("Vehicle speed: {}", vehicle.current_vehicle_speed);
for wheel in &vehicle.wheels {
println!(
"Wheel in contact: {}, suspension length: {}",
wheel.state.is_in_contact, wheel.state.suspension_length
);
}
}
}
The forces applied by the vehicle controller are computed once per update of the plugin, using the length of the last simulation timestep. So it is best used with a fixed timestep.
The engine force, brake, and steering angle of a wheel are set with r3DynamicRayCastVehicleController_SetWheelControls
(from the index of the wheel), and the controller is updated with r3DynamicRayCastVehicleController_UpdateVehicle
before each r3Step. The colliders attached to the chassis are always excluded from the ray-casts, and the other
obstacles can be filtered with the R3QueryOptions given to the update (see the
scene query filters). Note that its predicate callback, if any, is called once
for every collider of the world before the update, while the world is locked for writing: it may only use the Read
functions of its R3ReadContext.
// The wheels are ray-casted against the scene: the chassis itself is always excluded, and
// every other dynamic body is generally excluded from these ray-casts too.
R3QueryOptions options = r3DefaultQueryOptions();
options.filter.flags = R3_QUERY_EXCLUDE_DYNAMIC;
for (int i = 0; i < 200; i++) {
// The vehicle is driven by setting the steering angle, the engine force, and the brake of
// its wheels. Here the two front wheels (indices 0 and 1) are the driving and steering ones.
r3DynamicRayCastVehicleController_SetWheelControls(vehicle, 0, 0.2, 30.0, 0.0);
r3DynamicRayCastVehicleController_SetWheelControls(vehicle, 1, 0.2, 30.0, 0.0);
r3DynamicRayCastVehicleController_UpdateVehicle(vehicle, r3TimeStep(world), &options);
r3Step(world, NULL, NULL);
}
printf("Vehicle speed: %f\n", (double)r3DynamicRayCastVehicleController_CurrentVehicleSpeed(vehicle));
The engine force, brake, and steering angle of a wheel are set with the apply_engine_force, set_brake, and
set_steering methods of the controller (from the index of the wheel), and the controller is updated with its
update_vehicle method before each PhysicsWorld.step. The colliders attached to the chassis are always excluded
from the ray-casts, and the other obstacles can be filtered with the optional QueryFilter given to update_vehicle
(see the scene query filters). The predicate of that filter, if any, is called
once for every collider before the update, while the sets are locked: it must not modify them.
for _ in range(200):
# The vehicle is driven by setting the engine force, the brake, and the steering angle of
# its wheels (from their indices). Here the two front wheels are the driving and steering ones.
vehicle.apply_engine_force(0, 30.0)
vehicle.set_steering(0, 0.2)
vehicle.apply_engine_force(1, 30.0)
vehicle.set_steering(1, 0.2)
# The wheels are ray-casted against the scene: the chassis itself is always excluded from
# these ray-casts, and every other dynamic body is generally excluded too.
vehicle.update_vehicle(
world.integration_parameters.dt,
world.rigid_bodies,
world.colliders,
world.query_pipeline,
rp.QueryFilter.exclude_dynamic(),
)
world.step()
print("Vehicle speed:", vehicle.current_vehicle_speed)
The state of each wheel after an update (whether it touches the floor, the compression of its suspension, its rotation angle, etc.) is readable from the controller. This is what the rendering of the wheels can be based on since there is no actual per-wheel rigid-bodies to read their state from.
The state of each wheel is written back by the plugin into its VehicleWheel::state field (a
VehicleWheelState), and the speed of the vehicle into RayCastVehicleController::current_vehicle_speed. The
VehicleWheel::local_transform method computes the transform of a wheel relative to the chassis (including its
suspension length, steering, and rotation), which can be given directly to the child entity rendering that wheel.
The state of the wheels is copied into a buffer of R3WheelState by r3DynamicRayCastVehicleController_Wheels: the
world-space center of each wheel, the world-space directions of its suspension and axle, its rotation angle, the length
and force of its suspension, and its contact with the ground (whether it touches it, the collider touched, and the
world-space contact point and normal). The speed of the vehicle along its forward axis is given by
r3DynamicRayCastVehicleController_CurrentVehicleSpeed.
// The wheels are given in the order they were added to the controller.
R3WheelState wheels[4];
size_t num_wheels = r3DynamicRayCastVehicleController_Wheels(vehicle, wheels, 4);
for (size_t i = 0; i < num_wheels; i++) {
// The world-space center of the wheel, its current suspension length, rotation angle, etc.
printf("Wheel %zu: center (%f, %f, %f), suspension length %f, rotation %f, in contact: %u\n", i,
(double)wheels[i].center.x, (double)wheels[i].center.y, (double)wheels[i].center.z,
(double)wheels[i].suspension_length, (double)wheels[i].rotation,
(unsigned)wheels[i].is_in_contact);
}
The state of the wheels is given by the wheels method of the controller (or by its wheel method for a single
wheel) as a list of Wheel objects. These are copies: modifying them doesn’t affect the vehicle. Each wheel gives its
world-space center, the world-space directions of its suspension and axle, its rotation angle, the force of its
suspension (wheel_suspension_force), and its raycast_info (a RayCastInfo): the length of its suspension
(suspension_length) and its contact with the ground (is_in_contact, the collider touched ground_object, and the
world-space contact point contact_point_ws and normal contact_normal_ws). The speed of the vehicle along its
forward axis is given by the current_vehicle_speed property of the controller (in meters per second, or with the
current_speed_km_hour method in kilometers per hour).
# The wheels are copies of the state of the wheels after the last update.
for wheel in vehicle.wheels():
contact = wheel.raycast_info
print(
"Wheel center:", wheel.center, # World-space center of the wheel.
"axle:", wheel.axle, # World-space direction of its axle.
"rotation:", wheel.rotation, # Rotation angle around its axle.
"suspension length:", contact.suspension_length,
"touches the ground:", contact.is_in_contact,
"ground collider:", contact.ground_object, # None if it doesn’t touch the ground.
)
PID controller
It is generally not recommended to move a rigid-body by setting its pose directly: teleporting it would ignore every obstacle on the way. The recommended alternative is generally to push it with a force (or an impulse) that is strong enough to reach the target. However, pushing with a single constant force/impulse will generally overshoot the target. Thus, ideally, the force or impulse should be carefully selected and updated each frame as the rigid-body gets closer to its target.
This is what a PID controller (Proportional-Integral-Derivative) is designed to calculate: given the target pose, it computes the ideal velocity change bringing the body closer to it. This is the building block of the velocity-based character controllers, but it is useful for anything that must follow a target without being teleported: a dynamic moving platform, an object held by the player, a following camera, etc.
The gains of the controller are what makes it reach its target quickly or smoothly. The proportional gain is applied to the position errors and is usually set to a multiple of the inverse of the timestep length (e.g. for a timestep of seconds). The derivative gain is applied to the velocity errors and is usually set in , where means no damping and means that the velocity errors are corrected within a single timestep.
The PID controller is the PidController component, added to the entity of a (non-fixed)
rigid-body. Its target field is a PidTarget holding the world-space pose (and optionally the velocities) the
rigid-body must be driven toward. Before each simulation step, the plugin computes the velocity correction bringing
the rigid-body closer to its target, and adds it to its velocity. So moving the target is just a matter of modifying
the PidController::target field.
The PID controller is the R3PidController object, created by r3NewPidController and freed by
r3FreePidController. It is created with a proportional gain of , an integral gain of , and a derivative gain
of , on every coordinate axis (all of them being controlled). Its gains are set per coordinate axis with
r3PidController_SetGains, from an R3PidGains structure (its current gains being given by r3PidController_Gains).
At each frame, r3PidController_RigidBodyCorrection computes the velocity correction bringing a rigid-body closer to
its target pose (and target velocities): it is up to you to add it to the velocities of that rigid-body before the
next r3Step.
The PID controller is the PidController class. It is created with a proportional gain of , an integral gain of
, and a derivative gain of , on every coordinate axis (all of them being controlled), unless other gains are
given to the Kp, Ki, and Kd arguments of its constructor (either a single float for every linear and angular
coordinate axis, or one gain per coordinate axis). They can be read and modified afterwards, per coordinate axis, with
its lin_kp, ang_kp, lin_ki, ang_ki, lin_kd, and ang_kd properties (each one is a Vec3, and can be set
from a vector or from a single float for every axis). At each frame, PidController.rigid_body_correction computes
the velocity correction (a PidCorrection, with its linear and angular parts) bringing a rigid-body closer to its
target pose (and to its target velocities, given as an optional RigidBodyVelocity to its target_vels argument,
zero by default): it is up to you to add it to the linvel and angvel of that rigid-body before the next
PhysicsWorld.step.
The coordinate axes (linear and/or angular) controlled by the controller can be selected in order, for example, to
only control the translations of a body while leaving its rotations to the simulationaxes field, an
AxesMask)r3PidController_SetAxes, from a combination of the R3_AXES_MASK_* bits, the current ones being given by r3PidController_Axes)axes argument of its constructor or its axes property, a combination of the AxesMask flags)
- Example 2D
- Example 3D
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let axes = AxesMask::LIN_X | AxesMask::LIN_Y;
let mut pid = PidController::new(60.0, 0.0, 0.8, axes);
let target = Vector::new(3.0, 2.0);
for _ in 0..200 {
let dt = world.integration_parameters.dt;
let body = &mut world.bodies[body_handle];
// The correction is the velocity change bringing the body closer to its target pose.
let correction = pid.rigid_body_correction(
dt,
body,
Pose::from_translation(target),
RigidBodyVelocity::zero(),
);
let new_velocities = *body.vels() + correction;
body.set_vels(new_velocities, true);
world.step();
}
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let axes = AxesMask::LIN_X | AxesMask::LIN_Y | AxesMask::LIN_Z;
let mut pid = PidController::new(60.0, 0.0, 0.8, axes);
let target = Vector::new(3.0, 2.0, 0.0);
for _ in 0..200 {
let dt = world.integration_parameters.dt;
let body = &mut world.bodies[body_handle];
// The correction is the velocity change bringing the body closer to its target pose.
let correction = pid.rigid_body_correction(
dt,
body,
Pose::from_translation(target),
RigidBodyVelocity::zero(),
);
let new_velocities = *body.vels() + correction;
body.set_vels(new_velocities, true);
world.step();
}
- Example 2D
- Example 3D
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = world.createPidController(60.0, 0.0, 0.8, RAPIER.PidAxesMask.AllLin);
let target = { x: 3.0, y: 2.0 };
for (let k = 0; k < 200; ++k) {
// The correction is applied to the velocity of the rigid-body.
pid.applyLinearCorrection(body, target, { x: 0.0, y: 0.0 });
world.step();
}
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = world.createPidController(60.0, 0.0, 0.8, RAPIER.PidAxesMask.AllLin);
let target = { x: 3.0, y: 2.0, z: 0.0 };
for (let k = 0; k < 200; ++k) {
// The correction is applied to the velocity of the rigid-body.
pid.applyLinearCorrection(body, target, { x: 0.0, y: 0.0, z: 0.0 });
world.step();
}
- Example 2D
- Example 3D
fn setup_physics(mut commands: Commands) {
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = PidController::new(60.0, 0.0, 0.8, AxesMask::LIN_AXES)
.with_target(PidTarget::from_translation(Vec2::new(300.0, 200.0)));
commands.spawn((
Transform::from_xyz(0.0, 100.0, 0.0),
RigidBody::Dynamic,
Collider::ball(50.0),
pid,
));
}
/* Move the target of the controller inside of a system. */
fn update_target(time: Res<Time>, mut controllers: Query<&mut PidController>) {
let t = time.elapsed_secs();
for mut controller in controllers.iter_mut() {
// The plugin drives the rigid-body toward this pose before each simulation step.
controller.target = PidTarget::from_translation(Vec2::new(300.0 * t.cos(), 200.0));
}
}
fn setup_physics(mut commands: Commands) {
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = PidController::new(60.0, 0.0, 0.8, AxesMask::LIN_AXES)
.with_target(PidTarget::from_translation(Vec3::new(3.0, 2.0, 0.0)));
commands.spawn((
Transform::from_xyz(0.0, 1.0, 0.0),
RigidBody::Dynamic,
Collider::ball(0.5),
pid,
));
}
/* Move the target of the controller inside of a system. */
fn update_target(time: Res<Time>, mut controllers: Query<&mut PidController>) {
let t = time.elapsed_secs();
for mut controller in controllers.iter_mut() {
// The plugin drives the rigid-body toward this pose before each simulation step.
controller.target = PidTarget::from_translation(Vec3::new(3.0 * t.cos(), 2.0, 0.0));
}
}
- Example 2D
- Example 3D
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
R2PidController *pid = r2NewPidController();
R2PidGains gains = r2PidController_Gains(pid);
gains.lin_kp = r2Vector(60.0, 60.0);
gains.lin_ki = r2Vector(0.0, 0.0);
gains.lin_kd = r2Vector(0.8, 0.8);
r2PidController_SetGains(pid, gains);
r2PidController_SetAxes(pid, R2_AXES_MASK_LIN_X | R2_AXES_MASK_LIN_Y);
R2Vector target = r2Vector(3.0, 2.0);
for (int i = 0; i < 200; i++) {
R2Real dt = r2TimeStep(world);
// The correction is the velocity change bringing the body closer to its target pose.
R2VelocityCorrection correction = r2PidController_RigidBodyCorrection(
pid, dt, body_handle,
r2TranslationPose(target), // The target pose.
r2Vector(0.0, 0.0), // The target linear velocity.
0.0); // The target angular velocity.
R2Vector linvel = r2VectorAdd(r2RigidBody_Linvel(body_handle), correction.linear);
R2AngVector angvel = r2RigidBody_Angvel(body_handle) + correction.angularVelocity;
r2RigidBody_SetLinvel(body_handle, linvel, 1);
r2RigidBody_SetAngvel(body_handle, angvel, 1);
r2Step(world, NULL, NULL);
}
r2FreePidController(pid);
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
R3PidController *pid = r3NewPidController();
R3PidGains gains = r3PidController_Gains(pid);
gains.lin_kp = r3Vector(60.0, 60.0, 60.0);
gains.lin_ki = r3Vector(0.0, 0.0, 0.0);
gains.lin_kd = r3Vector(0.8, 0.8, 0.8);
r3PidController_SetGains(pid, gains);
r3PidController_SetAxes(pid, R3_AXES_MASK_LIN_X | R3_AXES_MASK_LIN_Y | R3_AXES_MASK_LIN_Z);
R3Vector target = r3Vector(3.0, 2.0, 0.0);
for (int i = 0; i < 200; i++) {
R3Real dt = r3TimeStep(world);
// The correction is the velocity change bringing the body closer to its target pose.
R3VelocityCorrection correction = r3PidController_RigidBodyCorrection(
pid, dt, body_handle,
r3TranslationPose(target), // The target pose.
r3Vector(0.0, 0.0, 0.0), // The target linear velocity.
r3Vector(0.0, 0.0, 0.0)); // The target angular velocity.
R3Vector linvel = r3VectorAdd(r3RigidBody_Linvel(body_handle), correction.linear);
R3AngVector angvel = r3VectorAdd(r3RigidBody_Angvel(body_handle), correction.angularVelocity);
r3RigidBody_SetLinvel(body_handle, linvel, 1);
r3RigidBody_SetAngvel(body_handle, angvel, 1);
r3Step(world, NULL, NULL);
}
r3FreePidController(pid);
# The proportional, integral, and derivative gains of the controller, acting on the linear
# axes only: the body is pushed toward its target without its rotation being controlled.
axes = rp.AxesMask.LIN_X | rp.AxesMask.LIN_Y | rp.AxesMask.LIN_Z
pid = rp.PidController(axes=axes, Kp=60.0, Ki=0.0, Kd=0.8)
target = rp.Isometry3.from_translation(3.0, 2.0, 0.0)
for _ in range(200):
dt = world.integration_parameters.dt
body = world.rigid_bodies[body_handle]
# The correction is the velocity change bringing the body closer to its target pose
# (and to its target velocities, zero here).
correction = pid.rigid_body_correction(dt, body, target, target_vels=rp.RigidBodyVelocity())
body.linvel = body.linvel + correction.linear
body.angvel = body.angvel + correction.angular
world.step()
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). This is also what makes its API
mutable, and what has to be reset with PidController::reset_integrals whenever the controller is given a target it
never had a chance to reach. The PdController is the variant without that integral part: its API is immutable, and
its behavior is generally good enough for games.
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). These accumulated errors are
stored in the PidController::lin_integral and PidController::ang_integral fields updated by the plugin, and have
to be reset with PidController::reset_integrals whenever the controller is given a target it never had a chance to
reach. The PdController component is the variant without that integral part, and its behavior is generally good
enough for games. Use either one or the other on a given entity, but not both.
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). This is also what makes
r3PidController_RigidBodyCorrection modify the controller, and what has to be reset with
r3PidController_ResetIntegrals whenever the controller is given a target it never had a chance to reach. The
R3PdController is the variant without that integral part: it is a plain structure (initialized by
r3DefaultPdController) that r3PdController_RigidBodyCorrection doesn't modify, and its behavior is generally good
enough for games.
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). These accumulated errors are
readable with the PidController.lin_integral and PidController.ang_integral properties. This is also what makes
PidController.rigid_body_correction modify the controller, and what has to be reset with PidController.reset
whenever the controller is given a target it never had a chance to reach. The PdController is the variant without
that integral part (its constructor takes no Ki argument): its rigid_body_correction method doesn’t modify it (and
doesn’t need the timestep length), and its behavior is generally good enough for games.
PID controller
It is generally not recommended to move a rigid-body by setting its pose directly: teleporting it would ignore every obstacle on the way. The recommended alternative is generally to push it with a force (or an impulse) that is strong enough to reach the target. However, pushing with a single constant force/impulse will generally overshoot the target. Thus, ideally, the force or impulse should be carefully selected and updated each frame as the rigid-body gets closer to its target.
This is what a PID controller (Proportional-Integral-Derivative) is designed to calculate: given the target pose, it computes the ideal velocity change bringing the body closer to it. This is the building block of the velocity-based character controllers, but it is useful for anything that must follow a target without being teleported: a dynamic moving platform, an object held by the player, a following camera, etc.
The gains of the controller are what makes it reach its target quickly or smoothly. The proportional gain is applied to the position errors and is usually set to a multiple of the inverse of the timestep length (e.g. for a timestep of seconds). The derivative gain is applied to the velocity errors and is usually set in , where means no damping and means that the velocity errors are corrected within a single timestep.
The PID controller is the PidController component, added to the entity of a (non-fixed)
rigid-body. Its target field is a PidTarget holding the world-space pose (and optionally the velocities) the
rigid-body must be driven toward. Before each simulation step, the plugin computes the velocity correction bringing
the rigid-body closer to its target, and adds it to its velocity. So moving the target is just a matter of modifying
the PidController::target field.
The PID controller is the R3PidController object, created by r3NewPidController and freed by
r3FreePidController. It is created with a proportional gain of , an integral gain of , and a derivative gain
of , on every coordinate axis (all of them being controlled). Its gains are set per coordinate axis with
r3PidController_SetGains, from an R3PidGains structure (its current gains being given by r3PidController_Gains).
At each frame, r3PidController_RigidBodyCorrection computes the velocity correction bringing a rigid-body closer to
its target pose (and target velocities): it is up to you to add it to the velocities of that rigid-body before the
next r3Step.
The PID controller is the PidController class. It is created with a proportional gain of , an integral gain of
, and a derivative gain of , on every coordinate axis (all of them being controlled), unless other gains are
given to the Kp, Ki, and Kd arguments of its constructor (either a single float for every linear and angular
coordinate axis, or one gain per coordinate axis). They can be read and modified afterwards, per coordinate axis, with
its lin_kp, ang_kp, lin_ki, ang_ki, lin_kd, and ang_kd properties (each one is a Vec3, and can be set
from a vector or from a single float for every axis). At each frame, PidController.rigid_body_correction computes
the velocity correction (a PidCorrection, with its linear and angular parts) bringing a rigid-body closer to its
target pose (and to its target velocities, given as an optional RigidBodyVelocity to its target_vels argument,
zero by default): it is up to you to add it to the linvel and angvel of that rigid-body before the next
PhysicsWorld.step.
The coordinate axes (linear and/or angular) controlled by the controller can be selected in order, for example, to
only control the translations of a body while leaving its rotations to the simulationaxes field, an
AxesMask)r3PidController_SetAxes, from a combination of the R3_AXES_MASK_* bits, the current ones being given by r3PidController_Axes)axes argument of its constructor or its axes property, a combination of the AxesMask flags)
- Example 2D
- Example 3D
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let axes = AxesMask::LIN_X | AxesMask::LIN_Y;
let mut pid = PidController::new(60.0, 0.0, 0.8, axes);
let target = Vector::new(3.0, 2.0);
for _ in 0..200 {
let dt = world.integration_parameters.dt;
let body = &mut world.bodies[body_handle];
// The correction is the velocity change bringing the body closer to its target pose.
let correction = pid.rigid_body_correction(
dt,
body,
Pose::from_translation(target),
RigidBodyVelocity::zero(),
);
let new_velocities = *body.vels() + correction;
body.set_vels(new_velocities, true);
world.step();
}
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let axes = AxesMask::LIN_X | AxesMask::LIN_Y | AxesMask::LIN_Z;
let mut pid = PidController::new(60.0, 0.0, 0.8, axes);
let target = Vector::new(3.0, 2.0, 0.0);
for _ in 0..200 {
let dt = world.integration_parameters.dt;
let body = &mut world.bodies[body_handle];
// The correction is the velocity change bringing the body closer to its target pose.
let correction = pid.rigid_body_correction(
dt,
body,
Pose::from_translation(target),
RigidBodyVelocity::zero(),
);
let new_velocities = *body.vels() + correction;
body.set_vels(new_velocities, true);
world.step();
}
- Example 2D
- Example 3D
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = world.createPidController(60.0, 0.0, 0.8, RAPIER.PidAxesMask.AllLin);
let target = { x: 3.0, y: 2.0 };
for (let k = 0; k < 200; ++k) {
// The correction is applied to the velocity of the rigid-body.
pid.applyLinearCorrection(body, target, { x: 0.0, y: 0.0 });
world.step();
}
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = world.createPidController(60.0, 0.0, 0.8, RAPIER.PidAxesMask.AllLin);
let target = { x: 3.0, y: 2.0, z: 0.0 };
for (let k = 0; k < 200; ++k) {
// The correction is applied to the velocity of the rigid-body.
pid.applyLinearCorrection(body, target, { x: 0.0, y: 0.0, z: 0.0 });
world.step();
}
- Example 2D
- Example 3D
fn setup_physics(mut commands: Commands) {
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = PidController::new(60.0, 0.0, 0.8, AxesMask::LIN_AXES)
.with_target(PidTarget::from_translation(Vec2::new(300.0, 200.0)));
commands.spawn((
Transform::from_xyz(0.0, 100.0, 0.0),
RigidBody::Dynamic,
Collider::ball(50.0),
pid,
));
}
/* Move the target of the controller inside of a system. */
fn update_target(time: Res<Time>, mut controllers: Query<&mut PidController>) {
let t = time.elapsed_secs();
for mut controller in controllers.iter_mut() {
// The plugin drives the rigid-body toward this pose before each simulation step.
controller.target = PidTarget::from_translation(Vec2::new(300.0 * t.cos(), 200.0));
}
}
fn setup_physics(mut commands: Commands) {
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = PidController::new(60.0, 0.0, 0.8, AxesMask::LIN_AXES)
.with_target(PidTarget::from_translation(Vec3::new(3.0, 2.0, 0.0)));
commands.spawn((
Transform::from_xyz(0.0, 1.0, 0.0),
RigidBody::Dynamic,
Collider::ball(0.5),
pid,
));
}
/* Move the target of the controller inside of a system. */
fn update_target(time: Res<Time>, mut controllers: Query<&mut PidController>) {
let t = time.elapsed_secs();
for mut controller in controllers.iter_mut() {
// The plugin drives the rigid-body toward this pose before each simulation step.
controller.target = PidTarget::from_translation(Vec3::new(3.0 * t.cos(), 2.0, 0.0));
}
}
- Example 2D
- Example 3D
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
R2PidController *pid = r2NewPidController();
R2PidGains gains = r2PidController_Gains(pid);
gains.lin_kp = r2Vector(60.0, 60.0);
gains.lin_ki = r2Vector(0.0, 0.0);
gains.lin_kd = r2Vector(0.8, 0.8);
r2PidController_SetGains(pid, gains);
r2PidController_SetAxes(pid, R2_AXES_MASK_LIN_X | R2_AXES_MASK_LIN_Y);
R2Vector target = r2Vector(3.0, 2.0);
for (int i = 0; i < 200; i++) {
R2Real dt = r2TimeStep(world);
// The correction is the velocity change bringing the body closer to its target pose.
R2VelocityCorrection correction = r2PidController_RigidBodyCorrection(
pid, dt, body_handle,
r2TranslationPose(target), // The target pose.
r2Vector(0.0, 0.0), // The target linear velocity.
0.0); // The target angular velocity.
R2Vector linvel = r2VectorAdd(r2RigidBody_Linvel(body_handle), correction.linear);
R2AngVector angvel = r2RigidBody_Angvel(body_handle) + correction.angularVelocity;
r2RigidBody_SetLinvel(body_handle, linvel, 1);
r2RigidBody_SetAngvel(body_handle, angvel, 1);
r2Step(world, NULL, NULL);
}
r2FreePidController(pid);
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
R3PidController *pid = r3NewPidController();
R3PidGains gains = r3PidController_Gains(pid);
gains.lin_kp = r3Vector(60.0, 60.0, 60.0);
gains.lin_ki = r3Vector(0.0, 0.0, 0.0);
gains.lin_kd = r3Vector(0.8, 0.8, 0.8);
r3PidController_SetGains(pid, gains);
r3PidController_SetAxes(pid, R3_AXES_MASK_LIN_X | R3_AXES_MASK_LIN_Y | R3_AXES_MASK_LIN_Z);
R3Vector target = r3Vector(3.0, 2.0, 0.0);
for (int i = 0; i < 200; i++) {
R3Real dt = r3TimeStep(world);
// The correction is the velocity change bringing the body closer to its target pose.
R3VelocityCorrection correction = r3PidController_RigidBodyCorrection(
pid, dt, body_handle,
r3TranslationPose(target), // The target pose.
r3Vector(0.0, 0.0, 0.0), // The target linear velocity.
r3Vector(0.0, 0.0, 0.0)); // The target angular velocity.
R3Vector linvel = r3VectorAdd(r3RigidBody_Linvel(body_handle), correction.linear);
R3AngVector angvel = r3VectorAdd(r3RigidBody_Angvel(body_handle), correction.angularVelocity);
r3RigidBody_SetLinvel(body_handle, linvel, 1);
r3RigidBody_SetAngvel(body_handle, angvel, 1);
r3Step(world, NULL, NULL);
}
r3FreePidController(pid);
# The proportional, integral, and derivative gains of the controller, acting on the linear
# axes only: the body is pushed toward its target without its rotation being controlled.
axes = rp.AxesMask.LIN_X | rp.AxesMask.LIN_Y | rp.AxesMask.LIN_Z
pid = rp.PidController(axes=axes, Kp=60.0, Ki=0.0, Kd=0.8)
target = rp.Isometry3.from_translation(3.0, 2.0, 0.0)
for _ in range(200):
dt = world.integration_parameters.dt
body = world.rigid_bodies[body_handle]
# The correction is the velocity change bringing the body closer to its target pose
# (and to its target velocities, zero here).
correction = pid.rigid_body_correction(dt, body, target, target_vels=rp.RigidBodyVelocity())
body.linvel = body.linvel + correction.linear
body.angvel = body.angvel + correction.angular
world.step()
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). This is also what makes its API
mutable, and what has to be reset with PidController::reset_integrals whenever the controller is given a target it
never had a chance to reach. The PdController is the variant without that integral part: its API is immutable, and
its behavior is generally good enough for games.
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). These accumulated errors are
stored in the PidController::lin_integral and PidController::ang_integral fields updated by the plugin, and have
to be reset with PidController::reset_integrals whenever the controller is given a target it never had a chance to
reach. The PdController component is the variant without that integral part, and its behavior is generally good
enough for games. Use either one or the other on a given entity, but not both.
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). This is also what makes
r3PidController_RigidBodyCorrection modify the controller, and what has to be reset with
r3PidController_ResetIntegrals whenever the controller is given a target it never had a chance to reach. The
R3PdController is the variant without that integral part: it is a plain structure (initialized by
r3DefaultPdController) that r3PdController_RigidBodyCorrection doesn't modify, and its behavior is generally good
enough for games.
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). These accumulated errors are
readable with the PidController.lin_integral and PidController.ang_integral properties. This is also what makes
PidController.rigid_body_correction modify the controller, and what has to be reset with PidController.reset
whenever the controller is given a target it never had a chance to reach. The PdController is the variant without
that integral part (its constructor takes no Ki argument): its rigid_body_correction method doesn’t modify it (and
doesn’t need the timestep length), and its behavior is generally good enough for games.
PID controller
It is generally not recommended to move a rigid-body by setting its pose directly: teleporting it would ignore every obstacle on the way. The recommended alternative is generally to push it with a force (or an impulse) that is strong enough to reach the target. However, pushing with a single constant force/impulse will generally overshoot the target. Thus, ideally, the force or impulse should be carefully selected and updated each frame as the rigid-body gets closer to its target.
This is what a PID controller (Proportional-Integral-Derivative) is designed to calculate: given the target pose, it computes the ideal velocity change bringing the body closer to it. This is the building block of the velocity-based character controllers, but it is useful for anything that must follow a target without being teleported: a dynamic moving platform, an object held by the player, a following camera, etc.
The gains of the controller are what makes it reach its target quickly or smoothly. The proportional gain is applied to the position errors and is usually set to a multiple of the inverse of the timestep length (e.g. for a timestep of seconds). The derivative gain is applied to the velocity errors and is usually set in , where means no damping and means that the velocity errors are corrected within a single timestep.
The PID controller is the PidController component, added to the entity of a (non-fixed)
rigid-body. Its target field is a PidTarget holding the world-space pose (and optionally the velocities) the
rigid-body must be driven toward. Before each simulation step, the plugin computes the velocity correction bringing
the rigid-body closer to its target, and adds it to its velocity. So moving the target is just a matter of modifying
the PidController::target field.
The PID controller is the R3PidController object, created by r3NewPidController and freed by
r3FreePidController. It is created with a proportional gain of , an integral gain of , and a derivative gain
of , on every coordinate axis (all of them being controlled). Its gains are set per coordinate axis with
r3PidController_SetGains, from an R3PidGains structure (its current gains being given by r3PidController_Gains).
At each frame, r3PidController_RigidBodyCorrection computes the velocity correction bringing a rigid-body closer to
its target pose (and target velocities): it is up to you to add it to the velocities of that rigid-body before the
next r3Step.
The PID controller is the PidController class. It is created with a proportional gain of , an integral gain of
, and a derivative gain of , on every coordinate axis (all of them being controlled), unless other gains are
given to the Kp, Ki, and Kd arguments of its constructor (either a single float for every linear and angular
coordinate axis, or one gain per coordinate axis). They can be read and modified afterwards, per coordinate axis, with
its lin_kp, ang_kp, lin_ki, ang_ki, lin_kd, and ang_kd properties (each one is a Vec3, and can be set
from a vector or from a single float for every axis). At each frame, PidController.rigid_body_correction computes
the velocity correction (a PidCorrection, with its linear and angular parts) bringing a rigid-body closer to its
target pose (and to its target velocities, given as an optional RigidBodyVelocity to its target_vels argument,
zero by default): it is up to you to add it to the linvel and angvel of that rigid-body before the next
PhysicsWorld.step.
The coordinate axes (linear and/or angular) controlled by the controller can be selected in order, for example, to
only control the translations of a body while leaving its rotations to the simulationaxes field, an
AxesMask)r3PidController_SetAxes, from a combination of the R3_AXES_MASK_* bits, the current ones being given by r3PidController_Axes)axes argument of its constructor or its axes property, a combination of the AxesMask flags)
- Example 2D
- Example 3D
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let axes = AxesMask::LIN_X | AxesMask::LIN_Y;
let mut pid = PidController::new(60.0, 0.0, 0.8, axes);
let target = Vector::new(3.0, 2.0);
for _ in 0..200 {
let dt = world.integration_parameters.dt;
let body = &mut world.bodies[body_handle];
// The correction is the velocity change bringing the body closer to its target pose.
let correction = pid.rigid_body_correction(
dt,
body,
Pose::from_translation(target),
RigidBodyVelocity::zero(),
);
let new_velocities = *body.vels() + correction;
body.set_vels(new_velocities, true);
world.step();
}
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let axes = AxesMask::LIN_X | AxesMask::LIN_Y | AxesMask::LIN_Z;
let mut pid = PidController::new(60.0, 0.0, 0.8, axes);
let target = Vector::new(3.0, 2.0, 0.0);
for _ in 0..200 {
let dt = world.integration_parameters.dt;
let body = &mut world.bodies[body_handle];
// The correction is the velocity change bringing the body closer to its target pose.
let correction = pid.rigid_body_correction(
dt,
body,
Pose::from_translation(target),
RigidBodyVelocity::zero(),
);
let new_velocities = *body.vels() + correction;
body.set_vels(new_velocities, true);
world.step();
}
- Example 2D
- Example 3D
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = world.createPidController(60.0, 0.0, 0.8, RAPIER.PidAxesMask.AllLin);
let target = { x: 3.0, y: 2.0 };
for (let k = 0; k < 200; ++k) {
// The correction is applied to the velocity of the rigid-body.
pid.applyLinearCorrection(body, target, { x: 0.0, y: 0.0 });
world.step();
}
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = world.createPidController(60.0, 0.0, 0.8, RAPIER.PidAxesMask.AllLin);
let target = { x: 3.0, y: 2.0, z: 0.0 };
for (let k = 0; k < 200; ++k) {
// The correction is applied to the velocity of the rigid-body.
pid.applyLinearCorrection(body, target, { x: 0.0, y: 0.0, z: 0.0 });
world.step();
}
- Example 2D
- Example 3D
fn setup_physics(mut commands: Commands) {
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = PidController::new(60.0, 0.0, 0.8, AxesMask::LIN_AXES)
.with_target(PidTarget::from_translation(Vec2::new(300.0, 200.0)));
commands.spawn((
Transform::from_xyz(0.0, 100.0, 0.0),
RigidBody::Dynamic,
Collider::ball(50.0),
pid,
));
}
/* Move the target of the controller inside of a system. */
fn update_target(time: Res<Time>, mut controllers: Query<&mut PidController>) {
let t = time.elapsed_secs();
for mut controller in controllers.iter_mut() {
// The plugin drives the rigid-body toward this pose before each simulation step.
controller.target = PidTarget::from_translation(Vec2::new(300.0 * t.cos(), 200.0));
}
}
fn setup_physics(mut commands: Commands) {
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = PidController::new(60.0, 0.0, 0.8, AxesMask::LIN_AXES)
.with_target(PidTarget::from_translation(Vec3::new(3.0, 2.0, 0.0)));
commands.spawn((
Transform::from_xyz(0.0, 1.0, 0.0),
RigidBody::Dynamic,
Collider::ball(0.5),
pid,
));
}
/* Move the target of the controller inside of a system. */
fn update_target(time: Res<Time>, mut controllers: Query<&mut PidController>) {
let t = time.elapsed_secs();
for mut controller in controllers.iter_mut() {
// The plugin drives the rigid-body toward this pose before each simulation step.
controller.target = PidTarget::from_translation(Vec3::new(3.0 * t.cos(), 2.0, 0.0));
}
}
- Example 2D
- Example 3D
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
R2PidController *pid = r2NewPidController();
R2PidGains gains = r2PidController_Gains(pid);
gains.lin_kp = r2Vector(60.0, 60.0);
gains.lin_ki = r2Vector(0.0, 0.0);
gains.lin_kd = r2Vector(0.8, 0.8);
r2PidController_SetGains(pid, gains);
r2PidController_SetAxes(pid, R2_AXES_MASK_LIN_X | R2_AXES_MASK_LIN_Y);
R2Vector target = r2Vector(3.0, 2.0);
for (int i = 0; i < 200; i++) {
R2Real dt = r2TimeStep(world);
// The correction is the velocity change bringing the body closer to its target pose.
R2VelocityCorrection correction = r2PidController_RigidBodyCorrection(
pid, dt, body_handle,
r2TranslationPose(target), // The target pose.
r2Vector(0.0, 0.0), // The target linear velocity.
0.0); // The target angular velocity.
R2Vector linvel = r2VectorAdd(r2RigidBody_Linvel(body_handle), correction.linear);
R2AngVector angvel = r2RigidBody_Angvel(body_handle) + correction.angularVelocity;
r2RigidBody_SetLinvel(body_handle, linvel, 1);
r2RigidBody_SetAngvel(body_handle, angvel, 1);
r2Step(world, NULL, NULL);
}
r2FreePidController(pid);
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
R3PidController *pid = r3NewPidController();
R3PidGains gains = r3PidController_Gains(pid);
gains.lin_kp = r3Vector(60.0, 60.0, 60.0);
gains.lin_ki = r3Vector(0.0, 0.0, 0.0);
gains.lin_kd = r3Vector(0.8, 0.8, 0.8);
r3PidController_SetGains(pid, gains);
r3PidController_SetAxes(pid, R3_AXES_MASK_LIN_X | R3_AXES_MASK_LIN_Y | R3_AXES_MASK_LIN_Z);
R3Vector target = r3Vector(3.0, 2.0, 0.0);
for (int i = 0; i < 200; i++) {
R3Real dt = r3TimeStep(world);
// The correction is the velocity change bringing the body closer to its target pose.
R3VelocityCorrection correction = r3PidController_RigidBodyCorrection(
pid, dt, body_handle,
r3TranslationPose(target), // The target pose.
r3Vector(0.0, 0.0, 0.0), // The target linear velocity.
r3Vector(0.0, 0.0, 0.0)); // The target angular velocity.
R3Vector linvel = r3VectorAdd(r3RigidBody_Linvel(body_handle), correction.linear);
R3AngVector angvel = r3VectorAdd(r3RigidBody_Angvel(body_handle), correction.angularVelocity);
r3RigidBody_SetLinvel(body_handle, linvel, 1);
r3RigidBody_SetAngvel(body_handle, angvel, 1);
r3Step(world, NULL, NULL);
}
r3FreePidController(pid);
# The proportional, integral, and derivative gains of the controller, acting on the linear
# axes only: the body is pushed toward its target without its rotation being controlled.
axes = rp.AxesMask.LIN_X | rp.AxesMask.LIN_Y | rp.AxesMask.LIN_Z
pid = rp.PidController(axes=axes, Kp=60.0, Ki=0.0, Kd=0.8)
target = rp.Isometry3.from_translation(3.0, 2.0, 0.0)
for _ in range(200):
dt = world.integration_parameters.dt
body = world.rigid_bodies[body_handle]
# The correction is the velocity change bringing the body closer to its target pose
# (and to its target velocities, zero here).
correction = pid.rigid_body_correction(dt, body, target, target_vels=rp.RigidBodyVelocity())
body.linvel = body.linvel + correction.linear
body.angvel = body.angvel + correction.angular
world.step()
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). This is also what makes its API
mutable, and what has to be reset with PidController::reset_integrals whenever the controller is given a target it
never had a chance to reach. The PdController is the variant without that integral part: its API is immutable, and
its behavior is generally good enough for games.
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). These accumulated errors are
stored in the PidController::lin_integral and PidController::ang_integral fields updated by the plugin, and have
to be reset with PidController::reset_integrals whenever the controller is given a target it never had a chance to
reach. The PdController component is the variant without that integral part, and its behavior is generally good
enough for games. Use either one or the other on a given entity, but not both.
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). This is also what makes
r3PidController_RigidBodyCorrection modify the controller, and what has to be reset with
r3PidController_ResetIntegrals whenever the controller is given a target it never had a chance to reach. The
R3PdController is the variant without that integral part: it is a plain structure (initialized by
r3DefaultPdController) that r3PdController_RigidBodyCorrection doesn't modify, and its behavior is generally good
enough for games.
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). These accumulated errors are
readable with the PidController.lin_integral and PidController.ang_integral properties. This is also what makes
PidController.rigid_body_correction modify the controller, and what has to be reset with PidController.reset
whenever the controller is given a target it never had a chance to reach. The PdController is the variant without
that integral part (its constructor takes no Ki argument): its rigid_body_correction method doesn’t modify it (and
doesn’t need the timestep length), and its behavior is generally good enough for games.
PID controller
It is generally not recommended to move a rigid-body by setting its pose directly: teleporting it would ignore every obstacle on the way. The recommended alternative is generally to push it with a force (or an impulse) that is strong enough to reach the target. However, pushing with a single constant force/impulse will generally overshoot the target. Thus, ideally, the force or impulse should be carefully selected and updated each frame as the rigid-body gets closer to its target.
This is what a PID controller (Proportional-Integral-Derivative) is designed to calculate: given the target pose, it computes the ideal velocity change bringing the body closer to it. This is the building block of the velocity-based character controllers, but it is useful for anything that must follow a target without being teleported: a dynamic moving platform, an object held by the player, a following camera, etc.
The gains of the controller are what makes it reach its target quickly or smoothly. The proportional gain is applied to the position errors and is usually set to a multiple of the inverse of the timestep length (e.g. for a timestep of seconds). The derivative gain is applied to the velocity errors and is usually set in , where means no damping and means that the velocity errors are corrected within a single timestep.
The PID controller is the PidController component, added to the entity of a (non-fixed)
rigid-body. Its target field is a PidTarget holding the world-space pose (and optionally the velocities) the
rigid-body must be driven toward. Before each simulation step, the plugin computes the velocity correction bringing
the rigid-body closer to its target, and adds it to its velocity. So moving the target is just a matter of modifying
the PidController::target field.
The PID controller is the R3PidController object, created by r3NewPidController and freed by
r3FreePidController. It is created with a proportional gain of , an integral gain of , and a derivative gain
of , on every coordinate axis (all of them being controlled). Its gains are set per coordinate axis with
r3PidController_SetGains, from an R3PidGains structure (its current gains being given by r3PidController_Gains).
At each frame, r3PidController_RigidBodyCorrection computes the velocity correction bringing a rigid-body closer to
its target pose (and target velocities): it is up to you to add it to the velocities of that rigid-body before the
next r3Step.
The PID controller is the PidController class. It is created with a proportional gain of , an integral gain of
, and a derivative gain of , on every coordinate axis (all of them being controlled), unless other gains are
given to the Kp, Ki, and Kd arguments of its constructor (either a single float for every linear and angular
coordinate axis, or one gain per coordinate axis). They can be read and modified afterwards, per coordinate axis, with
its lin_kp, ang_kp, lin_ki, ang_ki, lin_kd, and ang_kd properties (each one is a Vec3, and can be set
from a vector or from a single float for every axis). At each frame, PidController.rigid_body_correction computes
the velocity correction (a PidCorrection, with its linear and angular parts) bringing a rigid-body closer to its
target pose (and to its target velocities, given as an optional RigidBodyVelocity to its target_vels argument,
zero by default): it is up to you to add it to the linvel and angvel of that rigid-body before the next
PhysicsWorld.step.
The coordinate axes (linear and/or angular) controlled by the controller can be selected in order, for example, to
only control the translations of a body while leaving its rotations to the simulationaxes field, an
AxesMask)r3PidController_SetAxes, from a combination of the R3_AXES_MASK_* bits, the current ones being given by r3PidController_Axes)axes argument of its constructor or its axes property, a combination of the AxesMask flags)
- Example 2D
- Example 3D
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let axes = AxesMask::LIN_X | AxesMask::LIN_Y;
let mut pid = PidController::new(60.0, 0.0, 0.8, axes);
let target = Vector::new(3.0, 2.0);
for _ in 0..200 {
let dt = world.integration_parameters.dt;
let body = &mut world.bodies[body_handle];
// The correction is the velocity change bringing the body closer to its target pose.
let correction = pid.rigid_body_correction(
dt,
body,
Pose::from_translation(target),
RigidBodyVelocity::zero(),
);
let new_velocities = *body.vels() + correction;
body.set_vels(new_velocities, true);
world.step();
}
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let axes = AxesMask::LIN_X | AxesMask::LIN_Y | AxesMask::LIN_Z;
let mut pid = PidController::new(60.0, 0.0, 0.8, axes);
let target = Vector::new(3.0, 2.0, 0.0);
for _ in 0..200 {
let dt = world.integration_parameters.dt;
let body = &mut world.bodies[body_handle];
// The correction is the velocity change bringing the body closer to its target pose.
let correction = pid.rigid_body_correction(
dt,
body,
Pose::from_translation(target),
RigidBodyVelocity::zero(),
);
let new_velocities = *body.vels() + correction;
body.set_vels(new_velocities, true);
world.step();
}
- Example 2D
- Example 3D
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = world.createPidController(60.0, 0.0, 0.8, RAPIER.PidAxesMask.AllLin);
let target = { x: 3.0, y: 2.0 };
for (let k = 0; k < 200; ++k) {
// The correction is applied to the velocity of the rigid-body.
pid.applyLinearCorrection(body, target, { x: 0.0, y: 0.0 });
world.step();
}
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = world.createPidController(60.0, 0.0, 0.8, RAPIER.PidAxesMask.AllLin);
let target = { x: 3.0, y: 2.0, z: 0.0 };
for (let k = 0; k < 200; ++k) {
// The correction is applied to the velocity of the rigid-body.
pid.applyLinearCorrection(body, target, { x: 0.0, y: 0.0, z: 0.0 });
world.step();
}
- Example 2D
- Example 3D
fn setup_physics(mut commands: Commands) {
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = PidController::new(60.0, 0.0, 0.8, AxesMask::LIN_AXES)
.with_target(PidTarget::from_translation(Vec2::new(300.0, 200.0)));
commands.spawn((
Transform::from_xyz(0.0, 100.0, 0.0),
RigidBody::Dynamic,
Collider::ball(50.0),
pid,
));
}
/* Move the target of the controller inside of a system. */
fn update_target(time: Res<Time>, mut controllers: Query<&mut PidController>) {
let t = time.elapsed_secs();
for mut controller in controllers.iter_mut() {
// The plugin drives the rigid-body toward this pose before each simulation step.
controller.target = PidTarget::from_translation(Vec2::new(300.0 * t.cos(), 200.0));
}
}
fn setup_physics(mut commands: Commands) {
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = PidController::new(60.0, 0.0, 0.8, AxesMask::LIN_AXES)
.with_target(PidTarget::from_translation(Vec3::new(3.0, 2.0, 0.0)));
commands.spawn((
Transform::from_xyz(0.0, 1.0, 0.0),
RigidBody::Dynamic,
Collider::ball(0.5),
pid,
));
}
/* Move the target of the controller inside of a system. */
fn update_target(time: Res<Time>, mut controllers: Query<&mut PidController>) {
let t = time.elapsed_secs();
for mut controller in controllers.iter_mut() {
// The plugin drives the rigid-body toward this pose before each simulation step.
controller.target = PidTarget::from_translation(Vec3::new(3.0 * t.cos(), 2.0, 0.0));
}
}
- Example 2D
- Example 3D
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
R2PidController *pid = r2NewPidController();
R2PidGains gains = r2PidController_Gains(pid);
gains.lin_kp = r2Vector(60.0, 60.0);
gains.lin_ki = r2Vector(0.0, 0.0);
gains.lin_kd = r2Vector(0.8, 0.8);
r2PidController_SetGains(pid, gains);
r2PidController_SetAxes(pid, R2_AXES_MASK_LIN_X | R2_AXES_MASK_LIN_Y);
R2Vector target = r2Vector(3.0, 2.0);
for (int i = 0; i < 200; i++) {
R2Real dt = r2TimeStep(world);
// The correction is the velocity change bringing the body closer to its target pose.
R2VelocityCorrection correction = r2PidController_RigidBodyCorrection(
pid, dt, body_handle,
r2TranslationPose(target), // The target pose.
r2Vector(0.0, 0.0), // The target linear velocity.
0.0); // The target angular velocity.
R2Vector linvel = r2VectorAdd(r2RigidBody_Linvel(body_handle), correction.linear);
R2AngVector angvel = r2RigidBody_Angvel(body_handle) + correction.angularVelocity;
r2RigidBody_SetLinvel(body_handle, linvel, 1);
r2RigidBody_SetAngvel(body_handle, angvel, 1);
r2Step(world, NULL, NULL);
}
r2FreePidController(pid);
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
R3PidController *pid = r3NewPidController();
R3PidGains gains = r3PidController_Gains(pid);
gains.lin_kp = r3Vector(60.0, 60.0, 60.0);
gains.lin_ki = r3Vector(0.0, 0.0, 0.0);
gains.lin_kd = r3Vector(0.8, 0.8, 0.8);
r3PidController_SetGains(pid, gains);
r3PidController_SetAxes(pid, R3_AXES_MASK_LIN_X | R3_AXES_MASK_LIN_Y | R3_AXES_MASK_LIN_Z);
R3Vector target = r3Vector(3.0, 2.0, 0.0);
for (int i = 0; i < 200; i++) {
R3Real dt = r3TimeStep(world);
// The correction is the velocity change bringing the body closer to its target pose.
R3VelocityCorrection correction = r3PidController_RigidBodyCorrection(
pid, dt, body_handle,
r3TranslationPose(target), // The target pose.
r3Vector(0.0, 0.0, 0.0), // The target linear velocity.
r3Vector(0.0, 0.0, 0.0)); // The target angular velocity.
R3Vector linvel = r3VectorAdd(r3RigidBody_Linvel(body_handle), correction.linear);
R3AngVector angvel = r3VectorAdd(r3RigidBody_Angvel(body_handle), correction.angularVelocity);
r3RigidBody_SetLinvel(body_handle, linvel, 1);
r3RigidBody_SetAngvel(body_handle, angvel, 1);
r3Step(world, NULL, NULL);
}
r3FreePidController(pid);
# The proportional, integral, and derivative gains of the controller, acting on the linear
# axes only: the body is pushed toward its target without its rotation being controlled.
axes = rp.AxesMask.LIN_X | rp.AxesMask.LIN_Y | rp.AxesMask.LIN_Z
pid = rp.PidController(axes=axes, Kp=60.0, Ki=0.0, Kd=0.8)
target = rp.Isometry3.from_translation(3.0, 2.0, 0.0)
for _ in range(200):
dt = world.integration_parameters.dt
body = world.rigid_bodies[body_handle]
# The correction is the velocity change bringing the body closer to its target pose
# (and to its target velocities, zero here).
correction = pid.rigid_body_correction(dt, body, target, target_vels=rp.RigidBodyVelocity())
body.linvel = body.linvel + correction.linear
body.angvel = body.angvel + correction.angular
world.step()
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). This is also what makes its API
mutable, and what has to be reset with PidController::reset_integrals whenever the controller is given a target it
never had a chance to reach. The PdController is the variant without that integral part: its API is immutable, and
its behavior is generally good enough for games.
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). These accumulated errors are
stored in the PidController::lin_integral and PidController::ang_integral fields updated by the plugin, and have
to be reset with PidController::reset_integrals whenever the controller is given a target it never had a chance to
reach. The PdController component is the variant without that integral part, and its behavior is generally good
enough for games. Use either one or the other on a given entity, but not both.
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). This is also what makes
r3PidController_RigidBodyCorrection modify the controller, and what has to be reset with
r3PidController_ResetIntegrals whenever the controller is given a target it never had a chance to reach. The
R3PdController is the variant without that integral part: it is a plain structure (initialized by
r3DefaultPdController) that r3PdController_RigidBodyCorrection doesn't modify, and its behavior is generally good
enough for games.
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). These accumulated errors are
readable with the PidController.lin_integral and PidController.ang_integral properties. This is also what makes
PidController.rigid_body_correction modify the controller, and what has to be reset with PidController.reset
whenever the controller is given a target it never had a chance to reach. The PdController is the variant without
that integral part (its constructor takes no Ki argument): its rigid_body_correction method doesn’t modify it (and
doesn’t need the timestep length), and its behavior is generally good enough for games.
PID controller
It is generally not recommended to move a rigid-body by setting its pose directly: teleporting it would ignore every obstacle on the way. The recommended alternative is generally to push it with a force (or an impulse) that is strong enough to reach the target. However, pushing with a single constant force/impulse will generally overshoot the target. Thus, ideally, the force or impulse should be carefully selected and updated each frame as the rigid-body gets closer to its target.
This is what a PID controller (Proportional-Integral-Derivative) is designed to calculate: given the target pose, it computes the ideal velocity change bringing the body closer to it. This is the building block of the velocity-based character controllers, but it is useful for anything that must follow a target without being teleported: a dynamic moving platform, an object held by the player, a following camera, etc.
The gains of the controller are what makes it reach its target quickly or smoothly. The proportional gain is applied to the position errors and is usually set to a multiple of the inverse of the timestep length (e.g. for a timestep of seconds). The derivative gain is applied to the velocity errors and is usually set in , where means no damping and means that the velocity errors are corrected within a single timestep.
The PID controller is the PidController component, added to the entity of a (non-fixed)
rigid-body. Its target field is a PidTarget holding the world-space pose (and optionally the velocities) the
rigid-body must be driven toward. Before each simulation step, the plugin computes the velocity correction bringing
the rigid-body closer to its target, and adds it to its velocity. So moving the target is just a matter of modifying
the PidController::target field.
The PID controller is the R3PidController object, created by r3NewPidController and freed by
r3FreePidController. It is created with a proportional gain of , an integral gain of , and a derivative gain
of , on every coordinate axis (all of them being controlled). Its gains are set per coordinate axis with
r3PidController_SetGains, from an R3PidGains structure (its current gains being given by r3PidController_Gains).
At each frame, r3PidController_RigidBodyCorrection computes the velocity correction bringing a rigid-body closer to
its target pose (and target velocities): it is up to you to add it to the velocities of that rigid-body before the
next r3Step.
The PID controller is the PidController class. It is created with a proportional gain of , an integral gain of
, and a derivative gain of , on every coordinate axis (all of them being controlled), unless other gains are
given to the Kp, Ki, and Kd arguments of its constructor (either a single float for every linear and angular
coordinate axis, or one gain per coordinate axis). They can be read and modified afterwards, per coordinate axis, with
its lin_kp, ang_kp, lin_ki, ang_ki, lin_kd, and ang_kd properties (each one is a Vec3, and can be set
from a vector or from a single float for every axis). At each frame, PidController.rigid_body_correction computes
the velocity correction (a PidCorrection, with its linear and angular parts) bringing a rigid-body closer to its
target pose (and to its target velocities, given as an optional RigidBodyVelocity to its target_vels argument,
zero by default): it is up to you to add it to the linvel and angvel of that rigid-body before the next
PhysicsWorld.step.
The coordinate axes (linear and/or angular) controlled by the controller can be selected in order, for example, to
only control the translations of a body while leaving its rotations to the simulationaxes field, an
AxesMask)r3PidController_SetAxes, from a combination of the R3_AXES_MASK_* bits, the current ones being given by r3PidController_Axes)axes argument of its constructor or its axes property, a combination of the AxesMask flags)
- Example 2D
- Example 3D
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let axes = AxesMask::LIN_X | AxesMask::LIN_Y;
let mut pid = PidController::new(60.0, 0.0, 0.8, axes);
let target = Vector::new(3.0, 2.0);
for _ in 0..200 {
let dt = world.integration_parameters.dt;
let body = &mut world.bodies[body_handle];
// The correction is the velocity change bringing the body closer to its target pose.
let correction = pid.rigid_body_correction(
dt,
body,
Pose::from_translation(target),
RigidBodyVelocity::zero(),
);
let new_velocities = *body.vels() + correction;
body.set_vels(new_velocities, true);
world.step();
}
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let axes = AxesMask::LIN_X | AxesMask::LIN_Y | AxesMask::LIN_Z;
let mut pid = PidController::new(60.0, 0.0, 0.8, axes);
let target = Vector::new(3.0, 2.0, 0.0);
for _ in 0..200 {
let dt = world.integration_parameters.dt;
let body = &mut world.bodies[body_handle];
// The correction is the velocity change bringing the body closer to its target pose.
let correction = pid.rigid_body_correction(
dt,
body,
Pose::from_translation(target),
RigidBodyVelocity::zero(),
);
let new_velocities = *body.vels() + correction;
body.set_vels(new_velocities, true);
world.step();
}
- Example 2D
- Example 3D
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = world.createPidController(60.0, 0.0, 0.8, RAPIER.PidAxesMask.AllLin);
let target = { x: 3.0, y: 2.0 };
for (let k = 0; k < 200; ++k) {
// The correction is applied to the velocity of the rigid-body.
pid.applyLinearCorrection(body, target, { x: 0.0, y: 0.0 });
world.step();
}
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = world.createPidController(60.0, 0.0, 0.8, RAPIER.PidAxesMask.AllLin);
let target = { x: 3.0, y: 2.0, z: 0.0 };
for (let k = 0; k < 200; ++k) {
// The correction is applied to the velocity of the rigid-body.
pid.applyLinearCorrection(body, target, { x: 0.0, y: 0.0, z: 0.0 });
world.step();
}
- Example 2D
- Example 3D
fn setup_physics(mut commands: Commands) {
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = PidController::new(60.0, 0.0, 0.8, AxesMask::LIN_AXES)
.with_target(PidTarget::from_translation(Vec2::new(300.0, 200.0)));
commands.spawn((
Transform::from_xyz(0.0, 100.0, 0.0),
RigidBody::Dynamic,
Collider::ball(50.0),
pid,
));
}
/* Move the target of the controller inside of a system. */
fn update_target(time: Res<Time>, mut controllers: Query<&mut PidController>) {
let t = time.elapsed_secs();
for mut controller in controllers.iter_mut() {
// The plugin drives the rigid-body toward this pose before each simulation step.
controller.target = PidTarget::from_translation(Vec2::new(300.0 * t.cos(), 200.0));
}
}
fn setup_physics(mut commands: Commands) {
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
let pid = PidController::new(60.0, 0.0, 0.8, AxesMask::LIN_AXES)
.with_target(PidTarget::from_translation(Vec3::new(3.0, 2.0, 0.0)));
commands.spawn((
Transform::from_xyz(0.0, 1.0, 0.0),
RigidBody::Dynamic,
Collider::ball(0.5),
pid,
));
}
/* Move the target of the controller inside of a system. */
fn update_target(time: Res<Time>, mut controllers: Query<&mut PidController>) {
let t = time.elapsed_secs();
for mut controller in controllers.iter_mut() {
// The plugin drives the rigid-body toward this pose before each simulation step.
controller.target = PidTarget::from_translation(Vec3::new(3.0 * t.cos(), 2.0, 0.0));
}
}
- Example 2D
- Example 3D
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
R2PidController *pid = r2NewPidController();
R2PidGains gains = r2PidController_Gains(pid);
gains.lin_kp = r2Vector(60.0, 60.0);
gains.lin_ki = r2Vector(0.0, 0.0);
gains.lin_kd = r2Vector(0.8, 0.8);
r2PidController_SetGains(pid, gains);
r2PidController_SetAxes(pid, R2_AXES_MASK_LIN_X | R2_AXES_MASK_LIN_Y);
R2Vector target = r2Vector(3.0, 2.0);
for (int i = 0; i < 200; i++) {
R2Real dt = r2TimeStep(world);
// The correction is the velocity change bringing the body closer to its target pose.
R2VelocityCorrection correction = r2PidController_RigidBodyCorrection(
pid, dt, body_handle,
r2TranslationPose(target), // The target pose.
r2Vector(0.0, 0.0), // The target linear velocity.
0.0); // The target angular velocity.
R2Vector linvel = r2VectorAdd(r2RigidBody_Linvel(body_handle), correction.linear);
R2AngVector angvel = r2RigidBody_Angvel(body_handle) + correction.angularVelocity;
r2RigidBody_SetLinvel(body_handle, linvel, 1);
r2RigidBody_SetAngvel(body_handle, angvel, 1);
r2Step(world, NULL, NULL);
}
r2FreePidController(pid);
// The proportional, integral, and derivative gains of the controller, acting on the linear
// axes only: the body is pushed toward its target without its rotation being controlled.
R3PidController *pid = r3NewPidController();
R3PidGains gains = r3PidController_Gains(pid);
gains.lin_kp = r3Vector(60.0, 60.0, 60.0);
gains.lin_ki = r3Vector(0.0, 0.0, 0.0);
gains.lin_kd = r3Vector(0.8, 0.8, 0.8);
r3PidController_SetGains(pid, gains);
r3PidController_SetAxes(pid, R3_AXES_MASK_LIN_X | R3_AXES_MASK_LIN_Y | R3_AXES_MASK_LIN_Z);
R3Vector target = r3Vector(3.0, 2.0, 0.0);
for (int i = 0; i < 200; i++) {
R3Real dt = r3TimeStep(world);
// The correction is the velocity change bringing the body closer to its target pose.
R3VelocityCorrection correction = r3PidController_RigidBodyCorrection(
pid, dt, body_handle,
r3TranslationPose(target), // The target pose.
r3Vector(0.0, 0.0, 0.0), // The target linear velocity.
r3Vector(0.0, 0.0, 0.0)); // The target angular velocity.
R3Vector linvel = r3VectorAdd(r3RigidBody_Linvel(body_handle), correction.linear);
R3AngVector angvel = r3VectorAdd(r3RigidBody_Angvel(body_handle), correction.angularVelocity);
r3RigidBody_SetLinvel(body_handle, linvel, 1);
r3RigidBody_SetAngvel(body_handle, angvel, 1);
r3Step(world, NULL, NULL);
}
r3FreePidController(pid);
# The proportional, integral, and derivative gains of the controller, acting on the linear
# axes only: the body is pushed toward its target without its rotation being controlled.
axes = rp.AxesMask.LIN_X | rp.AxesMask.LIN_Y | rp.AxesMask.LIN_Z
pid = rp.PidController(axes=axes, Kp=60.0, Ki=0.0, Kd=0.8)
target = rp.Isometry3.from_translation(3.0, 2.0, 0.0)
for _ in range(200):
dt = world.integration_parameters.dt
body = world.rigid_bodies[body_handle]
# The correction is the velocity change bringing the body closer to its target pose
# (and to its target velocities, zero here).
correction = pid.rigid_body_correction(dt, body, target, target_vels=rp.RigidBodyVelocity())
body.linvel = body.linvel + correction.linear
body.angvel = body.angvel + correction.angular
world.step()
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). This is also what makes its API
mutable, and what has to be reset with PidController::reset_integrals whenever the controller is given a target it
never had a chance to reach. The PdController is the variant without that integral part: its API is immutable, and
its behavior is generally good enough for games.
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). These accumulated errors are
stored in the PidController::lin_integral and PidController::ang_integral fields updated by the plugin, and have
to be reset with PidController::reset_integrals whenever the controller is given a target it never had a chance to
reach. The PdController component is the variant without that integral part, and its behavior is generally good
enough for games. Use either one or the other on a given entity, but not both.
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). This is also what makes
r3PidController_RigidBodyCorrection modify the controller, and what has to be reset with
r3PidController_ResetIntegrals whenever the controller is given a target it never had a chance to reach. The
R3PdController is the variant without that integral part: it is a plain structure (initialized by
r3DefaultPdController) that r3PdController_RigidBodyCorrection doesn't modify, and its behavior is generally good
enough for games.
The integral part of the controller accumulates the position errors of the previous timesteps, which is what allows it
to compensate a permanent perturbation (e.g. the gravity applied to a hovering body). These accumulated errors are
readable with the PidController.lin_integral and PidController.ang_integral properties. This is also what makes
PidController.rigid_body_correction modify the controller, and what has to be reset with PidController.reset
whenever the controller is given a target it never had a chance to reach. The PdController is the variant without
that integral part (its constructor takes no Ki argument): its rigid_body_correction method doesn’t modify it (and
doesn’t need the timestep length), and its behavior is generally good enough for games.