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:
parent
d2cd775309
commit
e909ec122c
11 changed files with 169 additions and 89 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
BIN
packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib
(Stored with Git LFS)
BIN
packages/ludic.physics/lib/macos-arm64/libludicjolt.dylib
(Stored with Git LFS)
Binary file not shown.
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue