Browse Source

Add joints test to 2D/3D physics tests

PouleyKetchoupp 4 years ago
parent
commit
7a437d1d23

+ 21 - 10
2d/physics_tests/test.gd

@@ -22,9 +22,9 @@ var _drawn_nodes = []
 
 
 
 
 func _physics_process(_delta):
 func _physics_process(_delta):
-	if (_wait_physics_ticks_counter > 0):
+	if _wait_physics_ticks_counter > 0:
 		_wait_physics_ticks_counter -= 1
 		_wait_physics_ticks_counter -= 1
-		if (_wait_physics_ticks_counter == 0):
+		if _wait_physics_ticks_counter == 0:
 			emit_signal("wait_done")
 			emit_signal("wait_done")
 
 
 
 
@@ -61,17 +61,28 @@ func clear_drawn_nodes():
 	_drawn_nodes.clear()
 	_drawn_nodes.clear()
 
 
 
 
-func create_rigidbody_box(size):
-	var template_shape = RectangleShape2D.new()
-	template_shape.extents = 0.5 * size
+func create_rigidbody_box(size, pickable = false, use_icon = false):
+	var shape = RectangleShape2D.new()
+	shape.extents = 0.5 * size
 
 
-	var template_collision = CollisionShape2D.new()
-	template_collision.shape = template_shape
+	var collision = CollisionShape2D.new()
+	collision.shape = shape
+
+	var body = RigidBody2D.new()
+	body.add_child(collision)
+
+	if pickable:
+		var script = load("res://utils/rigidbody_pick.gd")
+		body.set_script(script)
 
 
-	var template_body = RigidBody2D.new()
-	template_body.add_child(template_collision)
+	if use_icon:
+		var texture = load("res://icon.png")
+		var icon = Sprite.new()
+		icon.texture = texture
+		icon.scale = size / texture.get_size()
+		body.add_child(icon)
 
 
-	return template_body
+	return body
 
 
 
 
 func start_timer(timeout):
 func start_timer(timeout):

+ 4 - 0
2d/physics_tests/tests.gd

@@ -18,6 +18,10 @@ var _tests = [
 		"id": "Functional Tests/Collision Pairs",
 		"id": "Functional Tests/Collision Pairs",
 		"path": "res://tests/functional/test_collision_pairs.tscn",
 		"path": "res://tests/functional/test_collision_pairs.tscn",
 	},
 	},
+	{
+		"id": "Functional Tests/Joints",
+		"path": "res://tests/functional/test_joints.tscn",
+	},
 	{
 	{
 		"id": "Functional Tests/Raycasting",
 		"id": "Functional Tests/Raycasting",
 		"path": "res://tests/functional/test_raycasting.tscn",
 		"path": "res://tests/functional/test_raycasting.tscn",

+ 6 - 6
2d/physics_tests/tests/functional/test_collision_pairs.gd

@@ -52,16 +52,16 @@ func _ready():
 
 
 func _input(event):
 func _input(event):
 	var key_event = event as InputEventKey
 	var key_event = event as InputEventKey
-	if (key_event and not key_event.pressed):
-		if (key_event.scancode == KEY_1):
+	if key_event and not key_event.pressed:
+		if key_event.scancode == KEY_1:
 			_on_option_selected(OPTION_TYPE_RECTANGLE)
 			_on_option_selected(OPTION_TYPE_RECTANGLE)
-		elif (key_event.scancode == KEY_2):
+		elif key_event.scancode == KEY_2:
 			_on_option_selected(OPTION_TYPE_SPHERE)
 			_on_option_selected(OPTION_TYPE_SPHERE)
-		elif (key_event.scancode == KEY_3):
+		elif key_event.scancode == KEY_3:
 			_on_option_selected(OPTION_TYPE_CAPSULE)
 			_on_option_selected(OPTION_TYPE_CAPSULE)
-		elif (key_event.scancode == KEY_4):
+		elif key_event.scancode == KEY_4:
 			_on_option_selected(OPTION_TYPE_CONVEX_POLYGON)
 			_on_option_selected(OPTION_TYPE_CONVEX_POLYGON)
-		elif (key_event.scancode == KEY_5):
+		elif key_event.scancode == KEY_5:
 			_on_option_selected(OPTION_TYPE_CONCAVE_SEGMENTS)
 			_on_option_selected(OPTION_TYPE_CONCAVE_SEGMENTS)
 
 
 
 

+ 140 - 0
2d/physics_tests/tests/functional/test_joints.gd

@@ -0,0 +1,140 @@
+extends Test
+
+
+const OPTION_JOINT_TYPE = "Joint Type/%s Joint (%d)"
+
+const OPTION_TEST_CASE_BODIES_COLLIDE = "Test case/Attached bodies collide"
+const OPTION_TEST_CASE_WORLD_ATTACHMENT = "Test case/No parent body"
+const OPTION_TEST_CASE_DYNAMIC_ATTACHMENT = "Test case/Parent body is dynamic (no gravity)"
+const OPTION_TEST_CASE_DESTROY_BODY = "Test case/Destroy attached body"
+const OPTION_TEST_CASE_CHANGE_POSITIONS = "Test case/Set body positions after added to scene"
+
+const BOX_SIZE = Vector2(64, 64)
+
+var _update_joint = false
+var _selected_joint = null
+
+var _joint_type = PinJoint2D
+var _bodies_collide = false
+var _world_attachement = false
+var _dynamic_attachement = false
+var _destroy_body = false
+var _change_positions = false
+
+var _joint_types = {}
+
+
+func _ready():
+	for joint_index in $Joints.get_child_count():
+		var joint_node = $Joints.get_child(joint_index)
+		joint_node.visible = false
+		var joint_name = joint_node.name
+		var joint_short = joint_name.substr(0, joint_name.length() - 7)
+		var option_name = OPTION_JOINT_TYPE % [joint_short, joint_index + 1]
+		$Options.add_menu_item(option_name)
+		_joint_types[option_name] = joint_node
+
+	$Options.add_menu_item(OPTION_TEST_CASE_BODIES_COLLIDE, true, false)
+	$Options.add_menu_item(OPTION_TEST_CASE_WORLD_ATTACHMENT, true, false)
+	$Options.add_menu_item(OPTION_TEST_CASE_DYNAMIC_ATTACHMENT, true, false)
+	$Options.add_menu_item(OPTION_TEST_CASE_DESTROY_BODY, true, false)
+	$Options.add_menu_item(OPTION_TEST_CASE_CHANGE_POSITIONS, true, false)
+
+	$Options.connect("option_selected", self, "_on_option_selected")
+	$Options.connect("option_changed", self, "_on_option_changed")
+
+	_selected_joint = _joint_types.values()[0]
+	_update_joint = true
+
+
+func _process(_delta):
+	if _update_joint:
+		_update_joint = false
+		_create_joint()
+		$LabelJointType.text = "Joint Type: " + _selected_joint.name
+
+
+func _input(event):
+	var key_event = event as InputEventKey
+	if key_event and not key_event.pressed:
+		var joint_index = key_event.scancode - KEY_1
+		if joint_index >= 0 and joint_index < _joint_types.size():
+			_selected_joint = _joint_types.values()[joint_index]
+			_update_joint = true
+
+
+func _on_option_selected(option):
+	if _joint_types.has(option):
+		_selected_joint = _joint_types[option]
+		_update_joint = true
+
+
+func _on_option_changed(option, checked):
+	match option:
+		OPTION_TEST_CASE_BODIES_COLLIDE:
+			_bodies_collide = checked
+			_update_joint = true
+		OPTION_TEST_CASE_WORLD_ATTACHMENT:
+			_world_attachement = checked
+			_update_joint = true
+		OPTION_TEST_CASE_DYNAMIC_ATTACHMENT:
+			_dynamic_attachement = checked
+			_update_joint = true
+		OPTION_TEST_CASE_DESTROY_BODY:
+			_destroy_body = checked
+			_update_joint = true
+		OPTION_TEST_CASE_CHANGE_POSITIONS:
+			_change_positions = checked
+			_update_joint = true
+
+
+func _create_joint():
+	cancel_timer()
+
+	var root = $Objects
+
+	while root.get_child_count():
+		var last_child_index = root.get_child_count() - 1
+		var last_child = root.get_child(last_child_index)
+		root.remove_child(last_child)
+		last_child.queue_free()
+
+	var child_body = create_rigidbody_box(BOX_SIZE, true, true)
+	child_body.mode = RigidBody2D.MODE_RIGID
+	if _change_positions:
+		root.add_child(child_body)
+		child_body.position = Vector2(0.0, 40)
+	else:
+		child_body.position = Vector2(0.0, 40)
+		root.add_child(child_body)
+
+	var parent_body = null
+	if not _world_attachement:
+		parent_body = create_rigidbody_box(BOX_SIZE, true, true)
+		if _dynamic_attachement:
+			parent_body.mode = RigidBody2D.MODE_RIGID
+			parent_body.gravity_scale = 0.0
+			child_body.gravity_scale = 0.0
+		else:
+			parent_body.mode = RigidBody2D.MODE_STATIC
+		if _change_positions:
+			root.add_child(parent_body)
+			parent_body.position = Vector2(0.0, -40)
+		else:
+			parent_body.position = Vector2(0.0, -40)
+			root.add_child(parent_body)
+
+	var joint = _selected_joint.duplicate()
+	joint.visible = true
+	joint.disable_collision = not _bodies_collide
+	if parent_body:
+		joint.node_a = parent_body.get_path()
+	joint.node_b = child_body.get_path()
+	root.add_child(joint)
+
+	if _destroy_body:
+		yield(start_timer(0.5), "timeout")
+		if is_timer_canceled():
+			return
+
+		child_body.queue_free()

+ 31 - 0
2d/physics_tests/tests/functional/test_joints.tscn

@@ -0,0 +1,31 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://tests/functional/test_joints.gd" type="Script" id=2]
+[ext_resource path="res://tests/test_options.tscn" type="PackedScene" id=4]
+
+[node name="JointTest2D" type="Node2D"]
+script = ExtResource( 2 )
+
+[node name="LabelJointType" type="Label" parent="."]
+margin_left = 14.0
+margin_top = 79.0
+margin_right = 145.0
+margin_bottom = 93.0
+text = "Joint Type: "
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Options" parent="." instance=ExtResource( 4 )]
+
+[node name="Joints" type="Node2D" parent="."]
+position = Vector2( 512, 200 )
+
+[node name="PinJoint2D" type="PinJoint2D" parent="Joints"]
+
+[node name="DampedSpringJoint2D" type="DampedSpringJoint2D" parent="Joints"]
+
+[node name="GrooveJoint2D" type="GrooveJoint2D" parent="Joints"]
+
+[node name="Objects" type="Node2D" parent="."]
+position = Vector2( 512, 200 )

+ 1 - 1
2d/physics_tests/tests/functional/test_pyramid.gd

@@ -13,7 +13,7 @@ func _ready():
 func _create_pyramid():
 func _create_pyramid():
 	var root_node = $Pyramid
 	var root_node = $Pyramid
 
 
-	var template_body = create_rigidbody_box(box_size)
+	var template_body = create_rigidbody_box(box_size, true)
 
 
 	var pos_y = -0.5 * box_size.y - box_spacing.y
 	var pos_y = -0.5 * box_size.y - box_spacing.y
 
 

+ 1 - 1
2d/physics_tests/tests/functional/test_stack.gd

@@ -14,7 +14,7 @@ func _ready():
 func _create_stack():
 func _create_stack():
 	var root_node = $Stack
 	var root_node = $Stack
 
 
-	var template_body = create_rigidbody_box(box_size)
+	var template_body = create_rigidbody_box(box_size, true)
 
 
 	var pos_y = -0.5 * box_size.y - box_spacing.y
 	var pos_y = -0.5 * box_size.y - box_spacing.y
 
 

+ 32 - 0
2d/physics_tests/utils/rigidbody_pick.gd

@@ -0,0 +1,32 @@
+extends RigidBody2D
+
+
+var _picked = false
+var _last_mouse_pos = Vector2.ZERO
+
+
+func _ready():
+	input_pickable = true
+
+
+func _input(event):
+	var mouse_event = event as InputEventMouseButton
+	if mouse_event and not mouse_event.pressed:
+		_picked = false
+
+
+func _input_event(_viewport, event, _shape_idx):
+	var mouse_event = event as InputEventMouseButton
+	if mouse_event and mouse_event.pressed:
+		_picked = true
+		_last_mouse_pos = get_global_mouse_position()
+
+
+func _physics_process(delta):
+	if _picked:
+		var mouse_pos = get_global_mouse_position()
+		if mode == MODE_STATIC:
+			global_position = mouse_pos
+		else:
+			linear_velocity = (mouse_pos - _last_mouse_pos) / delta
+			_last_mouse_pos = mouse_pos

+ 14 - 10
3d/physics_tests/test.gd

@@ -13,9 +13,9 @@ var _drawn_nodes = []
 
 
 
 
 func _physics_process(_delta):
 func _physics_process(_delta):
-	if (_wait_physics_ticks_counter > 0):
+	if _wait_physics_ticks_counter > 0:
 		_wait_physics_ticks_counter -= 1
 		_wait_physics_ticks_counter -= 1
-		if (_wait_physics_ticks_counter == 0):
+		if _wait_physics_ticks_counter == 0:
 			emit_signal("wait_done")
 			emit_signal("wait_done")
 
 
 
 
@@ -60,17 +60,21 @@ func clear_drawn_nodes():
 	_drawn_nodes.clear()
 	_drawn_nodes.clear()
 
 
 
 
-func create_rigidbody_box(size):
-	var template_shape = BoxShape.new()
-	template_shape.extents = 0.5 * size
+func create_rigidbody_box(size, pickable):
+	var shape = BoxShape.new()
+	shape.extents = 0.5 * size
 
 
-	var template_collision = CollisionShape.new()
-	template_collision.shape = template_shape
+	var collision = CollisionShape.new()
+	collision.shape = shape
+
+	var body = RigidBody.new()
+	body.add_child(collision)
 
 
-	var template_body = RigidBody.new()
-	template_body.add_child(template_collision)
+	if pickable:
+		var script = load("res://utils/rigidbody_pick.gd")
+		body.set_script(script)
 
 
-	return template_body
+	return body
 
 
 
 
 func start_timer(timeout):
 func start_timer(timeout):

+ 4 - 0
3d/physics_tests/tests.gd

@@ -26,6 +26,10 @@ var _tests = [
 		"id": "Functional Tests/Collision Pairs",
 		"id": "Functional Tests/Collision Pairs",
 		"path": "res://tests/functional/test_collision_pairs.tscn",
 		"path": "res://tests/functional/test_collision_pairs.tscn",
 	},
 	},
+	{
+		"id": "Functional Tests/Joints",
+		"path": "res://tests/functional/test_joints.tscn",
+	},
 	{
 	{
 		"id": "Functional Tests/Raycasting",
 		"id": "Functional Tests/Raycasting",
 		"path": "res://tests/functional/test_raycasting.tscn",
 		"path": "res://tests/functional/test_raycasting.tscn",

+ 6 - 6
3d/physics_tests/tests/functional/test_collision_pairs.gd

@@ -52,16 +52,16 @@ func _ready():
 
 
 func _input(event):
 func _input(event):
 	var key_event = event as InputEventKey
 	var key_event = event as InputEventKey
-	if (key_event and not key_event.pressed):
-		if (key_event.scancode == KEY_1):
+	if key_event and not key_event.pressed:
+		if key_event.scancode == KEY_1:
 			_on_option_selected(OPTION_TYPE_BOX)
 			_on_option_selected(OPTION_TYPE_BOX)
-		elif (key_event.scancode == KEY_2):
+		elif key_event.scancode == KEY_2:
 			_on_option_selected(OPTION_TYPE_SPHERE)
 			_on_option_selected(OPTION_TYPE_SPHERE)
-		elif (key_event.scancode == KEY_3):
+		elif key_event.scancode == KEY_3:
 			_on_option_selected(OPTION_TYPE_CAPSULE)
 			_on_option_selected(OPTION_TYPE_CAPSULE)
-		elif (key_event.scancode == KEY_4):
+		elif key_event.scancode == KEY_4:
 			_on_option_selected(OPTION_TYPE_CYLINDER)
 			_on_option_selected(OPTION_TYPE_CYLINDER)
-		elif (key_event.scancode == KEY_5):
+		elif key_event.scancode == KEY_5:
 			_on_option_selected(OPTION_TYPE_CONVEX_POLYGON)
 			_on_option_selected(OPTION_TYPE_CONVEX_POLYGON)
 
 
 
 

+ 139 - 0
3d/physics_tests/tests/functional/test_joints.gd

@@ -0,0 +1,139 @@
+extends Test
+
+
+const OPTION_JOINT_TYPE = "Joint Type/%s Joint (%d)"
+
+const OPTION_TEST_CASE_BODIES_COLLIDE = "Test case/Attached bodies collide"
+const OPTION_TEST_CASE_WORLD_ATTACHMENT = "Test case/No parent body"
+const OPTION_TEST_CASE_DYNAMIC_ATTACHMENT = "Test case/Parent body is dynamic (no gravity)"
+const OPTION_TEST_CASE_DESTROY_BODY = "Test case/Destroy attached body"
+const OPTION_TEST_CASE_CHANGE_POSITIONS = "Test case/Set body positions after added to scene"
+
+const BOX_SIZE = Vector3(1.0, 1.0, 1.0)
+
+var _update_joint = false
+var _selected_joint = null
+
+var _bodies_collide = false
+var _world_attachement = false
+var _dynamic_attachement = false
+var _destroy_body = false
+var _change_positions = false
+
+var _joint_types = {}
+
+
+func _ready():
+	for joint_index in $Joints.get_child_count():
+		var joint_node = $Joints.get_child(joint_index)
+		joint_node.visible = false
+		var joint_name = joint_node.name
+		var joint_short = joint_name.substr(0, joint_name.length() - 5)
+		var option_name = OPTION_JOINT_TYPE % [joint_short, joint_index + 1]
+		$Options.add_menu_item(option_name)
+		_joint_types[option_name] = joint_node
+
+	$Options.add_menu_item(OPTION_TEST_CASE_BODIES_COLLIDE, true, false)
+	$Options.add_menu_item(OPTION_TEST_CASE_WORLD_ATTACHMENT, true, false)
+	$Options.add_menu_item(OPTION_TEST_CASE_DYNAMIC_ATTACHMENT, true, false)
+	$Options.add_menu_item(OPTION_TEST_CASE_DESTROY_BODY, true, false)
+	$Options.add_menu_item(OPTION_TEST_CASE_CHANGE_POSITIONS, true, false)
+
+	$Options.connect("option_selected", self, "_on_option_selected")
+	$Options.connect("option_changed", self, "_on_option_changed")
+
+	_selected_joint = _joint_types.values()[0]
+	_update_joint = true
+
+
+func _process(_delta):
+	if _update_joint:
+		_update_joint = false
+		_create_joint()
+		$LabelJointType.text = "Joint Type: " + _selected_joint.name
+
+
+func _input(event):
+	var key_event = event as InputEventKey
+	if key_event and not key_event.pressed:
+		var joint_index = key_event.scancode - KEY_1
+		if joint_index >= 0 and joint_index < _joint_types.size():
+			_selected_joint = _joint_types.values()[joint_index]
+			_update_joint = true
+
+
+func _on_option_selected(option):
+	if _joint_types.has(option):
+		_selected_joint = _joint_types[option]
+		_update_joint = true
+
+
+func _on_option_changed(option, checked):
+	match option:
+		OPTION_TEST_CASE_BODIES_COLLIDE:
+			_bodies_collide = checked
+			_update_joint = true
+		OPTION_TEST_CASE_WORLD_ATTACHMENT:
+			_world_attachement = checked
+			_update_joint = true
+		OPTION_TEST_CASE_DYNAMIC_ATTACHMENT:
+			_dynamic_attachement = checked
+			_update_joint = true
+		OPTION_TEST_CASE_DESTROY_BODY:
+			_destroy_body = checked
+			_update_joint = true
+		OPTION_TEST_CASE_CHANGE_POSITIONS:
+			_change_positions = checked
+			_update_joint = true
+
+
+func _create_joint():
+	cancel_timer()
+
+	var root = $Objects
+
+	while root.get_child_count():
+		var last_child_index = root.get_child_count() - 1
+		var last_child = root.get_child(last_child_index)
+		root.remove_child(last_child)
+		last_child.queue_free()
+
+	var child_body = create_rigidbody_box(BOX_SIZE, true)
+	child_body.mode = RigidBody.MODE_RIGID
+	if _change_positions:
+		root.add_child(child_body)
+		child_body.transform.origin = Vector3(0.0, -1.5, 0.0)
+	else:
+		child_body.transform.origin = Vector3(0.0, -1.5, 0.0)
+		root.add_child(child_body)
+
+	var parent_body = null
+	if not _world_attachement:
+		parent_body = create_rigidbody_box(BOX_SIZE, true)
+		if _dynamic_attachement:
+			parent_body.mode = RigidBody.MODE_RIGID
+			parent_body.gravity_scale = 0.0
+			child_body.gravity_scale = 0.0
+		else:
+			parent_body.mode = RigidBody.MODE_STATIC
+		if _change_positions:
+			root.add_child(parent_body)
+			parent_body.transform.origin = Vector3(0.0, 1.5, 0.0)
+		else:
+			parent_body.transform.origin = Vector3(0.0, 1.5, 0.0)
+			root.add_child(parent_body)
+
+	var joint = _selected_joint.duplicate()
+	joint.visible = true
+	joint.set_exclude_nodes_from_collision(not _bodies_collide)
+	if parent_body:
+		joint.set_node_a(parent_body.get_path())
+	joint.set_node_b(child_body.get_path())
+	root.add_child(joint)
+
+	if _destroy_body:
+		yield(start_timer(0.5), "timeout")
+		if is_timer_canceled():
+			return
+
+		child_body.queue_free()

+ 41 - 0
3d/physics_tests/tests/functional/test_joints.tscn

@@ -0,0 +1,41 @@
+[gd_scene load_steps=4 format=2]
+
+[ext_resource path="res://utils/camera_orbit.gd" type="Script" id=1]
+[ext_resource path="res://tests/functional/test_joints.gd" type="Script" id=2]
+[ext_resource path="res://tests/test_options.tscn" type="PackedScene" id=3]
+
+[node name="Test" type="Spatial"]
+script = ExtResource( 2 )
+
+[node name="LabelJointType" type="Label" parent="."]
+margin_left = 14.0
+margin_top = 78.0
+margin_right = 171.0
+margin_bottom = 92.0
+text = "Joint Type: "
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Options" parent="." instance=ExtResource( 3 )]
+
+[node name="Joints" type="Spatial" parent="."]
+
+[node name="PinJoint" type="PinJoint" parent="Joints"]
+
+[node name="HingeJoint" type="HingeJoint" parent="Joints"]
+
+[node name="SliderJoint" type="SliderJoint" parent="Joints"]
+
+[node name="ConeTwistJoint" type="ConeTwistJoint" parent="Joints"]
+
+[node name="Generic6DOFJoint" type="Generic6DOFJoint" parent="Joints"]
+
+[node name="Objects" type="Spatial" parent="."]
+
+[node name="Camera" type="Camera" parent="."]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 6.19796 )
+script = ExtResource( 1 )
+
+[node name="OmniLight" type="OmniLight" parent="Camera"]
+omni_range = 50.0

+ 1 - 8
3d/physics_tests/tests/functional/test_pyramid.gd

@@ -15,14 +15,7 @@ func _ready():
 func _create_pyramid():
 func _create_pyramid():
 	var root_node = $Pyramid
 	var root_node = $Pyramid
 
 
-	var template_shape = BoxShape.new()
-	template_shape.extents = 0.5 * box_size
-
-	var template_collision = CollisionShape.new()
-	template_collision.shape = template_shape
-
-	var template_body = RigidBody.new()
-	template_body.add_child(template_collision)
+	var template_body = create_rigidbody_box(box_size, true)
 
 
 	var pos_y = 0.5 * box_size.y + box_spacing.y
 	var pos_y = 0.5 * box_size.y + box_spacing.y
 
 

+ 1 - 8
3d/physics_tests/tests/functional/test_stack.gd

@@ -15,14 +15,7 @@ func _ready():
 func _create_stack():
 func _create_stack():
 	var root_node = $Stack
 	var root_node = $Stack
 
 
-	var template_shape = BoxShape.new()
-	template_shape.extents = 0.5 * box_size
-
-	var template_collision = CollisionShape.new()
-	template_collision.shape = template_shape
-
-	var template_body = RigidBody.new()
-	template_body.add_child(template_collision)
+	var template_body = create_rigidbody_box(box_size, true)
 
 
 	var pos_y = 0.5 * box_size.y + box_spacing.y
 	var pos_y = 0.5 * box_size.y + box_spacing.y
 
 

+ 1 - 1
3d/physics_tests/utils/camera_orbit.gd

@@ -14,7 +14,7 @@ func _ready():
 func _unhandled_input(event):
 func _unhandled_input(event):
 	var mouse_button_event = event as InputEventMouseButton
 	var mouse_button_event = event as InputEventMouseButton
 	if mouse_button_event:
 	if mouse_button_event:
-		if mouse_button_event.button_index == BUTTON_LEFT:
+		if mouse_button_event.button_index == BUTTON_RIGHT:
 			_rotation_enabled = mouse_button_event.pressed
 			_rotation_enabled = mouse_button_event.pressed
 		return
 		return
 
 

+ 56 - 0
3d/physics_tests/utils/rigidbody_pick.gd

@@ -0,0 +1,56 @@
+extends RigidBody
+
+
+const MOUSE_DELTA_COEFFICIENT = 0.01
+const CAMERA_DISTANCE_COEFFICIENT = 0.2
+
+var _picked = false
+var _last_mouse_pos = Vector2.ZERO
+var _mouse_pos = Vector2.ZERO
+
+
+func _ready():
+	input_ray_pickable = true
+
+
+func _input(event):
+	var mouse_event = event as InputEventMouseButton
+	if mouse_event and not mouse_event.pressed:
+		if mouse_event.button_index == BUTTON_LEFT:
+			_picked = false
+
+	var mouse_motion = event as InputEventMouseMotion
+	if mouse_motion:
+		_mouse_pos = mouse_motion.position
+
+
+func _input_event(_viewport, event, _click_pos, _click_normal, _shape_idx):
+	var mouse_event = event as InputEventMouseButton
+	if mouse_event and mouse_event.pressed:
+		if mouse_event.button_index == BUTTON_LEFT:
+			_picked = true
+			_mouse_pos = mouse_event.position
+			_last_mouse_pos = _mouse_pos
+
+
+func _physics_process(delta):
+	if _picked:
+		var mouse_delta = _mouse_pos - _last_mouse_pos
+
+		var world_delta = Vector3.ZERO
+		world_delta.x = mouse_delta.x * MOUSE_DELTA_COEFFICIENT
+		world_delta.y = -mouse_delta.y * MOUSE_DELTA_COEFFICIENT
+
+		var camera = get_viewport().get_camera()
+		if camera:
+			var camera_basis = camera.global_transform.basis
+			world_delta = camera_basis * world_delta
+
+			var camera_dist = camera.global_transform.origin.distance_to(global_transform.origin)
+			world_delta *= CAMERA_DISTANCE_COEFFICIENT * camera_dist
+
+		if mode == MODE_STATIC:
+			global_transform.origin += world_delta
+		else:
+			linear_velocity = world_delta / delta
+		_last_mouse_pos = _mouse_pos