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:
+77
-29
@@ -419,7 +419,6 @@ func _physics_process(_delta: float) -> void:
|
||||
_restore_authored_state()
|
||||
if current_mode == StageMode.PLAY:
|
||||
_update_area_entry()
|
||||
_update_stickman_prop_collision()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Input
|
||||
@@ -682,11 +681,14 @@ func _enter_play_mode() -> void:
|
||||
|
||||
|
||||
func _save_object_state(node: Node2D) -> void:
|
||||
_authored[node.get_instance_id()] = {
|
||||
var entry := {
|
||||
"node": node,
|
||||
"position": node.global_position,
|
||||
"rotation": node.global_rotation,
|
||||
}
|
||||
if node is STICKMAN_RIG:
|
||||
entry["facing_profile"] = (node as STICKMAN_RIG).get_facing_profile()
|
||||
_authored[node.get_instance_id()] = entry
|
||||
|
||||
|
||||
func _clear_object_state(node: Node2D) -> void:
|
||||
@@ -702,6 +704,8 @@ func _restore_authored_state() -> void:
|
||||
continue
|
||||
node.global_position = entry.get("position", node.global_position)
|
||||
node.global_rotation = entry.get("rotation", node.global_rotation)
|
||||
if node is STICKMAN_RIG and entry.has("facing_profile"):
|
||||
(node as STICKMAN_RIG).set_facing_profile(int(entry["facing_profile"]))
|
||||
if node is RigidBody2D:
|
||||
var body := node as RigidBody2D
|
||||
body.linear_velocity = Vector2.ZERO
|
||||
@@ -753,10 +757,12 @@ func _finalize_placed_node(node: Node2D) -> void:
|
||||
rig.arrived.connect(_on_rig_arrived.bind(rig))
|
||||
rig.action_finished.connect(_on_rig_action_finished.bind(rig))
|
||||
rig.speech_finished.connect(_on_rig_speech_finished.bind(rig))
|
||||
rig.state_changed.connect(_on_rig_state_changed.bind(rig))
|
||||
_director_visuals.mark_dirty()
|
||||
if node is PropBlock:
|
||||
var prop := node as PropBlock
|
||||
prop.collided.connect(_on_prop_collided.bind(prop))
|
||||
prop.separated.connect(_on_prop_separated.bind(prop))
|
||||
|
||||
|
||||
func _spawn_ghost() -> void:
|
||||
@@ -797,6 +803,7 @@ func _configure_ghost_collision(ghost: Node2D) -> void:
|
||||
var anim := rig.get_node_or_null(NodePath("AnimationPlayer")) as AnimationPlayer
|
||||
if anim != null:
|
||||
anim.stop()
|
||||
rig.set_collision_bodies_enabled(false)
|
||||
|
||||
|
||||
func _free_ghost() -> void:
|
||||
@@ -1099,6 +1106,8 @@ func delete_selected() -> void:
|
||||
if node is TerrainBlock:
|
||||
nav_changed = true
|
||||
_clear_object_state(node)
|
||||
var deleted_ids: Array[int] = [node.get_instance_id()]
|
||||
_purge_collision_pairs_for_ids(deleted_ids)
|
||||
node.queue_free()
|
||||
_rebuild_grid_cells()
|
||||
_cleanup_rules_for_nodes(selected)
|
||||
@@ -2399,8 +2408,72 @@ func _on_rig_speech_finished(rig: StickmanRig) -> void:
|
||||
_handle_event({ "type": "speech_finished", "source": rig, "target": null, "position": rig.global_position, "action": {} })
|
||||
|
||||
|
||||
func _on_prop_collided(other: Node, prop: PropBlock) -> void:
|
||||
_handle_event({ "type": "collided", "source": prop, "target": other, "position": prop.global_position, "action": {} })
|
||||
func _on_prop_collided(other: Node, normal: Vector2, position: Vector2, prop: PropBlock) -> void:
|
||||
if other == null or not is_instance_valid(other):
|
||||
return
|
||||
if bool(other.get_meta("stickman_ragdoll_body", false)):
|
||||
return
|
||||
var has_rig_metadata := other.has_meta("stickman_rig") or other.has_meta("part_name")
|
||||
var rig := other.get_meta("stickman_rig") as StickmanRig if other.has_meta("stickman_rig") else null
|
||||
var part := String(other.get_meta("part_name")) if other.has_meta("part_name") else ""
|
||||
if rig == null or not is_instance_valid(rig) or part.is_empty():
|
||||
if has_rig_metadata:
|
||||
return
|
||||
_handle_event({ "type": "collided", "source": prop, "target": other, "position": position, "action": {} })
|
||||
return
|
||||
if rig.state == StickmanRig.RigState.RAGDOLL:
|
||||
return
|
||||
var key := "%d:%d" % [rig.get_instance_id(), prop.get_instance_id()]
|
||||
var pair: Dictionary = _collision_pairs.get(key, {
|
||||
"rig_id": rig.get_instance_id(),
|
||||
"prop_id": prop.get_instance_id(),
|
||||
"bodies": {},
|
||||
})
|
||||
var bodies: Dictionary = pair.get("bodies", {})
|
||||
var body_id := other.get_instance_id()
|
||||
if bodies.has(body_id):
|
||||
return
|
||||
var first_contact := bodies.is_empty()
|
||||
bodies[body_id] = true
|
||||
pair["bodies"] = bodies
|
||||
_collision_pairs[key] = pair
|
||||
rig.apply_collision_reaction(part, normal, prop.linear_velocity.length(), position)
|
||||
if first_contact:
|
||||
_handle_event({ "type": "collided", "source": rig, "target": prop, "position": position, "action": {} })
|
||||
|
||||
|
||||
func _on_prop_separated(other: Node, prop: PropBlock) -> void:
|
||||
if other == null or not is_instance_valid(other):
|
||||
return
|
||||
if not other.has_meta("stickman_rig"):
|
||||
return
|
||||
var rig := other.get_meta("stickman_rig") as StickmanRig
|
||||
if rig == null or not is_instance_valid(rig):
|
||||
return
|
||||
var key := "%d:%d" % [rig.get_instance_id(), prop.get_instance_id()]
|
||||
var pair: Dictionary = _collision_pairs.get(key, {})
|
||||
if pair.is_empty():
|
||||
return
|
||||
var bodies: Dictionary = pair.get("bodies", {})
|
||||
bodies.erase(other.get_instance_id())
|
||||
if bodies.is_empty():
|
||||
_collision_pairs.erase(key)
|
||||
else:
|
||||
pair["bodies"] = bodies
|
||||
_collision_pairs[key] = pair
|
||||
|
||||
|
||||
func _on_rig_state_changed(new_state: int, rig: StickmanRig) -> void:
|
||||
if new_state == StickmanRig.RigState.RAGDOLL:
|
||||
var rig_ids: Array[int] = [rig.get_instance_id()]
|
||||
_purge_collision_pairs_for_ids(rig_ids)
|
||||
|
||||
|
||||
func _purge_collision_pairs_for_ids(ids: Array[int]) -> void:
|
||||
for key: String in _collision_pairs.keys().duplicate():
|
||||
var pair: Dictionary = _collision_pairs[key]
|
||||
if ids.has(int(pair.get("rig_id", -1))) or ids.has(int(pair.get("prop_id", -1))):
|
||||
_collision_pairs.erase(key)
|
||||
|
||||
|
||||
## Iterates every rule in order; every matching rule executes (no short-circuit).
|
||||
@@ -2501,31 +2574,6 @@ func _update_area_entry() -> void:
|
||||
_area_overlap[key] = false
|
||||
|
||||
|
||||
## Geometric stickman-vs-prop collision (ANIMATED rigs x unfrozen props),
|
||||
## edge-triggered via the prop's world AABB containing the rig's feet/root point.
|
||||
func _update_stickman_prop_collision() -> void:
|
||||
var rigs: Array[StickmanRig] = []
|
||||
var props: Array[PropBlock] = []
|
||||
for node: Node2D in _world_children_selectable():
|
||||
if node is STICKMAN_RIG:
|
||||
var rig := node as STICKMAN_RIG
|
||||
if rig.state == StickmanRig.RigState.ANIMATED:
|
||||
rigs.append(rig)
|
||||
elif node is PropBlock and not (node as PropBlock).freeze:
|
||||
props.append(node as PropBlock)
|
||||
for rig: StickmanRig in rigs:
|
||||
for prop: PropBlock in props:
|
||||
var key := "%d:%d" % [rig.get_instance_id(), prop.get_instance_id()]
|
||||
var feet: Vector2 = rig.global_position - StickmanRig.FOOT_OFFSET
|
||||
var overlap: bool = STAGE_SELECTION.get_world_aabb(prop).has_point(feet)
|
||||
var was: bool = bool(_collision_pairs.get(key, false))
|
||||
if overlap and not was:
|
||||
_collision_pairs[key] = true
|
||||
_handle_event({ "type": "collided", "source": rig, "target": prop, "position": rig.global_position, "action": {} })
|
||||
elif not overlap and was:
|
||||
_collision_pairs[key] = false
|
||||
|
||||
|
||||
## Stickman point = feet (root - FOOT_OFFSET); prop point = global position.
|
||||
func _movable_point(node: Node2D) -> Vector2:
|
||||
if node is STICKMAN_RIG:
|
||||
|
||||
Reference in New Issue
Block a user