Implement collision bodies, physics events, and hit reactions for StickmanRig

- Added a new specification for collision bodies, detailing the implementation requirements and corrections needed in the StickmanRig, PropBlock, and SandboxStage scripts.
- Created a comprehensive test suite for collision bodies, including tests for proxy geometry, physics alignment, reaction handling, ghost collision, and stage contact routing.
- Ensured that the collision system correctly handles ragdoll transitions and maintains the integrity of existing event rules.
- Introduced new methods in StickmanRig for managing collision bodies and applying reactions based on impact direction and speed.
- Updated PropBlock to emit signals for collisions and separations, preserving the order of events for stickman and prop interactions.
This commit is contained in:
2026-09-13 10:31:41 -04:00
parent 04e0f1bea2
commit ce6bcecd00
12 changed files with 1698 additions and 58 deletions
+26 -3
View File
@@ -33,6 +33,7 @@ const CIRCLE_SEGMENTS: int = 48
const DEFAULT_FILL_COLOR := Color(0.55, 0.35, 0.15, 1.0)
const DEFAULT_OUTLINE_COLOR := Color(0.15, 0.08, 0.02, 1.0)
const DEFAULT_OUTLINE_WIDTH: float = 2.0
const MAX_CONTACTS_REPORTED := 32
# ---------------------------------------------------------------------------
# Exported properties
@@ -94,7 +95,8 @@ var _circle_shape: CircleShape2D
## Emitted when this prop's body makes contact with another physics body.
## `other` is the body that entered contact (Phase 4 trigger source).
signal collided(other: Node)
signal collided(other: Node, contact_normal: Vector2, contact_position: Vector2)
signal separated(other: Node)
# ---------------------------------------------------------------------------
# Lifecycle
@@ -106,13 +108,34 @@ func _ready() -> void:
_apply_style()
# Enable contact reporting so the body_entered signal fires (Phase 4).
contact_monitor = true
max_contacts_reported = 8
max_contacts_reported = MAX_CONTACTS_REPORTED
continuous_cd = RigidBody2D.CCD_MODE_CAST_SHAPE
if not body_entered.is_connected(_on_body_entered):
body_entered.connect(_on_body_entered)
if not body_exited.is_connected(_on_body_exited):
body_exited.connect(_on_body_exited)
func _on_body_entered(body: Node) -> void:
collided.emit(body)
var center: Vector2 = (body as Node2D).global_position if body is Node2D else global_position
var normal: Vector2 = (center - global_position).normalized()
var position: Vector2 = global_position
var direct_state := PhysicsServer2D.body_get_direct_state(get_rid())
if direct_state != null:
for index: int in direct_state.get_contact_count():
if direct_state.get_contact_collider_id(index) == body.get_instance_id():
var reported: Vector2 = direct_state.get_contact_local_normal(index)
if not reported.is_zero_approx():
normal = reported.normalized()
if normal.dot(center - global_position) < 0.0:
normal = -normal
position = direct_state.get_contact_local_position(index)
break
collided.emit(body, normal, position)
func _on_body_exited(body: Node) -> void:
separated.emit(body)
# ---------------------------------------------------------------------------
# Internal build / apply