dynamis-character 0.7.0

Query driven kinematic character controller
Documentation
mod common;

use common::{DT, gravity_config, new_world};
use dynamis_character::{Character, CharacterDesc};
use dynamis_model::BodyDesc;
use dynamis_world::World;

fn walk_and_settle(
    world: &mut World,
    character: &mut Character,
    frames: usize,
    direction: [f32; 3],
) {
    for _ in 0..frames {
        world.step(DT);
        character.step(world, DT, direction, false);
    }
    world.wait();
}

#[test]
fn character_walks_on_flat_ground() {
    let mut world = new_world(gravity_config());
    world.spawn(
        BodyDesc::cuboid([20.0, 0.5, 20.0])
            .mass(0.0)
            .position([0.0, -0.5, 0.0]),
    );
    let mut character = Character::spawn(&mut world, [0.0, 1.0, 0.0], CharacterDesc::default());
    walk_and_settle(&mut world, &mut character, 30, [1.0, 0.0, 0.0]);
    assert!(character.grounded(), "character must stay grounded");
    assert!(
        character.position()[0] > 0.5,
        "character must advance along its move direction, got x={}",
        character.position()[0]
    );
    assert!(
        (character.position()[1] - 1.0).abs() < 0.2,
        "character height must stay constant, got y={}",
        character.position()[1]
    );
}

#[test]
fn character_climbs_step() {
    let mut world = new_world(gravity_config());
    world.spawn(
        BodyDesc::cuboid([20.0, 0.5, 20.0])
            .mass(0.0)
            .position([0.0, -0.5, 0.0]),
    );
    world.spawn(
        BodyDesc::cuboid([1.0, 0.2, 4.0])
            .mass(0.0)
            .position([2.0, 0.1, 0.0]),
    );
    let mut character = Character::spawn(
        &mut world,
        [0.0, 1.0, 0.0],
        CharacterDesc {
            step_height: 0.3,
            ..Default::default()
        },
    );
    walk_and_settle(&mut world, &mut character, 40, [1.0, 0.0, 0.0]);
    walk_and_settle(&mut world, &mut character, 30, [0.0, 0.0, 0.0]);
    assert!(
        character.position()[0] > 2.2,
        "character must cross the step, got x={}",
        character.position()[0]
    );
    assert!(
        (character.position()[1] - 1.2).abs() < 0.12,
        "character must stand on top of the step, got y={}",
        character.position()[1]
    );
    assert!(
        character.grounded() || character.position()[1] > 1.1,
        "character must rest on the step surface"
    );
}

#[test]
fn character_blocked_by_tall_wall() {
    let mut world = new_world(gravity_config());
    world.spawn(
        BodyDesc::cuboid([20.0, 0.5, 20.0])
            .mass(0.0)
            .position([0.0, -0.5, 0.0]),
    );
    world.spawn(
        BodyDesc::cuboid([0.5, 3.0, 8.0])
            .mass(0.0)
            .position([2.0, 1.5, 0.0]),
    );
    let mut character = Character::spawn(&mut world, [0.0, 1.0, 0.0], CharacterDesc::default());
    walk_and_settle(&mut world, &mut character, 60, [1.0, 0.0, 0.0]);
    assert!(
        character.position()[0] < 2.0 - 0.3,
        "character must stop at the wall, got x={}",
        character.position()[0]
    );
}

#[test]
fn character_lands_after_jump() {
    let mut world = new_world(gravity_config());
    world.spawn(
        BodyDesc::cuboid([20.0, 0.5, 20.0])
            .mass(0.0)
            .position([0.0, -0.5, 0.0]),
    );
    let mut character = Character::spawn(&mut world, [0.0, 1.0, 0.0], CharacterDesc::default());
    world.step(DT);
    character.step(&mut world, DT, [0.0, 0.0, 0.0], true);
    world.wait();
    let mut apex = character.position()[1];
    for _ in 0..10 {
        world.step(DT);
        character.step(&mut world, DT, [0.0, 0.0, 0.0], false);
        apex = apex.max(character.position()[1]);
    }
    assert!(apex > 1.35, "jump must lift the character, got apex {apex}");
    for _ in 0..60 {
        world.step(DT);
        character.step(&mut world, DT, [0.0, 0.0, 0.0], false);
    }
    world.wait();
    assert!(
        character.grounded(),
        "character must land back on the ground"
    );
}

#[test]
fn character_pushes_dynamic_box() {
    let mut world = new_world(gravity_config());
    world.spawn(
        BodyDesc::cuboid([20.0, 0.5, 20.0])
            .mass(0.0)
            .position([0.0, -0.5, 0.0]),
    );
    let box_body = world.spawn(
        BodyDesc::cuboid([0.5, 0.75, 0.5])
            .position([1.6, 0.75, 0.0])
            .friction(0.6),
    );
    let mut character = Character::spawn(&mut world, [0.0, 1.0, 0.0], CharacterDesc::default());
    walk_and_settle(&mut world, &mut character, 100, [1.0, 0.0, 0.0]);
    let moved = world.read_state(box_body).position[0];
    assert!(
        moved > 1.75,
        "character must push the box forward, box at x={moved}"
    );
}

#[test]
fn character_climbs_walkable_slope() {
    let mut world = new_world(gravity_config());
    let angle = 20.0_f32.to_radians();
    let tilt = [0.0, 0.0, (angle * 0.5).sin(), (angle * 0.5).cos()];
    world.spawn(
        BodyDesc::cuboid([10.0, 0.5, 10.0])
            .mass(0.0)
            .position([0.0, -0.5, 0.0]),
    );
    let rise = 4.0 * angle.tan();
    world.spawn(
        BodyDesc::cuboid([4.0, 0.3, 4.0])
            .mass(0.0)
            .position([2.0, rise * 0.5 - 0.15, 0.0])
            .orientation(tilt),
    );
    let mut character = Character::spawn(
        &mut world,
        [0.0, 1.0, 0.0],
        CharacterDesc {
            slope_limit: 30.0_f32.to_radians(),
            ..Default::default()
        },
    );
    walk_and_settle(&mut world, &mut character, 60, [1.0, 0.0, 0.0]);
    let position = character.position();
    assert!(
        position[0] > 2.5,
        "character must advance up the ramp, got x={}",
        position[0]
    );
    assert!(
        position[1] > 1.4,
        "character must gain height on the ramp, got y={}",
        position[1]
    );
}

#[test]
fn character_queries_ride_the_world_step() {
    let mut world = new_world(gravity_config());
    world.spawn(
        BodyDesc::cuboid([20.0, 0.5, 20.0])
            .mass(0.0)
            .position([0.0, -0.5, 0.0]),
    );
    let mut character = Character::spawn(&mut world, [0.0, 1.0, 0.0], CharacterDesc::default());
    let body = character.body();
    for _ in 0..10 {
        world.step(DT);
        character.step(&mut world, DT, [1.0, 0.0, 0.0], false);
    }
    world.wait();
    let base = world.submissions();
    for _ in 0..10 {
        world.try_state(body);
        world.step(DT);
        character.step(&mut world, DT, [1.0, 0.0, 0.0], false);
    }
    assert_eq!(
        world.submissions() - base,
        10,
        "a character step must ride the world step instead of opening a submission"
    );
    assert!(
        character.grounded(),
        "a pipelined character must stay grounded on flat ground"
    );
}