Changing size of RigidBody2D at runtime

Godot Version

v4.5.1.limboai+v1.5.2.gha [f62fdbde1]

Question

The classic way of implementing a rope or other articulated segment is a sequence of RigidBody2D’s with a CollisionShape2D, often an capsule or rectangle, with a PinJoint2D at the end of the CollisionShape2D. The PinJoint2D is attached to it’s parent RigidBody2D and the RigidBody2D of the next “link” in the chain.

I have this working.

I want to gradually increase the length of the rope, however. If you imagine a rope hanging from the ceiling, subject to gravity, I would like to “unspool” the rope as if it were spooled up on a spindle at the ceiling - thus the “new rope” should come from the top, and not be added at the bottom.

Attempts Made:

A. Increase the size of the Shape2D

I’ve gradually increased the length of the Shape2D and increased the position of the PinJoint2D:

(piece.CollisionShape2D.shape as CapsuleShape2D).height = piece_length
piece.CollisionShape2D.position.y = piece_length / 2
piece.PinJoint2D.position.y = piece_length

This moves the joint and the piece, but the next rope piece remains “joint’ed” at the same place. If I double the size of the first piece, it ends up overlapping the second piece, which doesn’t move.

B. Insert another piece of the same length

var new_piece = add_piece()
var last_node_b = last_piece.PinJoint2D.node_b
last_piece.PinJoint2D.node_b = new_piece.get_path()
new_piece.PinJoint2D.node_b = last_node_b

This also adds another element, but physics then gets broken and the CollisionShape2D loses attachment on one of it’s ends.

Question

How do I increase the length of a rope, either by adding new PinJoint2D+RigidBody2Dlinks, or by changing the size of an existing link?

This was particularly obnoxious to implement. I ended up adding a GrooveJoint2D with a “new” anchor that moved the new anchor to the position of the previous anchor before deleting the previous anchor.

I’m going to drop some code here for SEO, but feel free to find me (@gambitdash) on the Discord if you have questions:

# In the joint, node_a always points to yourself, and node_b always points to the next node
# in the chain.  When allocating a new piece, set the prev_piece's node_b to the newly
# allocated piece.
func add_piece(prev_piece: RopePiece, id: int, shape: CapsuleShape2D, spawn_angle: float) -> RopePiece:
	var prev_joint: PinJoint2D = get_joint(prev_piece)
	
	var piece := RopePieceScene.instantiate() as RopePiece
	get_shape(piece).shape = shape
	get_shape(piece).position.y = piece_length / 2
	get_joint(piece).position.y = piece_length
	piece.global_position = prev_joint.global_position
	piece.rotation = spawn_angle
	piece.gravity_scale = 0.0
	piece.set_name("rope_piece_" + str(id))

	add_child(piece)
	
	# Set the prev_piece.joint.node_b to point at the new piece.
	prev_joint.node_a = prev_piece.get_path()
	prev_joint.node_b = piece.get_path()
	
	# Defensively set the new piece node_a
	get_joint(piece).node_a = piece.get_path()
	
	return piece



func spool_next_piece():
	var old_first_piece := get_next(rope_start)
	var common_shape := get_shape(old_first_piece).shape
		
	# Determine the direction of the first piece in the rope
	var start_angle := get_angle(get_joint(get_next(get_next(rope_start))))
	var back_angle_vec := Vector2.from_angle(start_angle - PI / 2)
	
	# Find the position behind the current starting position
	var start_position := rope_start.global_position
	var new_position := start_position + back_angle_vec * piece_length

	# Create a new End Piece to act as a temporary anchor during physics
	var new_start: RopePiece = RopeEndPieceScene.instantiate()
	new_start.gravity_scale = 0.0
	new_start.global_position = new_position
	add_child(new_start)

	# Create the new piece to insert into the rope
	var new_piece := add_piece(new_start, 99, common_shape, start_angle)
	# Connect the old first piece after the new piece
	get_joint(new_piece).node_b = old_first_piece.get_path()
	
	# Decouple the old start anchor
	get_joint(rope_start).node_b = ""
	rope_start.visible = false
	
	# Now set up the force to unspool it:
	# await new_start.relocate_to(start_position)
	await new_start.relocate_to(piece_length, start_angle, rope_start)
	
	# Reattach the old start and free the new start when the new start arrives
	get_joint(rope_start).node_b = get_joint(new_start).node_b
	new_start.queue_free()
	rope_start.visible = true


###############################
# Then, in the RopePiece class (which is a RigidBody2D → ColliisionShape2D, PinJoint2Dobject):

func relocate_to(length: float, angle: float, target_anchor: RopePiece, force: float = 50):
	var groove := GrooveJoint2D.new()
	add_child(groove)
	groove.global_position = global_position
	groove.initial_offset = 0
	groove.length = length
	groove.rotate(angle)
	groove.node_a = target_anchor.get_path()
	groove.node_b = get_path()
	
	location_target = target_anchor.global_position
	add_constant_force((location_target - global_position) * force)
	await on_relocation_done
	# Don't need to clean up because we're in the temporary `new_start` object
	# and it'll get free'd after on_relocation_done is emitted.


func update_relocation() -> bool:
	if location_target == Vector2.INF:
		return false
		
	if global_position.distance_to(location_target) < LOCATION_TOLERANCE:
		location_target = Vector2.INF
		on_relocation_done.emit.call_deferred()
		return false
		
	return true


func _integrate_forces(state: PhysicsDirectBodyState2D) -> void:
	update_relocation()


Good luck, hope that helps!