- Moved the rig scene from `res://master_rig.tscn` to `res://scenes/stickman_rig.tscn`, preserving its UID. - Split the `StickmanRig` controller into three components: `StickmanLocomotion`, `StickmanRagdoll`, and `StickmanActionRunner`. - Updated the architecture to ensure `StickmanRig` acts as the public facade for the new components. - Created a new test suite `test_phase8_rig_modularization.gd` to verify the integrity of the relocated rig and its components. - Added checks for scene loading, component resolution, action queue delegation, and state authority.
707 lines
37 KiB
GDScript
707 lines
37 KiB
GDScript
class_name StickmanRagdoll
|
|
extends Node
|
|
## Owns procedural ragdoll physics. StickmanRig remains the public state facade.
|
|
|
|
signal recovery_requested()
|
|
signal rest_state_changed(at_rest: bool)
|
|
signal state_transition_requested(next_state: int)
|
|
|
|
const CONTAINER_NAME: StringName = &"RagdollBodyContainer"
|
|
const LIMB_RADIUS: float = 8.0
|
|
const TORSO_RADIUS: float = 12.0
|
|
const HEAD_RADIUS: float = 100.0
|
|
const VISUAL_COLOR: Color = Color(0.445488, 0.445488, 0.445488)
|
|
const CIRCLE_SEGMENTS: int = 32
|
|
const REST_LINEAR_THRESHOLD: float = 5.0
|
|
const REST_ANGULAR_THRESHOLD: float = 0.1
|
|
const STABILIZATION_DELAY: float = 0.1
|
|
const TARGET_SOFTNESS: float = 0.2
|
|
const GROUND_PROBE_START_OFFSET := 50.0
|
|
const GROUND_PROBE_END_OFFSET := 1500.0
|
|
const GROUND_PROBE_TOLERANCE := 200.0
|
|
const GROUND_PROBE_X_OFFSETS: Array[float] = [0.0, 25.0, -25.0, 50.0, -50.0, 100.0, -100.0]
|
|
const STAND_UP_DURATION := 2.0
|
|
const STAND_POSE: Dictionary = {
|
|
"Torso": {"pos": Vector2(0, 10), "rot": 0.0},
|
|
"Head": {"pos": Vector2(100, -614), "rot": 0.0},
|
|
"Left_Hand": {"pos": Vector2(90, 110), "rot": 0.0},
|
|
"Right_Hand": {"pos": Vector2(-90, 110), "rot": 0.0},
|
|
"Left_Leg": {"pos": Vector2(-110, 380), "rot": 0.0},
|
|
"Right_Leg": {"pos": Vector2(110, 390), "rot": 0.0},
|
|
}
|
|
const IK_TARGET_PATHS: Dictionary = {
|
|
"Torso": "IK_Targets/Torso", "Head": "IK_Targets/Head",
|
|
"Left_Hand": "IK_Targets/Left_Hand", "Right_Hand": "IK_Targets/Right_Hand",
|
|
"Left_Leg": "IK_Targets/Left_Leg", "Right_Leg": "IK_Targets/Right_Leg",
|
|
}
|
|
const LIMB_BONE_PATHS: Dictionary = {
|
|
"LeftArm": ["Torso/LeftUpperArm", "Torso/LeftUpperArm/LeftLowerArm"],
|
|
"RightArm": ["Torso/RightUpperArm", "Torso/RightUpperArm/RightLowerArm"],
|
|
"LeftLeg": ["Torso/LeftUpperLeg", "Torso/LeftUpperLeg/LeftLowerLeg"],
|
|
"RightLeg": ["Torso/RightUpperLeg", "Torso/RightUpperLeg/RightLowerLeg"],
|
|
}
|
|
const LIMB_BODY_KEYS: Dictionary = {
|
|
"LeftArm": ["left_upper_arm", "left_lower_arm"],
|
|
"RightArm": ["right_upper_arm", "right_lower_arm"],
|
|
"LeftLeg": ["left_upper_leg", "left_lower_leg"],
|
|
"RightLeg": ["right_upper_leg", "right_lower_leg"],
|
|
}
|
|
const RECOVERY_LEG_SIDE_FORCE: Dictionary = {
|
|
StickmanRig.FacingProfile.FORWARD: {}, StickmanRig.FacingProfile.LEFT: {}, StickmanRig.FacingProfile.RIGHT: {},
|
|
}
|
|
const RECOVERY_JOINT_PATH: Dictionary = {
|
|
StickmanRig.FacingProfile.FORWARD: {}, StickmanRig.FacingProfile.LEFT: {}, StickmanRig.FacingProfile.RIGHT: {},
|
|
}
|
|
|
|
const BODIES: Array[Dictionary] = [
|
|
{ "key": "torso", "kind": "bone", "node_path": "Torso", "visual_path": "Body/Body", "radius": TORSO_RADIUS, "mass": 8.0, "linear_damp": 1.0, "angular_damp": 4.0 },
|
|
{ "key": "head", "kind": "visual", "node_path": "Body/Head", "visual_path": "Body/Head", "radius": HEAD_RADIUS, "mass": 2.0, "linear_damp": 0.5, "angular_damp": 2.0 },
|
|
{ "key": "left_upper_arm", "kind": "bone", "node_path": "Torso/LeftUpperArm", "visual_path": "Body/LeftUpperArm", "radius": LIMB_RADIUS, "mass": 1.5, "linear_damp": 0.5, "angular_damp": 3.0 }, { "key": "left_lower_arm", "kind": "bone", "node_path": "Torso/LeftUpperArm/LeftLowerArm", "visual_path": "Body/LeftLowerArm", "radius": LIMB_RADIUS, "mass": 1.0, "linear_damp": 0.5, "angular_damp": 3.0 },
|
|
{ "key": "right_upper_arm", "kind": "bone", "node_path": "Torso/RightUpperArm", "visual_path": "Body/RightUpperArm", "radius": LIMB_RADIUS, "mass": 1.5, "linear_damp": 0.5, "angular_damp": 3.0 }, { "key": "right_lower_arm", "kind": "bone", "node_path": "Torso/RightUpperArm/RightLowerArm", "visual_path": "Body/RightLowerArm", "radius": LIMB_RADIUS, "mass": 1.0, "linear_damp": 0.5, "angular_damp": 3.0 },
|
|
{ "key": "left_upper_leg", "kind": "bone", "node_path": "Torso/LeftUpperLeg", "visual_path": "Body/LeftUpperLeg", "radius": LIMB_RADIUS, "mass": 2.0, "linear_damp": 0.5, "angular_damp": 3.0 }, { "key": "left_lower_leg", "kind": "bone", "node_path": "Torso/LeftUpperLeg/LeftLowerLeg", "visual_path": "Body/LeftLowerLeg", "radius": LIMB_RADIUS, "mass": 1.5, "linear_damp": 0.5, "angular_damp": 3.0 },
|
|
{ "key": "right_upper_leg", "kind": "bone", "node_path": "Torso/RightUpperLeg", "visual_path": "Body/RightUpperLeg", "radius": LIMB_RADIUS, "mass": 2.0, "linear_damp": 0.5, "angular_damp": 3.0 }, { "key": "right_lower_leg", "kind": "bone", "node_path": "Torso/RightUpperLeg/RightLowerLeg", "visual_path": "Body/RightLowerLeg", "radius": LIMB_RADIUS, "mass": 1.5, "linear_damp": 0.5, "angular_damp": 3.0 },
|
|
]
|
|
const JOINTS: Array[Dictionary] = [
|
|
{ "child": "head", "parent": "torso", "pin": "Torso/Head", "limit": "neck" }, { "child": "left_upper_arm", "parent": "torso", "pin": "Torso/LeftUpperArm", "limit": "shoulder" }, { "child": "left_lower_arm", "parent": "left_upper_arm", "pin": "Torso/LeftUpperArm/LeftLowerArm", "limit": "cw" }, { "child": "right_upper_arm", "parent": "torso", "pin": "Torso/RightUpperArm", "limit": "shoulder" }, { "child": "right_lower_arm", "parent": "right_upper_arm", "pin": "Torso/RightUpperArm/RightLowerArm", "limit": "ccw" }, { "child": "left_upper_leg", "parent": "torso", "pin": "Torso/LeftUpperLeg", "limit": "shoulder" }, { "child": "left_lower_leg", "parent": "left_upper_leg", "pin": "Torso/LeftUpperLeg/LeftLowerLeg", "limit": "ccw" }, { "child": "right_upper_leg", "parent": "torso", "pin": "Torso/RightUpperLeg", "limit": "shoulder" }, { "child": "right_lower_leg", "parent": "right_upper_leg", "pin": "Torso/RightUpperLeg/RightLowerLeg", "limit": "cw" },
|
|
]
|
|
|
|
## Per-profile elbow/knee limits retained from the monolithic rig. The component
|
|
## owns these construction settings because it creates the PinJoint2D network.
|
|
const JOINT_BAND_LIMITS: Dictionary = {
|
|
StickmanRig.FacingProfile.FORWARD: {
|
|
"left_lower_arm": { "lower": -150.0, "upper": 5.0 }, "right_lower_arm": { "lower": -150.0, "upper": 5.0 },
|
|
"left_lower_leg": { "lower": -5.0, "upper": 150.0 }, "right_lower_leg": { "lower": -5.0, "upper": 150.0 },
|
|
},
|
|
StickmanRig.FacingProfile.LEFT: {
|
|
"left_lower_arm": { "lower": 60.0, "upper": 100.0 }, "right_lower_arm": { "lower": -150.0, "upper": 5.0 },
|
|
"left_lower_leg": { "lower": -100.0, "upper": -90.0 }, "right_lower_leg": { "lower": -5.0, "upper": 150.0 },
|
|
},
|
|
StickmanRig.FacingProfile.RIGHT: {
|
|
"left_lower_arm": { "lower": -100.0, "upper": -60.0 }, "right_lower_arm": { "lower": -150.0, "upper": 5.0 },
|
|
"left_lower_leg": { "lower": 90.0, "upper": 100.0 }, "right_lower_leg": { "lower": -5.0, "upper": 150.0 },
|
|
},
|
|
}
|
|
|
|
var _rig: StickmanRig = null
|
|
var _locomotion: StickmanLocomotion = null
|
|
var _skeleton: Skeleton2D = null
|
|
var _body_container: Node2D = null
|
|
var _anim_player: AnimationPlayer = null
|
|
var _torso_bone: Bone2D = null
|
|
var _head_bone: Bone2D = null
|
|
var _head_look_at: SkeletonModification2DLookAt = null
|
|
var _limb_bones: Dictionary = {}
|
|
var _bend_modifications: Dictionary = {}
|
|
var _root: Node2D = null
|
|
var _bodies: Dictionary = {}
|
|
var _previous_position: Vector2 = Vector2.ZERO
|
|
var _previous_rotation: float = 0.0
|
|
var _linear_velocity: Vector2 = Vector2.ZERO
|
|
var _angular_velocity: float = 0.0
|
|
var _rest_timer: float = 0.0
|
|
var _stabilize_timer: float = 0.0
|
|
var _at_rest: bool = false
|
|
var _captured_pose: Dictionary = {}
|
|
var _captured_landing_center := Vector2.ZERO
|
|
var _captured_ground_y := 0.0
|
|
var _stand_up_tween: Tween = null
|
|
var _stand_up_generation := 0
|
|
var _recovery_bones: Array[Dictionary] = []
|
|
var _pre_ragdoll_state: Dictionary = {}
|
|
var _probe_top_y := 0.0
|
|
var _probe_bottom_y := 0.0
|
|
var _probe_exclude: Array[RID] = []
|
|
var _spawn_pose: Dictionary = {}
|
|
var _spawn_limb_rotations: Dictionary = {}
|
|
|
|
func configure(rig: StickmanRig, locomotion: StickmanLocomotion) -> void:
|
|
_rig = rig
|
|
_locomotion = locomotion
|
|
|
|
func initialize(skeleton: Skeleton2D) -> void:
|
|
_skeleton = skeleton
|
|
_body_container = _rig.get_node_or_null(NodePath("Body")) as Node2D
|
|
_anim_player = _rig.get_node_or_null(NodePath("AnimationPlayer")) as AnimationPlayer
|
|
if _skeleton != null:
|
|
_torso_bone = _skeleton.get_node_or_null(NodePath("Torso")) as Bone2D
|
|
_head_bone = _skeleton.get_node_or_null(NodePath("Torso/Head")) as Bone2D
|
|
_resolve_limb_bones()
|
|
var stack: SkeletonModificationStack2D = _skeleton.modification_stack
|
|
if stack != null:
|
|
for index: int in stack.modification_count:
|
|
var modification: SkeletonModification2D = stack.get_modification(index)
|
|
if modification is SkeletonModification2DLookAt:
|
|
_head_look_at = modification as SkeletonModification2DLookAt
|
|
elif modification is SkeletonModification2DTwoBoneIK:
|
|
var ik := modification as SkeletonModification2DTwoBoneIK
|
|
for joint: String in LIMB_BONE_PATHS:
|
|
if ik.joint_two_bone2d_node == NodePath(String((LIMB_BONE_PATHS[joint] as Array)[1])):
|
|
_bend_modifications[joint] = ik
|
|
break
|
|
_previous_position = _rig.global_position
|
|
_previous_rotation = _rig.global_rotation
|
|
|
|
func _resolve_limb_bones() -> void:
|
|
_limb_bones.clear()
|
|
for joint: String in LIMB_BONE_PATHS:
|
|
var paths: Array = LIMB_BONE_PATHS[joint]
|
|
var upper := _skeleton.get_node_or_null(NodePath(String(paths[0]))) as Bone2D
|
|
var lower := _skeleton.get_node_or_null(NodePath(String(paths[1]))) as Bone2D
|
|
if upper != null and lower != null:
|
|
_limb_bones[joint] = {"upper": upper, "lower": lower}
|
|
|
|
func tick_pre_movement(delta: float, rig_state: int, rest_timeout: float, auto_recover: bool) -> void:
|
|
if _rig == null: return
|
|
if delta > 0.0:
|
|
_linear_velocity = (_rig.global_position - _previous_position) / delta
|
|
_angular_velocity = wrapf(_rig.global_rotation - _previous_rotation, -PI, PI) / delta
|
|
_previous_position = _rig.global_position
|
|
_previous_rotation = _rig.global_rotation
|
|
if rig_state != StickmanRig.RigState.RAGDOLL: return
|
|
var torso: RigidBody2D = _bodies.get("torso") as RigidBody2D
|
|
if torso == null or not is_instance_valid(torso): return
|
|
var resting: bool = torso.sleeping or (torso.linear_velocity.length() <= REST_LINEAR_THRESHOLD and absf(torso.angular_velocity) <= REST_ANGULAR_THRESHOLD)
|
|
if resting != _at_rest: _at_rest = resting; rest_state_changed.emit(resting)
|
|
if not resting: _rest_timer = 0.0; _stabilize_timer = 0.0; return
|
|
if not auto_recover: _rest_timer = 0.0; return
|
|
_rest_timer += delta
|
|
if _rest_timer >= rest_timeout:
|
|
_stabilize_timer += delta
|
|
if _stabilize_timer >= STABILIZATION_DELAY: recovery_requested.emit()
|
|
|
|
func build() -> bool:
|
|
if _rig == null or _skeleton == null: return false
|
|
var parent: Node = _rig.get_parent()
|
|
if parent == null: parent = _rig.get_tree().current_scene
|
|
if parent == null: return false
|
|
_root = Node2D.new(); _root.name = CONTAINER_NAME; parent.add_child(_root); _bodies.clear()
|
|
for entry: Dictionary in BODIES: _build_body(entry)
|
|
for entry: Dictionary in JOINTS: _build_joint(entry)
|
|
var torso: RigidBody2D = _bodies.get("torso") as RigidBody2D
|
|
if torso != null: torso.linear_velocity = _linear_velocity; torso.angular_velocity = _angular_velocity
|
|
reset_rest_state()
|
|
return torso != null
|
|
|
|
func destroy() -> void:
|
|
if _root != null and is_instance_valid(_root): _root.name = StringName(str(CONTAINER_NAME) + "_retired"); _root.queue_free()
|
|
_root = null; _bodies.clear(); reset_rest_state()
|
|
|
|
func get_bodies() -> Dictionary: return _bodies
|
|
func is_at_rest() -> bool: return _at_rest
|
|
func reset_rest_state() -> void: _at_rest = false; _rest_timer = 0.0; _stabilize_timer = 0.0
|
|
func apply_velocity_boost(velocity: Vector2) -> void:
|
|
for body: RigidBody2D in _bodies.values():
|
|
if body != null and is_instance_valid(body): body.apply_central_impulse(velocity * body.mass)
|
|
|
|
func enter_ragdoll() -> bool:
|
|
if _rig == null or _skeleton == null or _body_container == null:
|
|
push_warning("StickmanRagdoll: cannot enter ragdoll; missing rig nodes.")
|
|
return false
|
|
_capture_pre_ragdoll_state()
|
|
if _locomotion != null:
|
|
_locomotion.clear_impact_reactions()
|
|
_locomotion.set_collision_bodies_enabled(false)
|
|
_locomotion.cancel_walk(false)
|
|
if _anim_player != null and is_instance_valid(_anim_player):
|
|
_anim_player.stop(true)
|
|
if not build():
|
|
return false
|
|
_body_container.visible = false
|
|
_body_container.modulate.a = 1.0
|
|
if _skeleton.modification_stack != null:
|
|
_skeleton.modification_stack.enabled = false
|
|
reset_rest_state()
|
|
return true
|
|
|
|
func request_recovery() -> bool:
|
|
if _rig == null or _rig.state != StickmanRig.RigState.RAGDOLL:
|
|
return false
|
|
_capture_ragdoll_pose()
|
|
destroy()
|
|
_reanchor_root_to_landing()
|
|
state_transition_requested.emit(StickmanRig.RigState.RECOVERING)
|
|
_snap_skeleton_to_pose()
|
|
if _locomotion != null:
|
|
_locomotion.set_collision_bodies_enabled(true)
|
|
_play_stand_up()
|
|
return true
|
|
|
|
func cancel_recovery() -> void:
|
|
_stand_up_generation += 1
|
|
if _stand_up_tween != null and _stand_up_tween.is_valid():
|
|
_stand_up_tween.kill()
|
|
_stand_up_tween = null
|
|
|
|
func snap_to_standing(previous_state: int) -> void:
|
|
if _locomotion != null:
|
|
_locomotion.set_terrain_following_enabled(false)
|
|
if previous_state == StickmanRig.RigState.RAGDOLL:
|
|
destroy()
|
|
elif previous_state == StickmanRig.RigState.RECOVERING:
|
|
cancel_recovery()
|
|
if _locomotion != null:
|
|
_locomotion.clear_impact_reactions(true)
|
|
_rig._hide_speech()
|
|
_restore_standing_markers(false)
|
|
_rearm_ik_stack()
|
|
_set_limb_mods_enabled(true)
|
|
if previous_state == StickmanRig.RigState.ANIMATED:
|
|
_rig._apply_head_lookat_mirror_mode()
|
|
else:
|
|
_restore_head_lookat_state()
|
|
_recovery_bones.clear()
|
|
reset_rest_state()
|
|
_clear_recovery_capture()
|
|
if _locomotion != null:
|
|
_locomotion.set_collision_bodies_enabled(true)
|
|
if _body_container != null:
|
|
_body_container.visible = true
|
|
_body_container.modulate.a = 1.0
|
|
_rig._apply_idle_limb_solver_mode(_spawn_limb_rotations)
|
|
if previous_state != StickmanRig.RigState.ANIMATED:
|
|
state_transition_requested.emit(StickmanRig.RigState.ANIMATED)
|
|
|
|
func initialize_spawn_pose_from_skeleton() -> void:
|
|
if _rig == null:
|
|
return
|
|
var skeleton := _rig.get_node_or_null(NodePath("Skeleton2D")) as Skeleton2D
|
|
if skeleton == null:
|
|
push_warning("StickmanRagdoll: cannot initialize spawn pose; missing Skeleton2D.")
|
|
return
|
|
var pose: Dictionary = {}
|
|
var torso := skeleton.get_node_or_null(NodePath("Torso")) as Bone2D
|
|
if torso != null:
|
|
pose["Torso"] = {"pos": _rig.to_local(torso.global_position), "rot": torso.rotation}
|
|
var head := skeleton.get_node_or_null(NodePath("Torso/Head")) as Bone2D
|
|
if head != null:
|
|
var head_tip := head.to_global(Vector2(maxf(head.length, 1.0), 0.0).rotated(deg_to_rad(head.bone_angle)))
|
|
pose["Head"] = {"pos": _rig.to_local(head_tip), "rot": 0.0}
|
|
var endpoints: Dictionary = {
|
|
"Left_Hand": "Torso/LeftUpperArm/LeftLowerArm", "Right_Hand": "Torso/RightUpperArm/RightLowerArm",
|
|
"Left_Leg": "Torso/LeftUpperLeg/LeftLowerLeg", "Right_Leg": "Torso/RightUpperLeg/RightLowerLeg",
|
|
}
|
|
for marker_name: String in endpoints:
|
|
var bone := skeleton.get_node_or_null(NodePath(String(endpoints[marker_name]))) as Bone2D
|
|
if bone == null:
|
|
return
|
|
var endpoint := bone.to_global(Vector2(bone.length, 0.0).rotated(deg_to_rad(bone.bone_angle)))
|
|
pose[marker_name] = {"pos": _rig.to_local(endpoint), "rot": 0.0}
|
|
if pose.size() != IK_TARGET_PATHS.size():
|
|
return
|
|
for marker_name: String in pose:
|
|
var marker := _rig.get_node_or_null(NodePath(String(IK_TARGET_PATHS[marker_name]))) as Marker2D
|
|
if marker == null:
|
|
return
|
|
var target: Dictionary = pose[marker_name]
|
|
marker.position = target.get("pos", marker.position)
|
|
marker.rotation = target.get("rot", marker.rotation)
|
|
if marker_name == "Left_Leg" or marker_name == "Right_Leg":
|
|
var base_marker := _rig.get_node_or_null(NodePath("AnimationTargets/%s_Base" % marker_name)) as Marker2D
|
|
if base_marker != null:
|
|
base_marker.position = marker.position
|
|
_spawn_pose = pose.duplicate(true)
|
|
_spawn_limb_rotations.clear()
|
|
for joint: String in LIMB_BONE_PATHS:
|
|
var pair: Array = LIMB_BONE_PATHS[joint]
|
|
var upper := skeleton.get_node_or_null(NodePath(String(pair[0]))) as Bone2D
|
|
var lower := skeleton.get_node_or_null(NodePath(String(pair[1]))) as Bone2D
|
|
if upper != null and lower != null:
|
|
_spawn_limb_rotations[joint] = {"upper": upper.rotation, "lower": lower.rotation}
|
|
|
|
func get_spawn_limb_rotations() -> Dictionary:
|
|
return _spawn_limb_rotations
|
|
|
|
func get_captured_pose() -> Dictionary:
|
|
return _captured_pose
|
|
|
|
func _capture_pre_ragdoll_state() -> void:
|
|
_pre_ragdoll_state.clear()
|
|
var markers: Dictionary = {}
|
|
for marker_name: String in STAND_POSE:
|
|
var marker := _get_ik_marker(marker_name)
|
|
if marker != null:
|
|
markers[marker_name] = {"pos": marker.position, "rot": marker.rotation}
|
|
_pre_ragdoll_state["markers"] = markers
|
|
var rotations: Dictionary = {}
|
|
for joint: String in _limb_bones:
|
|
var pair: Dictionary = _limb_bones[joint]
|
|
var upper := pair.get("upper") as Bone2D
|
|
var lower := pair.get("lower") as Bone2D
|
|
rotations[joint] = {"upper": upper.rotation if upper != null else 0.0, "lower": lower.rotation if lower != null else 0.0}
|
|
_pre_ragdoll_state["limb_rotations"] = rotations
|
|
_pre_ragdoll_state["head_rotation"] = _head_bone.rotation if _head_bone != null else 0.0
|
|
_pre_ragdoll_state["facing_profile"] = int(_rig.facing_profile)
|
|
var bends: Dictionary = {}
|
|
for joint: String in StickmanRig.BEND_JOINTS:
|
|
bends[joint] = _rig.get_joint_bend_flipped(joint)
|
|
_pre_ragdoll_state["bends"] = bends
|
|
_pre_ragdoll_state["head_lookat_enabled"] = _head_look_at.enabled if _head_look_at != null else false
|
|
var leg_sides: Dictionary = {}
|
|
for leg_joint: String in ["LeftLeg", "RightLeg"]:
|
|
var pair: Dictionary = _limb_bones.get(leg_joint, {})
|
|
var upper := pair.get("upper") as Bone2D
|
|
var lower := pair.get("lower") as Bone2D
|
|
var marker := _get_ik_marker("Left_Leg" if leg_joint == "LeftLeg" else "Right_Leg")
|
|
if upper == null or lower == null or marker == null:
|
|
continue
|
|
var side := (marker.global_position - upper.global_position).cross(lower.global_position - upper.global_position)
|
|
leg_sides[leg_joint] = {"side": 1.0 if side >= 0.0 else -1.0, "flag": _rig.get_joint_bend_flipped(leg_joint)}
|
|
_pre_ragdoll_state["leg_sides"] = leg_sides
|
|
var torso_entry_rot := 0.0
|
|
var torso_marker := _get_ik_marker("Torso")
|
|
if torso_marker != null:
|
|
torso_entry_rot = torso_marker.rotation
|
|
var spine_entry_rel := 0.0
|
|
if _head_bone != null and _torso_bone != null:
|
|
spine_entry_rel = (_head_bone.global_position - _torso_bone.global_position).angle() - _rig.global_rotation
|
|
_pre_ragdoll_state["torso_chain_offset"] = _chain_angle_from_captured(spine_entry_rel) - torso_entry_rot
|
|
var head_entry_chain := torso_entry_rot + float(_pre_ragdoll_state.get("head_rotation", 0.0))
|
|
_pre_ragdoll_state["head_chain_offset"] = _chain_angle_from_captured(-_rig.global_rotation) - head_entry_chain
|
|
|
|
func _capture_ragdoll_pose() -> void:
|
|
_captured_pose.clear()
|
|
_captured_landing_center = Vector2.ZERO
|
|
_captured_ground_y = 0.0
|
|
var top_y := INF
|
|
var bottom_y := -INF
|
|
var exclude_rids: Array[RID] = []
|
|
for key: String in _bodies:
|
|
var body := _bodies[key] as RigidBody2D
|
|
if body == null or not is_instance_valid(body):
|
|
continue
|
|
var half := float(body.get_meta("half_height", 0.0))
|
|
var radius := float(body.get_meta("radius", 0.0))
|
|
var extent := half + radius
|
|
top_y = minf(top_y, body.global_position.y - extent)
|
|
bottom_y = maxf(bottom_y, body.global_position.y + extent)
|
|
exclude_rids.append(body.get_rid())
|
|
_captured_pose[key] = {"pos": _rig.to_local(body.global_position), "rot": body.global_rotation - _rig.global_rotation, "half": half}
|
|
var torso := _bodies.get("torso") as RigidBody2D
|
|
if torso != null and is_instance_valid(torso):
|
|
_captured_landing_center = torso.global_position
|
|
_captured_ground_y = _probe_ground_y(torso.global_position.x, top_y, bottom_y, exclude_rids)
|
|
if is_equal_approx(_captured_ground_y, bottom_y):
|
|
_captured_ground_y = torso.global_position.y + float(torso.get_meta("radius", 0.0))
|
|
_probe_top_y = top_y
|
|
_probe_bottom_y = bottom_y
|
|
_probe_exclude = exclude_rids
|
|
|
|
func _probe_ground_y(world_x: float, top_y: float, bottom_y: float, exclude: Array[RID]) -> float:
|
|
if _locomotion != null:
|
|
return _locomotion.probe_ground_y(world_x, top_y, bottom_y, exclude)
|
|
var best_y := bottom_y
|
|
var space_state := _rig.get_world_2d().direct_space_state
|
|
if space_state == null:
|
|
return best_y
|
|
var best_dist := INF
|
|
for offset: float in GROUND_PROBE_X_OFFSETS:
|
|
var query := PhysicsRayQueryParameters2D.create(Vector2(world_x + offset, top_y - GROUND_PROBE_START_OFFSET), Vector2(world_x + offset, bottom_y + GROUND_PROBE_END_OFFSET))
|
|
query.collision_mask = 1
|
|
query.exclude = exclude
|
|
var hit := space_state.intersect_ray(query)
|
|
if hit.is_empty():
|
|
continue
|
|
var y := (hit.get("position", Vector2.ZERO) as Vector2).y
|
|
if y < top_y - GROUND_PROBE_TOLERANCE or y > bottom_y + GROUND_PROBE_TOLERANCE:
|
|
continue
|
|
var distance := absf(y - bottom_y)
|
|
if distance < best_dist:
|
|
best_dist = distance
|
|
best_y = y
|
|
return best_y
|
|
|
|
func _reanchor_root_to_landing() -> void:
|
|
if not _captured_pose.has("torso"):
|
|
return
|
|
var feet_world := Vector2(_captured_landing_center.x, _captured_ground_y)
|
|
var old_root := _rig.global_position
|
|
var foot_offset := _locomotion.get_foot_offset() if _locomotion != null else Vector2(0.0, -385.0)
|
|
_rig.global_position = feet_world + foot_offset
|
|
var shift := old_root - _rig.global_position
|
|
var local_shift := _rig.to_local(shift) - _rig.to_local(Vector2.ZERO)
|
|
for key: String in _captured_pose:
|
|
var entry: Dictionary = _captured_pose[key]
|
|
entry["pos"] = entry.get("pos", Vector2.ZERO) + local_shift
|
|
|
|
func _clear_recovery_capture() -> void:
|
|
_captured_pose.clear()
|
|
_captured_landing_center = Vector2.ZERO
|
|
_captured_ground_y = 0.0
|
|
_pre_ragdoll_state.clear()
|
|
_probe_top_y = 0.0
|
|
_probe_bottom_y = 0.0
|
|
_probe_exclude.clear()
|
|
|
|
func _chain_angle_from_captured(captured_rel: float) -> float:
|
|
return -captured_rel if _rig.facing_profile == StickmanRig.FacingProfile.LEFT else captured_rel
|
|
|
|
func _wrap_toward(value: float, target: float) -> float:
|
|
return target + wrapf(value - target, -PI, PI)
|
|
|
|
func _natural_joint_target(current: float, target: float, offset: float) -> float:
|
|
var a := wrapf(current + offset, -PI, PI)
|
|
var b := wrapf(target + offset, -PI, PI)
|
|
var delta := wrapf(b - a, -PI, PI)
|
|
if a * b < 0.0 and absf(a) + absf(b) > PI:
|
|
delta += TAU if delta < 0.0 else -TAU
|
|
return current + delta
|
|
|
|
func _set_limb_mods_enabled(enabled: bool) -> void:
|
|
for joint: String in StickmanRig.BEND_JOINTS:
|
|
var modification := _bend_modifications.get(joint) as SkeletonModification2DTwoBoneIK
|
|
if modification != null and is_instance_valid(modification):
|
|
modification.enabled = enabled
|
|
|
|
func _restore_head_lookat_state() -> void:
|
|
if _head_look_at != null and is_instance_valid(_head_look_at):
|
|
_head_look_at.enabled = bool(_pre_ragdoll_state.get("head_lookat_enabled", false))
|
|
|
|
func _rearm_ik_stack() -> void:
|
|
if _skeleton == null or not is_instance_valid(_skeleton):
|
|
return
|
|
var stack: SkeletonModificationStack2D = _skeleton.modification_stack
|
|
if stack != null:
|
|
if not stack.get_is_setup():
|
|
stack.setup()
|
|
stack.enabled = true
|
|
_skeleton.set_process_internal(true)
|
|
var torso_marker := _get_ik_marker("Torso")
|
|
if torso_marker != null:
|
|
var driver := torso_marker.get_node_or_null("RemoteTransform2D") as RemoteTransform2D
|
|
if driver != null:
|
|
driver.update_position = true
|
|
driver.update_rotation = true
|
|
driver.update_scale = true
|
|
|
|
func _restore_standing_markers(use_spawn_pose: bool = true) -> void:
|
|
var pose := _spawn_pose if use_spawn_pose and not _spawn_pose.is_empty() else STAND_POSE
|
|
for marker_name: String in pose:
|
|
var marker := _get_ik_marker(marker_name)
|
|
if marker == null:
|
|
continue
|
|
var target: Dictionary = pose[marker_name]
|
|
marker.position = target.get("pos", marker.position)
|
|
if marker_name == "Torso":
|
|
marker.rotation = target.get("rot", marker.rotation)
|
|
|
|
func _get_ik_marker(marker_name: String) -> Marker2D:
|
|
if _rig == null:
|
|
return null
|
|
var path := String(IK_TARGET_PATHS.get(marker_name, ""))
|
|
return _rig.get_node_or_null(NodePath(path)) as Marker2D if not path.is_empty() else null
|
|
|
|
func _set_marker_from_body(marker_name: String, body_key: String) -> void:
|
|
var marker := _get_ik_marker(marker_name)
|
|
var pose: Dictionary = _captured_pose.get(body_key, {})
|
|
if marker == null or pose.is_empty():
|
|
return
|
|
var direction := Vector2.from_angle(float(pose.get("rot", 0.0)) + _rig.global_rotation)
|
|
var center_world := _rig.to_global(pose.get("pos", Vector2.ZERO))
|
|
marker.position = _rig.to_local(center_world + direction * float(pose.get("half", 0.0)))
|
|
|
|
func _snap_skeleton_to_pose() -> void:
|
|
_recovery_bones.clear()
|
|
var pre_markers: Dictionary = _pre_ragdoll_state.get("markers", {})
|
|
var pre_limb_rotations: Dictionary = _pre_ragdoll_state.get("limb_rotations", {})
|
|
var pre_head_rotation := float(_pre_ragdoll_state.get("head_rotation", 0.0))
|
|
var torso_chain_offset := float(_pre_ragdoll_state.get("torso_chain_offset", 0.0))
|
|
var head_chain_offset := float(_pre_ragdoll_state.get("head_chain_offset", 0.0))
|
|
var theta_t := 0.0
|
|
var torso_marker := _get_ik_marker("Torso")
|
|
if torso_marker != null:
|
|
var torso_pose: Dictionary = _captured_pose.get("torso", {})
|
|
if not torso_pose.is_empty():
|
|
var torso_rel := float(torso_pose.get("rot", 0.0))
|
|
var spine_dir := Vector2.from_angle(torso_rel + _rig.global_rotation)
|
|
var center_world := _rig.to_global(torso_pose.get("pos", Vector2.ZERO))
|
|
torso_marker.position = _rig.to_local(center_world - spine_dir * float(torso_pose.get("half", 0.0)))
|
|
var raw_rotation := _chain_angle_from_captured(torso_rel) - torso_chain_offset
|
|
var pre_torso_rotation := float((pre_markers.get("Torso", {}) as Dictionary).get("rot", raw_rotation))
|
|
theta_t = wrapf(_wrap_toward(raw_rotation, pre_torso_rotation), -PI, PI)
|
|
torso_marker.rotation = theta_t
|
|
var head_marker := _get_ik_marker("Head")
|
|
var head_pose: Dictionary = _captured_pose.get("head", {})
|
|
if head_marker != null and not head_pose.is_empty():
|
|
head_marker.position = head_pose.get("pos", head_marker.position)
|
|
_set_marker_from_body("Left_Hand", "left_lower_arm")
|
|
_set_marker_from_body("Right_Hand", "right_lower_arm")
|
|
_set_marker_from_body("Left_Leg", "left_lower_leg")
|
|
_set_marker_from_body("Right_Leg", "right_lower_leg")
|
|
for marker_name: String in ["Left_Leg", "Right_Leg"]:
|
|
var marker := _get_ik_marker(marker_name)
|
|
var body_key := "left_lower_leg" if marker_name == "Left_Leg" else "right_lower_leg"
|
|
if marker == null or (_captured_pose.get(body_key, {}) as Dictionary).is_empty():
|
|
continue
|
|
var ankle_world := _rig.to_global(marker.position)
|
|
var ground_y := _probe_ground_y(ankle_world.x, _probe_top_y, _probe_bottom_y, _probe_exclude)
|
|
if ankle_world.y < ground_y:
|
|
ankle_world.y = ground_y
|
|
marker.position = _rig.to_local(ankle_world)
|
|
if not (_captured_pose.get("torso", {}) as Dictionary).is_empty():
|
|
for joint: String in LIMB_BONE_PATHS:
|
|
var pair: Dictionary = _limb_bones.get(joint, {})
|
|
var upper := pair.get("upper") as Bone2D
|
|
var lower := pair.get("lower") as Bone2D
|
|
if upper == null or lower == null:
|
|
continue
|
|
var keys: Array = LIMB_BODY_KEYS[joint]
|
|
var upper_pose: Dictionary = _captured_pose.get(keys[0], {})
|
|
var lower_pose: Dictionary = _captured_pose.get(keys[1], {})
|
|
if upper_pose.is_empty() or lower_pose.is_empty():
|
|
continue
|
|
var upper_chain := _chain_angle_from_captured(float(upper_pose.get("rot", 0.0)))
|
|
var lower_chain := _chain_angle_from_captured(float(lower_pose.get("rot", 0.0)))
|
|
var pre_pair: Dictionary = pre_limb_rotations.get(joint, {})
|
|
upper.rotation = _wrap_toward(upper_chain - theta_t - deg_to_rad(upper.bone_angle), float(pre_pair.get("upper", upper.rotation)))
|
|
lower.rotation = _wrap_toward(lower_chain - upper_chain + deg_to_rad(upper.bone_angle) - deg_to_rad(lower.bone_angle), float(pre_pair.get("lower", lower.rotation)))
|
|
upper.rotation = wrapf(upper.rotation, -PI, PI)
|
|
lower.rotation = wrapf(lower.rotation, -PI, PI)
|
|
if joint == "LeftLeg" or joint == "RightLeg":
|
|
continue
|
|
var target_upper := upper.rotation + wrapf(float(pre_pair.get("upper", upper.rotation)) - upper.rotation, -PI, PI)
|
|
var pre_lower := float(pre_pair.get("lower", lower.rotation))
|
|
var path_override := String((RECOVERY_JOINT_PATH.get(_rig.facing_profile, {}) as Dictionary).get(joint, ""))
|
|
var target_lower := lower.rotation + wrapf(pre_lower - lower.rotation, -PI, PI) if path_override == "shortest" else _natural_joint_target(lower.rotation, pre_lower, deg_to_rad(lower.bone_angle - upper.bone_angle))
|
|
_recovery_bones.append({"bone": upper, "target": target_upper})
|
|
_recovery_bones.append({"bone": lower, "target": target_lower})
|
|
if not head_pose.is_empty() and _head_bone != null:
|
|
var raw_head := _chain_angle_from_captured(float(head_pose.get("rot", 0.0))) - head_chain_offset - theta_t
|
|
_head_bone.rotation = wrapf(_wrap_toward(raw_head, pre_head_rotation), -PI, PI)
|
|
_recovery_bones.append({"bone": _head_bone, "target": _head_bone.rotation + wrapf(pre_head_rotation - _head_bone.rotation, -PI, PI)})
|
|
if _body_container != null:
|
|
_body_container.visible = true
|
|
_body_container.modulate.a = 1.0
|
|
_rearm_ik_stack()
|
|
_set_limb_mods_enabled(false)
|
|
if _head_look_at != null:
|
|
_head_look_at.enabled = false
|
|
for leg_joint: String in ["LeftLeg", "RightLeg"]:
|
|
var modification := _bend_modifications.get(leg_joint) as SkeletonModification2DTwoBoneIK
|
|
if modification == null:
|
|
continue
|
|
var entry: Dictionary = (_pre_ragdoll_state.get("leg_sides", {}) as Dictionary).get(leg_joint, {})
|
|
var forced: Dictionary = RECOVERY_LEG_SIDE_FORCE.get(_rig.facing_profile, {})
|
|
if forced.has(leg_joint):
|
|
modification.flip_bend_direction = bool(forced[leg_joint])
|
|
elif not entry.is_empty():
|
|
var upper_key := "left_upper_leg" if leg_joint == "LeftLeg" else "right_upper_leg"
|
|
var lower_key := "left_lower_leg" if leg_joint == "LeftLeg" else "right_lower_leg"
|
|
var up_pose: Dictionary = _captured_pose.get(upper_key, {})
|
|
var low_pose: Dictionary = _captured_pose.get(lower_key, {})
|
|
if not up_pose.is_empty() and not low_pose.is_empty():
|
|
var dir_u := Vector2.from_angle(float(up_pose.get("rot", 0.0)) + _rig.global_rotation)
|
|
var dir_l := Vector2.from_angle(float(low_pose.get("rot", 0.0)) + _rig.global_rotation)
|
|
var center_u := _rig.to_global(up_pose.get("pos", Vector2.ZERO))
|
|
var hip := center_u - dir_u * float(up_pose.get("half", 0.0))
|
|
var knee := center_u + dir_u * float(up_pose.get("half", 0.0))
|
|
var ankle := _rig.to_global(low_pose.get("pos", Vector2.ZERO)) + dir_l * float(low_pose.get("half", 0.0))
|
|
var captured_side := 1.0 if (ankle - hip).cross(knee - hip) >= 0.0 else -1.0
|
|
modification.flip_bend_direction = bool(entry.get("flag", false)) if captured_side == float(entry.get("side", captured_side)) else not bool(entry.get("flag", false))
|
|
modification.enabled = true
|
|
|
|
func _play_stand_up() -> void:
|
|
_stand_up_generation += 1
|
|
var generation := _stand_up_generation
|
|
var target_markers: Dictionary = _pre_ragdoll_state.get("markers", STAND_POSE)
|
|
_stand_up_tween = _tween_markers_to(target_markers, STAND_UP_DURATION)
|
|
for entry: Dictionary in _recovery_bones:
|
|
var bone := entry.get("bone") as Bone2D
|
|
if bone != null:
|
|
_stand_up_tween.tween_property(bone, "rotation", float(entry.get("target", bone.rotation)), STAND_UP_DURATION)
|
|
_stand_up_tween.finished.connect(_on_stand_up_finished.bind(generation))
|
|
|
|
func _tween_markers_to(target_pose: Dictionary, duration: float) -> Tween:
|
|
var tween := create_tween()
|
|
tween.set_parallel(true).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
|
|
for marker_name: String in target_pose:
|
|
var marker := _get_ik_marker(marker_name)
|
|
if marker == null:
|
|
continue
|
|
var target: Dictionary = target_pose[marker_name]
|
|
tween.tween_property(marker, "position", target.get("pos", marker.position), duration)
|
|
if marker_name == "Torso":
|
|
tween.tween_property(marker, "rotation", target.get("rot", marker.rotation), duration)
|
|
return tween
|
|
|
|
func _on_stand_up_finished(generation: int) -> void:
|
|
if generation != _stand_up_generation or _rig.state != StickmanRig.RigState.RECOVERING:
|
|
return
|
|
var recovered_rotations: Dictionary = _pre_ragdoll_state.get("limb_rotations", {})
|
|
_rig.set_facing_profile(int(_pre_ragdoll_state.get("facing_profile", int(_rig.facing_profile))))
|
|
var bends: Dictionary = _pre_ragdoll_state.get("bends", {})
|
|
for joint: String in StickmanRig.BEND_JOINTS:
|
|
if bends.has(joint):
|
|
_rig.set_joint_bend_flipped(joint, bool(bends[joint]))
|
|
_set_limb_mods_enabled(true)
|
|
_restore_head_lookat_state()
|
|
_recovery_bones.clear()
|
|
_rearm_ik_stack()
|
|
if _body_container != null:
|
|
_body_container.visible = true
|
|
_body_container.modulate.a = 1.0
|
|
if _locomotion != null:
|
|
_locomotion.set_collision_bodies_enabled(true)
|
|
_rig._apply_idle_limb_solver_mode(recovered_rotations)
|
|
state_transition_requested.emit(StickmanRig.RigState.ANIMATED)
|
|
|
|
func _build_body(entry: Dictionary) -> void:
|
|
var key: String = str(entry["key"]); var body := RigidBody2D.new(); body.name = "Ragdoll_" + key
|
|
body.mass = float(entry["mass"]); body.linear_damp = float(entry["linear_damp"]); body.angular_damp = float(entry["angular_damp"]); body.collision_layer = 1; body.collision_mask = 1
|
|
body.set_meta("stickman_rig", _rig); body.set_meta("part_name", key); body.set_meta("stickman_ragdoll_body", true)
|
|
var shape := CollisionShape2D.new()
|
|
if str(entry["kind"]) == "visual":
|
|
var visual := _rig.get_node_or_null(NodePath(str(entry["node_path"]))) as Node2D
|
|
if visual == null: return
|
|
var circle := CircleShape2D.new(); circle.radius = float(entry["radius"]); shape.shape = circle; body.add_child(shape); body.position = visual.global_position; body.set_meta("half_height", 0.0); body.set_meta("radius", float(entry["radius"]))
|
|
if not _clone_visuals(body, str(entry["visual_path"])): _add_circle(body, float(entry["radius"]))
|
|
else:
|
|
var bone := _skeleton.get_node_or_null(NodePath(str(entry["node_path"]))) as Bone2D
|
|
if bone == null: return
|
|
var origin: Vector2 = bone.global_position; var tip: Vector2
|
|
if key == "torso":
|
|
var head := _skeleton.get_node_or_null(NodePath("Torso/Head")) as Bone2D
|
|
if head == null: return
|
|
tip = head.global_position
|
|
else: tip = bone.to_global(Vector2(bone.length, 0.0).rotated(deg_to_rad(bone.bone_angle)))
|
|
var length: float = origin.distance_to(tip); var capsule := CapsuleShape2D.new(); capsule.height = length; capsule.radius = float(entry["radius"]); shape.shape = capsule; shape.rotation = -PI * 0.5; body.add_child(shape); body.position = (origin + tip) * 0.5; body.rotation = (tip - origin).angle(); body.set_meta("half_height", length * 0.5); body.set_meta("radius", float(entry["radius"]))
|
|
if not _clone_visuals(body, str(entry["visual_path"])): _add_capsule(body, length, float(entry["radius"]))
|
|
_root.add_child(body); _bodies[key] = body
|
|
|
|
func _build_joint(entry: Dictionary) -> void:
|
|
var child := _bodies.get(str(entry["child"])) as RigidBody2D; var parent := _bodies.get(str(entry["parent"])) as RigidBody2D; var bone := _skeleton.get_node_or_null(NodePath(str(entry["pin"]))) as Bone2D
|
|
if child == null or parent == null or bone == null: return
|
|
var pin := PinJoint2D.new(); pin.name = "RagdollPin_" + str(entry["child"]); pin.position = bone.global_position; _root.add_child(pin); pin.node_a = pin.get_path_to(parent); pin.node_b = pin.get_path_to(child); pin.softness = TARGET_SOFTNESS
|
|
_apply_joint_limits(pin, str(entry["limit"]), str(entry["child"]))
|
|
|
|
func _apply_joint_limits(pin: PinJoint2D, limit: String, child_key: String) -> void:
|
|
if limit == "shoulder":
|
|
pin.angular_limit_enabled = true; pin.angular_limit_lower = -deg_to_rad(160.0); pin.angular_limit_upper = deg_to_rad(160.0)
|
|
elif limit != "neck":
|
|
var profile: int = int(_rig.facing_profile) if _rig != null else StickmanRig.FacingProfile.FORWARD
|
|
var band: Dictionary = JOINT_BAND_LIMITS.get(profile, {}).get(child_key, {})
|
|
if band.is_empty(): band = { "lower": -5.0 if limit == "cw" else -150.0, "upper": 150.0 if limit == "cw" else 5.0 }
|
|
pin.angular_limit_enabled = true; pin.angular_limit_lower = deg_to_rad(float(band["lower"])); pin.angular_limit_upper = deg_to_rad(float(band["upper"]))
|
|
else: pin.angular_limit_enabled = false
|
|
|
|
func _clone_visuals(body: RigidBody2D, path: String) -> bool:
|
|
var source := _rig.get_node_or_null(NodePath(path)) as Node2D
|
|
if source == null: return false
|
|
var count: int = 0
|
|
for child: Node in source.get_children():
|
|
if child is Line2D:
|
|
var src := child as Line2D; var copy := Line2D.new(); var points := PackedVector2Array()
|
|
for point: Vector2 in src.points: points.append(body.to_local(src.to_global(point)))
|
|
copy.points = points; copy.width = src.width; copy.default_color = src.default_color; copy.begin_cap_mode = src.begin_cap_mode; copy.end_cap_mode = src.end_cap_mode; copy.joint_mode = src.joint_mode; copy.closed = src.closed; body.add_child(copy); count += 1
|
|
elif child is Polygon2D:
|
|
var src := child as Polygon2D; var copy := Polygon2D.new(); var points := PackedVector2Array()
|
|
for point: Vector2 in src.polygon: points.append(body.to_local(src.to_global(point)))
|
|
copy.polygon = points; copy.color = src.color; body.add_child(copy); count += 1
|
|
return count > 0
|
|
|
|
func _add_capsule(body: RigidBody2D, length: float, radius: float) -> void:
|
|
var line := Line2D.new(); line.points = PackedVector2Array([Vector2(-length * 0.5, 0.0), Vector2(length * 0.5, 0.0)]); line.width = radius * 2.0; line.default_color = VISUAL_COLOR; line.begin_cap_mode = Line2D.LINE_CAP_ROUND; line.end_cap_mode = Line2D.LINE_CAP_ROUND; body.add_child(line)
|
|
func _add_circle(body: RigidBody2D, radius: float) -> void:
|
|
var polygon := Polygon2D.new(); var points := PackedVector2Array()
|
|
for index: int in CIRCLE_SEGMENTS: points.append(Vector2.from_angle(TAU * float(index) / float(CIRCLE_SEGMENTS)) * radius)
|
|
polygon.polygon = points; polygon.color = Color.WHITE; body.add_child(polygon)
|