feat(character): 16.8 - on land a physics walker moves the body (CharacterGround.walk); the package's own gravity, ballistic air and slope-probe refusal are gone

The package keeps the rules and hands them over each step: speed and heading, whether a jump
leaves, CHAR_STEP and the boots' slope limit. The walker (Jolt CharacterVirtual) does gravity,
landing, steps, slopes and following the ground down. Too steep is ground too steep facing the way
the body wanted to go; stepping off an edge is not. ludic.physics: phys_walker_move (follow the body
if something else moved it, then step), the slope limit, the jump always honoured. The character's
tests run on a real Jolt walker over the same fake world: 15 pass, a knee-high boulder now a step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 00:47:14 +03:00
parent d2cd775309
commit e909ec122c
11 changed files with 169 additions and 89 deletions

View file

@ -1,7 +1,7 @@
# ludic.character
One walking body under a player's stick, seen from over its shoulder: the gait's walk and run with
their acceleration, a jump with gravity, the step-versus-slope rule, wading that shelves into a swim,
their acceleration, a jump, the step-versus-slope rule, wading that shelves into a swim,
diving on a breath, sitting, one answer to "where can this body be put down", holds, being carried,
and the orbit camera with its first-person eyes. Uses [`ludic.base`](../ludic.base/README.md) and
nothing else.
@ -19,10 +19,13 @@ sounds and charges the energy.
- **A pure query of the world stays pure.** `CharacterGround.height` is the world's ground and never
the body's; what the feet can stand on is `char_stand_at(x, z, from_y)`, and only the movement asks.
- **A step up and a slope up are different refusals** (`char_slope_blocks`). Within `CHAR_STEP`
(0.55 m) of the feet is a step, taken whatever its gradient; above it the boots
(`slope_limit`, a rise per metre) decide over `CHAR_SLOPE_PROBE` (0.6 m), never over the frame's
own step - so 30 Hz and 240 Hz refuse at the same place.
- **On land the game's physics walker moves the body** (`CharacterGround.walk`, phase 16: a Jolt
CharacterVirtual in Maroon Lake). The package keeps the rules and hands them over each step: the
speed and heading, whether a jump leaves (at 5 m/s), `CHAR_STEP` (0.55 m) as the step taken
whatever its gradient, and the boots' `slope_limit` (a rise per metre) as the steepest ground.
The walker does gravity, landing, steps, slopes and following the ground down, so 30 Hz and
240 Hz refuse at the same place. `CHAR_WALK_STEEP` - ground too steep facing the way it wanted
to go - is the refusal and the `CHAR_TOO_STEEP` fact; stepping off an edge is not.
- **Where a body is put down is one question with one answer** (`char_safe_spot`): clear of every
collider, no deeper than asked, within `CHAR_DROP` of the height left, on ground it can walk off.
- **A collider has a height**: `push` takes the body's span (y0, y1), and `top_at` reports a top it
@ -43,6 +46,8 @@ export port CharacterGround {
slope: fn(float, float) -> float # 0 flat .. 1 wall (for sitting)
slope_limit: fn() -> float # the boots: rise a metre (1.05)
current: fn(float, float, float, float) -> float # (x, z, fx, fz): a stroke's share lost
walk: fn(x, y, z, vx, vz, jump, step, max_slope_deg, dt) -> int # the land mover (required)
walked_x / walked_y / walked_z: fn() -> float # ... to here
}
export port CharacterBody {
pace, sneaking, breath, riding, ride(ix, iz, run, dt), holding, hold_x, hold_z, eyes_ok
@ -52,7 +57,7 @@ export port CharacterInput {
}
```
Only `height` is required; unbound, the rest is a dry plain with no colliders, a fresh body and a
`height`, `walk` and `walked_*` are required (the tests bind a real `ludic.physics` walker); unbound, the rest is a dry plain with no colliders, a fresh body and a
still stick.
## API
@ -66,7 +71,7 @@ still stick.
| `char_hold(reason)`, `char_release(reason)` (`CHAR_HOLD_SCREEN`, `CHAR_HOLD_STAGE`) | holds by reason: two holders cannot release each other |
| `char_board(x, z, yaw)`, `char_ride_at(x, y, z, yaw, speed)`, `char_step_off(x, z)` | carried by something (`CharacterBody.riding`/`ride`) |
| `char_safe_spot(x, z, from_y, max_depth) -> bool`, `body_safe_x()`, `body_safe_z()` | somewhere to put a body down |
| `char_stand_at(x, z, from_y)`, `char_slope_blocks(nh, fx, fz)` | the step rule, for a game's own movers |
| `char_stand_at(x, z, from_y)` | the ground with a collider's top when it is a step, for a put-down |
| `char_camera(dt)`, `char_camera_third(dt)`, `char_look(yaw, pitch)`, `char_look_pitch`, `char_orbit(dist)`, `char_lock_orbit`, `char_set_fps`, `char_fps_toggle() -> CHAR_EYES_*` | the camera; read it back with `body_eye_x/y/z/yaw/pitch()` and `body_eyes_now()` |
| `body_x()`, `body_y()`, `body_z()`, `body_yaw()`, `body_speed()`, `body_moving()`, `body_run()`, `body_phase()`, `body_time()`, `body_water()`, `body_airborne()`, `body_vy()`, `body_landing()`, `body_steep()`, `body_step()`, `body_sitting()`, `body_sit_mode()`, `body_sit_t()`, `body_swimming()`, `body_diving()`, `body_depth()`, `body_breath()`, `body_breath_max()`, `body_floating()`, `body_swim_phase()`, `body_can_run()`, `body_held()`, `body_fps()`, `body_cam_yaw()`, `body_cam_pitch()`, `body_cam_dist()` | the queries |
| `char_wrap(a)`, `char_toward(cur, target, rate)`, `char_ease(t)` | the helpers a pose needs |

View file

@ -7,16 +7,6 @@ export function char_stand_at(x: float, z: float, from_y: float) -> float {
return CharacterGround.top_at(x, z, 0.35, Math.max(from_y, base), CHAR_STEP, base)
}
# A step up and a slope up are different refusals. Within CHAR_STEP of the feet is a step, taken
# whatever the gradient into it (a 45 cm deck arrives in one frame); above that the boots decide,
# measured over CHAR_SLOPE_PROBE, so the frame rate never changes the answer.
export function char_slope_blocks(character_st: CharacterState, nh: float, fx: float, fz: float) -> bool {
if nh - character_st.chr_y > CHAR_STEP { return true }
let sx = character_st.chr_x + fx * CHAR_SLOPE_PROBE
let sz = character_st.chr_z + fz * CHAR_SLOPE_PROBE
return CharacterGround.height(sx, sz) - character_st.chr_y > CHAR_SLOPE_PROBE * CharacterGround.slope_limit()
}
# a body of radius r at (x, z) from y0 to y1, out of every collider: the resolved point is chr_out_*
function chr_push(character_st: mut CharacterState, x: float, z: float, r: float, y0: float, y1: float) -> bool {
character_st.chr_ask_x = x

View file

@ -13,6 +13,13 @@ export port CharacterGround {
slope: fn(float, float) -> float = fn chr__slope_plain # 0 flat .. 1 a wall
slope_limit: fn() -> float = fn chr__boots # the rise a metre the boots take
current: fn(float, float, float, float) -> float = fn chr__still # (x, z, fx, fz): 0..1 of a stroke lost
# (x, y, z, vx, vz, jump, step, max_slope_deg, dt): the body on land moved from where it stands at
# (vx, vz) across, jump > 0 leaving the ground at that speed up; the ground after it (CHAR_WALK_*),
# and walked_x / _y / _z where it is. The game's physics walker; there is no default mover.
walk: fn(float, float, float, float, float, float, float, float, float) -> int
walked_x: fn() -> float
walked_y: fn() -> float
walked_z: fn() -> float
}
export port CharacterBody {

View file

@ -1,7 +1,12 @@
# rules.ludic - the numbers that are the body's rules, and what it reports as facts
export const CHAR_STEP: float = 0.55 # a rise within this of the feet is a step, whatever its gradient
export const CHAR_BODY: float = 1.8 # feet to head
# a slope is measured over this, never over the frame's own step: 60 Hz and 240 Hz refuse alike
# what CharacterGround.walk says of the ground after a step (the same order as ludic.physics')
export const CHAR_WALK_GROUND: int = 0
export const CHAR_WALK_STEEP: int = 1
export const CHAR_WALK_UNSUPPORTED: int = 2
export const CHAR_WALK_AIR: int = 3
# how far a put-down spot's ground is measured round it (char_safe_spot)
export const CHAR_SLOPE_PROBE: float = 0.6
export const CHAR_BREATH_MAX: float = 45.0 # seconds of air under the surface
export const CHAR_WADE_MAX: float = 1.35 # a bed deeper than this under the surface is a swim

View file

@ -1,18 +1,22 @@
# character_test.ludic - a body on a fake world: flat meadow, a 45 cm deck, a gentle rise and a cliff,
# a boulder with a step on top, and a lake that shelves. The step against the slope at every frame
# rate, wading into a swim and out, the dive and the breath, the jump, sitting, holds, the safe
# spot, riding and the camera.
# spot, riding and the camera. The walk is a real Jolt walker (ludic.physics) on the same world.
import "ludic.character"
import "ludic.base"
import "ludic.physics"
program CharacterTest {
numbers float
# the world: x in 4..8 is a deck 45 cm up; z < -10 a cliff; z > 10 a gentle rise; x > 20 the lake
function fk_height(x: float, z: float) -> float {
if x > 4.0 and x < 8.0 and z > -2.0 and z < 2.0 { return 0.45 }
return fk_land(x, z)
}
function fk_land(x: float, z: float) -> float {
if x > 20.0 { return -(x - 20.0) * 0.5 }
if z < -10.0 { return (-10.0 - z) * 2.0 }
if z > 10.0 { return (z - 10.0) * 0.3 }
if x > 4.0 and x < 8.0 and z > -2.0 and z < 2.0 { return 0.45 }
return 0.0
}
function fk_water(x: float, z: float) -> float {
@ -28,6 +32,11 @@ program CharacterTest {
sneaking: bool = false
pace: float = 1.0
mz: float = 0.0
boots: float = 1.05
walker: int = -1
wx: float = 0.0
wy: float = 0.0
wz: float = 0.0
}
function fk_ring(character_test_st: mut CharacterTestState, x: float, z: float, cx: float, cz: float, r: float) -> bool {
let dx = x - cx
@ -61,7 +70,35 @@ program CharacterTest {
function fk_move_z(character_test_st: CharacterTestState) -> float { return character_test_st.mz }
bind CharacterGround { height: fn fk_height, water: fn fk_water, push: fn fk_push, pushed_x: fn fk_px, pushed_z: fn fk_pz, top_at: fn fk_top }
# the same world in Jolt: the land as a heightfield, the deck a box, the boulder a 40 cm drum and
# the pillar a post
function fk_jolt(physics_st: mut PhysicsState) -> int {
phys_open(physics_st, 256, 1)
let n = 128
let h = floats(n * n)
for j in 0 .. n { for i in 0 .. n { h[j * n + i] = fk_land(float(i) - 40.0, float(j) - 60.0) } }
phys_ground_add(physics_st, phys_heightfield(physics_st, h, n, -40.0, -60.0, 1.0))
phys_static_add(physics_st, phys_offset(physics_st, phys_box(physics_st, 2.0, 0.225, 2.0), 0.0, 0.225, 0.0, 0.0), 6.0, 0.0, 0.0, 0.0)
phys_static_add(physics_st, phys_offset(physics_st, phys_cylinder(physics_st, 0.2, 1.0), 0.0, 0.2, 0.0, 0.0), 0.0, 0.0, 6.0, 0.0)
phys_static_add(physics_st, phys_offset(physics_st, phys_cylinder(physics_st, 3.0, 0.5), 0.0, 3.0, 0.0, 0.0), -6.0, 0.0, 0.0, 0.0)
phys_settle(physics_st)
return phys_walker_add(physics_st, 0.35, CHAR_BODY, 0.0, 0.0, 0.0, 50.0, 70.0)
}
function fk_walk(physics_st: mut PhysicsState, character_test_st: mut CharacterTestState, x: float, y: float, z: float, vx: float, vz: float, jump: float, step: float, slope: float, dt: float) -> int {
if character_test_st.walker < 0 { character_test_st.walker = fk_jolt(physics_st) }
let g = phys_walker_move(physics_st, character_test_st.walker, x, y, z, vx, vz, jump, step, slope, dt)
let p = phys_walker_pose(physics_st, character_test_st.walker)
character_test_st.wx = p.x
character_test_st.wy = p.y
character_test_st.wz = p.z
return g
}
function fk_wx(character_test_st: CharacterTestState) -> float { return character_test_st.wx }
function fk_wy(character_test_st: CharacterTestState) -> float { return character_test_st.wy }
function fk_wz(character_test_st: CharacterTestState) -> float { return character_test_st.wz }
function fk_boots(character_test_st: CharacterTestState) -> float { return character_test_st.boots }
bind CharacterGround { height: fn fk_height, water: fn fk_water, push: fn fk_push, pushed_x: fn fk_px, pushed_z: fn fk_pz, top_at: fn fk_top, walk: fn fk_walk, walked_x: fn fk_wx, walked_y: fn fk_wy, walked_z: fn fk_wz, slope_limit: fn fk_boots }
bind CharacterBody { riding: fn fk_riding, ride: fn fk_ride, sneaking: fn fk_sneak, pace: fn fk_pace, eyes_ok: fn fk_eyes }
bind CharacterInput { move_z: fn fk_move_z }
@ -117,10 +154,13 @@ program CharacterTest {
expect(body_y(character_st) > 1.0)
}
test "the boots set the limit: a rise they cannot take is refused" (character_st: mut CharacterState) {
test "the boots set the limit: a rise they cannot take is refused" (character_st: mut CharacterState, character_test_st: mut CharacterTestState) {
character_test_st.boots = 0.2
fresh(character_st, 0.0, 8.0, SOUTH)
walk(character_st, 0.2, 60.0, false)
expect(not char_slope_blocks(character_st, body_y(character_st) + 0.1, 0.0, 1.0))
walk(character_st, 3.0, 60.0, false)
expect(body_z(character_st) < 11.5)
expect(body_y(character_st) < 0.45)
expect(count(facts(character_st), CHAR_TOO_STEEP) >= 1)
}
test "the walk reaches the walk speed, the run the run speed, a sneak its own" (character_st: mut CharacterState, character_test_st: mut CharacterTestState) {
@ -141,10 +181,14 @@ program CharacterTest {
expect_near(body_speed(character_st), 1.3, 0.05)
}
test "a boulder is a wall to a body below its step and a step onto its top" (character_st: mut CharacterState) {
test "a knee-high boulder is a step onto its top, and a post is a wall" (character_st: mut CharacterState) {
fresh(character_st, 0.0, 3.0, SOUTH)
walk(character_st, 1.3, 60.0, false)
expect(body_z(character_st) > 5.2)
expect_near(body_y(character_st), 0.4, 0.03)
fresh(character_st, -3.0, 0.0, -EAST)
walk(character_st, 2.0, 60.0, false)
expect(body_z(character_st) < 5.1)
expect(body_x(character_st) > -5.2)
expect_near(char_stand_at(0.0, 6.0, 0.0), 0.4, 0.001)
expect_near(char_stand_at(3.0, 6.0, 0.0), 0.0, 0.001)
# and the world's own ground never includes it

View file

@ -4,66 +4,62 @@ function chr_travel(character_st: mut CharacterState, dx: float, dz: float, fx:
chr_swim_tick(character_st, dx, dz, jump, dt)
return character_st.chr_speed * dt
}
if character_st.chr_air { return chr_fly(character_st, dx, dz, dt) }
return chr_walk(character_st, fx, fz, jump, dt)
return chr_walk(character_st, fx, fz, dx, dz, jump, dt)
}
# ballistic: gravity, the takeoff's velocity carried, a little steering
function chr_fly(character_st: mut CharacterState, dx: float, dz: float, dt: float) -> float {
character_st.chr_vy = character_st.chr_vy - 14.0 * dt
character_st.chr_air_vx = character_st.chr_air_vx + dx * (4.0 * dt)
character_st.chr_air_vz = character_st.chr_air_vz + dz * (4.0 * dt)
chr_push(character_st, character_st.chr_x + character_st.chr_air_vx * dt, character_st.chr_z + character_st.chr_air_vz * dt, 0.35, character_st.chr_y, character_st.chr_y + CHAR_BODY)
character_st.chr_y = character_st.chr_y + character_st.chr_vy * dt
let ground = char_stand_at(character_st.chr_out_x, character_st.chr_out_z, character_st.chr_y)
character_st.chr_x = character_st.chr_out_x
character_st.chr_z = character_st.chr_out_z
if character_st.chr_y < ground and character_st.chr_vy < 0.0 {
character_st.chr_y = ground
character_st.chr_air = false
character_st.chr_land = 0.22
character_st.chr_speed = Math.sqrt(character_st.chr_air_vx * character_st.chr_air_vx + character_st.chr_air_vz * character_st.chr_air_vz)
chr_say(character_st, CHAR_LANDED, character_st.chr_speed)
}
return character_st.chr_speed * (dt * 0.4)
}
# on the ground: a step, unless a slope refuses it or the water takes the body; then a jump
function chr_walk(character_st: mut CharacterState, fx: float, fz: float, jump: bool, dt: float) -> float {
var dist = character_st.chr_speed * dt
if dist > 0.0 { dist = chr_walk_step(character_st, fx, fz, dist) } else { character_st.chr_y = CharacterGround.height(character_st.chr_x, character_st.chr_z) }
if jump and character_st.chr_can_run and character_st.chr_land < 0.05 {
character_st.chr_air = true
character_st.chr_vy = 5.0
character_st.chr_air_vx = fx * character_st.chr_speed
character_st.chr_air_vz = fz * character_st.chr_speed
# on land the game's walker moves the body (CharacterGround.walk): gravity, landing, a step up to
# CHAR_STEP, a slope up to what the boots take. The rules stay here: how fast, where, when a jump
# leaves, when the lake takes the body, and what is said about it.
function chr_walk(character_st: mut CharacterState, fx: float, fz: float, dx: float, dz: float, jump: bool, dt: float) -> float {
var vx = fx * character_st.chr_speed
var vz = fz * character_st.chr_speed
var up = 0.0
if character_st.chr_air {
character_st.chr_air_vx = character_st.chr_air_vx + dx * (4.0 * dt)
character_st.chr_air_vz = character_st.chr_air_vz + dz * (4.0 * dt)
vx = character_st.chr_air_vx
vz = character_st.chr_air_vz
} else if jump and character_st.chr_can_run and character_st.chr_land < 0.05 {
up = 5.0
character_st.chr_air_vx = vx
character_st.chr_air_vz = vz
chr_say(character_st, CHAR_JUMPED, 0.0)
}
return dist
let slope = Math.atan2(CharacterGround.slope_limit(), 1.0) * 57.29578
let g = CharacterGround.walk(character_st.chr_x, character_st.chr_y, character_st.chr_z, vx, vz, up, CHAR_STEP, slope, dt)
return chr_walked(character_st, g, up > 0.0, dt)
}
function chr_walk_step(character_st: mut CharacterState, fx: float, fz: float, dist: float) -> float {
chr_push(character_st, character_st.chr_x + fx * dist, character_st.chr_z + fz * dist, 0.35, character_st.chr_y, character_st.chr_y + CHAR_BODY)
let nx = character_st.chr_out_x
let nz = character_st.chr_out_z
let nh = char_stand_at(nx, nz, character_st.chr_y)
if char_slope_blocks(character_st, nh, fx, fz) {
if not (character_st.chr_steep > 0.0) { chr_say(character_st, CHAR_TOO_STEEP, nh - character_st.chr_y) }
# where the walker put the body, and what that means: the lake, a landing, a slope too steep
function chr_walked(character_st: mut CharacterState, g: int, jumped: bool, dt: float) -> float {
let nx = CharacterGround.walked_x()
let ny = CharacterGround.walked_y()
let nz = CharacterGround.walked_z()
if ny < CharacterGround.water(nx, nz) - CHAR_WADE_MAX {
chr_enter_water(character_st, nx, nz)
return character_st.chr_speed * dt
}
let was_air = character_st.chr_air
character_st.chr_air = g == CHAR_WALK_AIR or jumped
if was_air and not character_st.chr_air { chr_landed(character_st) }
if g == CHAR_WALK_STEEP and not character_st.chr_air {
if not (character_st.chr_steep > 0.0) { chr_say(character_st, CHAR_TOO_STEEP, ny - character_st.chr_y) }
character_st.chr_steep = 1.2
character_st.chr_speed = 0.0
return 0.0
}
# the lake shelves: shin-deep is a wade, and past CHAR_WADE_MAX it is a swim. Entering owns
# the position and the height this frame, or the body sits on the bed for one frame.
if nh < CharacterGround.water(nx, nz) - CHAR_WADE_MAX {
chr_enter_water(character_st, nx, nz)
return dist
}
let mx = nx - character_st.chr_x
let mz = nz - character_st.chr_z
character_st.chr_step = Math.sqrt(mx * mx + mz * mz)
character_st.chr_vy = (ny - character_st.chr_y) / Math.max(dt, 0.0001)
character_st.chr_x = nx
character_st.chr_y = ny
character_st.chr_z = nz
character_st.chr_y = nh
return dist
if character_st.chr_air { return character_st.chr_speed * (dt * 0.4) }
return character_st.chr_speed * dt
}
function chr_landed(character_st: mut CharacterState) -> void {
character_st.chr_land = 0.22
character_st.chr_speed = Math.sqrt(character_st.chr_air_vx * character_st.chr_air_vx + character_st.chr_air_vz * character_st.chr_air_vz)
chr_say(character_st, CHAR_LANDED, character_st.chr_speed)
}

Binary file not shown.

View file

@ -33,7 +33,7 @@ extern function jph_push(w: pointer, x: float, z: float, r: float, y0: float, y1
extern function jph_step_top(w: pointer, x: float, z: float, r: float, feet: float, reach: float, mask: int) -> float = "jph_step_top"
extern function jph_char_new(w: pointer, r: float, h: float, x: float, y: float, z: float, max_slope: float, mass: float) -> pointer = "jph_char_new"
extern function jph_char_free(k: pointer) -> void = "jph_char_free"
extern function jph_char_limits(k: pointer, step_up: float, stick: float) -> void = "jph_char_limits"
extern function jph_char_limits(k: pointer, step_up: float, stick: float, max_slope: float) -> void = "jph_char_limits"
extern function jph_char_step(w: pointer, k: pointer, dt: float, vx: float, vz: float, jump: float) -> int = "jph_char_step"
extern function jph_char_read(k: pointer, out: pointer) -> void = "jph_char_read"
extern function jph_char_place(w: pointer, k: pointer, x: float, y: float, z: float) -> void = "jph_char_place"

View file

@ -34,11 +34,31 @@ JPH_SHIM void jph_char_free(void *k) {
delete wk;
}
// how high a step it takes without a jump, and how far it follows the ground down
JPH_SHIM void jph_char_limits(void *k, float step_up, float stick) {
// how high a step it takes without a jump, how far it follows the ground down, and the steepest
// slope in degrees that is still ground
JPH_SHIM void jph_char_limits(void *k, float step_up, float stick, float max_slope) {
Walker *wk = static_cast<Walker *>(k);
wk->step_up = step_up;
wk->stick = stick;
wk->c->SetMaxSlopeAngle(DegreesToRadians(max_slope));
}
// what the step comes to: TOO STEEP (1) only when GROUND too steep for it faces against the way it
// wanted to go (a cliff, a rise the boots cannot take) - a still thing's edge is a step or a wall;
// steep ground merely under the feet, as when stepping off an edge, is UNSUPPORTED (2)
static int jph_char_says(World *ww, CharacterVirtual *c, float vx, float vz) {
int state = int(c->GetGroundState());
if (state == int(CharacterBase::EGroundState::InAir)) return state;
Vec3 want(vx, 0.0f, vz);
for (const CharacterContact &ct : c->GetActiveContacts()) {
if (!ct.mHadCollision || ct.mWasDiscarded || ct.mBodyB.IsInvalid()) continue;
if (ww->sys.GetBodyInterface().GetObjectLayer(ct.mBodyB) != L_GROUND) continue;
Vec3 n = ct.mSurfaceNormal;
bool slope = n.GetY() > 0.05f && c->IsSlopeTooSteep(n);
if (slope && Vec3(n.GetX(), 0.0f, n.GetZ()).Dot(want) < -1.0e-4f) return int(CharacterBase::EGroundState::OnSteepGround);
}
if (state == int(CharacterBase::EGroundState::OnSteepGround)) return int(CharacterBase::EGroundState::NotSupported);
return state;
}
// one step of dt: (vx, vz) the speed it wants across; jump > 0 leaves the ground at that speed up
@ -50,18 +70,17 @@ JPH_SHIM int jph_char_step(void *w, void *k, float dt, float vx, float vz, float
bool ground = c->GetGroundState() == CharacterBase::EGroundState::OnGround;
float vy = c->GetLinearVelocity().GetY() + g.GetY() * dt;
Vec3 v(vx, vy, vz);
if (ground) {
v = Vec3(vx, 0.0f, vz) + c->GetGroundVelocity();
if (jump > 0.0f) v.SetY(jump);
}
if (ground) v = Vec3(vx, 0.0f, vz) + c->GetGroundVelocity();
// whether a jump may leave is the game's rule; here it only leaves
if (jump > 0.0f) v.SetY(jump);
c->SetLinearVelocity(v);
CharacterVirtual::ExtendedUpdateSettings u;
u.mWalkStairsStepUp = Vec3(0.0f, wk->step_up, 0.0f);
u.mStickToFloorStepDown = Vec3(0.0f, -wk->stick, 0.0f);
u.mStickToFloorStepDown = Vec3(0.0f, jump > 0.0f ? 0.0f : -wk->stick, 0.0f);
IgnoreSingleBodyFilter self(c->GetInnerBodyID());
c->ExtendedUpdate(dt, g, u, ww->sys.GetDefaultBroadPhaseLayerFilter(L_MOVING), ww->sys.GetDefaultLayerFilter(L_MOVING),
self, {}, ww->temp);
return int(c->GetGroundState());
return jph_char_says(ww, c, vx, vz);
}
// position, velocity, the ground state (0 on ground, 1 too steep, 2 touching but unsupported,

View file

@ -38,7 +38,7 @@ program WalkerTest {
phys_static_add(physics_st, wall, 3.0, 0.0, 10.0, 0.0)
phys_settle(physics_st)
let a = phys_walker_add(physics_st, 0.35, 1.8, 0.0, 0.0, 0.0, 50.0, 70.0)
phys_walker_limits(physics_st, a, 0.55, 0.5)
phys_walker_limits(physics_st, a, 0.55, 0.5, 50.0)
walk(physics_st, a, 1.5, 2.0, 0.0)
let pa = phys_walker_pose(physics_st, a)
expect(pa.x > 2.5)

View file

@ -31,10 +31,11 @@ export function phys_walker_add(physics_st: mut PhysicsState, r: float, h: float
return len(physics_st.ph_walkers) - 1
}
# how high a step it takes without a jump, and how far it follows the ground down
export function phys_walker_limits(physics_st: PhysicsState, id: int, step_up: float, stick: float) -> void {
# how high a step it takes without a jump, how far it follows the ground down, and the steepest
# slope in degrees that is still ground
export function phys_walker_limits(physics_st: PhysicsState, id: int, step_up: float, stick: float, max_slope: float) -> void {
let k = ph_walker(physics_st, id)
if k != null { jph_char_limits(k, step_up, stick) }
if k != null { jph_char_limits(k, step_up, stick, max_slope) }
}
export function phys_walker_remove(physics_st: mut PhysicsState, id: int) -> void {
@ -72,3 +73,16 @@ export function phys_walker_place(physics_st: mut PhysicsState, id: int, x: floa
let k = ph_walker(physics_st, id)
if k != null and physics_st.ph_world != null { jph_char_place(physics_st.ph_world, k, x, y, z) }
}
# a body the game keeps elsewhere, moved by its walker: put where the body is if something else
# moved it (a teleport, a ride, a swim), then one step under the given limits; the ground after it
export function phys_walker_move(physics_st: mut PhysicsState, id: int, x: float, y: float, z: float, vx: float, vz: float, jump: float, step: float, max_slope: float, dt: float) -> int {
let p = phys_walker_pose(physics_st, id)
if p == null { return PHYS_IN_AIR }
let dx = p.x - x
let dy = p.y - y
let dz = p.z - z
if dx * dx + dy * dy + dz * dz > 0.0025 { phys_walker_place(physics_st, id, x, y, z) }
phys_walker_limits(physics_st, id, step, 0.5, max_slope)
return phys_walker_step(physics_st, id, dt, vx, vz, jump)
}