Hey, my comment would be on the different approach crowd but if someone else has a suggestion on how to fix your current one then that’s awesome.
Anyway, I’m working on some ropes and they have the same issue at high speeds (like shooting an arrow with a rope attached to it) so I’m using verlet integration for it and if you constrain it properly with very stiff sticks, it works much better. Verlet is great for ragdolls as well, you just have to do the particle/stick thing and simulate it yourself. This is a bit of a rabbit hole but you can use the physics server to do collisions as well. I can share some code if you want to see an example, although my collision code is not great for the ropes haha.
Here’s a video of the rope, although in my game they don’t go that fast, I just modified it to follow the mouse to show you. You can see some stretching but I could fix it with better constraints or more iterations in the constraint part.
Here’s a good tutorial but it’s in unity:
There’s a lot of verlet integration tutorials out there. It’s a bit of a rabbit hole so take my comment with a big grain of salt hahaha
I promised some code for my rope, it’s a bit lengthy and in my game it does a lot of things but you should be able to copy the whole thing into your project and try it yourself.
Finally, a ragdoll is like a rope but there’s sticks connecting different joints so it’s more solid so you’d have to modify this heavily to make it work. Hope it helps!
@tool
extends Area2D
class_name VerletRope
@export var color := Color.WHITE
@export_range(-TAU, TAU) var starting_angle := 0.0
@export var texture : Texture
@export var texture_size := Vector2(1,1)
@export var texture_offset := Vector2()
@export_range(-TAU, TAU) var texture_rotation := 0.0
@export_range(0, 1) var friction := 0.95
@export var dampening := 0.95
@export var thickness := 5.0
@export var thickness_curve : Curve
@export var particles := 5
@export var segment_length := 20.0
@export var pin_start := true
@export var pin_end := false
@export_flags_2d_physics var collide_with := 0
var is_copy := false
var points : PackedVector2Array
var prevPoints : PackedVector2Array
var segments : Array[VerletRopeSegment]
var forces : PackedVector2Array
var shape : RID
var draw_points : PackedVector2Array
func _ready():
if !Engine.is_editor_hint():
shape = PhysicsServer2D.circle_shape_create()
PhysicsServer2D.shape_set_data(shape, thickness)
if !is_copy:
make_rope()
func make_rope():
for seg in segments:
seg.queue_free()
segments.clear()
forces.resize(particles)
points.resize(particles)
prevPoints.resize(particles)
for i in particles: # -1 to get correct segment ocunt
if i == particles - 1:
var start := Vector2.from_angle(starting_angle) * segment_length * i
points[i] = start
else:
var start := Vector2.from_angle(starting_angle) * segment_length * i
var shape := VerletRopeSegment.new()
var segment_shape := SegmentShape2D.new()
#shape.z_index = -1
var t := remap(i, 0, particles - 2, 0, 1)
var t2 := remap(i + 1, 0, particles - 2, 0, 1)
var thickness_t := thickness_curve.sample_baked(t) if thickness_curve else 1.0
var thickness_t2 := thickness_curve.sample_baked(t2) if thickness_curve else 1.0
shape.segment_length = segment_length
shape.color = color
shape.left_thickness = thickness * thickness_t
shape.right_thickness = thickness * thickness_t2
shape.texture = texture
shape.texture_size = texture_size
shape.texture_offset = texture_offset
shape.texture_repeat = CanvasItem.TEXTURE_REPEAT_ENABLED
add_child(shape)
shape.position = start
segment_shape.a = Vector2(0,0)
segment_shape.b = Vector2(segment_length, 0)
shape.shape = segment_shape
points[i] = start
segments.append(shape)
prevPoints = points.duplicate()
func _draw():
for p in points:
draw_circle(p, 4, Color.RED)
for p in draw_points:
draw_circle(p, 20, Color.GREEN)
draw_points.clear()
func _physics_process(delta : float):
#return
if points.size() != particles:
make_rope()
apply_force(Vector2.DOWN * 98)
calculate_points(delta)
for _j in 5:
apply_constrains()
for i in segments.size():
var shape := segments[i]
shape.position = points[i]
queue_redraw()
func calculate_points(delta : float):
var space_state := get_world_2d().direct_space_state
for i in particles:
if (i != 0 && i != particles - 1) || (i == 0 && !pin_start) || (i == particles - 1 && !pin_end):
var collision := collide_point(space_state,shape, to_global(points[i]))
var is_colliding := !collision.is_empty()
var velocity := (points[i] - prevPoints[i]) * dampening
velocity += forces[i] * delta
if is_colliding:
#velocity -= Vector2.DOWN * minf(Vector2.DOWN.dot(velocity), 0) # remove gravity force if colliding
velocity.y = minf(velocity.y, 0)
if is_colliding && collision.normal.dot(velocity) < 0: # only when going into the collision
points[i] = to_local(collision.point + collision.normal)
prevPoints[i] = to_local(collision.point + collision.normal)
else:
prevPoints[i] = points[i]
points[i] += velocity
forces.fill(Vector2.ZERO)
func apply_constrains():
for i in particles:
if i == particles - 1:
return
var distance := points[i].distance_to(points[i+1])
var difference := segment_length - distance
var percent := difference / distance
var offset := points[i+1] - points[i]
segments[i].rotation = offset.angle()
if i == 0:
if pin_start:
points[i+1] += offset * percent
else:
points[i] -= offset * (percent / 2)
points[i+1] += offset * (percent / 2)
else:
if pin_end && i + 1 == particles - 1:
points[i] -= offset * percent
else:
points[i] -= offset * (percent / 2)
points[i+1] += offset * (percent / 2)
func apply_force_to_segment(index : int, force : Vector2, wave_factor := 0.5, limit := -1):
if Utils.index_in_array(forces, index):
forces[index] += force
var left_propagation := range(index - 1, -1, -1)
var right_propagation := range(index + 1, points.size())
var left_factor := force * wave_factor
var right_factor := force * wave_factor
var left_count := 0
var right_count := 0
for i in left_propagation:
if left_factor.is_zero_approx() || (limit > -1 && left_count >= limit):
break
forces[i] += left_factor
left_factor *= wave_factor
left_count += 1
for i in right_propagation:
if right_factor.is_zero_approx() || (limit > -1 && right_count >= limit):
break
forces[i] += right_factor
right_factor *= wave_factor
right_count += 1
func apply_force(force : Vector2):
for i in forces.size():
forces[i] += force
func get_closest_point_index(anchor : Vector2, skip := -1) -> int:
var min := INF
var index := -1
for i in points.size():
if i == skip:
continue
var point := points[i]
var global := to_global(point)
var dist := global.distance_to(anchor)
if dist < min:
min = dist
index = i
return index
func get_closest_point(anchor : Vector2) -> Vector2:
var min := INF
var index := -1
for i in points.size():
var point := points[i]
var global := to_global(point)
var dist := global.distance_squared_to(anchor)
if dist < min:
min = dist
index = i
return to_global(points[index])
func get_point(index : int) -> Vector2:
var clamped := clampi(index, 0, points.size())
if Utils.index_in_array(points, clamped):
if clamped > 0:
var prev := points[index - 1]
return to_global(points[clamped].lerp(prev, 0.5))
return to_global(points[clamped])
return global_position
func split_rope(index : int):
if Utils.index_in_array(points,index) && index > 0 && index < points.size() - 1:
var rope := new_rope_from_index(index)
var dangler := segments[index - 1]
points = points.slice(0, index)
particles = points.size()
prevPoints = prevPoints.slice(0, index)
segments = segments.slice(0, index - 1)
forces = forces.slice(0, index)
dangler.call_deferred("queue_free")
func new_rope_from_index(index : int) -> VerletRope:
var rope := VerletRope.new()
rope.is_copy = true
rope.texture = texture
rope.texture_size = texture_size
rope.texture_offset = texture_offset
rope.texture_rotation = texture_rotation
rope.dampening = dampening
rope.thickness = thickness
rope.collide_with = collide_with
rope.segment_length = segment_length
get_parent().add_child(rope)
rope.points = points.slice(index)
rope.global_position = global_position
rope.particles = rope.points.size()
rope.prevPoints = prevPoints.slice(index)
rope.segments = segments.slice(index)
for seg in rope.segments:
seg.reparent(rope)
rope.forces = forces.slice(index)
rope.pin_end = false
rope.pin_start = false
return rope
func can_hang_from() -> bool:
return pin_end || pin_start
func collide_point(space_state : PhysicsDirectSpaceState2D,shape : RID, point : Vector2) -> Dictionary:
if Engine.is_editor_hint():
return {}
var params := PhysicsShapeQueryParameters2D.new()
params.shape_rid = shape
params.collision_mask = collide_with
params.transform = Transform2D(0, point)
return space_state.get_rest_info(params)
func get_velocity(index : int) -> Vector2:
if Utils.index_in_array(points, index):
return points[index] - prevPoints[index]
return Vector2.ZERO
func _exit_tree():
if shape.is_valid():
PhysicsServer2D.free_rid(shape)