Refactor animation generation: replace create_walk.gd with create_animations.gd
- Removed the old create_walk.gd script, which generated walk animations. - Introduced create_animations.gd to unify the generation of walk_left, walk_right, and stand_up animations. - Added a new SpinBox for rest timeout configuration in physics_test_harness.gd. - Enhanced StickmanRig to support automatic recovery from ragdoll state with configurable timeout. - Implemented recovery logic in StickmanRig, allowing for smooth transitions from ragdoll to animated state. - Updated animation generation logic to use new pose templates for standing and lying down positions.
This commit is contained in:
@@ -218,7 +218,8 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
Task 4). Attached to the `Master` root node of `master_rig.tscn`. **Non-`@tool`** — node
|
||||
resolution, flag writes, and z-order reordering run only at runtime (`_ready` + setters on a
|
||||
live instance). Enums `FacingProfile { LEFT, RIGHT, FORWARD }` (values are the harness facing-menu
|
||||
ids) and `BendDirection { NORMAL, INVERTED }`. Constants (moved from the harness): `SKELETON_PATH`,
|
||||
ids), `BendDirection { NORMAL, INVERTED }`, and `RigState { ANIMATED, RAGDOLL, RECOVERING }`.
|
||||
Constants (moved from the harness): `SKELETON_PATH`,
|
||||
`BODY_CONTAINER_PATH`, `BEND_JOINTS` (`["LeftArm","RightArm","LeftLeg","RightLeg"]`),
|
||||
`BEND_JOINT_BONE_PATHS` (each joint → its lower `Bone2D` NodePath relative to `Skeleton2D`),
|
||||
`PROFILE_FLAGS` (per-profile `flip_bend_direction` sets), `Z_ORDER_BY_PROFILE` (per-profile
|
||||
@@ -226,7 +227,11 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
`FORWARD`, a preset whose setter writes the four per-joint vars + reorders `Body/*`) and an
|
||||
`@export_group("Bend Direction")` of four `@export_enum("Normal","Inverted")` vars
|
||||
`left_arm_bend`/`right_arm_bend`/`left_leg_bend`/`right_leg_bend` (defaults
|
||||
NORMAL/INVERTED/INVERTED/NORMAL = FORWARD). Signals `facing_profile_changed(profile)` /
|
||||
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`.
|
||||
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
|
||||
`get_bend_joint_global_position(joint)` (unknown joint → `push_warning` + no-op/`false`/
|
||||
@@ -237,26 +242,57 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
setter timing during `PackedScene.instantiate()`). Null-guards + `push_warning` prefixed
|
||||
`"StickmanRig: "` throughout; never crashes.
|
||||
- **Phase 10 ragdoll state system:** `StickmanRig` owns a reversible `ANIMATED ⇄ RAGDOLL`
|
||||
physics mode switch. `enum RigState { ANIMATED, RAGDOLL }`, `var state: RigState`
|
||||
(default `ANIMATED`), `signal state_changed(new_state: int)`, and public API
|
||||
`set_ragdoll(enabled: bool)` / `toggle_ragdoll()` / `is_in_ragdoll() -> bool`.
|
||||
`_physics_process()` → `_track_momentum(delta)` caches the rig root's linear/angular
|
||||
velocity from per-frame `global_position`/`global_rotation` deltas. `_enter_ragdoll()`
|
||||
disables the IK modification stack, `stop()`s the `AnimationPlayer` (`ANIMATION_PLAYER_PATH`
|
||||
const), hides `Body/*`, builds the ragdoll, then sets `state` + emits `state_changed`.
|
||||
`_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) and the `RAGDOLL_JOINTS` table (**9** `PinJoint2D`,
|
||||
one per non-root body pinned at the child bone's origin, `softness` 0.0). 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. `_exit_ragdoll()` `queue_free()`s the container, re-shows `Body/*`, re-enables
|
||||
the IK stack, `stop()`s the animation, and reverts `state` to `ANIMATED`. All ragdoll nodes
|
||||
are spawned procedurally — `master_rig.tscn` is **not** modified.
|
||||
physics mode switch plus a `RECOVERING` stand-up state. `enum RigState { ANIMATED, RAGDOLL,
|
||||
RECOVERING }`, `var state: RigState` (default `ANIMATED`), `signal state_changed(new_state:
|
||||
int)`, and public API `set_ragdoll(enabled: bool)` / `toggle_ragdoll()` / `is_in_ragdoll() ->
|
||||
bool` / `request_recovery()`. `_physics_process()` → `_track_momentum(delta)` caches the rig
|
||||
root's linear/angular velocity from per-frame `global_position`/`global_rotation` deltas, then
|
||||
drives `_update_rest_detection()` (auto-recovery trigger). `_enter_ragdoll()` `stop(true)`s the
|
||||
`AnimationPlayer` (`ANIMATION_PLAYER_PATH` const, keep_state — no pose reset), builds the
|
||||
ragdoll from the CURRENT solved bone positions while the IK stack is still enabled (disabling
|
||||
it first would revert the bones to the authored rest pose, popping the figure), then hides
|
||||
`Body/*` and disables the IK stack **immediately** — an instant handoff with no crossfade (the
|
||||
ragdoll spawns at exactly the same pose, so a fade would only read as ghosting), sets `state =
|
||||
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.
|
||||
- **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), `_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/*` before re-enabling the IK stack so TwoBoneIK solves toward the end-effectors. 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` (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").
|
||||
- `scripts/stickman_factory.gd` — `class_name StickmanFactory`, `extends RefCounted`; a **static
|
||||
factory** and the **runtime entry point** (Phase 9, **not used by the editor**) that turns a
|
||||
`.stk` file into a live, rigged `master_rig.tscn` instance:
|
||||
@@ -267,6 +303,16 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
(typed as `StickmanRig` since the rig now carries the `StickmanRig` root script).
|
||||
- `static func spawn(path: String) -> StickmanRig` — `load_stk()` then `spawn_from_data()`;
|
||||
returns `null` on empty data.
|
||||
- `scripts/create_animations.gd` — `@tool extends EditorScript`; a **standalone editor utility**
|
||||
(run manually with `master_rig.tscn` open, **not** auto-loaded or referenced at runtime).
|
||||
Supersedes the deleted `scripts/create_walk.gd`. `_run()` bakes `walk_left`/`walk_right`
|
||||
(same keyframes as the old script, via `_generate_walk_animation()`) and a one-shot `stand_up`
|
||||
(via `_generate_pose_animation()`, `STAND_UP_DURATION` = 0.8, `loop_mode = LOOP_NONE`) into the
|
||||
open scene's default `AnimationLibrary`. `stand_up` keys the 6 `IK_Targets/*:position` tracks
|
||||
plus a `IK_Targets/Torso:rotation` track from `POSE_DOWN` (a generic "lying on back" pose) to
|
||||
`POSE_STANDING` (matching `master_rig.tscn` defaults), with `POSE_PATHS`/`POSE_MARKERS` consts.
|
||||
The baked `stand_up` is an **authored reference only** — runtime recovery does not play it
|
||||
(StickmanRig tweens the IK targets directly from the captured ragdoll pose).
|
||||
- `scripts/test_harness.gd` — **standalone staging scene** (Phase 9, **not wired into the editor**;
|
||||
run via **F6** on `res://scenes/test_harness.tscn`) for debugging bone scales, vector drawing
|
||||
offsets, and IK limits in isolation. Top UI bar: "Open .stk…" button → `FileDialog` (`*.stk`);
|
||||
@@ -458,6 +504,15 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
KNOCK_UP_VELOCITY = (0, -450))` (mass-scaled `apply_central_impulse` on every ragdoll body,
|
||||
preserving internal structure), and applies the same upward velocity delta to every dynamic
|
||||
prop (`RigidBody2D` child of `_environment`) so the whole pile flies up together.
|
||||
- **Phase 11 recovery UI:** a `Label("Rest")` + `SpinBox` (`_rest_timeout_spinbox`, min 0.1 /
|
||||
max 10.0 / step 0.1, initialized to `_rig.rest_timeout` after `_build_ui`) and a **"Recover
|
||||
Now"** button (`_recover_now()` → `_rig.request_recovery()`) are added after the "Knock Up"
|
||||
button. `_on_rest_timeout_changed(v)` writes `_rig.rest_timeout = v` (runtime-only, no
|
||||
settings.json). `_spawn_rig()` connects `_rig.state_changed` → `_on_rig_state_changed()`,
|
||||
which removes the collision proxy on `RAGDOLL`, re-adds it on `ANIMATED`/`RECOVERING`, and
|
||||
always `_update_ragdoll_toggle()` (proxy helpers are idempotent, so the toggle handler's own
|
||||
add/remove is harmless). The toggle label reads "Stickman" during `RECOVERING` (since
|
||||
`is_in_ragdoll()` is false).
|
||||
- Scenes:
|
||||
- `scenes/stickman_editor.tscn` — main editor layout; unique-name nodes (`%Prefix`) used
|
||||
for typed `@onready` access: `%MenuBar`, `%StickmanNameEdit`, `%LeftColumn`,
|
||||
|
||||
Reference in New Issue
Block a user