Unable to properly move arms in a radius and attach them.

Godot Version

Godot_v4.7.1-stable (Windows 64-bit)

Context:

I want to make a hand movement system. After holding down a button a hand moves towards a point that is dictated by the mouse. The distance between the a shoulder of the player and the hand must not go over a certain circular radius. The hand is basically locked in a circle and cannot move out of it. This hand must move in a way which allows godot physics to handle collisions, it must also be able to collide with other rigidbodies, staticbodies and such (this is so i can push objects with it). That means the hand position cannot be set, something like velocity must be used. The hand must be a child of a certain node and cannot be top level, as it must use local position relative to its parent, because im planning to animate the parent, which will also allow the hand to be animated (the child of said parent). The hand must move along two axis, yet the player has the ability to rotate.

PLEASE suggest a valid way to make something like this work, hinges, skeleton, rigidbody2d, etc. If youre wondering, i have tried all of them myself but failed miserably mostly due to two main issues:

Actual Issues:

  1. All joints are not fixed. Whenever i move anything with velocity it has a tendency to lag behind or go too far. Ive tried tinkering with the damping to limit this but it has not worked. Ive tried to find an alternative joint on the asset store, a joint that allows free rotation but keeps the nodes stuck together with no lagging behind, however that either doesnt seem to exist or i simply cant find it.

  2. The script ive previously made allowed for movement, however i was unable to animate it via parent due to the nodes being top-level to allow physics to work properly.

I will post code and examples i currently have set up, you do not need to follow them, but it should suit as good reference to tell you what i want code-wise.

CODE:

extends Node3D
#ARM MOVEMENT SCRIPT

@export_group("Initialization")
@onready var Center: Node3D = get_node("Center")
@onready var Test: Node3D = get_node("Mouse")
@export var Sensitivity: float = 0.005
@export var MaxRadius: float = 1.0
@export var Speed: float = 5.0 # Control how aggressively it snaps to the mouse position

@export_group("Left")
@onready var LeftShoulder: Node3D = get_node("LeftShoulder")
@export var HandL: RigidBody3D

@export_group("Right")
@onready var RightShoulder: Node3D = get_node("RightShoulder")
@export var HandR: RigidBody3D

#how much i wanna move by
var MoveTo := Vector2.ZERO 


func _ready() -> void:
	Test.top_level = true
	HandL.top_level = true
	Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

#ive also realised this script doesnt really work when the player (mitchel) turns
#however im currently just trying to connect the arms properly

func _physics_process(_delta: float) -> void:
	## test
	#MoveTo += Global.MouseVel * Sensitivity
	#Global.MouseVel = Vector2.ZERO
	#
	#if MoveTo.length() > MaxRadius:
		#MoveTo = MoveTo.limit_length(MaxRadius)
		#
	#var local_x_axis: Vector3 = Center.global_transform.basis.x
	#var local_y_axis: Vector3 = Center.global_transform.basis.y
	#
	#var local_offset: Vector3 = (local_x_axis * MoveTo.x) + (local_y_axis * MoveTo.y)
	#var target_pos: Vector3 = Center.global_position + local_offset
		#
	#var displacement: Vector3 = target_pos - Test.global_position
	#
	#Test.sleeping = false
	#Test.linear_velocity = displacement * Speed
	#
	## rotate testblock the same as mitchel
	#Test.global_rotation.y = Center.global_rotation.y
	pass
	
	HandL.global_rotation.y = LeftShoulder.global_rotation.y
	if Input.is_action_pressed("RightClick"):
		MoveTo += Global.MouseVel * Sensitivity
		#gets the vector its suppose to move by, based on mouse inputs in the global script
		Global.MouseVel = Vector2.ZERO
		#manually setting velocity to 0 so it doesnt stay
		if MoveTo.length() > MaxRadius:
			MoveTo = MoveTo.limit_length(MaxRadius)
		#checks if the vector is too large and "clamps" it
		
		var MoveTo3 = Vector3(MoveTo.x, MoveTo.y, 0)
		#converts the vector2 to vector3 so i can put it in linear_velocity
		
		HandL.sleeping = false
		HandL.linear_velocity = MoveTo3* Speed
		#actually moves the hand
		
	HandR.global_rotation.y = RightShoulder.global_rotation.y
	if Input.is_action_pressed("LeftClick"):
		MoveTo += Global.MouseVel * Sensitivity
		Global.MouseVel = Vector2.ZERO
		if MoveTo.length() > MaxRadius:
			MoveTo = MoveTo.limit_length(MaxRadius)
		
		var MoveTo3 = Vector3(MoveTo.x, MoveTo.y, 0)
		
		HandR.sleeping = false
		HandR.linear_velocity = MoveTo3 * Speed

extends CharacterBody3D
#GENERAL MOVEMENT SCRIPT (currently working)

@export var Collision: CollisionShape3D
@export var MitchelAnim: AnimatedSprite3D
@export var WalkSound: AudioStreamPlayer3D
@export var EventAnim: AnimationPlayer
@export var ArmAnim: AnimationPlayer
@export var FloorDetect: RayCast3D
@export var RoofDetect: RayCast3D

var IsCrouching:bool = false
var IsMoving:bool = false
var AlreadyJ:bool = false

const YOffset = 0.1
const CHeight = 1.667
const JUMP_VELOCITY = 3

var SPEED : float = 1.0
var target_rotation_y : float = 0.0
var meta = ""

func animHandle():
	if not is_on_floor():
		setHeight(1)
		MitchelAnim.animation = "Crouch"
		if !AlreadyJ:
			playSound()
		AlreadyJ = true

	elif IsCrouching and IsMoving:
		setHeight(1)
		MitchelAnim.animation = "CrouchWalk"
		EventAnim.play("CWalk")
	elif IsCrouching and not IsMoving:
		setHeight(1)
		MitchelAnim.animation = "Crouch"
		EventAnim.stop()
	elif IsMoving and not IsCrouching:
		setHeight(2)
		MitchelAnim.animation = "Walk"
		#ArmAnim.play("Walk")
		EventAnim.play("Walk")
	else:
		setHeight(2)
		MitchelAnim.animation = "Idle"
		#ArmAnim.play("Idle")
		EventAnim.stop()
		
func playSound():
	if FloorDetect.get_collider() != null:
		print(FloorDetect.get_collider())
		meta = FloorDetect.get_collider().get_meta("Type")
		if meta != null and meta != "Ignore":
			WalkSound.stream = load("res://Mitchel2/FootstepSound/" + meta + ".mp3")
		elif meta == null:
			WalkSound.stream = load("res://Mitchel2/FootstepSound/Concrete.mp3")
	else:
		print(FloorDetect.get_collider())
		WalkSound.stream = preload("res://Mitchel2/FootstepSound/Concrete.mp3")

	WalkSound.pitch_scale = randf_range(0.9,1.1)
	WalkSound.play()

func setHeight(Height):
	match Height:
		1:
			MitchelAnim.position.y = YOffset
			Collision.shape.size.y = CHeight
		2:
			MitchelAnim.position.y = YOffset*0
			Collision.shape.size.y = CHeight*1.27414517097
			
func _process(delta: float) -> void:
	# Update the target angle when pressed
	if Input.is_action_just_pressed("RightRotate"):
		target_rotation_y += deg_to_rad(90)
	elif Input.is_action_just_pressed("LeftRotate"):
		target_rotation_y -= deg_to_rad(90)
	
	# Smoothly interpolate towards target_rotation_y every frame
	rotation.y = lerp_angle(rotation.y, target_rotation_y, delta * 10.0)
	
	
func _physics_process(delta: float) -> void:
	# Add the gravity.
	if not is_on_floor():
		velocity += get_gravity() * delta

	# Handle jump and crouching.
	if Input.is_action_just_pressed("Crouch") and RoofDetect.get_collider() == null:
		IsCrouching = !IsCrouching
		
	if Input.is_action_just_pressed("Jump") and is_on_floor():
		AlreadyJ = false
		velocity.y = JUMP_VELOCITY

	# Get the input direction and handle the movement/deceleration.
	# As good practice, you should replace UI actions with custom gameplay actions.
	var input_dir := Input.get_vector("Left", "Right", "Backwards", "Forward")
	var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
	if direction:
		IsMoving = true
		velocity.x = direction.x * SPEED
		velocity.z = direction.z * SPEED
	else:
		IsMoving = false
		velocity.x = move_toward(velocity.x, 0, SPEED)
		velocity.z = move_toward(velocity.z, 0, SPEED)
		
	

	move_and_slide()
	animHandle()

extends Node2D
#GLOBAL SCRIPT (also working)
#UNUSED
var handMove = 0
var hurtTimer : float = 0.0 # Use floats for time
var throw = false
var angle = 0
var throw_direction : Vector3 = Vector3.ZERO
var throwcooldown : float = 0.0
var chunkscale = 0
var angleDIR = 1

#USED
var MousePos = Vector2(0,0)
var MouseVel = Vector2(0,0)
var HandR_LIVE = true
var HandL_LIVE = true
var HandR_INUSE = false
var HandL_INUSE = false

func _process(delta: float) -> void:
	MousePos = get_global_mouse_position()
	
	# unused
	#hurtTimer -= delta
	#throwcooldown -= delta


func _input(event: InputEvent) -> void:
	if event is InputEventMouseMotion:
		MouseVel = Vector2(event.relative.x, -event.relative.y)

TREE:

WHEN IDLE:

WHEN MOVING:

ROTATION TOWARDS CURSOR WHEN LEFT AND RIGHT CLICK HELD:

I would provide a video for better understanding but i am unable to as i am new to forums (i cannot upload any attachments as of right now).
If anyone is confused in any way, ask away. I will try to be as helpful as possible. :]

I’m happy to take a look at your problem here, but I’ll have to do it a bit later. There are a couple of things that I notice when I read your post (which is nicely detailed btw). The first major point is:

I am hesitant to spend time producing a solution for you when you seem to already have one. You note that the [hand’s] movement cannot be influenced by the parent’s animation due to the node hierarchy. However, a node hierarchy is not the only way to move objects with one another. You can compute the local position of the hand relative to another node, and then track changes in this local space with which you can manually influence the hand.

Furthermore, you should not parent a RigidBody3D under a moving node, as stated in the documentation:

Note: Changing the 3D transform or linear_velocity of a RigidBody3D very often may lead to some unpredictable behaviors. This also happens when a RigidBody3D is the descendant of a constantly moving node, like another RigidBody3D, as that will cause its global transform to be set whenever its ancestor moves.


As I’ve mentioned before (in other posts), the linear_velocity is not something that should be constantly set within physics_process(). This is also clearly stated in its documentation:

linear_velocity

The body’s linear velocity in units per second. Can be used sporadically, but don’t set this every frame, because physics may run in another thread and runs at a different granularity. Use _integrate_forces() as your process loop for precise control of the body state.

Setting the linear_velocity is a direct state manipulation, so it should be performed through either _integrate_forces() or the PhysicsServer3D to maintain stability – for the reasons outlined in the documentation.

You are not doing this so I suggest you try it out and see if it helps your case.


Unfortunately, this is just how joints work in real-time physics (as far as I know). Something like a stiff spring is used to apply forces to the body such that the constraint is satisfied. I haven’t played around with 3D joints much so I don’t have any real advice for your use of them – I would just be guessing.


In summary, I suggest you make use of _integrate_physics() and the PhysicsServer3D to assign velocities to your respective bodies. Don’t parent your bodies under one another. Instead, try to track the relevant data (e.g. computing the local space of the hand relative to the shoulder) and let that influence the velocity and other necessary state constraints (your circular constraint).

This is my initial reaction to your post. I will try to come back to it later.
If you have any questions, let me know.

Relevant links:

Ive implemented some of these fixes, however im rather confused about manually influencing the hand.

extends RigidBody3D
#HAND SCRIPT (this script is on both hands)

var target_velocity: Vector3 = Vector3.ZERO

func _integrate_forces(state: PhysicsDirectBodyState3D) -> void:
	state.linear_velocity = target_velocity
extends Node3D
#UPDATED ARM MOVEMENT SCRIPT

@export_group("Initialization")
@onready var Center: Node3D = get_node("Center")
@onready var Test: Node3D = get_node("Mouse")
@export var Sensitivity: float = 0.005
@export var MaxRadius: float = 1.0
@export var Speed: float = 5.0 # Control how aggressively it snaps to the mouse position

@export_group("Left")
@onready var LeftShoulder: Node3D = get_node("LeftShoulder")
@export var HandL: RigidBody3D

@export_group("Right")
@onready var RightShoulder: Node3D = get_node("RightShoulder")
@export var HandR: RigidBody3D

#how much i wanna move by
var MoveTo := Vector2.ZERO 


func _ready() -> void:
	Test.top_level = true
	HandL.top_level = true
	Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

#ive also realised this script doesnt really work when the player (mitchel) turns
#however im currently just trying to connect the arms properly

func _physics_process(_delta: float) -> void:
	## test
	#MoveTo += Global.MouseVel * Sensitivity
	#Global.MouseVel = Vector2.ZERO
	#
	#if MoveTo.length() > MaxRadius:
		#MoveTo = MoveTo.limit_length(MaxRadius)
		#
	#var local_x_axis: Vector3 = Center.global_transform.basis.x
	#var local_y_axis: Vector3 = Center.global_transform.basis.y
	#
	#var local_offset: Vector3 = (local_x_axis * MoveTo.x) + (local_y_axis * MoveTo.y)
	#var target_pos: Vector3 = Center.global_position + local_offset
		#
	#var displacement: Vector3 = target_pos - Test.global_position
	#
	#Test.sleeping = false
	#Test.linear_velocity = displacement * Speed
	#
	## rotate testblock the same as mitchel
	#Test.global_rotation.y = Center.global_rotation.y
	pass
	
	HandL.global_rotation.y = LeftShoulder.global_rotation.y
	if Input.is_action_pressed("RightClick"):
		MoveTo += Global.MouseVel * Sensitivity
		#gets the vector its suppose to move by, based on mouse inputs in the global script
		Global.MouseVel = Vector2.ZERO
		#manually setting velocity to 0 so it doesnt stay
		if MoveTo.length() > MaxRadius:
			MoveTo = MoveTo.limit_length(MaxRadius)
		#checks if the vector is too large and "clamps" it
		
		var MoveTo3 = Vector3(MoveTo.x, MoveTo.y, 0)
		#converts the vector2 to vector3 so i can put it in linear_velocity
		
		HandL.sleeping = false
		HandL.target_velocity = MoveTo3 * Speed
		#actually moves the hand
		
	HandR.global_rotation.y = RightShoulder.global_rotation.y
	if Input.is_action_pressed("LeftClick"):
		MoveTo += Global.MouseVel * Sensitivity
		Global.MouseVel = Vector2.ZERO
		if MoveTo.length() > MaxRadius:
			MoveTo = MoveTo.limit_length(MaxRadius)
		
		var MoveTo3 = Vector3(MoveTo.x, MoveTo.y, 0)
		
		HandR.sleeping = false
		HandR.target_velocity = MoveTo3 * Speed

Im trying to find a way to snap the shoulders to the next animation frames, which would require locally setting the position, but im afraid if i did that the RigidBody3Ds could potentially clip into something, which is why i wanted to do it through the AnimationPlayer. Im unsure if the AnimationPlayer would fix any clipping but im assuming it does, feel free to prove me wrong though im rather new.

Id also like to note that i did not move my RigidBody3Ds out of the ArmJoints node for better organization in the tree too, however im guessing i should probably remove it entirely or change it for something else, though im unsure what for. If there is any potential way to color code or put those arm nodes in some kind of folder, id appreciate knowing about it.

Aside from that, im still mostly having issues with joints lagging behind. Im unsure what you meant by “stiff spring”, im currently looking into it but im not understanding well. If you could elaborate on this i would be glad. :]