NEON-29: Ledge fall — multi foot probes + zero snap when void
Single center ray + flat floor-normal gate missed tilted lip contacts and still hit deck under capsule center while leading edge hung over void. Moving FLOOR_SNAP_MOVING then cancelled gravity each tick. Sample down rays at center and offsets along walk direction; depth 0.32; set floor_snap_length 0 when is_on_floor but no probe hit.pull/41/head
parent
6cb66d7567
commit
7666cce7e5
|
|
@ -55,7 +55,7 @@ Elevated walkables use **distinct albedo tints** in `scenes/main.tscn` so screen
|
|||
|
||||
With the game server running ([`server/README.md`](../server/README.md)), each valid floor click sends a **`MoveCommand`** (**`POST`**) and a follow-up **`GET`** for **`PositionState`**. The server still **snaps** authority to the target (NEON-4/19); the client **moves** toward that verified position using **`NavigationAgent3D`** + a **baked mesh** instead of teleporting on the **`GET`** (NEON-8). **Boot** `sync_from_server()` **snaps** once so spawn matches the server.
|
||||
|
||||
**Tradeoff (prototype):** Horizontal motion is a **direct XZ bee-line** toward the server’s verified target. The baked **`NavigationMesh`** is still used when **vertical routing** is active (goal surface clearly above/below the feet, with Schmitt latch) **and** the capsule is inside the **nav column** hysteresis band (`NAV_COLUMN_STEER_ENTER_DIST` / `EXIT`) — so terraces / steps steer via **`NavigationAgent3D.get_next_path_position()`** toward the next waypoint. (A prior **manual** path scan with a **5 cm** “skip near waypoints” threshold caused **180° velocity flips** each physics tick; **`get_next_path_position()`** is **not** used as the **default** bee-line steer for all walking — that still regressed under **Jolt** at **120 Hz** when it was global.) **No descend bypass** for gravity while on an upper deck (same as before). While walking, **gravity** still runs when **`is_on_floor()`** is false; if **snap / lip contacts** keep **`is_on_floor()`** true over open space (e.g. gold deck → gray floor), a short **down probe** under the feet (`WALK_SUPPORT_PROBE_DEPTH` in **`player.gd`**) forces **airborne gravity** when no **upward-facing** hit is that close — without tying gravity to “goal below feet” on the whole upper pad. **Automatic routing around tall obstacles on one click is still not guaranteed** — use **several clicks** to go around e.g. the gray **`Obstacle`** when needed.
|
||||
**Tradeoff (prototype):** Horizontal motion is a **direct XZ bee-line** toward the server’s verified target. The baked **`NavigationMesh`** is still used when **vertical routing** is active (goal surface clearly above/below the feet, with Schmitt latch) **and** the capsule is inside the **nav column** hysteresis band (`NAV_COLUMN_STEER_ENTER_DIST` / `EXIT`) — so terraces / steps steer via **`NavigationAgent3D.get_next_path_position()`** toward the next waypoint. (A prior **manual** path scan with a **5 cm** “skip near waypoints” threshold caused **180° velocity flips** each physics tick; **`get_next_path_position()`** is **not** used as the **default** bee-line steer for all walking — that still regressed under **Jolt** at **120 Hz** when it was global.) **No descend bypass** for gravity while on an upper deck (same as before). While walking, **gravity** still runs when **`is_on_floor()`** is false; if **snap / lip contacts** keep **`is_on_floor()`** true over open space (e.g. gold deck → gray floor), **`player.gd`** casts **several short down rays** under the **foot disk** (center + offsets along move direction so the **leading** edge off a deck is tested) and applies **gravity** when **none** hit upward-facing ground within **`WALK_SUPPORT_PROBE_DEPTH`**. In that case **`floor_snap_length`** is set to **0** for the tick so **moving snap** does not cancel **`velocity.y`** on every physics step. **Automatic routing around tall obstacles on one click is still not guaranteed** — use **several clicks** to go around e.g. the gray **`Obstacle`** when needed.
|
||||
|
||||
**NEON-7 / movement QA bumps:** On **run**, **`spawn_short_random_bumps`** adds **two** green cylinders, each on its own **`StaticBody3D`** **sibling** of **`Floor`** under **`NavigationRegion3D`** (**`walkable`** on bump roots — avoids compound **internal-edge** jitter vs floor+cylinder on one body). Bump meshes use Godot group **`random_floor_bump_mesh`**. **Collision radius** = mesh **+ `COLLISION_RADIUS_EXTRA`** (see **`scripts/random_floor_bump_collision_constants.gd`**, capped by **`COLLISION_RADIUS_MAX`**). **`bake_navigation_mesh(false)`** after spawn.
|
||||
|
||||
|
|
|
|||
|
|
@ -66,11 +66,9 @@ const WALK_STEP_ASSIST_MAX_SURFACE_DELTA: float = 0.35
|
|||
## Ray below capsule feet while walking: if [method CharacterBody3D.is_on_floor] stays true off a
|
||||
## ledge (snap + lip contacts), we still need gravity toward the lower floor — without re-applying
|
||||
## gravity for every “goal below feet” tick on a whole upper deck (see [member _apply_walk_air_gravity]).
|
||||
const WALK_SUPPORT_PROBE_DEPTH: float = 0.22
|
||||
## Down-ray length under the capsule foot disk while walking (~[member NavigationMesh.agent_max_climb]).
|
||||
const WALK_SUPPORT_PROBE_DEPTH: float = 0.32
|
||||
const WALK_SUPPORT_PROBE_MIN_UP_DOT: float = 0.42
|
||||
## Only second-guess [code]is_on_floor()[/code] when the reported floor is fairly level (skip
|
||||
## stair risers / seam normals where a down ray often hits vertical mesh).
|
||||
const WALK_LEDGE_PROBE_MAX_FLOOR_UP_DOT: float = 0.92
|
||||
## CapsuleShape3D in scene: height = 1.0 (cylinder portion), radius = 0.4.
|
||||
## Total half-height from body origin to physical bottom = 0.5 + 0.4 = 0.9
|
||||
## (`CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS`).
|
||||
|
|
@ -324,15 +322,31 @@ func _set_horizontal_velocity_toward(
|
|||
velocity.y = 0.0
|
||||
|
||||
|
||||
func _walk_has_close_floor_probe_below() -> bool:
|
||||
var w3d := get_world_3d()
|
||||
if w3d == null:
|
||||
return true
|
||||
var space := w3d.direct_space_state
|
||||
func _walk_probe_xz_offsets(move_dir_xz: Vector2) -> Array[Vector2]:
|
||||
var offs: Array[Vector2] = [Vector2.ZERO]
|
||||
var len2: float = move_dir_xz.length_squared()
|
||||
var r: float = PLAYER_CAPSULE_RADIUS
|
||||
if len2 > 1e-8:
|
||||
var inv_len: float = 1.0 / sqrt(len2)
|
||||
var f := Vector2(move_dir_xz.x * inv_len, move_dir_xz.y * inv_len)
|
||||
var perp := Vector2(-f.y, f.x)
|
||||
offs.append(f * r * 0.58)
|
||||
offs.append(f * r * 0.30 + perp * r * 0.42)
|
||||
offs.append(f * r * 0.30 - perp * r * 0.42)
|
||||
else:
|
||||
offs.append(Vector2(1.0, 0.0) * r * 0.52)
|
||||
offs.append(Vector2(-1.0, 0.0) * r * 0.52)
|
||||
offs.append(Vector2(0.0, 1.0) * r * 0.52)
|
||||
offs.append(Vector2(0.0, -1.0) * r * 0.52)
|
||||
return offs
|
||||
|
||||
|
||||
func _walk_ray_hit_up_floor(space: PhysicsDirectSpaceState3D, off_xz: Vector2) -> bool:
|
||||
var feet: Vector3 = global_position - Vector3(0.0, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS, 0.0)
|
||||
# Start slightly above the foot line so the ray is not born inside the deck collider after snap.
|
||||
var probe_from: Vector3 = feet + Vector3.UP * 0.04
|
||||
var probe_end: Vector3 = feet + Vector3.DOWN * WALK_SUPPORT_PROBE_DEPTH
|
||||
var probe_from := Vector3(feet.x + off_xz.x, feet.y + 0.04, feet.z + off_xz.y)
|
||||
var probe_end := Vector3(
|
||||
feet.x + off_xz.x, feet.y - WALK_SUPPORT_PROBE_DEPTH, feet.z + off_xz.y
|
||||
)
|
||||
var q := PhysicsRayQueryParameters3D.create(probe_from, probe_end)
|
||||
q.collision_mask = collision_mask
|
||||
q.exclude = [get_rid()]
|
||||
|
|
@ -345,29 +359,47 @@ func _walk_has_close_floor_probe_below() -> bool:
|
|||
return n.dot(Vector3.UP) > WALK_SUPPORT_PROBE_MIN_UP_DOT
|
||||
|
||||
|
||||
## True if **any** sample under the foot disk hits walk-like ground within [member WALK_SUPPORT_PROBE_DEPTH].
|
||||
## Uses [param move_dir_xz] so the **leading** edge off a deck is checked (center-only rays stayed over mesh).
|
||||
func _walk_has_close_floor_probe_below(move_dir_xz: Vector2) -> bool:
|
||||
var w3d := get_world_3d()
|
||||
if w3d == null:
|
||||
return true
|
||||
var space: PhysicsDirectSpaceState3D = w3d.direct_space_state
|
||||
for off2: Vector2 in _walk_probe_xz_offsets(move_dir_xz):
|
||||
if _walk_ray_hit_up_floor(space, off2):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
## Airborne walk ticks only. Do **not** add gravity just because the nav goal is below the
|
||||
## feet — that stays true for the whole cross of a raised pad (e.g. TerracePlatformA / B)
|
||||
## toward a floor click and poisons horizontal motion / slide resolution.
|
||||
## Do apply gravity when [code]is_on_floor()[/code] is still true but there is no walkable surface
|
||||
## within [member WALK_SUPPORT_PROBE_DEPTH] under the feet (ledge / void while moving).
|
||||
func _apply_walk_air_gravity(delta: float) -> void:
|
||||
## within [member WALK_SUPPORT_PROBE_DEPTH] under the foot disk (ledge / void while moving).
|
||||
## [param move_dir_xz] steers multi-sample rays (see [method _walk_probe_xz_offsets]).
|
||||
func _apply_walk_air_gravity(delta: float, move_dir_xz: Vector2 = Vector2.ZERO) -> void:
|
||||
var apply_gravity: bool = not is_on_floor()
|
||||
if (
|
||||
not apply_gravity
|
||||
and _has_walk_goal
|
||||
and is_on_floor()
|
||||
and get_floor_normal().dot(Vector3.UP) > WALK_LEDGE_PROBE_MAX_FLOOR_UP_DOT
|
||||
and not _walk_has_close_floor_probe_below()
|
||||
and not _walk_has_close_floor_probe_below(move_dir_xz)
|
||||
):
|
||||
apply_gravity = true
|
||||
if apply_gravity:
|
||||
velocity += get_gravity() * delta
|
||||
|
||||
|
||||
func _walk_floor_snap_length(feet_y: float, want_goal_h: Vector3) -> float:
|
||||
func _walk_floor_snap_length(
|
||||
feet_y: float, want_goal_h: Vector3, move_dir_xz: Vector2 = Vector2.ZERO
|
||||
) -> float:
|
||||
if _step_assist_active:
|
||||
return WALK_STEP_ASSIST_SNAP
|
||||
if _has_walk_goal:
|
||||
# Strong moving snap + lip [code]is_on_floor()[/code] otherwise cancels gravity every tick.
|
||||
if is_on_floor() and not _walk_has_close_floor_probe_below(move_dir_xz):
|
||||
return 0.0
|
||||
if want_goal_h.length_squared() > 1e-10:
|
||||
var want: Vector3 = want_goal_h.normalized()
|
||||
# Climb: cap snap only for a **meaningful** rise (not shallow tread-to-tread), or we
|
||||
|
|
@ -864,8 +896,12 @@ func _physics_process(delta: float) -> void:
|
|||
else:
|
||||
_set_horizontal_velocity_toward(_auth_walk_goal, want_goal_h)
|
||||
|
||||
_apply_walk_air_gravity(delta)
|
||||
floor_snap_length = _walk_floor_snap_length(feet_y, want_goal_h)
|
||||
var walk_move_dir_xz := Vector2(want_goal_h.x, want_goal_h.z)
|
||||
if walk_move_dir_xz.length_squared() < 1e-10:
|
||||
walk_move_dir_xz = Vector2(velocity.x, velocity.z)
|
||||
|
||||
_apply_walk_air_gravity(delta, walk_move_dir_xz)
|
||||
floor_snap_length = _walk_floor_snap_length(feet_y, want_goal_h, walk_move_dir_xz)
|
||||
move_and_slide()
|
||||
_after_walk_move_and_slide()
|
||||
_clear_step_assist_after_walk_move()
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ No new automated GDScript tests are added for this story — there is no new GDS
|
|||
- **Lip stuck (floor→platform / platform→floor):** QA screenshots for this were on **`TerracePlatformA`** (10 × 10 large SE pad, **0.3 m** single step — not the NW **Terrace B** step/platform pair). Step assist required **`vel_h.dot(want) ≤ 0.52`**, so a **head-on** push into the vertical face (dot ≈ 1) **never** triggered the lift. **Removed** that gate. For **platform→floor**, **`floor_block_on_wall = false`** only when the nav goal is **below** the feet **and** **`is_on_wall()`** or a **wall-ish slide normal** exists, so open deck walking stays stable.
|
||||
- **Teal pad still stuck (follow-up):** Step assist used **slide normals only**; Jolt often exposes the lip via **`is_on_wall()`** / **`get_wall_normal()`** instead — re-added a **directional** wall check (`wnh.dot(want) < -0.02`). Descend relax now also triggers on **shallow floor normal** (`get_floor_normal().dot(UP) < 0.992`) and slightly wider vertical-ish slide band. **`TerracePlatformA`** root **Y = 0.012 m** breaks **coplanarity** with the main floor without the larger gap that hurt **TerraceStepB** approach.
|
||||
- **Nav waypoint steering dropped (movement):** Following **`get_next_path_position()`** by default caused **near-zero** horizontal progress under **Jolt** + **120 Hz** (waypoint advance / finished edge cases), read as universal **stuck** movement even with the server up. **`player.gd`** now **bee-lines in XZ** toward **`_auth_walk_goal`** and uses path-based steer only when **`needs_vertical_routing`** (Schmitt **`WALK_VERT_ROUTE_LATCH_*`**) **and** **`NAV_COLUMN_STEER_*`** hysteresis. **`_set_horizontal_velocity_from_nav_path_or_goal`** uses **`get_next_path_position()`**; a prior **5 cm** path-point scan caused **180° velocity flips** when Jolt nudged the capsule across the threshold each tick.
|
||||
- **Walk ledge / void gravity (resolved):** **`_apply_walk_air_gravity`** only ran when **`not is_on_floor()`**. **`floor_snap_length`** + edge contacts could keep **`is_on_floor()`** true while moving horizontally over a gap to a **lower** floor (purple → gold → gray), so **`velocity.y`** stayed **0** from steering until arrival — capsule **coasted at deck height**. **Fix:** short **physics ray** under the feet (**`WALK_SUPPORT_PROBE_DEPTH`**) with upward-facing normal test; if no close hit while the reported floor is still fairly level, apply gravity anyway during **`_has_walk_goal`** (does **not** reintroduce global “goal below feet” gravity on a whole terrace deck).
|
||||
- **Walk ledge / void gravity (iterated):** **`is_on_floor()`** + **`floor_snap_length`** could keep the capsule **glued** while crossing void to a lower click. **Fix:** **multi-sample** down rays under the **foot disk** (offsets along **move direction**; depth ~**0.32**); apply gravity when **none** hit; **`floor_snap_length = 0`** that tick so snap does not cancel **`velocity.y`**. Dropped a **floor-normal** gate that skipped the probe on **lip** contacts.
|
||||
- **Step assist vs `agent_max_climb`:** Nav **`agent_max_climb = 0.35`** only constrained mesh links; **bee-line + chained step assist** could still climb **TerracePlatformB** (~**0.6 m** from floor) in one click. **`_try_walk_step_assist`** now returns false when **`_auth_walk_goal.y > feet + WALK_STEP_ASSIST_MAX_SURFACE_DELTA`** (**0.35**, same as the mesh).
|
||||
- **Teal cardinal / tread shake (rollback):** Aggressive tweaks (**0.16** lift, **1**-tick cooldown, **`descending_stall`** toggling **`floor_block_on_wall`**, **seam-based** step-assist clear, **walk-stall nav replan**) caused **oscillation** on the small approach treads. **Reverted** to **`WALK_STEP_ASSIST_DELTA = 0.11`**, **`WALK_STEP_ASSIST_COOLDOWN_TICKS = 8`**, **no** stall replan, **no** `descending_stall` branch, **no** seam-based assist clear — only the original clear (clean floor without wall, or airborne with no slides). **Climb** wallish snap cap applies only when **`goal.y > feet_y + 0.12`** so shallow tread-to-tread motion is not capped to assist snap every frame.
|
||||
- **Teal pad — geometry wins:** Script-only mitigations still failed in practice; **`TerracePlatformA_Approach`** adds **twelve** `walkable` **`StaticBody3D`** treads (three per cardinal) with **~0.104 m** rise each, **darker teal** albedo, flush to the platform lip — nav + `CharacterBody3D` use **sloped contact** instead of fighting a **single vertical face**. **Follow-up:** tread **run** was **0.45 m** along the climb axis while the capsule **diameter is 0.8 m** — the body was wider than each tread, causing straddle / stuck motion when **entering** and blocking horizontal progress toward the lip when **exiting**. Treads widened to **1.0 m** run with positions re-centered. **`NavigationAgent3D`** `path_desired_distance` / `target_desired_distance` were briefly lowered to **0.22** but that caused **floor** movement to **stall**; **reverted to 0.35**.
|
||||
|
|
|
|||
Loading…
Reference in New Issue