Unexpected physics from rope created at runtime vs manual setup

Godot Version

v4.2.1

Question

Apologies if I missed something glaringly obvious, I only have 2 days experience working in this engine. I’m trying to create a mobile game where the player can fire a hand away from their character and once it hits something, it spawns a rope between the hand and the player for them to swing from.

Essentially, small rope segments are attached together on pin joints so I can make the rope any length, the first rope segment’s top pin joint is attached to the hand, and the last rope segment’s bottom pin joint is attached to the player. However, the player barely swings at all, losing near all momentum once the rope is straight up & down. On top of that, the player continues slowly falling.

When I create the same sort of thing manually, hooking up all the pin joints, everything seems to work flawlessly. The player naturally swings back & forth for much longer, and the player properly stays attached to the pin joint.

Hopefully these videos show up to properly showcase what I mean.
Just kidding, new users can’t upload attachments :slightly_frowning_face:

Maybe links to the videos in Google Drive is acceptable?
Manuel Setup
Runtime

Here’s the code I’ve come up with so far-

player:

extends RigidBody2D

@export var player_hand: PackedScene
@export var hand_speed = 50000
@export var hold_time_detection = 0.1
@export var swipe_length_detection = 100.0
var hold_timer = hold_time_detection
var is_swipe = false
var is_hold = false
var first_touch = Vector2.ZERO
var second_touch = Vector2.ZERO
var hands_spawned = []

# Called when the node enters the scene tree for the first time.
func _ready():
	pass

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	if Input.is_action_just_pressed("touch_screen"):
		first_touch = get_viewport().get_mouse_position()
	
	if Input.is_action_pressed("touch_screen"):
		second_touch = get_viewport().get_mouse_position()
		hold_timer -= delta
		
		if hold_timer <= 0:
			is_hold = true
		
	if Input.is_action_just_released("touch_screen"):
		var distance = second_touch - first_touch
		# Did the user move their finger enough during the hold for the intention to be a swipe
		if distance.length() > swipe_length_detection:
			is_swipe = true
		
		if is_hold and is_swipe:
			print("User swiped the screen.")
			var hand = player_hand.instantiate()
			# Reference the player that spawned the hand; also used for initial position
			hand.initialize(self)
			# Track the number of hands in existence
			hands_spawned.append(hand)
			# Spawn the hand by adding it to the main scene
			var main = get_tree().current_scene
			main.add_child(hand)
			# Send the hand in the direction of the swipe
			hand.extend_hand(distance.normalized() * delta * hand_speed)
			
		elif is_hold and !is_swipe:
			print("User held the screen.")
		else:
			print("User tapped the screen.")
		
		# Reset variables
		hold_timer = hold_time_detection
		is_hold = false
		is_swipe = false

func on_hand_extended():
	# Remove the previous hand when a new hand collides with something
	if hands_spawned.size() > 1:
		print("A hand already exists")
		hands_spawned[0].queue_free()
		hands_spawned.remove_at(0)

func _physics_process(delta):
	if Input.is_action_pressed("move_right"):
		apply_force(Vector2(15, 0))
	if Input.is_action_pressed("move_left"):
		apply_force(Vector2(-15, 0))

hand:

extends RigidBody2D

var spawned_from
var stopped_pos
var is_extended

# Called when the node enters the scene tree for the first time.
func _ready():
	pass

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(_delta):
	if is_extended:
		# Stop all movement but keep rotation in line with the rope and player
		# Freeze would not accomplish this
		global_position = stopped_pos
		rotation = (stopped_pos - spawned_from.position).angle() + PI/2

func initialize(spawner):
	spawned_from = spawner
	# Set initial position
	global_position = spawner.position

func extend_hand(force):
	# Face the direction of the swipe and move in that direction
	rotate(force.angle() + PI/2)
	apply_impulse(force)

func _on_body_entered(_body):
	# When hitting something, stop all movement
	linear_velocity = Vector2(0, 0)
	angular_velocity = 0.0
	stopped_pos = global_position
	is_extended = true
	# Through the player, remove a previous hand
	spawned_from.on_hand_extended()
	# Spawn rope between the hand and the player
	var distance_traveled = stopped_pos - spawned_from.position
	# Distance traveled must be divided by the scale.y of the rope Sprite2D
	$RopeSpawner.spawn_rope_from_hand(distance_traveled.length()/20, self, spawned_from)

rope_spawner:

extends Node2D

var rope_segment_scene = preload("res://scenes/rope.tscn")
var rope_segments = []

func _ready():
	pass

func _process(_delta):
	pass

func spawn_rope_from_hand(length, hand_ref, player_ref):
	# Make sure length is an int
	length = round(length)
	for i in range(length):
		# Create the next rope segment
		var rope_segment = rope_segment_scene.instantiate()
		# Move the rope segments down to simulate a single, longer piece of rope
		rope_segment.position.y = i * rope_segment.get_node("Sprite2D").scale.y
		# Using call_deferred to avoid the error "Can't change this state while flushing queries"
		call_deferred("add_child", rope_segment)
		# Track spawned rope segments
		rope_segments.append(rope_segment)
	
	# A delay to avoid the error "Cannot get path of node as it is not in the scene tree"
	await get_tree().create_timer(.01).timeout
	
	for i in range(length):
		if i == 0:
			# Connect node a of the top pin joint to the hand
			rope_segments[i].get_node("PinJoint2DTop").node_a = hand_ref.get_path()
		elif i == (length - 1):
			# Connect node b of the bottom pin joing to the player
			rope_segments[i].get_node("PinJoint2DBottom").node_b = player_ref.get_path()
		else:
			# Connect node a of the top pin joint of this rope segment to the previous rope segment
			rope_segments[i].get_node("PinJoint2DTop").node_a = rope_segments[i - 1].get_path()
			# Connect node b of the bottom pin joint of the previous rope segment to this rope segment
			rope_segments[i - 1].get_node("PinJoint2DBottom").node_b = rope_segments[i].get_path()

These two line look wrong. You shouldn’t need to defer a call here. And you should set position after adding the child.

This is a snippet of my 3d segmented grapple hook code

Basically you instantiate, add child, position, and join.

That’s good to know. I tried reformatting the code similar to what you provided and call add_child() before changing the rope_segment’s position, but I still receive errors telling me to use call/set_deferred(). Maybe my issue has to do with how I join the rope_segments, either to each other, the hand, or the player. What do your join_to_seg() and join_to_winch() functions look like?

That’s very strange I’ve never had that issue. Are you running physics on a separate thread by chance?

Looking at your code again that hand replacement logic seems a little sus. Your hand may be queued for deletion and it is complaining about all the the new child nodes being added? I would need to look at Godots source code for the context of that log…

I looked around the to see if anyone else has seen this issue and it seem like you should not create or destroy nodes during the physics callbacks “on_body_entered”?

Since this engine is new to me, I don’t know how to run multiple threads yet.

Interesting. So if I move everything from _on_body_entered() to its own function, then call that using call_deferred, I do not get the error when add_child() runs.

Didn’t change the way the rope works at all though. It’s incredibly stiff when it spawns between the hand and player, remains in a completely straight line, but in the scene where I manually connected the hand, rope segments, and player, you can clearly see each rope segment moving based on the rotation & velocity of the other rope segments.

I remember reading something following one of Godot’s 2D example projects where messing with the rigidbody, like if you directly changed the player’s position or something, it would disable the rigidbody and forces like gravity wouldn’t affect it anymore. When the hand collides with something and has its position/rotation set, I guess the rope is following suit.

Let me try adding the rope segments to the main scene instead of the hand/rope spawner and use global_position of the hand/player to position the rope.

Never mind, the rope doesn’t move at all when spawning it like that.

I’m looking at your code and it seems like you have two pins per segment, so you end up making two pin connections between bodies. you only need one.

I kept my segments asymmetric with special joints that connected to player and anchor point.

Do you have connected body collisions turned off on the joint?

I’m also looking at how I create my joint, I actually spawn it dynamically if a node path is set when the segment becomes ready. I instantiate the joint scene set the paths to the connected seg and self, then set position and add the joint as child. I mainly dynamically spawned the joint because my ropes are synchronized over multiplayer, and the rope state is a little tricky to synchronize.

I don’t think this is an issue in general but after I establish the jointed segments i do give the new segment a slight random rotation so the rope isn’t perfectly stacked. making it collapse on itself more realistic. I also use a capsule shape I think for 2D you may want a capsule shape so the ends can rotate on eachother.

looking at your videos again the manual setup there are visible gaps between segs. on the dynamic video, I can’t see a gap. I think maybe then the rigidbody for the segments is a box shape and collisions are on so they collide on the corners when you perfectly stack them. making it really stiff.

you may be able to see this if you turn on debug collisions…

image

The PinJoints have Disable Collision On, and the rope’s rigidbody’s layer/mask doesn’t have anything selected, so from my understanding the rope segments shouldn’t collide with each other at all.

I switched the collision shape on the rope to a capsule and removed the top pin joint. Had to position a pin joint on the hand so I could still connect the rope to it.

The gap you see when I manually set up the rope is simply there cause of their rotation and moving them on the grid. If I used a value larger than the rope’s sprite’s y scale in code, it would introduce a gap between the rope segments, but I’d like the rope to spawn in without gaps so it looks like one piece of rope.

Now of course the physics are worse than before XD. Turning on debug collisions definitely helped unveil one problem:
Colliders Falling?
The one capsule collider that stays around the player is from the rope segment that has its pin joint attach to the player. If I set gravity scale on the rope to 0 the colliders don’t fall anymore but the player still breaks off the collider connected to the last rope segment.

I got it stable again by not zeroing the hand’s velocity when it collides with something, but still the rope is stiff and the player slowly falls.
Here’s what it looks like after I spawn the rope segments in the main scene rather than as a child of the rope spawner, which is a child of the hand, just in case something else about the hand is causing the rope to behave strangely.
Rope Spawns in Main Scene

I guess since they fall, then maybe the rope isn’t connecting to the “wall” body I don’t readily see it in the provided code.

Also I don’t see how the hand is pinned to the player body.

It seems like one end of the rope is connected to the hand and the other end to the player. Which doesn’t seem correct? Maybe I’m confused by the naming…

Also You are also deleting and spawning hands. Not sure there is code to connect to player.

Is the rope like the players arm? And the hand is grabbing a surface?

In the rope spawner, the first rope segment is pinned to the hand, or in my old code-
if i == 0:
# Connect node a of the top pin joint to the hand
rope_segments[i].get_node(“PinJoint2DTop”).node_a = hand_ref.get_path()

The last rope segment created is pinned to the player,
elif i == (length - 1):
# Connect node b of the bottom pin joing to the player
rope_segments[i].get_node(“PinJoint2DBottom”).node_b = player_ref.get_path()

The hand isn’t directly pinned to the player body. When the hand collides with something, it is meant to stop exactly where it collided and act like an anchor for the rope. Then the rope spawner attached to the hand is told to start spawning rope segments, which are pinned to the hand, each other, and the player. So you are correct here- “It seems like one end of the rope is connected to the hand and the other end to the player.”

You’re assumptions are spot on! The hand is meant to seem like its grabbing a surface, rotating towards the player while its position stays the same, and the rope is like the player’s arms, simply connecting the hand and the player.

I know the if/else block is working as intended as I had print() statements in them at one point, but I’m curious if there is a different method for connecting rigidbodys to pin joints than-
get_node(“PinJoint2DTop”).node_a = hand_ref.get_path()

not really no.

You could maybe make your life a little easier if give the responsibility to the segments, if they have a script attached. You can give them a NodePath when they are instantiated, and then internally setup a joint.

hint
# use a static function for the segment class
extends RigidBody
class_name Segment
static func new_segment(path:NodePath) -> Segment:
  var segment = load("res://segment.tscn").instanciate()
  segment.setup_joint(path)
  return segment
...
# on player
 rope_segments[i] = Segment.new_segment(rope_segments[i-1].get_path)
...

anyway what is the hand “grabbing” onto? do you have a anchor joint? and code to attach a joint to the colliding body? I guess an easier way to fake it is put a temporary StaticBody where you determined where the hand collided to act as the physical anchor the hand is jointed too.

But I’m surprised the rope does not stay connected to the player…

The hand’s position is continually set to its current position when it hits something so it doesn’t continue to slide along or bounce off a surface. I could try instead spawning a StaticBody at the hand’s position and attach the first rope segment to that, as that is how the rope is connected in my manual set up.

Oh okay I see the hand is just a rigid body, yea if there is gravity it will just fall after you stop it’s movement or be dragged down by anything that is connected to it.

Do you intend for the hand to grab walls and swing/pull the playrr? Or, grab objects and pull them to the player? Or both?

If it’s the former a static body is a solution. if the latter, then you need to add some collision code on the hand to spawn a joint to attach the hand to whatever objective it’s colliding with.

Spawning a static body and attaching the first rope segment to it instead of the hand didn’t change a thing.

I think I need to start over, whatever I’m doing right now is never going to replicate the natural swing I can only get from manually connecting rope segments.

1 Like

Darn, was the static body a sibling to the rigidbody and not a child? I think child rigidbody will typically ignore transforms, but I’m not sure about static bodies if they are spawned as children.

1 Like

Well, I started over. Results are WAY better than before, just took a slightly different approach. I’ll find out tomorrow if it breaks when I add back in my hand-slinging code. I guess I can mark this as the solution to my post as well! Thanks for the suggestions, pennyloafers.

I actually learned from a Godot Github post that the node’s position/rotation should be set before adding it as a child. Using the height of the rope_segment’s collision body seems more appropriate than using the sprite’s y scale. I also realized that in my conditions for spawning & connecting the rope to each other, the final rope segment that is connected to the player was never being connected to the previous rope segment, hence why it was always break off from the rest of the rope but stay connected to the player. Somehow I don’t need the call_deferred() when spawning rope anymore, too.

Here’s a demonstration of the new test.

The Main scene only contains the player scene.

The player scene is a rigidbody with a child collision shape and sprite.
Player Code:

extends RigidBody2D

@export var anchor_scene: PackedScene
@export var rope_segment_scene: PackedScene

var main_scene
var anchor_spawn_coords = Vector2(550, 100)
var anchor_to_player
var spawned_anchor
var rope_spawned = []

# Called when the node enters the scene tree for the first time.
func _ready():
	main_scene = get_tree().current_scene

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(_delta):
	if Input.is_action_just_pressed("spawn_rope"):
		anchor_to_player = anchor_spawn_coords - global_position
		spawn_anchor()
		spawn_multiple_rope(round(anchor_to_player.length()/30))
		
	if Input.is_action_just_pressed("delete_rope"):
		for n in rope_spawned.size():
			rope_spawned[n].queue_free()
		rope_spawned.clear()
		spawned_anchor.queue_free()

func spawn_anchor():
	var anchor = anchor_scene.instantiate()
	anchor.global_position = anchor_spawn_coords
	main_scene.add_child(anchor)
	spawned_anchor = anchor

func spawn_multiple_rope(count):
	print(count)
	for n in count:
		if n == count - 1:
			print("Last rope")
			spawn_rope()
			attach_rope_to_rope()
			attach_rope_to_player()
		elif n == 0:
			print("First rope")
			spawn_rope()
			attach_rope_to_anchor()
		else:
			print("Middle rope")
			spawn_rope()
			attach_rope_to_rope()

func spawn_rope():
	var rope = rope_segment_scene.instantiate()
	rope.global_position = anchor_spawn_coords
	rope.global_position -= anchor_to_player.normalized() * (rope_spawned.size()) * rope.get_length()
	rope.rotation = anchor_to_player.angle() + PI/2
	main_scene.add_child(rope)
	rope_spawned.append(rope)

func attach_rope_to_anchor():
	spawned_anchor.get_node("PinJoint2D").node_b = rope_spawned[0].get_path()

func attach_rope_to_rope():
	rope_spawned[rope_spawned.size()-2].get_node("PinJoint2D").node_b = rope_spawned[rope_spawned.size()-1].get_path()

func attach_rope_to_player():
	rope_spawned[rope_spawned.size()-1].get_node("PinJoint2D").node_b = get_path()

The rope_segment is a rigidbody with a child collision shape, pin joint, and sprite.
Rope Segment Code:

extends RigidBody2D

# Called when the node enters the scene tree for the first time.
func _ready():
	pass # Replace with function body.

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(_delta):
	pass

func attach_to_pinjoint(obj):
	$PinJoint2D.node_b = obj.get_path()

func get_length():
	print($CollisionShape2D.shape.height)
	return $CollisionShape2D.shape.height

The anchor is a static body with a child collision shape, pin joint, and sprite.
Anchor Code:

extends StaticBody2D

# Called when the node enters the scene tree for the first time.
func _ready():
	pass # Replace with function body.

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(_delta):
	pass

func attach_to_pinjoint(obj):
	$PinJoint2D.node_b = obj.get_path()

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.