Godot equivalent to the Roblox function 'Model:MoveTo()'

Godot Version

4.3

Question

I am attempting to recreate the old drag / move tool from Roblox in Godot, however one of the main problems is that Nodes dragged against 2 surfaces will intersect those 2 surfaces. I tried to solve this by adding the nodes AABB to the position calculation, but it is still (albeit to a lesser extent) a problem:

var aabb = _calculate_spatial_bounds(saved_part, false)
var targ_pos = get_collision_point()
var norm = get_collision_normal()
var rounded_pos = snappedvec3(targ_pos, 0.25)

saved_part.position = (rounded_pos - offset) + norm * (aabb.size / 2)

I have learned that most attempts at recreating the drag tool in Roblox often use a function called “Model:MoveTo(vector3)”, which tries to perform a ‘safe move’ on the Model (collection of objects), which is where it tries to move the Model to the given position, and if there is an intersection, it moves it upward to avoid intersecting any other Parts. Is there an equivalent function or simple method of performing a ‘safe move’ in Godot?

MoveTo documentation: https://create.roblox.com/docs/reference/engine/classes/Model/MoveTo

Here’s the full program in case it helps or you wanna use it (most of the “welding” portions don’t work)

extends RayCast3D

var text
var offset = Vector3(0, 0, 0)

var collider

var saved_part
var dragged_to_part

var can_grab = false

var drag_count = 0 # if print(get_collider()) zero, then change can_grab but like otherwise you are already grabbing a part

var rot_count_x = 0
var rot_count_y = 0
var rot_count_z = 0
var weld_count = 0 

var part_rotation = Vector3(0, 0, 0)

var pos_diff = Vector3.ZERO

var saved_part_name = ""

func _physics_process(_delta):
   #if button_toggle.button_toggle: # debug
   var par = get_parent()
   var play = par.get_parent()
   
   collider = get_collider()
   #print(saved_part)
   if collider:
   	if not collider.name == "Floor": # exclude floor
   		saved_part_name = collider.name
   		#play.get_child(4).text = saved_part_name # debug
   		#print(get_collision_normal().inverse()) # debug
   		
   		if drag_count == 0:
   			saved_part = collider

   			add_exception(collider)
   			can_grab = true
   			drag_count += 1
   		
   			
   	
   #if not collider or collider and collider.name == "Floor": # debug
   	#play.get_child(4).text = saved_part_name # debug
   	
   if can_grab and Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
   	weld_count = 0
   	#if button_toggle.button_toggle: # debug
   	if saved_part.find_child("CollisionShape3D"):
   		saved_part.find_child("CollisionShape3D").disabled = true
   	
   	var targ_pos = get_collision_point()
   	var norm = get_collision_normal()
   	
   	if saved_part is RigidBody3D:
   		saved_part.freeze = true
   	
   	var aabb = _calculate_spatial_bounds(saved_part, false)
   	
   	var rounded_pos = snappedvec3(targ_pos, 0.25)
   	if can_grab:
   		force_raycast_update()
   		if get_collider():
   			dragged_to_part = get_collider()
   			
   		#print("Ground: ", dragged_to_part) # debug
   		#print("dragged: ", saved_part)saved_part # debug
   	
   	saved_part.position = (rounded_pos - offset) + norm * (aabb.size / 2)

   		
   	if Input.is_key_pressed(KEY_R) and rot_count_x == 0:
   		saved_part.rotate(Vector3(0, 1, 0), PI / 2)
   		rot_count_x += 1
   		
   	if Input.is_key_pressed(KEY_T) and rot_count_z == 0:
   		saved_part.rotate(Vector3(1, 0, 0), PI / 2)
   		rot_count_z += 1

   	if not Input.is_key_pressed(KEY_R):
   		rot_count_x = 0
   	if not Input.is_key_pressed(KEY_T):
   		rot_count_z = 0
   	
   	if saved_part.get_node("Area3D"): # welding related stuff
   			for body in saved_part.get_node("Area3D").get_overlapping_bodies():
   				if body is HingeJoint3D:
   					print(body.node_a)
   					body.queue_free()
   
   if can_grab and Input.is_action_just_pressed("click"):
   	var offset = get_collision_point() - saved_part.position
   	
   if not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
   	if can_grab:
   		can_grab = false
   		dragged_to_part = null
   		
   		if saved_part is RigidBody3D:
   			saved_part.freeze = false
   			
   	if saved_part:
   		if saved_part.find_child("CollisionShape3D"):
   			saved_part.find_child("CollisionShape3D").disabled = false
   		drag_count = 0
   		remove_exception(saved_part)
   		if dragged_to_part:
   			if dragged_to_part.get_node("HingeJoint3D"):
   				print("has weld") # debug
   		
   		if saved_part.get_node("Area3D") and weld_count == 0: # welding stuff
   			for body in saved_part.get_node("Area3D").get_overlapping_bodies():
   				if not body == saved_part:
   					var weld = HingeJoint3D.new()
   					
   					weld.set_flag(HingeJoint3D.FLAG_USE_LIMIT, true)
   					weld.set_param(HingeJoint3D.PARAM_BIAS, 0.99)
   					weld.set_param(HingeJoint3D.PARAM_LIMIT_LOWER, 0)
   					weld.set_param(HingeJoint3D.PARAM_LIMIT_UPPER, 0)
   					
   					weld.set_node_a(saved_part.get_path())
   					weld.set_node_b(body.get_path())
   					
   					saved_part.add_child(weld)
   					
   					weld_count = 1
   					print("New weld created between the touching objects " + saved_part.name + " and " + body.name)
   
   
func _calculate_spatial_bounds(parent : Node3D, exclude_top_level_transform: bool) -> AABB: # taken from reddit
   var bounds : AABB = AABB()
   if parent is VisualInstance3D:
   	bounds = parent.get_aabb();

   for i in range(parent.get_child_count()):
   	var child : Node3D = parent.get_child(i)
   	if child:
   		var child_bounds : AABB = _calculate_spatial_bounds(child, false)
   		if bounds.size == Vector3.ZERO && parent:
   			bounds = child_bounds
   		else:
   			bounds = bounds.merge(child_bounds)
   if bounds.size == Vector3.ZERO && !parent:
   	bounds = AABB(Vector3(-0.2, -0.2, -0.2), Vector3(0.4, 0.4, 0.4))
   if !exclude_top_level_transform:
   	bounds = parent.transform * bounds
   return bounds


func snappedvec3(vec3: Vector3, snap_rate: float):
   return Vector3(snapped(vec3.x, snap_rate), snapped(vec3.y, snap_rate), snapped(vec3.z, snap_rate))

In Roblox, you use models and primary parts to move a myriad of descendant baseparts around while keeping relative positions.

In Godot, the position property is relative to the parent node, not the entire world coordinates. That being said, you just need to modify the position of the top-level node (that you would consider your model)

As said, editing the position is relative to the parent’s one, meaning if you want your model to be in 0, 0, 0, it wouldn’t be centered in world coordinates, it would be centered to the parent instead (which may lead to 0,0,0 in particular scenarios but that’s something else)
Fortunately, you can manipulate the global_position and use that to express your world coords !

Basically, nodes work like attachments in Roblox

Position <=> position
WorldPosition <=> global_position

And

Model:MoveTo(Vec3)
<=>
parent_node.global_position = vec3

There’s no generalized built in functionality like that in Godot. You could implement is relatively simply but for specific suggestions you’ll need to describe the constraints of the system, or post a video that shows how it works in Roblox.

Sorry for the lack of info, this is the current behavior in Godot:

And this is the wanted behavior in Roblox (old Roblox client is being used (with Novetus) because they deprecated HopperBins a long time ago, and I have disagreed with their business decisions for a while now):

Unity also has a function that does something similar: Unity - Scripting API: Collider.ClosestPoint

I think I could probably do something to mimic this behavior using either variable size ShapeCasts, or by just having the object be moved with a high force, as I plan to use RigidBody3D nodes for this, as opposed to directly setting the position. A way to directly set the position to a known area that won’t intersect would be ideal, as it would avoid issues with Area3D nodes not detecting the object, despite the object being hovered over them.

Movement looks snapped to a grid. You can simply check if the new position would intersect and in case it would just keep the block where it is.

You can also instantiate a character body stand in for the moving block and use its move_and_collide() or even move_and_slide() methods.

I think both of these options would work well, especially move_and_collide() as it emits data when it collides, meaning I can store the last position and keep it there until it isn’t colliding anymore, and this seems compatible with my idea of making a prototype vehicle builder.

One possible problem is that I plan to have the ability to attach objects onto other off grid objects (for example, a vehicle), and Roblox seems to handle that by adding the position and rotation of the object being dragged against to the grid, as shown in this video

One other problem is that I don’t know how to detect if a part is going to intersect with another part without already knowing the position of all of the other parts, which I’m pretty sure you also need to do if you use move_and_collide() as you need to know when the part isn’t intersecting, so that way the object can be moved, and then check again for intersecting parts.

You can use raycasts to find adjacent candidates to snap/align to.

I got it working, using RigidBody3D’s linear_velocityvariable to move the object instead, along with a damping system to avoid overshooting, and like in the Novetus video, I added rotation and position support for dragging onto off-grid objects. This script has a few problems however, mainly being that it does not disable the collision on the RigidBody3D (as it would cause it to not collide with the dragged_to object, meaning the safe move behavior would be disabled), and this causes the player to be able to ride atop the RigidBody3D, and also causes the part to suffer friction on the ground, meaning it will be less precise. Most of these problems are solvable however, by reducing the friction, and setting the collision mask on the object whilst it is dragged. Here’s the full program:

extends RayCast3D

var text
var offset = Vector3(0, 0, 0)

var collider

var saved_part
var dragged_to_part

var can_grab = false

var drag_count = 0 # if print(get_collider()) zero, then change can_grab but like otherwise you are already grabbing a part

var rot_count_x = 0
var rot_count_y = 0
var rot_count_z = 0
var weld_count = 0 

var part_rotation = Vector3(0, 0, 0)

var saved_part_name = ""

func _physics_process(delta):
	#if button_toggle.button_toggle: # debug
	var par = get_parent()
	var play = par.get_parent()
	
	collider = get_collider()
	#print(saved_part)
	if collider:
		if not collider.name == "Floor": # exclude floor
			saved_part_name = collider.name
			#play.get_child(4).text = saved_part_name # debug
			#print(get_collision_normal().inverse()) # debug
			
			if drag_count == 0:
				saved_part = collider
	
				add_exception(collider)
				can_grab = true
				drag_count += 1
			
				
		
	#if not collider or collider and collider.name == "Floor": # debug
		#play.get_child(4).text = saved_part_name # debug
		
	if can_grab and Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
		weld_count = 0
		#if button_toggle.button_toggle: # debug
		
		var targ_pos = get_collision_point()
		var norm = get_collision_normal()
		
		
		var aabb = _calculate_spatial_bounds(saved_part, false)
		var rounded_pos = snappedvec3(targ_pos, 0.25)
		
		if can_grab:
			force_raycast_update()
			if get_collider():
				dragged_to_part = get_collider()
				
			#print("Ground: ", dragged_to_part) # debug
			#print("dragged: ", saved_part)saved_part # debug
		
		#saved_part.position = (rounded_pos - offset) + norm * (aabb.size / 2)
		var pos_offset
		if dragged_to_part:
			pos_offset = dragged_to_part.position - Vector3(int(dragged_to_part.position.x), int(dragged_to_part.position.y), int(dragged_to_part.position.z)) # get decimal for position offset
		var pos = ((rounded_pos - offset) + (norm * pos_offset) * (aabb.size / 2))
		var dir
		
		if saved_part is RigidBody3D:
			dir = saved_part.global_position.direction_to(pos)
			var dist = saved_part.position.distance_to(pos)
			var highest_size = max(saved_part.scale.x, saved_part.scale.y, saved_part.scale.z) # avoid bugs with differently sized parts
			if dist > (4 + highest_size): # allows moving parts into enclosed spaces easily, e.g a submarine or enclosed car, as otherwise it will just get stuck on the dragged_to part
				saved_part.freeze = true
				print(highest_size)
				saved_part.position = pos
			else:
				saved_part.linear_velocity = dir * 60 * dist # 60 = drag speed
				saved_part.lock_rotation = true
				saved_part.freeze = false
			
		if Input.is_key_pressed(KEY_R) and rot_count_x == 0:
			part_rotation += Vector3(0, 90, 0)
			rot_count_x += 1
			
		if Input.is_key_pressed(KEY_T) and rot_count_z == 0:
			part_rotation += Vector3(0, 0, 90)
			rot_count_z += 1

		if not Input.is_key_pressed(KEY_R):
			rot_count_x = 0
		if not Input.is_key_pressed(KEY_T):
			rot_count_z = 0
		
		if saved_part is RigidBody3D:
			saved_part.set_rotation_degrees(part_rotation + dragged_to_part.rotation_degrees)

	if can_grab and Input.is_action_just_pressed("click"):
		var offset = get_collision_point() - saved_part.position
		
	if not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
		if can_grab and saved_part and drag_count == 1:
			if saved_part is RigidBody3D:
				saved_part.lock_rotation = false
				
		drag_count = 0
		remove_exception(saved_part)
		
		if can_grab:
			can_grab = false
			dragged_to_part = null
	
func _calculate_spatial_bounds(parent : Node3D, exclude_top_level_transform: bool) -> AABB: # taken from reddit
	var bounds : AABB = AABB()
	if parent is VisualInstance3D:
		bounds = parent.get_aabb();

	for i in range(parent.get_child_count()):
		var child : Node3D = parent.get_child(i)
		if child:
			var child_bounds : AABB = _calculate_spatial_bounds(child, false)
			if bounds.size == Vector3.ZERO && parent:
				bounds = child_bounds
			else:
				bounds = bounds.merge(child_bounds)
	if bounds.size == Vector3.ZERO && !parent:
		bounds = AABB(Vector3(-0.2, -0.2, -0.2), Vector3(0.4, 0.4, 0.4))
	if !exclude_top_level_transform:
		bounds = parent.transform * bounds
	return bounds


func snappedvec3(vec3: Vector3, snap_rate: float):
	return Vector3(snapped(vec3.x, snap_rate), snapped(vec3.y, snap_rate), snapped(vec3.z, snap_rate))

I also added a check for if the RigidBody3D is stuck on something, e.g if you are trying to build a submarine, and need to put in an engine or some component, and the part gets stuck on the outside of the submarine, the part will be moved automatically once it gets past a certain distance threshold (in this case it is 4).

I also tried using CharacterBody3D’s move_and_slide() for this, but as I plan to have the objects be mostly physics based, it didn’t work out very well

Sorry for asking such a broad question, I’ll try to narrow it down more next time!

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