help about 3d joints and distant constraints

Godot Version

Godot 4.3 stable

Question

I’m working on a snake-like movement system by dynamically generating bones, using the following structure:

@tool
extends Skeleton3D

const MAX_BONES = 10

func _ready() -> void:
	clear_bones()

	# Criando os bones dinamicamente
	for i in range(MAX_BONES):
		var bone_name = "bone{}".format([i], "{}")
		add_bone(bone_name)

Node setup:

Skeleton3D
└ PhysicalBoneSimulator3D
└ PhysicalBone3D (1 to 10)
└ PinJoint (1 to 9) connecting the bones

Initially, I used PinJoints to connect the bones, and while the movement was almost what I wanted, it felt too rigid and didn’t behave fluidly as I imagined.
So I decided to switch to distance constraints after watching this video. The movement improved and became more flexible, but when I try to move the character, the bone structure collapses and falls apart.

Here’s a GIF showing the issue:

example1

When using PinJoints, the bones remain stable as expected, especially when I lock the axes (axis_lock_angular_x, axis_lock_angular_z) on the bones I want to stay rigid. However, applying the same axis locking doesn’t seem to work when using distance constraints.

example2

My Question:
Why does the structure break apart when using distance constraints, and why doesn’t the axis locking work in this case? Is there a better way to maintain a stable yet flexible snake-like movement using distance constraints or other alternatives? How can I keep the structure intact while moving?

Here is the complete movement code:

extends PhysicalBoneSimulator3D

@export var head: PhysicalBone3D # The main bone (head) that will lead the movement
@export var distance: float = 1.0
@export var speed: float = 10.0

@onready var bone_list: Array[PhysicalBone3D] = []

func _ready() -> void:
	physical_bones_start_simulation()

	for child in self.get_children():
		if child is PhysicalBone3D:
			bone_list.append(child)

func _physics_process(delta: float) -> void:
	move_head(delta)

	for i in range(bone_list.size() - 1):
		apply_bone_constraint(bone_list[i], bone_list[i + 1])

func move_head(delta: float) -> void:
	var move_speed = 8.0
	var movement = Vector3.ZERO

	if Input.is_action_pressed("ui_right"):
		movement.x += move_speed
	if Input.is_action_pressed("ui_left"):
		movement.x -= move_speed
	if Input.is_action_pressed("ui_up"):
		movement.z -= move_speed
	if Input.is_action_pressed("ui_down"):
		movement.z += move_speed

	var target_position = head.global_position + movement * speed * delta
	var new_pos := constraint_distance(head.global_position, target_position, distance)
	head.global_position = head.global_position.lerp(new_pos, 0.1)

func apply_bone_constraint(bone_a: PhysicalBone3D, bone_b: PhysicalBone3D) -> void:
	var anchor := bone_a.position
	var new_pos := constraint_distance(anchor, bone_b.position, distance)

	bone_b.position = new_pos

func constraint_distance(anchor: Vector3, point: Vector3, dist: float) -> Vector3:
	var direction := point - anchor
	var current_distance := direction.length()
	
	if current_distance > dist:
		direction = direction.normalized() * dist
	else:
		direction = direction.normalized() * current_distance
	
	return anchor + direction