Skip to main content

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 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.

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.

note

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. Don’t hesitate to copy and customize it to fit your particular needs.

Setup and usage​

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).

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
);
}
}

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:

/// 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(())
}
warning

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.

info

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.

warning

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).

character offset

/* 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()
});
warning

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.

up vector and slope angles

/* 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;
}
}

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.
info

As always in Rapier, angles are specified in 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();
}
}

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

info

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.

/* 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,
});
}
}

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.

snap-to-ground

/* 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));
}
}

Filtering​

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 a ControllerFilterPredicate, 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>();
}
}
}
info

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.

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.

info

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.

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.

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:

/* 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.

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.

info

The vehicle controller is only available in 3D, i.e., with bevy_rapier3d.

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 WheelTuning shared by the wheels is what makes the vehicle feel heavy or light by controlling the elastic properties (stiffness and damping) of the suspension, as well as the grip of the wheels:

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),
));

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.

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
);
}
}
}
warning

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.

note

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.

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.

info

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. 6060 for a timestep of 1/601 / 60 seconds). The derivative gain is applied to the velocity errors and is usually set in [0,1][0, 1], where 00 means no damping and 11 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 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 simulation (with the axes field, an AxesMask):

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));
}
}
note

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.