Godot Version
v4.6.stable.steam [89cea1439]
Question
I’ve been making a prototype that involves moving characters around with spring joints, and one of the techniques I’ve thought of using was to dynamically create joints on the go, and destroy them when stretched beyond a certain length, or when a better anchor point is detected, but I’ve run into an issue:
It seems like I can’t define the attachment points of a DampedSpringJoint2D on the two bodies it connects, that is, if I create the joint and add it to the scene, the joint just attaches to whatever points it happens to land on on each body, and I can’t make it so that the spring attaches to specific points. This is something that in Box2D for example it can be done via the local frame of each attachment point. Changing the transform before or after it’s added to the scene doesn’t seem to work, and neither is changing length/rest_length of the spring joint.
Is there a way to work around this?
Ok I ended up solving this issue with a custom spring implementation that looks like the following:
class_name CustomSpring
var body_a: Node2D
var transform_a: Transform2D
var body_b: Node2D
var transform_b: Transform2D
var rest_length: float
var stiffness: float
var damping: float
@warning_ignore("shadowed_variable")
func _init(
body_a: Node2D,
transform_a: Transform2D,
body_b: Node2D,
transform_b: Transform2D,
rest_length: float,
stiffness: float,
damping: float,
) -> void:
self.body_a = body_a
self.transform_a = transform_a
self.body_b = body_b
self.transform_b = transform_b
self.rest_length = rest_length
self.stiffness = stiffness
self.damping = damping
func is_valid() -> bool:
return is_instance_valid(body_a) and is_instance_valid(body_b)
func physics_process(delta: float) -> void:
if not is_valid():
return
var force := calculate_spring_force(
attachment_a(), velocity_a(),
attachment_b(), velocity_b(),
rest_length,
stiffness,
damping,
)
apply_force_a(force * delta)
apply_force_b(-force * delta)
func attachment_a() -> Vector2:
return (body_a.global_transform * transform_a).origin
func velocity_a() -> Vector2:
if body_a is RigidBody2D or body_a is AnimatableBody2D:
return get_point_velocity(body_a, transform_a.origin)
return Vector2.ZERO
func apply_force_a(force: Vector2) -> void:
if body_a is RigidBody2D:
body_a.apply_force(force, body_a.to_local(attachment_a()))
func attachment_b() -> Vector2:
return (body_b.global_transform * transform_b).origin
func velocity_b() -> Vector2:
if body_b is RigidBody2D or body_b is AnimatableBody2D:
return get_point_velocity(body_b, transform_b.origin)
return Vector2.ZERO
func apply_force_b(force: Vector2) -> void:
if body_b is RigidBody2D:
body_b.apply_force(force, body_b.to_local(attachment_b()))
## Returns the velocity at a given offset point from a collision object's center
## of mass.
static func get_point_velocity(obj: CollisionObject2D, offset: Vector2) -> Vector2:
var state := PhysicsServer2D.body_get_direct_state(obj.get_rid())
if state == null:
return Vector2.ZERO
return state.get_velocity_at_local_position(offset)
## Calculates a spring force, given position, velocity, spring constant, and
## damping factor
static func calculate_spring_force(
pos_a: Vector2, vel_a: Vector2,
pos_b: Vector2, vel_b: Vector2,
distance: float,
spring_k: float,
spring_d: float
) -> Vector2:
var dist := pos_a.distance_to(pos_b)
if dist <= 0.0000005:
return Vector2.ZERO
var b_to_a := (pos_a - pos_b) / dist
dist = distance - dist
var rel_vel := vel_a - vel_b
var total_rel_vel := rel_vel.dot(b_to_a)
return b_to_a * ((dist * spring_k) - (total_rel_vel * spring_d))
With this custom spring I can control the attachment points via the two transforms passed in the constructor, and manage it on an external node, calling physics_process to apply the forces.