feat: Implement kinematic-to-ragdoll transition system
- Added KINEMATIC_BLENDING_AND_RECOVERY.md to outline features for smooth transitions between kinematic and ragdoll states, including visual and physical blending, and ragdoll recovery. - Introduced KINEMATIC_TO_RAGDOLL.md detailing the objectives, scope, and core architecture for transitioning the stickman from kinematic to ragdoll mode. - Created KINEMATIC_TO_RAGDOLL_SPEC.md as an implementation specification, verifying codebase facts and correcting the initial plan based on Godot 4.4 source. - Enhanced StickmanRig with state management for animated and ragdoll modes, including momentum preservation and ragdoll construction. - Updated physics_test_harness to support toggling between kinematic and ragdoll states with user input.
This commit is contained in:
+106
-17
@@ -4,8 +4,9 @@ extends Node2D
|
||||
##
|
||||
## Builds flat ground, an angled ramp and stepped terrain via the TerrainUtils
|
||||
## factory, spawns a master_rig.tscn instance standing on the flat ground, and
|
||||
## provides camera zoom/pan input. NOT wired into the editor — run standalone
|
||||
## via F6 on res://scenes/physics_test_harness.tscn.
|
||||
## provides camera zoom/pan input plus a top-bar UI (spawn buttons, a
|
||||
## Stickman/Ragdoll mode toggle, and a "Knock Up" force button). NOT wired into
|
||||
## the editor — run standalone via F6 on res://scenes/physics_test_harness.tscn.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
@@ -32,6 +33,9 @@ const RIG_SPAWN_POSITION := Vector2(0.0, -385.0)
|
||||
## Spawn point for dynamic props: above the angled ramp so they tumble down.
|
||||
const PROP_SPAWN_POSITION := Vector2(300.0, -300.0)
|
||||
|
||||
## Upward velocity delta applied by the "Knock Up" button (negative Y = up).
|
||||
const KNOCK_UP_VELOCITY := Vector2(0.0, -450.0)
|
||||
|
||||
## Best-effort collision proxy for the rig (which has no physics bodies): a
|
||||
## static box matching the standing figure's world bounds (x ±120, y 0..-1000).
|
||||
const RIG_PROXY_SIZE := Vector2(240.0, 1000.0)
|
||||
@@ -51,6 +55,13 @@ const RIG_PROXY_CENTER := Vector2(0.0, -500.0)
|
||||
var _is_panning: bool = false
|
||||
var _pan_last: Vector2 = Vector2.ZERO
|
||||
|
||||
## The spawned rig instance (StickmanRig). Stored so the mode toggle can
|
||||
## switch between Stickman (animated) and Ragdoll modes.
|
||||
var _rig: StickmanRig = null
|
||||
|
||||
## Top-bar mode toggle button (text flips "Stickman" <-> "Ragdoll").
|
||||
var _ragdoll_toggle: Button = null
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -59,6 +70,7 @@ func _ready() -> void:
|
||||
_camera.make_current()
|
||||
_build_environment()
|
||||
_spawn_rig()
|
||||
_build_ui()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Input (camera zoom / pan)
|
||||
@@ -69,20 +81,6 @@ func _input(event: InputEvent) -> void:
|
||||
_handle_mouse_button(event as InputEventMouseButton)
|
||||
elif event is InputEventMouseMotion:
|
||||
_handle_mouse_motion(event as InputEventMouseMotion)
|
||||
elif event is InputEventKey:
|
||||
_handle_key(event as InputEventKey)
|
||||
|
||||
|
||||
func _handle_key(key: InputEventKey) -> void:
|
||||
if not key.pressed or key.echo:
|
||||
return
|
||||
match key.keycode:
|
||||
KEY_1:
|
||||
_spawn_prop_crate()
|
||||
KEY_2:
|
||||
_spawn_prop_ball()
|
||||
KEY_3:
|
||||
_spawn_prop_plank()
|
||||
|
||||
|
||||
func _handle_mouse_button(mb: InputEventMouseButton) -> void:
|
||||
@@ -107,6 +105,80 @@ func _set_zoom(value: float) -> void:
|
||||
var z := clampf(value, MIN_ZOOM, MAX_ZOOM)
|
||||
_camera.zoom = Vector2(z, z)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Top-bar UI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _build_ui() -> void:
|
||||
var ui := CanvasLayer.new()
|
||||
ui.name = "UI"
|
||||
add_child(ui)
|
||||
|
||||
var top_bar := PanelContainer.new()
|
||||
top_bar.set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE)
|
||||
top_bar.offset_bottom = 40.0
|
||||
ui.add_child(top_bar)
|
||||
|
||||
var hbox := HBoxContainer.new()
|
||||
hbox.add_theme_constant_override("separation", 8)
|
||||
top_bar.add_child(hbox)
|
||||
|
||||
var crate_btn := Button.new()
|
||||
crate_btn.text = "Spawn Crate"
|
||||
crate_btn.pressed.connect(_spawn_prop_crate)
|
||||
hbox.add_child(crate_btn)
|
||||
|
||||
var ball_btn := Button.new()
|
||||
ball_btn.text = "Spawn Ball"
|
||||
ball_btn.pressed.connect(_spawn_prop_ball)
|
||||
hbox.add_child(ball_btn)
|
||||
|
||||
var plank_btn := Button.new()
|
||||
plank_btn.text = "Spawn Plank"
|
||||
plank_btn.pressed.connect(_spawn_prop_plank)
|
||||
hbox.add_child(plank_btn)
|
||||
|
||||
_ragdoll_toggle = Button.new()
|
||||
_ragdoll_toggle.toggle_mode = true
|
||||
_ragdoll_toggle.toggled.connect(_on_ragdoll_toggled)
|
||||
hbox.add_child(_ragdoll_toggle)
|
||||
_update_ragdoll_toggle()
|
||||
|
||||
var knock_btn := Button.new()
|
||||
knock_btn.text = "Knock Up"
|
||||
knock_btn.pressed.connect(_knock_up)
|
||||
hbox.add_child(knock_btn)
|
||||
|
||||
|
||||
func _on_ragdoll_toggled(pressed: bool) -> void:
|
||||
if _rig == null:
|
||||
return
|
||||
_rig.set_ragdoll(pressed)
|
||||
if pressed:
|
||||
_remove_rig_collision_proxy()
|
||||
else:
|
||||
_add_rig_collision_proxy()
|
||||
_update_ragdoll_toggle()
|
||||
|
||||
|
||||
func _update_ragdoll_toggle() -> void:
|
||||
if _ragdoll_toggle == null or _rig == null:
|
||||
return
|
||||
var in_ragdoll: bool = _rig.is_in_ragdoll()
|
||||
_ragdoll_toggle.set_pressed_no_signal(in_ragdoll)
|
||||
_ragdoll_toggle.text = "Ragdoll" if in_ragdoll else "Stickman"
|
||||
|
||||
|
||||
func _knock_up() -> void:
|
||||
# Ragdoll: boost every ragdoll body upward (preserves internal structure).
|
||||
if _rig != null and _rig.is_in_ragdoll():
|
||||
_rig.apply_ragdoll_velocity_boost(KNOCK_UP_VELOCITY)
|
||||
# Dynamic props: same upward delta so everything flies together.
|
||||
for child: Node in _environment.get_children():
|
||||
if child is RigidBody2D and is_instance_valid(child):
|
||||
var body := child as RigidBody2D
|
||||
body.apply_central_impulse(KNOCK_UP_VELOCITY * body.mass)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Environment / rig construction
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -144,12 +216,13 @@ func _build_environment() -> void:
|
||||
|
||||
|
||||
func _spawn_rig() -> void:
|
||||
var rig := RIG_SCENE.instantiate() as Node2D
|
||||
var rig := RIG_SCENE.instantiate() as StickmanRig
|
||||
if rig == null:
|
||||
push_warning("PhysicsTestHarness: failed to instantiate master_rig.tscn.")
|
||||
return
|
||||
rig.position = RIG_SPAWN_POSITION
|
||||
add_child(rig)
|
||||
_rig = rig
|
||||
_add_rig_collision_proxy()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -189,6 +262,9 @@ func _spawn_prop_plank() -> void:
|
||||
## The rig has no physics bodies, so a code-only StaticBody2D proxy provides a
|
||||
## collision surface matching its standing bounds. Props bounce/rest against it.
|
||||
func _add_rig_collision_proxy() -> void:
|
||||
if _find_rig_collision_proxy() != null:
|
||||
return
|
||||
|
||||
var proxy := StaticBody2D.new()
|
||||
proxy.name = "RigCollisionProxy"
|
||||
proxy.position = RIG_PROXY_CENTER
|
||||
@@ -201,3 +277,16 @@ func _add_rig_collision_proxy() -> void:
|
||||
proxy.add_child(shape)
|
||||
|
||||
add_child(proxy)
|
||||
|
||||
|
||||
func _remove_rig_collision_proxy() -> void:
|
||||
var proxy := _find_rig_collision_proxy()
|
||||
if proxy != null:
|
||||
proxy.queue_free()
|
||||
|
||||
|
||||
func _find_rig_collision_proxy() -> Node:
|
||||
for child: Node in get_children():
|
||||
if child.name == "RigCollisionProxy":
|
||||
return child
|
||||
return null
|
||||
|
||||
Reference in New Issue
Block a user