use alloc::vec::Vec;
use crate::memory::{Pool, PoolHandle};
use crate::physics::{
BodyHandle, CharacterMove, CharacterMoveInput, ColliderShape, ContactHit, DynamicParams,
Fanout, JointSpec, LayerMask, RayHit, SensorCrossing,
};
use super::body::Body;
use super::broadphase::{Proxy, Role, SweepPrune};
use super::ccd::{self, Ccd};
use super::character::{self, CharacterCapsule, CharacterConfig};
use super::collide::heightfield::{Heightfield, Heightfields};
use super::config::SimConfig;
use super::contact::{ContactCache, Manifold, carry_impulses};
use super::impact::Impacts;
use super::island::Islands;
use super::joint::{Joint, JointFrame, JointSet};
#[cfg(test)]
use super::math::Mat3;
use super::math::{Quat, Vec3};
use super::narrow::{self, Narrow};
use super::query::{self, RayQuery};
#[cfg(test)]
use super::query::{ShapeCast, ShapeCastHit};
use super::scene::Scene;
use super::sensor::Sensors;
use super::solver::{self, Solver, SolverBody};
const CHARACTER_FRICTION: f32 = 0.5;
const CONTACT_COST: usize = 20;
const JOINT_COST: usize = 16;
const MIN_FANOUT_COST: usize = 4000;
pub struct Simulation {
config: SimConfig,
character: CharacterConfig,
bodies: Pool<Body>,
broadphase: SweepPrune,
contacts: ContactCache,
fields: Heightfields,
narrow: Narrow,
joints: JointSet,
islands: Islands,
solver: Solver,
sensors: Sensors,
impacts: Impacts,
ccd: Ccd,
workers: usize,
worker_overflows: u32,
}
impl core::fmt::Debug for Simulation {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Simulation")
.field("bodies", &self.bodies.len())
.field("capacity", &self.bodies.capacity())
.field("joints", &self.joints.len())
.finish()
}
}
impl Simulation {
pub fn with_capacity(capacity: usize) -> Self {
Self::new(SimConfig::default(), capacity)
}
pub fn new(config: SimConfig, capacity: usize) -> Self {
Simulation {
config,
character: CharacterConfig::default(),
bodies: Pool::with_capacity(capacity),
broadphase: SweepPrune::with_capacity(capacity),
contacts: ContactCache::with_capacity(capacity * 2),
fields: Heightfields::new(),
narrow: Narrow::new(),
joints: JointSet::with_capacity(capacity),
islands: Islands::with_capacity(capacity),
solver: Solver::with_capacity(capacity),
sensors: Sensors::with_capacity(capacity),
impacts: Impacts::with_capacity(capacity * 2),
ccd: Ccd::with_capacity(capacity),
workers: 1,
worker_overflows: 0,
}
}
pub fn reserve_workers(&mut self, workers: usize) -> usize {
let capacity = self.bodies.capacity();
self.workers = workers.clamp(1, solver::MAX_WORKERS);
self.broadphase.reserve_workers(self.workers, capacity);
self.narrow.reserve_workers(self.workers, capacity);
self.workers
}
pub fn workers(&self) -> usize {
self.workers
}
#[cfg(test)]
pub(crate) fn worker_overflows(&self) -> u32 {
self.worker_overflows
}
#[cfg(test)]
pub(crate) fn clear_worker_overflows(&mut self) {
self.worker_overflows = 0;
}
pub fn configure_character(&mut self, max_slope_deg: f32, step_height: f32, grounded: bool) {
self.character = CharacterConfig::new(max_slope_deg, step_height, grounded);
}
pub fn character_shape(half_height: f32, radius: f32) -> CharacterCapsule {
CharacterCapsule::new(half_height, radius)
}
pub fn config(&self) -> &SimConfig {
&self.config
}
#[cfg(test)]
pub(crate) fn set_config(&mut self, config: SimConfig) {
self.config = config;
}
pub fn capacity(&self) -> usize {
self.bodies.capacity()
}
pub fn body_count(&self) -> usize {
self.bodies.len()
}
pub fn collider_count(&self) -> usize {
self.bodies.len()
}
pub fn joint_count(&self) -> usize {
self.joints.len()
}
#[cfg(test)]
pub(crate) fn sensor_overlap_count(&self) -> usize {
self.sensors.overlap_count()
}
#[cfg(test)]
pub(crate) fn contact_count(&self) -> usize {
self.contacts
.manifolds()
.iter()
.map(|m| m.count as usize)
.sum()
}
pub fn reserved_bytes(&self) -> u64 {
self.bodies.reserved_bytes()
+ self.broadphase.reserved_bytes()
+ self.contacts.reserved_bytes()
+ self.fields.reserved_bytes()
+ self.narrow.reserved_bytes()
+ self.joints.reserved_bytes()
+ self.islands.reserved_bytes()
+ self.solver.reserved_bytes()
+ self.sensors.reserved_bytes()
+ self.impacts.reserved_bytes()
+ self.ccd.reserved_bytes()
}
pub fn add_fixed(
&mut self,
shape: &ColliderShape,
pos: [f32; 3],
euler_deg: [f32; 3],
friction: f32,
mask: LayerMask,
) -> Option<BodyHandle> {
self.add(Body::fixed(
*shape,
Vec3::from_array(pos),
Quat::from_euler_deg(euler_deg),
friction,
mask,
))
}
pub fn add_kinematic(
&mut self,
shape: &ColliderShape,
pos: [f32; 3],
euler_deg: [f32; 3],
friction: f32,
mask: LayerMask,
) -> Option<BodyHandle> {
self.add(Body::kinematic(
*shape,
Vec3::from_array(pos),
Quat::from_euler_deg(euler_deg),
friction,
mask,
))
}
pub fn add_character(
&mut self,
half_height: f32,
radius: f32,
center: [f32; 3],
mask: LayerMask,
) -> Option<BodyHandle> {
self.add_kinematic(
&ColliderShape::Capsule {
half_height,
radius,
},
center,
[0.0; 3],
CHARACTER_FRICTION,
mask,
)
}
pub fn add_sensor(
&mut self,
shape: &ColliderShape,
pos: [f32; 3],
euler_deg: [f32; 3],
tag: u64,
mask: LayerMask,
) -> Option<BodyHandle> {
self.add(Body::sensor(
*shape,
Vec3::from_array(pos),
Quat::from_euler_deg(euler_deg),
tag,
mask,
))
}
pub fn drain_sensor_crossings_into(&mut self, out: &mut Vec<SensorCrossing>) {
self.sensors.drain_into(out);
}
#[cfg(test)]
pub(crate) fn sensor_overflows(&self) -> u32 {
self.sensors.overflows()
}
#[cfg(test)]
pub(crate) fn clear_sensor_overflows(&mut self) {
self.sensors.clear_overflows();
}
pub fn set_contact_min_impulse(&mut self, min_impulse: f32, tick_dt: f32) {
self.impacts.set_min_impulse(min_impulse, tick_dt);
}
pub fn drain_contact_hits_into(&mut self, out: &mut Vec<ContactHit>) {
self.impacts.drain_into(out);
}
#[cfg(test)]
pub(crate) fn contact_hit_overflows(&self) -> u32 {
self.impacts.overflows()
}
pub fn add_heightfield(
&mut self,
rows: usize,
cols: usize,
heights: Vec<f32>,
scale: [f32; 3],
pos: [f32; 3],
mask: LayerMask,
) -> Option<BodyHandle> {
if self.bodies.len() >= self.bodies.capacity() {
return None;
}
let origin = Vec3::from_array(pos);
let field = Heightfield::new(rows, cols, heights, Vec3::from_array(scale), origin)?;
let bounds = field.bounds();
let index = self.fields.push(field);
self.add(Body::terrain(index, bounds, origin, 1.0, mask))
}
#[cfg(test)]
pub(crate) fn heightfield_overflows(&self) -> u32 {
self.fields.overflows()
}
#[cfg(test)]
pub(crate) fn clear_heightfield_overflows(&mut self) {
self.fields.clear_overflows();
}
#[cfg(test)]
pub(crate) fn ccd_overflows(&self) -> u32 {
self.ccd.overflows()
}
#[cfg(test)]
pub(crate) fn swept_body_count(&self) -> usize {
self.ccd.mover_count()
}
pub fn add_dynamic(
&mut self,
shape: &ColliderShape,
pos: [f32; 3],
euler_deg: [f32; 3],
params: DynamicParams,
mask: LayerMask,
) -> Option<BodyHandle> {
self.add(Body::dynamic(
*shape,
Vec3::from_array(pos),
Quat::from_euler_deg(euler_deg),
params,
mask,
))
}
pub fn add_joint(
&mut self,
body_a: BodyHandle,
body_b: BodyHandle,
anchor_a: [f32; 3],
anchor_b: [f32; 3],
spec: JointSpec,
) -> bool {
let (slot_a, slot_b) = (body_a.index(), body_b.index());
if slot_a == slot_b {
return false;
}
let (anchor_a, anchor_b) = (Vec3::from_array(anchor_a), Vec3::from_array(anchor_b));
if !anchor_a.is_finite() || !anchor_b.is_finite() {
return false;
}
let (Some(a), Some(b)) = (
self.bodies.get(pool_handle(body_a)),
self.bodies.get(pool_handle(body_b)),
) else {
return false;
};
let frame = JointFrame::new(spec, a.orientation, b.orientation);
self.joints.push(Joint {
a: slot_a,
b: slot_b,
anchor_a,
anchor_b,
frame,
impulses: Default::default(),
});
for slot in [slot_a, slot_b] {
if let Some(body) = self.bodies.get_at_mut(slot as usize) {
body.wake();
}
}
true
}
pub fn set_kinematic_translation(&mut self, handle: BodyHandle, pos: [f32; 3]) -> bool {
let Some(body) = self.bodies.get_mut(pool_handle(handle)) else {
return false;
};
if !body.is_kinematic() {
return false;
}
body.kinematic_target = Some(Vec3::from_array(pos));
true
}
pub fn make_kinematic(&mut self, handle: BodyHandle) -> bool {
self.reclassify(handle, |body| body.make_kinematic())
}
pub fn make_dynamic(&mut self, handle: BodyHandle, linear_velocity: [f32; 3]) -> bool {
let velocity = Vec3::from_array(linear_velocity);
self.reclassify(handle, move |body| body.make_dynamic(velocity))
}
#[cfg(test)]
pub(crate) fn is_kinematic(&self, handle: BodyHandle) -> Option<bool> {
Some(self.bodies.get(pool_handle(handle))?.is_kinematic())
}
fn scene(&self) -> Scene<'_> {
Scene {
bodies: &self.bodies,
broadphase: &self.broadphase,
fields: &self.fields,
}
}
pub fn raycast(
&self,
origin: [f32; 3],
dir: [f32; 3],
max_dist: f32,
exclude: Option<BodyHandle>,
mask: LayerMask,
) -> Option<RayHit> {
query::raycast(
self.scene(),
&RayQuery {
origin,
dir,
max_dist,
exclude,
mask,
},
)
}
#[cfg(test)]
pub(crate) fn shape_cast(&self, cast: &ShapeCast) -> Option<ShapeCastHit> {
query::shape_cast(self.scene(), cast)
}
pub fn move_character(
&self,
shape: &CharacterCapsule,
input: &CharacterMoveInput,
) -> CharacterMove {
character::resolve(self.scene(), &self.character, shape, input)
}
pub fn remove_body(&mut self, handle: BodyHandle) -> bool {
let slot = handle.index();
if self.bodies.remove(pool_handle(handle)).is_none() {
return false;
}
self.broadphase.remove(slot);
self.wake_neighbours(slot);
self.joints.remove_incident(slot);
true
}
#[cfg(test)]
pub(crate) fn body_pose(&self, handle: BodyHandle) -> Option<([f32; 3], [f32; 3])> {
let body = self.bodies.get(pool_handle(handle))?;
Some((body.position.to_array(), body.orientation.to_euler_deg()))
}
pub fn body_pose_quat(&self, handle: BodyHandle) -> Option<([f32; 3], [f32; 4])> {
let body = self.bodies.get(pool_handle(handle))?;
Some((body.position.to_array(), body.orientation.to_xyzw()))
}
#[cfg(test)]
pub(crate) fn linear_velocity(&self, handle: BodyHandle) -> Option<[f32; 3]> {
Some(
self.bodies
.get(pool_handle(handle))?
.linear_velocity
.to_array(),
)
}
#[cfg(test)]
pub(crate) fn angular_velocity(&self, handle: BodyHandle) -> Option<[f32; 3]> {
Some(
self.bodies
.get(pool_handle(handle))?
.angular_velocity
.to_array(),
)
}
pub fn mass(&self, handle: BodyHandle) -> Option<f32> {
Some(self.bodies.get(pool_handle(handle))?.mass)
}
#[cfg(test)]
pub(crate) fn is_sleeping(&self, handle: BodyHandle) -> Option<bool> {
Some(self.bodies.get(pool_handle(handle))?.sleeping)
}
#[cfg(test)]
pub(crate) fn set_linear_velocity(&mut self, handle: BodyHandle, velocity: [f32; 3]) {
if let Some(body) = self.bodies.get_mut(pool_handle(handle)) {
body.linear_velocity = Vec3::from_array(velocity);
body.wake();
}
}
#[cfg(test)]
pub(crate) fn set_angular_velocity(&mut self, handle: BodyHandle, velocity: [f32; 3]) {
if let Some(body) = self.bodies.get_mut(pool_handle(handle)) {
body.angular_velocity = Vec3::from_array(velocity);
body.wake();
}
}
#[cfg(test)]
pub(crate) fn apply_impulse(&mut self, handle: BodyHandle, impulse: [f32; 3]) {
if let Some(body) = self.bodies.get_mut(pool_handle(handle)) {
body.linear_velocity += Vec3::from_array(impulse) * body.inv_mass;
body.wake();
}
}
#[cfg(test)]
pub(crate) fn total_energy(&self) -> f32 {
let mut total = 0.0;
for (_, body) in self.bodies.iter() {
if !body.is_dynamic() {
continue;
}
let momentum = Mat3::diagonal_conjugated(body.orientation, body.inertia_local)
.mul_vec3(body.angular_velocity);
total += 0.5 * body.mass * body.linear_velocity.length_squared()
+ 0.5 * body.angular_velocity.dot(momentum)
+ body.mass * self.config.gravity * body.gravity_scale * body.position.y;
}
total
}
pub fn step(&mut self, dt: f32) {
self.step_with(dt, &crate::physics::Inline);
}
pub fn step_with(&mut self, dt: f32, fanout: &impl Fanout) {
if !dt.is_finite() || dt <= 0.0 {
return;
}
let asked = fanout.workers().max(1);
if asked > self.workers {
self.worker_overflows = self.worker_overflows.saturating_add(1);
}
let workers = asked.min(self.workers);
self.drive_kinematics(dt);
let awake = self.refresh_bounds();
if workers > 1 && self.step_cost(awake) >= MIN_FANOUT_COST {
fanout.scope(|| self.advance(dt, fanout, workers));
} else {
self.advance(dt, &crate::physics::Inline, 1);
}
}
fn step_cost(&self, awake: usize) -> usize {
if awake == 0 {
return 0;
}
let contacts = self.broadphase.pair_count().min(awake * 4);
let joints = self.joints.len().min(awake * 2);
awake + contacts * CONTACT_COST + joints * JOINT_COST
}
fn advance(&mut self, dt: f32, fanout: &impl Fanout, workers: usize) {
let Simulation {
config,
bodies,
broadphase,
contacts,
fields,
narrow,
joints,
islands,
solver,
sensors,
impacts,
ccd,
..
} = self;
let sweeping = ccd::enabled(config);
let pairs = broadphase.sweep(fanout, workers);
sensors.resolve(bodies, pairs.sensors);
let (current, previous) = contacts.begin();
narrow.build(
narrow::Work {
bodies,
fields,
pairs: pairs.contacts,
previous,
out: current,
margin: config.speculative_margin,
},
fanout,
workers,
);
carry_impulses(previous, current);
wake_driven_contacts(bodies, current);
solver.begin();
gather(bodies, solver, current, joints.as_slice());
solver.run(
solver::Work {
manifolds: current,
joints: joints.as_mut_slice(),
islands,
config,
dt,
},
fanout,
workers,
);
impacts.collect(bodies, current, solver.loads(), dt);
ccd.begin();
for (handle, body) in bodies.iter_mut() {
if !body.is_simulated() {
continue;
}
let slot = handle.index() as u32;
let solved = solver.body(slot);
let began_at = body.position;
body.linear_velocity = solved.linear_velocity;
body.angular_velocity = solved.angular_velocity;
body.position = solved.position;
body.orientation = solved.rotation;
if let Some(target) = body.kinematic_target.take() {
body.position = target;
}
if sweeping {
ccd.observe(slot, body, began_at, config.ccd_motion_ratio);
}
}
if sweeping {
let scene = Scene {
bodies,
broadphase,
fields,
};
ccd.resolve(scene, config, dt);
ccd.report_crossings(bodies, sensors);
ccd.apply(bodies);
}
if !solver.is_idle() {
update_sleep(config, bodies, islands, current, joints.as_slice(), dt);
}
}
fn reclassify(&mut self, handle: BodyHandle, change: impl FnOnce(&mut Body) -> bool) -> bool {
let Some(body) = self.bodies.get_mut(pool_handle(handle)) else {
return false;
};
if !change(body) {
return true;
}
let proxy = proxy_for(body);
let slot = handle.index();
self.broadphase.set_proxy(slot, proxy);
self.wake_neighbours(slot);
true
}
fn drive_kinematics(&mut self, dt: f32) {
for (_, body) in self.bodies.iter_mut() {
if body.is_kinematic() {
body.drive_to_target(dt);
}
}
}
fn add(&mut self, mut body: Body) -> Option<BodyHandle> {
body.refresh_bounds(self.config.bounds_margin);
let proxy = proxy_for(&body);
let handle = self.bodies.insert(body)?;
let slot = handle.index() as u32;
self.broadphase.insert(slot);
self.broadphase.set_proxy(slot, proxy);
self.wake_neighbours(slot);
Some(body_handle(handle))
}
fn wake_neighbours(&mut self, slot: u32) {
let Simulation {
bodies,
contacts,
joints,
..
} = self;
let mut wake = |other: u32| {
if let Some(body) = bodies.get_at_mut(other as usize) {
body.wake();
}
};
for manifold in contacts.manifolds_mut() {
let other = if manifold.a == slot {
manifold.b
} else if manifold.b == slot {
manifold.a
} else {
continue;
};
wake(other);
}
for joint in joints.as_slice() {
if let Some(other) = joint.other(slot) {
wake(other);
}
}
}
fn refresh_bounds(&mut self) -> usize {
let margin = self.config.bounds_margin;
let Simulation {
bodies, broadphase, ..
} = self;
let mut awake = 0;
for (handle, body) in bodies.iter_mut() {
if !body.is_simulated() {
continue;
}
awake += 1;
if body.refresh_bounds(margin) {
broadphase.set_proxy(handle.index() as u32, proxy_for(body));
}
}
awake
}
}
fn gather(bodies: &Pool<Body>, solver: &mut Solver, manifolds: &[Manifold], joints: &[Joint]) {
for (handle, body) in bodies.iter() {
if body.is_simulated() {
solver.set_body(handle.index() as u32, SolverBody::from_body(body));
}
}
for manifold in manifolds {
gather_partner(bodies, solver, manifold.a, manifold.b);
}
for joint in joints {
gather_partner(bodies, solver, joint.a, joint.b);
}
}
fn gather_partner(bodies: &Pool<Body>, solver: &mut Solver, a: u32, b: u32) {
let simulated = |slot: u32| {
bodies
.get_at(slot as usize)
.is_some_and(|body| body.is_simulated())
};
let (moves_a, moves_b) = (simulated(a), simulated(b));
if moves_a == moves_b {
return;
}
let resting = if moves_a { b } else { a };
if let Some(body) = bodies.get_at(resting as usize) {
solver.set_body(resting, SolverBody::from_body(body));
}
}
fn proxy_for(body: &Body) -> Proxy {
let role = if body.is_sensor() {
Role::Sensor
} else if body.responds_to_contact() {
Role::Dynamic
} else if body.is_kinematic() {
Role::Driven
} else {
Role::Static
};
Proxy {
bounds: body.bounds,
mask: body.mask,
role,
}
}
fn pool_handle(handle: BodyHandle) -> PoolHandle {
PoolHandle::from_parts(handle.index(), handle.generation())
}
pub(super) fn body_at(bodies: &Pool<Body>, handle: BodyHandle) -> Option<&Body> {
bodies.get(pool_handle(handle))
}
fn body_handle(handle: PoolHandle) -> BodyHandle {
BodyHandle::from_parts(handle.index() as u32, handle.generation())
}
pub(super) fn handle_at(bodies: &Pool<Body>, slot: u32) -> Option<BodyHandle> {
bodies.handle_at(slot as usize).map(body_handle)
}
fn wake_driven_contacts(bodies: &mut Pool<Body>, manifolds: &[Manifold]) {
for manifold in manifolds {
for (slot, other) in [(manifold.a, manifold.b), (manifold.b, manifold.a)] {
let driving = bodies.get_at(slot as usize).is_some_and(|body| {
body.kinematic_target
.is_some_and(|target| target != body.position)
});
if driving && let Some(body) = bodies.get_at_mut(other as usize) {
body.wake();
}
}
}
}
fn update_sleep(
config: &SimConfig,
bodies: &mut Pool<Body>,
islands: &mut Islands,
manifolds: &[Manifold],
joints: &[Joint],
dt: f32,
) {
for (_, body) in bodies.iter_mut() {
if !body.is_dynamic() {
continue;
}
if !config.allow_sleep {
body.wake();
continue;
}
if body.sleeping {
continue;
}
if body.is_still(config.sleep_linear_velocity, config.sleep_angular_velocity) {
body.sleep_timer += dt;
} else {
body.sleep_timer = 0.0;
}
}
if !config.allow_sleep {
return;
}
islands.clear();
let movable = |slot: u32| {
bodies
.get_at(slot as usize)
.is_some_and(|body| body.is_dynamic())
};
for manifold in manifolds {
if movable(manifold.a) && movable(manifold.b) {
islands.union(manifold.a, manifold.b);
}
}
for joint in joints {
if movable(joint.a) && movable(joint.b) {
islands.union(joint.a, joint.b);
}
}
for (handle, body) in bodies.iter() {
if body.is_dynamic() {
islands.mark(
handle.index() as u32,
body.sleep_timer >= config.time_to_sleep,
);
}
}
for joint in joints {
if !joint.frame.is_driven() {
continue;
}
for slot in [joint.a, joint.b] {
if movable(slot) {
islands.mark(slot, false);
}
}
}
for (handle, body) in bodies.iter_mut() {
if !body.is_dynamic() {
continue;
}
if islands.island_is_still(handle.index() as u32) {
body.sleep();
} else {
body.sleeping = false;
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn a_shape_cast_stops_at_the_first_body_in_its_path() {
let mut sim = Simulation::with_capacity(1);
sim.add_fixed(
&ColliderShape::Cuboid {
half_extents: [10.0, 0.5, 10.0],
},
[0.0, -0.5, 0.0],
[0.0; 3],
0.8,
LayerMask::ALL,
);
let capsule = ColliderShape::Capsule {
half_height: 0.6,
radius: 0.3,
};
let hit = sim
.shape_cast(&ShapeCast::new(capsule, [0.0, 4.0, 0.0], [0.0, -8.0, 0.0]))
.expect("the floor is down there");
let landed = 4.0 - hit.toi * 8.0;
assert!((landed - 0.9).abs() < 0.01, "landed at {landed}");
assert!(hit.normal[1] > 0.99, "standing on it: {:?}", hit.normal);
}
use super::*;
const TICK: f32 = 1.0 / 60.0;
fn params(restitution: f32, damping: f32) -> DynamicParams {
DynamicParams {
mass: 1.0,
friction: 0.5,
restitution,
gravity_scale: 1.0,
linear_damping: damping,
}
}
fn floor(sim: &mut Simulation) -> BodyHandle {
sim.add_fixed(
&ColliderShape::Cuboid {
half_extents: [50.0, 1.0, 50.0],
},
[0.0, -1.0, 0.0],
[0.0; 3],
0.8,
LayerMask::ALL,
)
.expect("room")
}
#[test]
fn a_body_falls_under_gravity() {
let mut sim = Simulation::with_capacity(1);
let ball = sim
.add_dynamic(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 10.0, 0.0],
[0.0; 3],
params(0.0, 0.0),
LayerMask::ALL,
)
.expect("room");
for _ in 0..60 {
sim.step(TICK);
}
let (pos, _) = sim.body_pose(ball).expect("live");
assert!((pos[1] - (10.0 - 10.0)).abs() < 0.4, "y = {}", pos[1]);
assert!(sim.linear_velocity(ball).expect("live")[1] < -19.0);
}
#[test]
fn a_zero_or_negative_step_changes_nothing() {
let mut sim = Simulation::with_capacity(1);
let ball = sim
.add_dynamic(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 10.0, 0.0],
[0.0; 3],
params(0.0, 0.0),
LayerMask::ALL,
)
.expect("room");
sim.step(0.0);
sim.step(-1.0);
assert_eq!(sim.body_pose(ball).expect("live").0, [0.0, 10.0, 0.0]);
}
#[test]
fn a_full_pool_declines_rather_than_growing() {
let mut sim = Simulation::with_capacity(1);
assert!(
sim.add_dynamic(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 1.0, 0.0],
[0.0; 3],
params(0.0, 0.0),
LayerMask::ALL
)
.is_some()
);
assert!(
sim.add_dynamic(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 3.0, 0.0],
[0.0; 3],
params(0.0, 0.0),
LayerMask::ALL
)
.is_none()
);
assert_eq!(sim.body_count(), 1);
assert_eq!(sim.capacity(), 1);
assert!(sim.reserved_bytes() > 0);
}
#[test]
fn a_removed_bodys_handle_stops_naming_anything() {
let mut sim = Simulation::with_capacity(2);
let ball = sim
.add_dynamic(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 1.0, 0.0],
[0.0; 3],
params(0.0, 0.0),
LayerMask::ALL,
)
.expect("room");
assert!(sim.remove_body(ball));
assert!(!sim.remove_body(ball));
assert!(sim.body_pose(ball).is_none());
assert_eq!(sim.body_count(), 0);
}
#[test]
fn a_body_lands_on_the_floor_and_stays_on_it() {
let mut sim = Simulation::with_capacity(2);
floor(&mut sim);
let ball = sim
.add_dynamic(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 6.0, 0.0],
[0.0; 3],
params(0.0, 0.0),
LayerMask::ALL,
)
.expect("room");
for _ in 0..240 {
sim.step(TICK);
}
let (pos, _) = sim.body_pose(ball).expect("live");
assert!((pos[1] - 0.5).abs() < 0.02, "y = {}", pos[1]);
assert!(sim.contact_count() > 0);
}
#[test]
fn layers_that_do_not_interact_pass_through_each_other() {
let mut sim = Simulation::with_capacity(2);
sim.add_fixed(
&ColliderShape::Cuboid {
half_extents: [50.0, 1.0, 50.0],
},
[0.0, -1.0, 0.0],
[0.0; 3],
0.8,
LayerMask {
memberships: 0b01,
filter: 0b01,
},
);
let ball = sim
.add_dynamic(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 4.0, 0.0],
[0.0; 3],
params(0.0, 0.0),
LayerMask {
memberships: 0b10,
filter: 0b10,
},
)
.expect("room");
for _ in 0..120 {
sim.step(TICK);
}
assert!(sim.body_pose(ball).expect("live").0[1] < -2.0);
}
#[test]
fn an_impulse_moves_a_body_and_wakes_it() {
let mut sim = Simulation::with_capacity(2);
floor(&mut sim);
let ball = sim
.add_dynamic(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 0.5, 0.0],
[0.0; 3],
params(0.0, 0.5),
LayerMask::ALL,
)
.expect("room");
for _ in 0..120 {
sim.step(TICK);
}
assert_eq!(sim.is_sleeping(ball), Some(true));
sim.apply_impulse(ball, [0.0, 8.0, 0.0]);
assert_eq!(sim.is_sleeping(ball), Some(false));
sim.step(TICK);
assert!(sim.body_pose(ball).expect("live").0[1] > 0.55);
}
#[test]
fn velocity_can_be_set_and_read_back() {
let mut sim = Simulation::with_capacity(1);
let ball = sim
.add_dynamic(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 5.0, 0.0],
[0.0; 3],
params(0.0, 0.0),
LayerMask::ALL,
)
.expect("room");
sim.set_linear_velocity(ball, [1.0, 0.0, 0.0]);
sim.set_angular_velocity(ball, [0.0, 2.0, 0.0]);
assert_eq!(sim.linear_velocity(ball), Some([1.0, 0.0, 0.0]));
assert_eq!(sim.angular_velocity(ball), Some([0.0, 2.0, 0.0]));
assert!(sim.mass(ball).expect("live") > 0.0);
}
#[test]
fn two_identical_runs_agree_bit_for_bit() {
let run = || {
let mut sim = Simulation::with_capacity(16);
floor(&mut sim);
let mut handles = Vec::new();
for i in 0..12 {
handles.push(
sim.add_dynamic(
&ColliderShape::Cuboid {
half_extents: [0.4, 0.4, 0.4],
},
[(i % 3) as f32 * 0.9, 1.0 + (i / 3) as f32 * 0.9, 0.0],
[0.0, i as f32 * 7.0, 0.0],
params(0.3, 0.0),
LayerMask::ALL,
)
.expect("room"),
);
}
for _ in 0..90 {
sim.step(TICK);
}
handles
.iter()
.map(|&h| {
let (p, r) = sim.body_pose(h).expect("live");
(
[p[0].to_bits(), p[1].to_bits(), p[2].to_bits()],
[r[0].to_bits(), r[1].to_bits(), r[2].to_bits()],
)
})
.collect::<Vec<_>>()
};
assert_eq!(run(), run());
}
#[test]
fn a_ray_finds_the_nearest_body_along_it() {
let mut sim = Simulation::with_capacity(4);
for (index, z) in [2.0f32, 5.0, 9.0].into_iter().enumerate() {
sim.add_fixed(
&[
ColliderShape::Ball { radius: 0.5 },
ColliderShape::Cuboid {
half_extents: [0.5, 0.5, 0.5],
},
ColliderShape::Capsule {
half_height: 0.5,
radius: 0.25,
},
][index],
[0.0, 0.0, z],
[0.0; 3],
0.5,
LayerMask::ALL,
)
.expect("room");
}
let hit = sim
.raycast(
[0.0, 0.0, -5.0],
[0.0, 0.0, 1.0],
100.0,
None,
LayerMask::ALL,
)
.expect("a hit");
assert!((hit.distance - 6.5).abs() < 1.0e-4, "{hit:?}");
assert!((hit.normal[2] + 1.0).abs() < 1.0e-4, "{hit:?}");
assert!((hit.point[2] - 1.5).abs() < 1.0e-4, "{hit:?}");
}
#[test]
fn a_ray_answers_the_same_way_from_either_end_of_the_scene() {
let mut sim = Simulation::with_capacity(4);
for z in [2.0f32, 5.0, 9.0] {
sim.add_fixed(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 0.0, z],
[0.0; 3],
0.5,
LayerMask::ALL,
)
.expect("room");
}
sim.step(TICK);
let forward = sim
.raycast(
[0.0, 0.0, -5.0],
[0.0, 0.0, 1.0],
100.0,
None,
LayerMask::ALL,
)
.expect("a hit");
let backward = sim
.raycast(
[0.0, 0.0, 20.0],
[0.0, 0.0, -1.0],
100.0,
None,
LayerMask::ALL,
)
.expect("a hit");
assert!((forward.distance - 6.5).abs() < 1.0e-4, "{forward:?}");
assert!((backward.distance - 10.5).abs() < 1.0e-4, "{backward:?}");
}
#[test]
fn a_ray_skips_layers_it_does_not_interact_with() {
let mut sim = Simulation::with_capacity(2);
let near = LayerMask {
memberships: 0b01,
filter: 0b11,
};
let far = LayerMask {
memberships: 0b10,
filter: 0b11,
};
sim.add_fixed(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 0.0, 0.0],
[0.0; 3],
0.5,
near,
)
.expect("room");
sim.add_fixed(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 0.0, 5.0],
[0.0; 3],
0.5,
far,
)
.expect("room");
let only_far = LayerMask {
memberships: 0b11,
filter: 0b10,
};
let hit = sim
.raycast([0.0, 0.0, -5.0], [0.0, 0.0, 1.0], 100.0, None, only_far)
.expect("a hit");
assert!(
(hit.distance - 9.5).abs() < 1.0e-4,
"the near one is hidden"
);
assert!(
sim.raycast(
[0.0, 0.0, -5.0],
[0.0, 0.0, 1.0],
100.0,
None,
LayerMask {
memberships: 0b100,
filter: 0b100,
},
)
.is_none()
);
}
#[test]
fn a_ray_can_be_told_to_leave_one_body_out() {
let mut sim = Simulation::with_capacity(2);
let near = sim
.add_fixed(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 0.0, 0.0],
[0.0; 3],
0.5,
LayerMask::ALL,
)
.expect("room");
sim.add_fixed(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 0.0, 5.0],
[0.0; 3],
0.5,
LayerMask::ALL,
)
.expect("room");
let cast = |exclude| {
sim.raycast(
[0.0, 0.0, -5.0],
[0.0, 0.0, 1.0],
100.0,
exclude,
LayerMask::ALL,
)
};
assert!((cast(None).expect("a hit").distance - 4.5).abs() < 1.0e-4);
assert!((cast(Some(near)).expect("a hit").distance - 9.5).abs() < 1.0e-4);
}
#[test]
fn a_stale_exclusion_does_not_hide_the_slots_new_occupant() {
let mut sim = Simulation::with_capacity(1);
let first = sim
.add_fixed(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 0.0, 0.0],
[0.0; 3],
0.5,
LayerMask::ALL,
)
.expect("room");
assert!(sim.remove_body(first));
sim.add_fixed(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 0.0, 0.0],
[0.0; 3],
0.5,
LayerMask::ALL,
)
.expect("the freed slot");
assert!(
sim.raycast(
[0.0, 0.0, -5.0],
[0.0, 0.0, 1.0],
100.0,
Some(first),
LayerMask::ALL,
)
.is_some(),
"the stale handle names nobody"
);
}
#[test]
fn a_removed_body_stops_being_hit() {
let mut sim = Simulation::with_capacity(1);
let ball = sim
.add_fixed(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 0.0, 0.0],
[0.0; 3],
0.5,
LayerMask::ALL,
)
.expect("room");
let cast = |sim: &Simulation| {
sim.raycast(
[0.0, 0.0, -5.0],
[0.0, 0.0, 1.0],
100.0,
None,
LayerMask::ALL,
)
};
assert!(cast(&sim).is_some());
sim.remove_body(ball);
assert!(cast(&sim).is_none());
}
#[test]
fn a_ray_respects_its_distance_limit_and_needs_a_direction() {
let mut sim = Simulation::with_capacity(1);
sim.add_fixed(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 0.0, 0.0],
[0.0; 3],
0.5,
LayerMask::ALL,
)
.expect("room");
let cast =
|dir: [f32; 3], max| sim.raycast([0.0, 0.0, -5.0], dir, max, None, LayerMask::ALL);
assert!(cast([0.0, 0.0, 1.0], 4.5).is_some());
assert!(cast([0.0, 0.0, 1.0], 4.4).is_none());
assert!(cast([0.0, 0.0, 0.0], 100.0).is_none(), "no direction");
assert!(cast([0.0, 0.0, 1.0], 0.0).is_none(), "no reach");
assert!(cast([0.0, 0.0, 1.0], -1.0).is_none());
assert!(cast([f32::NAN, 0.0, 0.0], 100.0).is_none());
assert!((cast([0.0, 0.0, 7.0], 100.0).expect("a hit").distance - 4.5).abs() < 1.0e-4);
}
#[test]
fn a_ray_finds_a_body_added_since_the_last_step() {
let mut sim = Simulation::with_capacity(2);
floor(&mut sim);
sim.step(TICK);
sim.add_fixed(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 5.0, 0.0],
[0.0; 3],
0.5,
LayerMask::ALL,
)
.expect("room");
let hit = sim
.raycast(
[0.0, 9.0, 0.0],
[0.0, -1.0, 0.0],
100.0,
None,
LayerMask::ALL,
)
.expect("a hit");
assert!((hit.distance - 3.5).abs() < 1.0e-4, "{hit:?}");
}
#[test]
fn a_ray_hits_a_position_driven_body() {
let mut sim = Simulation::with_capacity(1);
sim.add_kinematic(
&ColliderShape::Cuboid {
half_extents: [1.0, 0.25, 1.0],
},
[0.0, 0.0, 0.0],
[0.0; 3],
0.5,
LayerMask::ALL,
)
.expect("room");
let hit = sim
.raycast(
[0.0, 5.0, 0.0],
[0.0, -1.0, 0.0],
100.0,
None,
LayerMask::ALL,
)
.expect("a hit");
assert!((hit.distance - 4.75).abs() < 1.0e-4, "{hit:?}");
}
#[test]
fn a_shape_cast_stops_at_the_nearest_body_and_names_it() {
let mut sim = Simulation::with_capacity(3);
let ground = floor(&mut sim);
let ledge = sim
.add_fixed(
&ColliderShape::Cuboid {
half_extents: [2.0, 0.5, 2.0],
},
[0.0, 3.0, 0.0],
[0.0; 3],
0.5,
LayerMask::ALL,
)
.expect("room");
let capsule = ColliderShape::Capsule {
half_height: 0.5,
radius: 0.25,
};
let hit = sim
.shape_cast(&ShapeCast::new(capsule, [0.0, 9.0, 0.0], [0.0, -12.0, 0.0]))
.expect("a hit");
assert_eq!(hit.body, ledge, "the ledge, not the ground under it");
let landed = 9.0 - hit.toi * 12.0;
assert!((landed - 4.25).abs() < 1.0e-2, "landed at {landed}");
assert!(!hit.started_touching);
assert_ne!(hit.body, ground);
}
#[test]
fn a_shape_cast_that_reaches_nothing_reports_nothing() {
let mut sim = Simulation::with_capacity(2);
floor(&mut sim);
let ball = ColliderShape::Ball { radius: 0.5 };
assert!(
sim.shape_cast(&ShapeCast::new(ball, [0.0, 9.0, 0.0], [0.0, -1.0, 0.0]))
.is_none()
);
assert!(
sim.shape_cast(&ShapeCast::new(ball, [0.0, 9.0, 0.0], [0.0, 5.0, 0.0]))
.is_none(),
"away from everything"
);
}
#[test]
fn a_shape_cast_says_when_it_began_in_contact() {
let mut sim = Simulation::with_capacity(2);
let ground = floor(&mut sim);
let ball = ColliderShape::Ball { radius: 0.5 };
let hit = sim
.shape_cast(&ShapeCast::new(ball, [0.0, 0.4, 0.0], [3.0, 0.0, 0.0]))
.expect("a hit");
assert_eq!(hit.body, ground);
assert_eq!(hit.toi, 0.0);
assert!(hit.started_touching);
assert!(hit.normal[1] > 0.9, "{hit:?}");
}
#[test]
fn a_shape_cast_honours_its_layer_filter_and_its_exclusion() {
let mut sim = Simulation::with_capacity(2);
let near = sim
.add_fixed(
&ColliderShape::Cuboid {
half_extents: [2.0, 0.5, 2.0],
},
[0.0, 2.0, 0.0],
[0.0; 3],
0.5,
LayerMask {
memberships: 0b01,
filter: 0b11,
},
)
.expect("room");
let far = sim
.add_fixed(
&ColliderShape::Cuboid {
half_extents: [2.0, 0.5, 2.0],
},
[0.0, 0.0, 0.0],
[0.0; 3],
0.5,
LayerMask {
memberships: 0b10,
filter: 0b11,
},
)
.expect("room");
let ball = ColliderShape::Ball { radius: 0.5 };
let straight = ShapeCast::new(ball, [0.0, 6.0, 0.0], [0.0, -8.0, 0.0]);
assert_eq!(sim.shape_cast(&straight).expect("a hit").body, near);
assert_eq!(
sim.shape_cast(&ShapeCast {
exclude: Some(near),
..straight
})
.expect("a hit")
.body,
far
);
assert_eq!(
sim.shape_cast(&ShapeCast {
mask: LayerMask {
memberships: 0b11,
filter: 0b10,
},
..straight
})
.expect("a hit")
.body,
far
);
}
#[test]
fn a_driven_body_arrives_exactly_where_it_was_sent() {
let mut sim = Simulation::with_capacity(1);
let platform = sim
.add_kinematic(
&ColliderShape::Cuboid {
half_extents: [1.0, 0.25, 1.0],
},
[0.0, 0.0, 0.0],
[0.0; 3],
0.5,
LayerMask::ALL,
)
.expect("room");
assert!(sim.set_kinematic_translation(platform, [1.5, 2.0, -3.0]));
sim.step(TICK);
assert_eq!(sim.body_pose(platform).expect("live").0, [1.5, 2.0, -3.0]);
sim.step(TICK);
assert_eq!(sim.body_pose(platform).expect("live").0, [1.5, 2.0, -3.0]);
assert_eq!(sim.linear_velocity(platform), Some([0.0; 3]));
}
#[test]
fn a_driven_body_ignores_gravity_and_impulses() {
let mut sim = Simulation::with_capacity(1);
let platform = sim
.add_kinematic(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 5.0, 0.0],
[0.0; 3],
0.5,
LayerMask::ALL,
)
.expect("room");
sim.apply_impulse(platform, [0.0, 100.0, 0.0]);
for _ in 0..120 {
sim.step(TICK);
}
assert_eq!(sim.body_pose(platform).expect("live").0, [0.0, 5.0, 0.0]);
assert_eq!(sim.mass(platform), Some(0.0));
assert_eq!(sim.is_kinematic(platform), Some(true));
}
#[test]
fn a_character_capsule_is_a_position_driven_body() {
let mut sim = Simulation::with_capacity(1);
let handle = sim
.add_character(0.6, 0.3, [0.0, 4.0, 0.0], LayerMask::ALL)
.expect("room in the pool");
assert_eq!(sim.is_kinematic(handle), Some(true));
for _ in 0..60 {
sim.step(TICK);
}
let (position, _) = sim.body_pose(handle).expect("a live body");
assert_eq!(position[1], 4.0, "gravity does not move a driven capsule");
assert!(sim.set_kinematic_translation(handle, [0.0, 3.0, 0.0]));
sim.step(TICK);
let (position, _) = sim.body_pose(handle).expect("a live body");
assert!((position[1] - 3.0).abs() < 1.0e-5, "{position:?}");
}
#[test]
fn only_a_position_driven_body_takes_a_translation_target() {
let mut sim = Simulation::with_capacity(2);
let fixed = floor(&mut sim);
let ball = sim
.add_dynamic(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 5.0, 0.0],
[0.0; 3],
params(0.0, 0.0),
LayerMask::ALL,
)
.expect("room");
assert!(!sim.set_kinematic_translation(fixed, [0.0, 9.0, 0.0]));
assert!(!sim.set_kinematic_translation(ball, [0.0, 9.0, 0.0]));
sim.step(TICK);
assert!(sim.body_pose(ball).expect("live").0[1] < 5.0, "still falls");
}
#[test]
fn a_driven_body_pushes_a_dynamic_one_it_is_moved_into() {
let mut sim = Simulation::with_capacity(3);
floor(&mut sim);
let crate_body = sim
.add_dynamic(
&ColliderShape::Cuboid {
half_extents: [0.5, 0.5, 0.5],
},
[0.0, 0.5, 0.0],
[0.0; 3],
params(0.0, 0.0),
LayerMask::ALL,
)
.expect("room");
let blade = sim
.add_kinematic(
&ColliderShape::Cuboid {
half_extents: [0.5, 0.5, 0.5],
},
[-3.0, 0.5, 0.0],
[0.0; 3],
0.5,
LayerMask::ALL,
)
.expect("room");
let mut x = -3.0f32;
for step in 0..180 {
if step == 60 {
assert_eq!(sim.is_sleeping(crate_body), Some(true), "settled first");
}
if step >= 60 {
x += 0.03;
assert!(sim.set_kinematic_translation(blade, [x, 0.5, 0.0]));
}
sim.step(TICK);
}
assert!((sim.body_pose(blade).expect("live").0[0] - x).abs() < 1.0e-5);
let pushed = sim.body_pose(crate_body).expect("live").0[0];
assert!(pushed > 1.5, "the crate was shoved along: {pushed}");
assert!(
pushed > x,
"and it stays ahead of the blade: {pushed} vs {x}"
);
}
#[test]
fn switching_a_bodys_kind_keeps_its_handle_and_leaves_the_world_standing() {
let mut sim = Simulation::with_capacity(2);
floor(&mut sim);
let ball = sim
.add_dynamic(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 4.0, 0.0],
[0.0; 3],
params(0.0, 0.0),
LayerMask::ALL,
)
.expect("room");
for _ in 0..180 {
sim.step(TICK);
}
let resting = sim.body_pose(ball).expect("live").0;
assert!((resting[1] - 0.5).abs() < 0.02, "{resting:?}");
assert!(sim.make_kinematic(ball));
assert_eq!(sim.is_kinematic(ball), Some(true));
assert!(sim.set_kinematic_translation(ball, [0.0, 6.0, 0.0]));
sim.step(TICK);
for _ in 0..60 {
sim.step(TICK);
}
assert_eq!(sim.body_pose(ball).expect("live").0, [0.0, 6.0, 0.0]);
assert!(sim.make_dynamic(ball, [0.0, 0.0, 2.0]));
assert_eq!(sim.is_kinematic(ball), Some(false));
assert!(sim.mass(ball).expect("live") > 0.0);
for _ in 0..300 {
sim.step(TICK);
}
let landed = sim.body_pose(ball).expect("live").0;
assert!(
(landed[1] - 0.5).abs() < 0.02,
"back on the floor: {landed:?}"
);
assert!(landed[2] > 0.5, "and it travelled: {landed:?}");
assert_eq!(sim.body_count(), 2);
assert_eq!(sim.collider_count(), sim.body_count());
}
#[test]
fn a_stack_stays_up_when_the_body_under_it_is_switched() {
let mut sim = Simulation::with_capacity(3);
floor(&mut sim);
let cube = ColliderShape::Cuboid {
half_extents: [0.5, 0.5, 0.5],
};
let lower = sim
.add_dynamic(
&cube,
[0.0, 0.5, 0.0],
[0.0; 3],
params(0.0, 0.0),
LayerMask::ALL,
)
.expect("room");
let upper = sim
.add_dynamic(
&cube,
[0.0, 1.5, 0.0],
[0.0; 3],
params(0.0, 0.0),
LayerMask::ALL,
)
.expect("room");
for _ in 0..180 {
sim.step(TICK);
}
let held = sim.body_pose(lower).expect("live").0;
assert!(sim.make_kinematic(lower));
for _ in 0..180 {
sim.step(TICK);
}
let top = sim.body_pose(upper).expect("live").0;
assert!(
(top[1] - 1.5).abs() < 0.05,
"the top box still rests: {top:?}"
);
assert_eq!(
sim.body_pose(lower).expect("live").0,
held,
"and the one below it has not stirred"
);
assert!(sim.set_kinematic_translation(lower, [held[0], 1.5, held[2]]));
for _ in 0..120 {
sim.step(TICK);
}
let lifted = sim.body_pose(upper).expect("live").0;
assert!(lifted[1] > 2.0, "carried up: {lifted:?}");
}
#[test]
fn switching_the_kind_of_a_body_that_is_not_there_reports_so() {
let mut sim = Simulation::with_capacity(1);
let ball = sim
.add_dynamic(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 1.0, 0.0],
[0.0; 3],
params(0.0, 0.0),
LayerMask::ALL,
)
.expect("room");
assert!(sim.remove_body(ball));
assert!(!sim.make_kinematic(ball));
assert!(!sim.make_dynamic(ball, [0.0; 3]));
assert!(!sim.set_kinematic_translation(ball, [0.0; 3]));
assert_eq!(sim.is_kinematic(ball), None);
}
#[test]
fn queries_answer_the_same_bits_twice_running() {
let mut sim = Simulation::with_capacity(17);
floor(&mut sim);
let side = 9usize;
let mut heights = Vec::with_capacity(side * side);
for row in 0..side {
for col in 0..side {
heights.push((row as f32 * 0.37).sin() * 0.4 + (col as f32 * 0.21).cos() * 0.3);
}
}
sim.add_heightfield(
side,
side,
heights,
[24.0, 1.0, 24.0],
[0.0, -3.0, 0.0],
LayerMask::ALL,
)
.expect("room");
for i in 0..12 {
sim.add_dynamic(
&ColliderShape::Cuboid {
half_extents: [0.4, 0.4, 0.4],
},
[(i % 3) as f32 * 0.9, 1.0 + (i / 3) as f32 * 0.9, 0.0],
[0.0, i as f32 * 7.0, 0.0],
params(0.3, 0.0),
LayerMask::ALL,
)
.expect("room");
}
for _ in 0..90 {
sim.step(TICK);
}
let ray = |sim: &Simulation| {
sim.raycast(
[-6.0, 1.3, 0.1],
[1.0, -0.1, 0.0],
40.0,
None,
LayerMask::ALL,
)
.map(|hit| (hit.distance.to_bits(), hit.point, hit.normal))
};
let sweep = |sim: &Simulation| {
sim.shape_cast(&ShapeCast::new(
ColliderShape::Capsule {
half_height: 0.3,
radius: 0.2,
},
[-6.0, 1.3, 0.1],
[12.0, 0.0, 0.0],
))
.map(|hit| (hit.toi.to_bits(), hit.point, hit.normal, hit.body))
};
let shape = Simulation::character_shape(0.3, 0.2);
let capsule = sim
.add_kinematic(
&ColliderShape::Capsule {
half_height: 0.3,
radius: 0.2,
},
[-6.0, 1.3, 0.1],
[0.0; 3],
0.8,
LayerMask::ALL,
)
.expect("room");
let walk = |sim: &Simulation| {
let moved = sim.move_character(
&shape,
&CharacterMoveInput {
center: [-6.0, 1.3, 0.1],
desired: [12.0, -0.05, 0.0],
dt: TICK,
exclude: capsule,
mask: LayerMask::ALL,
},
);
(moved.translation.map(f32::to_bits), moved.grounded)
};
let terrain_ray = |sim: &Simulation| {
sim.raycast(
[-1.7, 6.0, 2.3],
[0.0, -1.0, 0.0],
40.0,
None,
LayerMask::ALL,
)
.map(|hit| (hit.distance.to_bits(), hit.point, hit.normal))
};
let terrain_sweep = |sim: &Simulation| {
sim.shape_cast(&ShapeCast::new(
ColliderShape::Ball { radius: 0.4 },
[-9.0, -2.2, 2.3],
[18.0, 0.0, 0.0],
))
.map(|hit| (hit.toi.to_bits(), hit.point, hit.normal, hit.body))
};
assert!(
ray(&sim).is_some() && sweep(&sim).is_some(),
"the scene is in the way"
);
assert!(
terrain_ray(&sim).is_some() && terrain_sweep(&sim).is_some(),
"and so is the terrain"
);
assert_eq!(ray(&sim), ray(&sim));
assert_eq!(sweep(&sim), sweep(&sim));
assert_eq!(walk(&sim), walk(&sim));
assert_eq!(terrain_ray(&sim), terrain_ray(&sim));
assert_eq!(terrain_sweep(&sim), terrain_sweep(&sim));
}
#[test]
fn config_round_trips_and_takes_effect() {
let mut sim = Simulation::new(
SimConfig {
gravity: 0.0,
allow_sleep: false,
..SimConfig::default()
},
1,
);
assert_eq!(sim.config().gravity, 0.0);
let ball = sim
.add_dynamic(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 5.0, 0.0],
[0.0; 3],
params(0.0, 0.0),
LayerMask::ALL,
)
.expect("room");
for _ in 0..60 {
sim.step(TICK);
}
assert_eq!(sim.body_pose(ball).expect("live").0[1], 5.0);
sim.set_config(SimConfig::default());
assert_eq!(sim.config().gravity, crate::physics::GRAVITY);
sim.step(TICK);
assert!(sim.body_pose(ball).expect("live").0[1] < 5.0);
}
#[test]
fn gravity_scale_and_damping_do_what_they_say() {
let mut sim = Simulation::with_capacity(2);
let floating = sim
.add_dynamic(
&ColliderShape::Ball { radius: 0.5 },
[0.0, 5.0, 0.0],
[0.0; 3],
DynamicParams {
gravity_scale: 0.0,
..params(0.0, 0.0)
},
LayerMask::ALL,
)
.expect("room");
let damped = sim
.add_dynamic(
&ColliderShape::Ball { radius: 0.5 },
[5.0, 5.0, 0.0],
[0.0; 3],
DynamicParams {
gravity_scale: 0.0,
..params(0.0, 4.0)
},
LayerMask::ALL,
)
.expect("room");
sim.set_linear_velocity(floating, [3.0, 0.0, 0.0]);
sim.set_linear_velocity(damped, [3.0, 0.0, 0.0]);
for _ in 0..60 {
sim.step(TICK);
}
assert_eq!(sim.body_pose(floating).expect("live").0[1], 5.0);
let free = sim.linear_velocity(floating).expect("live")[0];
let slowed = sim.linear_velocity(damped).expect("live")[0];
assert!((free - 3.0).abs() < 1.0e-5, "{free}");
assert!(slowed < 0.5, "damping must bleed the speed off: {slowed}");
}
}