diff --git a/packages/ludic.character/README.md b/packages/ludic.character/README.md index 56f6a8ca..062fbf43 100644 --- a/packages/ludic.character/README.md +++ b/packages/ludic.character/README.md @@ -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 | diff --git a/packages/ludic.character/ground.ludic b/packages/ludic.character/ground.ludic index d178da10..bf9a5fa3 100644 --- a/packages/ludic.character/ground.ludic +++ b/packages/ludic.character/ground.ludic @@ -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 diff --git a/packages/ludic.character/ports.ludic b/packages/ludic.character/ports.ludic index 9c1eb472..0bd768f4 100644 --- a/packages/ludic.character/ports.ludic +++ b/packages/ludic.character/ports.ludic @@ -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 { diff --git a/packages/ludic.character/rules.ludic b/packages/ludic.character/rules.ludic index d630e4db..fc7e1073 100644 --- a/packages/ludic.character/rules.ludic +++ b/packages/ludic.character/rules.ludic @@ -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 diff --git a/packages/ludic.character/tests/character_test.ludic b/packages/ludic.character/tests/character_test.ludic index 11904354..d4e384a2 100644 --- a/packages/ludic.character/tests/character_test.ludic +++ b/packages/ludic.character/tests/character_test.ludic @@ -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 diff --git a/packages/ludic.character/walk.ludic b/packages/ludic.character/walk.ludic index 9a986f29..ee82a287 100644 --- a/packages/ludic.character/walk.ludic +++ b/packages/ludic.character/walk.ludic @@ -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) } diff --git a/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib b/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib index a84c85ec..3dd4778a 100755 --- a/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib +++ b/packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60f4c921a012a69e9bda29488e9ee64150ebc8f249b71c7fa1d166e78245d90a -size 1160704 +oid sha256:2191f0eb04f4bd5553495ae94f16c668377b98beea1a90bcc1847b43ee1a24bf +size 1160768 diff --git a/packages/ludic.physics/native.ludic b/packages/ludic.physics/native.ludic index c147b0bd..1961db78 100644 --- a/packages/ludic.physics/native.ludic +++ b/packages/ludic.physics/native.ludic @@ -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" diff --git a/packages/ludic.physics/native/shim/jph_character.inl b/packages/ludic.physics/native/shim/jph_character.inl index 1655b979..a8ebcd3b 100644 --- a/packages/ludic.physics/native/shim/jph_character.inl +++ b/packages/ludic.physics/native/shim/jph_character.inl @@ -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(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, diff --git a/packages/ludic.physics/tests/walker_test.ludic b/packages/ludic.physics/tests/walker_test.ludic index 77cd5e27..1917b8b1 100644 --- a/packages/ludic.physics/tests/walker_test.ludic +++ b/packages/ludic.physics/tests/walker_test.ludic @@ -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) diff --git a/packages/ludic.physics/walker.ludic b/packages/ludic.physics/walker.ludic index 58706e93..04c1b55b 100644 --- a/packages/ludic.physics/walker.ludic +++ b/packages/ludic.physics/walker.ludic @@ -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) +}