Refactor ragdoll recovery system: implement joint bend limits and recovery path overrides
- Updated `DEBUG_RECOVERY` to false for production. - Introduced `RAGDOLL_JOINT_BAND_LIMITS` for customizable joint bend limits based on facing profile. - Added `RECOVERY_LEG_SIDE_FORCE` for manual control of leg IK bend direction during recovery. - Implemented `RECOVERY_JOINT_PATH` to allow optional tween path overrides for elbow/knee recovery. - Removed debug print statements related to recovery visuals to clean up output. - Adjusted `_apply_ragdoll_joint_limits` to utilize new bend limits configuration. - Enhanced documentation in `ragdoll-cleanup.md` to reflect changes in the recovery pipeline and tuning tables.
This commit is contained in:
@@ -235,7 +235,16 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
NORMAL/INVERTED/INVERTED/NORMAL = FORWARD). Recovery exports: `rest_timeout` (2.0 s),
|
||||
`auto_recover` (true), plus recovery constants
|
||||
`STAND_POSE`/`IK_TARGET_PATHS`/`REST_LINEAR_THRESHOLD`/`REST_ANGULAR_THRESHOLD`/
|
||||
`STAND_UP_DURATION`/`STABILIZATION_DELAY`/`RAGDOLL_TARGET_SOFTNESS`.
|
||||
`STAND_UP_DURATION`/`STABILIZATION_DELAY`/`RAGDOLL_TARGET_SOFTNESS`. The
|
||||
**Joint bend configuration (EDIT ME)** block holds every per-joint bend decision of the
|
||||
ragdoll cycle, keyed by facing profile, so joints can be tuned without touching pipeline
|
||||
code: `RAGDOLL_JOINT_BAND_LIMITS[profile][joint] = {"lower": deg, "upper": deg}` (the
|
||||
fall's knee/elbow `PinJoint2D` band — 0 = straight, small side ~5 resists hyperextension,
|
||||
large side ~150 is the fold; missing entries fall back to the `RAGDOLL_JOINTS` `limit`
|
||||
string), `RECOVERY_LEG_SIDE_FORCE[profile][leg]` (optional leg IK `flip_bend_direction`
|
||||
override; missing = match the captured fall pose's knee side), and
|
||||
`RECOVERY_JOINT_PATH[profile][joint]` (optional `"shortest"` elbow/knee tween-path
|
||||
override; missing = pass through straight when the shortest path would cross ±180°).
|
||||
Signals `facing_profile_changed(profile)` /
|
||||
`bend_flag_changed(joint, flipped)`. Public API: `set_facing_profile`/`get_facing_profile`,
|
||||
`set_joint_bend_flipped`/`get_joint_bend_flipped`, `get_bend_joints()`, and
|
||||
@@ -283,56 +292,86 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
RAGDOLL` + emits. `_build_ragdoll()` creates a `Node2D`
|
||||
container `"RagdollBodyContainer"` under the rig's **parent** (world root; fallback
|
||||
`get_tree().current_scene`) and populates it from the `RAGDOLL_BODIES` table (**10**
|
||||
`RigidBody2D`: torso `CapsuleShape2D` radius 12 mass 8.0, head `CircleShape2D` radius 100 mass
|
||||
2.0, limb capsules radius 8 masses 1.0–2.0; `collision_layer`/`collision_mask` = 1, bodies
|
||||
spawn fully visible) and the `RAGDOLL_JOINTS` table (**9** `PinJoint2D`, one per
|
||||
non-root body pinned at the child bone's origin, `softness = RAGDOLL_TARGET_SOFTNESS` at
|
||||
build). Angular limits via `_apply_ragdoll_joint_limits()`: `elbow_knee` folds +CW
|
||||
`-5°..+150°`, `elbow_knee_ccw` `-150°..+5°`, `shoulder_hip` ±160°, default (neck) free. Cached
|
||||
momentum is applied to the torso body. `apply_ragdoll_velocity_boost(velocity)` applies the
|
||||
same velocity delta (mass-scaled `apply_central_impulse`) to every ragdoll body — used by the
|
||||
harness "Knock Up" button. All ragdoll nodes are spawned procedurally — `master_rig.tscn` is
|
||||
**not** modified.
|
||||
`RigidBody2D`: torso `CapsuleShape2D` radius 12 mass 8.0, head `CircleShape2D` radius 100 mass
|
||||
2.0, limb capsules radius 8 masses 1.0–2.0; `collision_layer`/`collision_mask` = 1, bodies
|
||||
spawn fully visible) and the `RAGDOLL_JOINTS` table (**9** `PinJoint2D`, one per
|
||||
non-root body pinned at the child bone's origin, `softness = RAGDOLL_TARGET_SOFTNESS` at
|
||||
build). Each body clones the rig's own mounted visual geometry into the ragdoll body frame
|
||||
via `_clone_part_visuals()` (reads the entry's `visual_path` `Body/*` node, copies each
|
||||
`Line2D`/`Polygon2D` child with points baked into the body's local space), falling back to a
|
||||
gray capsule / white circle when the part node is missing or has no mounted shapes. Angular
|
||||
limits via `_apply_ragdoll_joint_limits()`: elbow/knee bands come from the editable
|
||||
`RAGDOLL_JOINT_BAND_LIMITS` table (per profile, per joint) — FORWARD arms `{-150°, +5°}`,
|
||||
legs `{-5°, +150°}`; LEFT/RIGHT override both sides (e.g. locked `{60°, 100°}`); the
|
||||
`elbow_knee`/`elbow_knee_ccw` `limit` strings are only the fallback for joints missing from
|
||||
the table. Shoulder/hip ±160°, neck free. Cached
|
||||
momentum is applied to the torso body. `apply_ragdoll_velocity_boost(velocity)` applies the
|
||||
same velocity delta (mass-scaled `apply_central_impulse`) to every ragdoll body — used by the
|
||||
harness "Knock Up" button. All ragdoll nodes are spawned procedurally — `master_rig.tscn` is
|
||||
**not** modified.
|
||||
- **Phase 11 instant handoff + recovery:** `_update_rest_detection()` (only when `state ==
|
||||
RAGDOLL`)
|
||||
reads `_ragdoll_bodies["torso"]`: when its linear/angular velocity drops below
|
||||
`REST_LINEAR_THRESHOLD`/`REST_ANGULAR_THRESHOLD` it accumulates `_rest_timer`; after
|
||||
`rest_timeout` (and a `STABILIZATION_DELAY` hold) with `auto_recover` on it calls
|
||||
`_start_recovery()`. `_start_recovery()` (also `request_recovery()`, no-op unless in
|
||||
RAGDOLL) captures the 10 bodies' rig-local `{pos, rot, half}` into `_captured_pose` (`half` =
|
||||
each capsule's half-length from build-time metadata) plus a landing anchor
|
||||
(`_captured_landing_center` = the ragdoll torso's world center, `_captured_ground_y` =
|
||||
`torso.center.y + RAGDOLL_TORSO_RADIUS`), `_reanchor_root_to_landing()`s (translates the rig
|
||||
root so the standing figure's **feet** sit on the ground at the landing X — `feet =
|
||||
(_captured_landing_center.x, _captured_ground_y)`, `new_root = feet + FOOT_OFFSET` — and
|
||||
re-bases the captured rig-local positions by the root shift; the figure stands up **in place,
|
||||
on the ground**, where the ragdoll landed, since the ragdoll bodies live under a world sibling
|
||||
and the root never moved while it fell), `_destroy_ragdoll()`s,
|
||||
sets `state = RECOVERING` + emits, then `_snap_skeleton_to_pose()` — a **marker-driven** snap
|
||||
writing `IK_Targets/Torso.position`/`.rotation`, `IK_Targets/Head.position`, and the 4 limb
|
||||
markers (never the slaved `Torso` Bone2D), re-showing
|
||||
`Body/*`, then calling `_rearm_ik_stack()` — a defensive re-setup (re-sets up the
|
||||
`SkeletonModificationStack2D` when it reports `!get_is_setup()`, re-asserts `enabled = true`
|
||||
and `Skeleton2D.set_process_internal(true)`, and re-asserts the Torso marker's
|
||||
`RemoteTransform2D` update-position/rotation/scale flags) so the stack reliably resumes solving
|
||||
toward the end-effectors after a disable→enable toggle. Runtime diagnosis of the re-arm is
|
||||
gated behind `const DEBUG_RECOVERY := false` (off by default; `_recovery_dbg()` traces stack
|
||||
enabled/setup/internal + bone-following state). Snap
|
||||
geometry: the ragdoll capsules span joint origin→tip along their +X, so the **hip** is derived
|
||||
as `torso.pos − spine_dir·half` and the wrist/ankle targets as `lower_body.pos + dir·half`;
|
||||
the Torso marker rotation subtracts the Torso Bone2D's `bone_angle` (bone world angle =
|
||||
marker rotation + bone_angle — copying the body rotation directly would slam the skeleton −90°
|
||||
and lay it flat).
|
||||
`_play_stand_up()` then tweens the 6 markers **directly** from their captured values to
|
||||
`STAND_POSE` over `STAND_UP_DURATION` (2.0 s, sine ease-in-out, `_tween_markers_to()`); the baked
|
||||
`"stand_up"` animation is **not** played (a fixed first keyframe can never match an arbitrary
|
||||
ragdoll rest pose, so recovery starts from wherever the snap left the markers). On tween
|
||||
finish `_on_stand_up_finished()` re-enables IK, re-shows `Body/*`, sets `state = ANIMATED`,
|
||||
and emits. Interruptible: `set_ragdoll(true)`
|
||||
during `RECOVERING` kills the stand-up tween and rebuilds the ragdoll;
|
||||
`set_ragdoll(false)` during `RAGDOLL` routes through `_start_recovery()`; repeated
|
||||
`set_ragdoll` calls are idempotent. `is_in_ragdoll()` stays
|
||||
`state == RigState.RAGDOLL` (so `RECOVERING` reads as "Stickman").
|
||||
`REST_LINEAR_THRESHOLD`/`REST_ANGULAR_THRESHOLD` (or it is sleeping) it accumulates
|
||||
`_rest_timer`; after `rest_timeout` (and a `STABILIZATION_DELAY` hold) with `auto_recover`
|
||||
on it calls `_start_recovery()`.
|
||||
- **Pre-ragdoll capture:** `_enter_ragdoll()` first calls `_capture_pre_ragdoll_state()`,
|
||||
which snapshots, into `_pre_ragdoll_state`, the 6 IK marker values, the 8 limb bone
|
||||
rotations, the head bone rotation, the facing profile, the 4 live bend flags, the head
|
||||
LookAt enabled state, per-leg knee side + flag (`leg_sides`), and the chain-space entry
|
||||
offsets `torso_chain_offset` / `head_chain_offset` — captured at entry so the snap needs
|
||||
**no authored geometry constants** and the LEFT mirror's ambiguous `global_rotation`
|
||||
decomposition (±π) cancels out. This snapshot is the recovery **END target** (possibly a
|
||||
mid-walk pose; the animation is not resumed).
|
||||
- **Recovery capture + landing:** `_start_recovery()` (also `request_recovery()`, no-op
|
||||
unless in RAGDOLL) calls `_capture_ragdoll_pose()` — records the 10 bodies' rig-local
|
||||
`{pos, rot, half}` (`half` = each capsule's half-length from build-time metadata) and
|
||||
probes the terrain for the ground surface under the fallen figure via `_probe_ground_y()`
|
||||
(a **7-ray fan** around the landing X, `GROUND_PROBE_START_OFFSET`/`END_OFFSET`,
|
||||
`GROUND_PROBE_TOLERANCE` band, ragdoll RIDs excluded via `query.exclude`); the probe
|
||||
context is retained in `_probe_top_y`/`_probe_bottom_y`/`_probe_exclude` for the ankle
|
||||
clamp at snap time. It then `_destroy_ragdoll()`s, calls `_reanchor_root_to_landing()`
|
||||
(translates the rig root so the standing figure's **feet** sit on the ground at the
|
||||
landing X — `feet = (_captured_landing_center.x, _captured_ground_y)`, `new_root = feet +
|
||||
FOOT_OFFSET` — and re-bases the captured rig-local positions with a **mirror-aware local
|
||||
shift** `local_shift = to_local(shift) − to_local(Vector2.ZERO)`, so the re-base survives
|
||||
the LEFT X-mirror), sets `state = RECOVERING` + emits.
|
||||
- **Snap:** `_snap_skeleton_to_pose()` writes the captured pose: the torso marker via
|
||||
`to_local(center_world − spine_dir·half)` with rotation `chain(spine) − torso_chain_offset`;
|
||||
the limb bones `rot_u = chain(u) − θ_t − bone_angle_u` and
|
||||
`rot_l = chain(l) − chain(u) + bone_angle_u − bone_angle_l`; the head via
|
||||
`chain(head) − head_chain_offset − θ_t`. All snapped rotations are normalized into
|
||||
`(−π, π]` because Godot's `Skeleton2D` re-wraps bone rotations internally (which would
|
||||
otherwise corrupt the tween's captured start). The ankle markers are clamped up onto the
|
||||
terrain surface via a per-ankle `_probe_ground_y()` at each ankle's X. The snap also
|
||||
computes and stores the per-segment tween targets in `_recovery_bones`. It re-shows
|
||||
`Body/*`, calls `_rearm_ik_stack()` (a defensive re-setup — re-runs `setup()` when the
|
||||
stack reports `!get_is_setup()`, re-asserts `enabled = true` and
|
||||
`Skeleton2D.set_process_internal(true)`, and re-asserts the Torso marker's
|
||||
`RemoteTransform2D` update flags), then **disables** the 4 arm TwoBoneIK mods + the head
|
||||
LookAt and **enables** the 2 leg mods (bend side matched to the captured fall pose's knee
|
||||
side vs the entry side recorded in `leg_sides`, or forced via `RECOVERY_LEG_SIDE_FORCE`).
|
||||
- **Stand-up:** `_play_stand_up()` starts a 2.0 s sine-in-out tween (`STAND_UP_DURATION`)
|
||||
of the 6 markers → their **pre-ragdoll** values and of the arm + head bones to the
|
||||
`_recovery_bones` targets — upper arm bones take the shortest path, lower arm bones use
|
||||
`_natural_joint_target(current, target, offset)` (offset = `bone_angle_lower −
|
||||
bone_angle_upper`; the through-straight rule is evaluated in the joint's rendered space
|
||||
and a `"shortest"` entry in `RECOVERY_JOINT_PATH` bypasses it). The **legs are IK-driven**
|
||||
toward their tweening, ground-clamped ankle markers. The arm mods + head LookAt stay
|
||||
disabled during the tween. The baked `"stand_up"` animation is **not** played (a fixed
|
||||
first keyframe can never match an arbitrary ragdoll rest pose).
|
||||
- **Finish:** `_on_stand_up_finished()` restores the pre-ragdoll facing profile + per-joint
|
||||
bend flags + head LookAt state (`_restore_head_lookat_state()`), re-enables the arm mods,
|
||||
re-shows `Body/*`, sets `state = ANIMATED`, and emits. The resumed IK solve reproduces the
|
||||
pre-ragdoll pose exactly (the end pose matches the pre-ragdoll frame, including mid-walk
|
||||
poses).
|
||||
- `_pin_mirrored_head_rotation()` pins the head only while `ANIMATED`. Interruptible:
|
||||
`set_ragdoll(true)` during `RECOVERING` kills the stand-up tween and rebuilds the ragdoll;
|
||||
`set_ragdoll(false)` during `RAGDOLL` routes through `_start_recovery()`; repeated
|
||||
`set_ragdoll` calls are idempotent. `is_in_ragdoll()` stays
|
||||
`state == RigState.RAGDOLL` (so `RECOVERING` reads as "Stickman"). Runtime diagnosis is
|
||||
gated behind `const DEBUG_RECOVERY := false` (off by default; `_recovery_dbg()` traces the
|
||||
stack + bone-following state — the temporary per-step debug prints have been stripped).
|
||||
- **Phase 3a director functionality:** adds navigation/walking, speech, an action queue, and a
|
||||
queue-runner state machine. Enums `RunnerState { IDLE, EXECUTING }` and `ActionPhase { NONE,
|
||||
WALKING, SPEAKING, WAITING, RAGDOLLING, RECOVERING }`. Constants `FOOT_OFFSET := (0, -385)`
|
||||
|
||||
Reference in New Issue
Block a user