Browse Source

Updated 2d/3d physics contact performance tests

PouleyKetchoupp 4 years ago
parent
commit
0a0d44d4f1

+ 28 - 4
2d/physics_tests/test.gd

@@ -68,12 +68,10 @@ func clear_drawn_nodes():
 	_drawn_nodes.clear()
 	_drawn_nodes.clear()
 
 
 
 
-func create_rigidbody_box(size, pickable = false, use_icon = false):
-	var shape = RectangleShape2D.new()
-	shape.extents = 0.5 * size
-
+func create_rigidbody(shape, pickable = false, transform = Transform.IDENTITY):
 	var collision = CollisionShape2D.new()
 	var collision = CollisionShape2D.new()
 	collision.shape = shape
 	collision.shape = shape
+	collision.transform = transform
 
 
 	var body = RigidBody2D.new()
 	var body = RigidBody2D.new()
 	body.add_child(collision)
 	body.add_child(collision)
@@ -82,6 +80,32 @@ func create_rigidbody_box(size, pickable = false, use_icon = false):
 		var script = load("res://utils/rigidbody_pick.gd")
 		var script = load("res://utils/rigidbody_pick.gd")
 		body.set_script(script)
 		body.set_script(script)
 
 
+	return body
+
+
+func create_rigidbody_collision(collision, pickable = false, transform = Transform.IDENTITY):
+	var collision_copy = collision.duplicate()
+	collision_copy.transform = transform
+
+	if collision is CollisionShape2D:
+		collision_copy.shape = collision.shape.duplicate()
+
+	var body = RigidBody2D.new()
+	body.add_child(collision_copy)
+
+	if pickable:
+		var script = load("res://utils/rigidbody_pick.gd")
+		body.set_script(script)
+
+	return body
+
+
+func create_rigidbody_box(size, pickable = false, use_icon = false, transform = Transform.IDENTITY):
+	var shape = RectangleShape2D.new()
+	shape.extents = 0.5 * size
+
+	var body = create_rigidbody(shape, pickable, transform)
+
 	if use_icon:
 	if use_icon:
 		var texture = load("res://icon.png")
 		var texture = load("res://icon.png")
 		var icon = Sprite.new()
 		var icon = Sprite.new()

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

@@ -167,7 +167,7 @@ func _on_option_selected(option):
 
 
 
 
 func _find_type_index(type_name):
 func _find_type_index(type_name):
-	for type_index in _collision_shapes.size():
+	for type_index in range(_collision_shapes.size()):
 		var type_shape = _collision_shapes[type_index]
 		var type_shape = _collision_shapes[type_index]
 		if type_shape.resource_name.find(type_name) > -1:
 		if type_shape.resource_name.find(type_name) > -1:
 			return type_index
 			return type_index

+ 3 - 3
2d/physics_tests/tests/functional/test_joints.gd

@@ -11,8 +11,6 @@ const OPTION_TEST_CASE_CHANGE_POSITIONS = "Test case/Set body positions after ad
 
 
 const BOX_SIZE = Vector2(64, 64)
 const BOX_SIZE = Vector2(64, 64)
 
 
-onready var options = $Options
-
 var _update_joint = false
 var _update_joint = false
 var _selected_joint = null
 var _selected_joint = null
 
 
@@ -27,8 +25,10 @@ var _joint_types = {}
 
 
 
 
 func _ready():
 func _ready():
+	var options = $Options
+
 	var joints = $Joints
 	var joints = $Joints
-	for joint_index in joints.get_child_count():
+	for joint_index in range(joints.get_child_count()):
 		var joint_node = joints.get_child(joint_index)
 		var joint_node = joints.get_child(joint_index)
 		joint_node.visible = false
 		joint_node.visible = false
 		var joint_name = joint_node.name
 		var joint_name = joint_node.name

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

@@ -28,7 +28,7 @@ func _create_pyramid():
 
 
 		var pos_x = -0.5 * (num_boxes - 1) * (box_size.x + box_spacing.x)
 		var pos_x = -0.5 * (num_boxes - 1) * (box_size.x + box_spacing.x)
 
 
-		for box_index in num_boxes:
+		for box_index in range(num_boxes):
 			var box = template_body.duplicate()
 			var box = template_body.duplicate()
 			box.position = Vector2(pos_x, 0.0)
 			box.position = Vector2(pos_x, 0.0)
 			box.name = "Box%02d" % (box_index + 1)
 			box.name = "Box%02d" % (box_index + 1)

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

@@ -26,7 +26,7 @@ func _create_stack():
 
 
 		var pos_x = -0.5 * (width - 1) * (box_size.x + box_spacing.x)
 		var pos_x = -0.5 * (width - 1) * (box_size.x + box_spacing.x)
 
 
-		for box_index in width:
+		for box_index in range(width):
 			var box = template_body.duplicate()
 			var box = template_body.duplicate()
 			box.position = Vector2(pos_x, 0.0)
 			box.position = Vector2(pos_x, 0.0)
 			box.name = "Box%02d" % (box_index + 1)
 			box.name = "Box%02d" % (box_index + 1)

+ 1 - 1
2d/physics_tests/tests/performance/test_perf_broadphase.gd

@@ -148,7 +148,7 @@ func _remove_objects():
 
 
 	# Remove objects in reversed order to avoid the overhead of changing children index in parent.
 	# Remove objects in reversed order to avoid the overhead of changing children index in parent.
 	var object_count = _objects.size()
 	var object_count = _objects.size()
-	for object_index in object_count:
+	for object_index in range(object_count):
 		root_node.remove_child(_objects[object_count - object_index - 1])
 		root_node.remove_child(_objects[object_count - object_index - 1])
 
 
 	timer = OS.get_ticks_usec() - timer
 	timer = OS.get_ticks_usec() - timer

+ 62 - 33
2d/physics_tests/tests/performance/test_perf_contacts.gd

@@ -10,12 +10,15 @@ const OPTION_TYPE_CONCAVE_POLYGON = "Shape type/Concave Polygon"
 
 
 export(Array) var spawns = Array()
 export(Array) var spawns = Array()
 export(int) var spawn_count = 100
 export(int) var spawn_count = 100
-export(int, 1, 10) var spawn_multiplier = 5
 
 
 onready var options = $Options
 onready var options = $Options
 
 
 var _object_templates = []
 var _object_templates = []
 
 
+var _log_physics = false
+var _log_physics_time = 0
+var _log_physics_time_start = 0
+
 
 
 func _ready():
 func _ready():
 	yield(start_timer(0.5), "timeout")
 	yield(start_timer(0.5), "timeout")
@@ -40,6 +43,25 @@ func _ready():
 	_start_all_types()
 	_start_all_types()
 
 
 
 
+func _physics_process(_delta):
+	if _log_physics:
+		var time = OS.get_ticks_usec()
+		var time_delta = time - _log_physics_time
+		var time_total = time - _log_physics_time_start
+		_log_physics_time = time
+		Log.print_log("  Physics Tick: %.3f ms (total = %.3f ms)" % [0.001 * time_delta, 0.001 * time_total])
+
+
+func _log_physics_start():
+	_log_physics = true
+	_log_physics_time_start = OS.get_ticks_usec()
+	_log_physics_time = _log_physics_time_start
+
+
+func _log_physics_stop():
+	_log_physics = false
+
+
 func _exit_tree():
 func _exit_tree():
 	for object_template in _object_templates:
 	for object_template in _object_templates:
 		object_template.free()
 		object_template.free()
@@ -66,7 +88,7 @@ func _on_option_selected(option):
 
 
 
 
 func _find_type_index(type_name):
 func _find_type_index(type_name):
-	for type_index in _object_templates.size():
+	for type_index in range(_object_templates.size()):
 		var type_node = _object_templates[type_index]
 		var type_node = _object_templates[type_index]
 		if type_node.name.find(type_name) > -1:
 		if type_node.name.find(type_name) > -1:
 			return type_index
 			return type_index
@@ -85,44 +107,47 @@ func _start_type(type_index):
 	if is_timer_canceled():
 	if is_timer_canceled():
 		return
 		return
 
 
+	_log_physics_start()
+
 	_spawn_objects(type_index)
 	_spawn_objects(type_index)
 
 
+	yield(wait_for_physics_ticks(5), "wait_done")
+	_log_physics_stop()
+
 	yield(start_timer(1.0), "timeout")
 	yield(start_timer(1.0), "timeout")
 	if is_timer_canceled():
 	if is_timer_canceled():
 		return
 		return
 
 
+	_log_physics_start()
+
 	_activate_objects()
 	_activate_objects()
 
 
+	yield(wait_for_physics_ticks(5), "wait_done")
+	_log_physics_stop()
+
 	yield(start_timer(5.0), "timeout")
 	yield(start_timer(5.0), "timeout")
 	if is_timer_canceled():
 	if is_timer_canceled():
 		return
 		return
 
 
-	_despawn_objects()
-
-	Log.print_log("* Done.")
+	_log_physics_start()
 
 
+	_despawn_objects()
 
 
-func _start_all_types():
-	for type_index in _object_templates.size():
-		yield(start_timer(1.0), "timeout")
-		if is_timer_canceled():
-			return
+	yield(wait_for_physics_ticks(5), "wait_done")
+	_log_physics_stop()
 
 
-		_spawn_objects(type_index)
+	yield(start_timer(1.0), "timeout")
 
 
-		yield(start_timer(1.0), "timeout")
-		if is_timer_canceled():
-			return
 
 
-		_activate_objects()
+func _start_all_types():
+	Log.print_log("* Start all types.")
 
 
-		yield(start_timer(5.0), "timeout")
+	for type_index in range(_object_templates.size()):
+		yield(_start_type(type_index), "completed")
 		if is_timer_canceled():
 		if is_timer_canceled():
 			return
 			return
 
 
-		_despawn_objects()
-
-	Log.print_log("* Done.")
+	Log.print_log("* Done all types.")
 
 
 
 
 func _spawn_objects(type_index):
 func _spawn_objects(type_index):
@@ -132,33 +157,37 @@ func _spawn_objects(type_index):
 
 
 		Log.print_log("* Spawning: " + template_node.name)
 		Log.print_log("* Spawning: " + template_node.name)
 
 
-		for _index in range(spawn_multiplier):
-			for _node_index in spawn_count / spawn_multiplier:
-				var node = template_node.duplicate() as Node2D
-				spawn_parent.add_child(node)
+		for _node_index in range(spawn_count):
+			# Create a new object and shape every time to avoid the overhead of connecting many bodies to the same shape.
+			var collision = template_node.get_child(0)
+			var body = create_rigidbody_collision(collision, false, collision.transform)
+			body.set_sleeping(true)
+			spawn_parent.add_child(body)
 
 
 
 
 func _activate_objects():
 func _activate_objects():
-	var spawn_parent = $SpawnTarget1
+	for spawn in spawns:
+		var spawn_parent = get_node(spawn)
 
 
-	Log.print_log("* Activating")
+		Log.print_log("* Activating")
 
 
-	for node_index in spawn_parent.get_child_count():
-		var node = spawn_parent.get_child(node_index) as RigidBody2D
-		node.set_sleeping(false)
+		for node_index in range(spawn_parent.get_child_count()):
+			var node = spawn_parent.get_child(node_index) as RigidBody2D
+			node.set_sleeping(false)
 
 
 
 
 func _despawn_objects():
 func _despawn_objects():
 	for spawn in spawns:
 	for spawn in spawns:
 		var spawn_parent = get_node(spawn)
 		var spawn_parent = get_node(spawn)
 
 
-		if spawn_parent.get_child_count() == 0:
-			return
+		var object_count = spawn_parent.get_child_count()
+		if object_count == 0:
+			continue
 
 
 		Log.print_log("* Despawning")
 		Log.print_log("* Despawning")
 
 
-		while spawn_parent.get_child_count():
-			var node_index = spawn_parent.get_child_count() - 1
-			var node = spawn_parent.get_child(node_index)
+		# Remove objects in reversed order to avoid the overhead of changing children index in parent.
+		for object_index in range(object_count):
+			var node = spawn_parent.get_child(object_count - object_index - 1)
 			spawn_parent.remove_child(node)
 			spawn_parent.remove_child(node)
 			node.queue_free()
 			node.queue_free()

+ 2 - 0
2d/physics_tests/tests/performance/test_perf_contacts.tscn

@@ -17,7 +17,9 @@ height = 30.0
 
 
 [node name="Test" type="Node2D"]
 [node name="Test" type="Node2D"]
 script = ExtResource( 2 )
 script = ExtResource( 2 )
+_enable_debug_collision = false
 spawns = [ NodePath("SpawnTarget1") ]
 spawns = [ NodePath("SpawnTarget1") ]
+spawn_count = 200
 
 
 [node name="Options" parent="." instance=ExtResource( 4 )]
 [node name="Options" parent="." instance=ExtResource( 4 )]
 
 

+ 2 - 2
2d/physics_tests/utils/option_menu.gd

@@ -13,7 +13,7 @@ func add_menu_item(item_path, checkbox = false, checked = false, radio = false):
 
 
 	var path = ""
 	var path = ""
 	var popup = get_popup()
 	var popup = get_popup()
-	for element_index in path_element_count - 1:
+	for element_index in range(path_element_count - 1):
 		var popup_label = path_elements[element_index]
 		var popup_label = path_elements[element_index]
 		path += popup_label + "/"
 		path += popup_label + "/"
 		popup = _add_popup(popup, path, popup_label)
 		popup = _add_popup(popup, path, popup_label)
@@ -59,7 +59,7 @@ func _on_item_pressed(item_index, popup_menu, path):
 		var checked = popup_menu.is_item_checked(item_index)
 		var checked = popup_menu.is_item_checked(item_index)
 		if not checked:
 		if not checked:
 			popup_menu.set_item_checked(item_index, true)
 			popup_menu.set_item_checked(item_index, true)
-			for other_index in popup_menu.get_item_count():
+			for other_index in range(popup_menu.get_item_count()):
 				if other_index != item_index:
 				if other_index != item_index:
 					popup_menu.set_item_checked(other_index, false)
 					popup_menu.set_item_checked(other_index, false)
 			emit_signal("option_selected", item_path)
 			emit_signal("option_selected", item_path)

+ 9 - 4
3d/physics_tests/test.gd

@@ -67,12 +67,10 @@ func clear_drawn_nodes():
 	_drawn_nodes.clear()
 	_drawn_nodes.clear()
 
 
 
 
-func create_rigidbody_box(size, pickable = false):
-	var shape = BoxShape.new()
-	shape.extents = 0.5 * size
-
+func create_rigidbody(shape, pickable = false, transform = Transform.IDENTITY):
 	var collision = CollisionShape.new()
 	var collision = CollisionShape.new()
 	collision.shape = shape
 	collision.shape = shape
+	collision.transform = transform
 
 
 	var body = RigidBody.new()
 	var body = RigidBody.new()
 	body.add_child(collision)
 	body.add_child(collision)
@@ -84,6 +82,13 @@ func create_rigidbody_box(size, pickable = false):
 	return body
 	return body
 
 
 
 
+func create_rigidbody_box(size, pickable = false, transform = Transform.IDENTITY):
+	var shape = BoxShape.new()
+	shape.extents = 0.5 * size
+
+	return create_rigidbody(shape, pickable, transform)
+
+
 func start_timer(timeout):
 func start_timer(timeout):
 	if _timer == null:
 	if _timer == null:
 		_timer = Timer.new()
 		_timer = Timer.new()

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

@@ -170,7 +170,7 @@ func _on_option_selected(option):
 
 
 
 
 func _find_type_index(type_name):
 func _find_type_index(type_name):
-	for type_index in _collision_shapes.size():
+	for type_index in range(_collision_shapes.size()):
 		var type_shape = _collision_shapes[type_index]
 		var type_shape = _collision_shapes[type_index]
 		if type_shape.resource_name.find(type_name) > -1:
 		if type_shape.resource_name.find(type_name) > -1:
 			return type_index
 			return type_index

+ 13 - 10
3d/physics_tests/tests/functional/test_joints.gd

@@ -24,23 +24,26 @@ var _joint_types = {}
 
 
 
 
 func _ready():
 func _ready():
-	for joint_index in $Joints.get_child_count():
-		var joint_node = $Joints.get_child(joint_index)
+	var options = $Options
+
+	var joints = $Joints
+	for joint_index in joints.get_child_count():
+		var joint_node = joints.get_child(joint_index)
 		joint_node.visible = false
 		joint_node.visible = false
 		var joint_name = joint_node.name
 		var joint_name = joint_node.name
 		var joint_short = joint_name.substr(0, joint_name.length() - 5)
 		var joint_short = joint_name.substr(0, joint_name.length() - 5)
 		var option_name = OPTION_JOINT_TYPE % [joint_short, joint_index + 1]
 		var option_name = OPTION_JOINT_TYPE % [joint_short, joint_index + 1]
-		$Options.add_menu_item(option_name)
+		options.add_menu_item(option_name)
 		_joint_types[option_name] = joint_node
 		_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.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")
+	options.connect("option_selected", self, "_on_option_selected")
+	options.connect("option_changed", self, "_on_option_changed")
 
 
 	_selected_joint = _joint_types.values()[0]
 	_selected_joint = _joint_types.values()[0]
 	_update_joint = true
 	_update_joint = true

+ 1 - 1
3d/physics_tests/tests/performance/test_perf_broadphase.gd

@@ -154,7 +154,7 @@ func _remove_objects():
 
 
 	# Remove objects in reversed order to avoid the overhead of changing children index in parent.
 	# Remove objects in reversed order to avoid the overhead of changing children index in parent.
 	var object_count = _objects.size()
 	var object_count = _objects.size()
-	for object_index in object_count:
+	for object_index in range(object_count):
 		root_node.remove_child(_objects[object_count - object_index - 1])
 		root_node.remove_child(_objects[object_count - object_index - 1])
 
 
 	timer = OS.get_ticks_usec() - timer
 	timer = OS.get_ticks_usec() - timer

+ 61 - 35
3d/physics_tests/tests/performance/test_perf_contacts.gd

@@ -10,10 +10,13 @@ const OPTION_TYPE_CONVEX = "Shape type/Convex"
 
 
 export(Array) var spawns = Array()
 export(Array) var spawns = Array()
 export(int) var spawn_count = 100
 export(int) var spawn_count = 100
-export(int, 1, 10) var spawn_multiplier = 5
 
 
 var _object_templates = []
 var _object_templates = []
 
 
+var _log_physics = false
+var _log_physics_time = 0
+var _log_physics_time_start = 0
+
 
 
 func _ready():
 func _ready():
 	yield(start_timer(0.5), "timeout")
 	yield(start_timer(0.5), "timeout")
@@ -41,6 +44,25 @@ func _exit_tree():
 		object_template.free()
 		object_template.free()
 
 
 
 
+func _physics_process(_delta):
+	if _log_physics:
+		var time = OS.get_ticks_usec()
+		var time_delta = time - _log_physics_time
+		var time_total = time - _log_physics_time_start
+		_log_physics_time = time
+		Log.print_log("  Physics Tick: %.3f ms (total = %.3f ms)" % [0.001 * time_delta, 0.001 * time_total])
+
+
+func _log_physics_start():
+	_log_physics = true
+	_log_physics_time_start = OS.get_ticks_usec()
+	_log_physics_time = _log_physics_time_start
+
+
+func _log_physics_stop():
+	_log_physics = false
+
+
 func _on_option_selected(option):
 func _on_option_selected(option):
 	cancel_timer()
 	cancel_timer()
 
 
@@ -62,7 +84,7 @@ func _on_option_selected(option):
 
 
 
 
 func _find_type_index(type_name):
 func _find_type_index(type_name):
-	for type_index in _object_templates.size():
+	for type_index in range(_object_templates.size()):
 		var type_node = _object_templates[type_index]
 		var type_node = _object_templates[type_index]
 		if type_node.name.find(type_name) > -1:
 		if type_node.name.find(type_name) > -1:
 			return type_index
 			return type_index
@@ -81,44 +103,47 @@ func _start_type(type_index):
 	if is_timer_canceled():
 	if is_timer_canceled():
 		return
 		return
 
 
+	_log_physics_start()
+
 	_spawn_objects(type_index)
 	_spawn_objects(type_index)
 
 
+	yield(wait_for_physics_ticks(5), "wait_done")
+	_log_physics_stop()
+
 	yield(start_timer(1.0), "timeout")
 	yield(start_timer(1.0), "timeout")
 	if is_timer_canceled():
 	if is_timer_canceled():
 		return
 		return
 
 
+	_log_physics_start()
+
 	_activate_objects()
 	_activate_objects()
 
 
+	yield(wait_for_physics_ticks(5), "wait_done")
+	_log_physics_stop()
+
 	yield(start_timer(5.0), "timeout")
 	yield(start_timer(5.0), "timeout")
 	if is_timer_canceled():
 	if is_timer_canceled():
 		return
 		return
 
 
-	_despawn_objects()
-
-	Log.print_log("* Done.")
+	_log_physics_start()
 
 
+	_despawn_objects()
 
 
-func _start_all_types():
-	for type_index in _object_templates.size():
-		yield(start_timer(1.0), "timeout")
-		if is_timer_canceled():
-			return
+	yield(wait_for_physics_ticks(5), "wait_done")
+	_log_physics_stop()
 
 
-		_spawn_objects(type_index)
+	yield(start_timer(1.0), "timeout")
 
 
-		yield(start_timer(1.0), "timeout")
-		if is_timer_canceled():
-			return
 
 
-		_activate_objects()
+func _start_all_types():
+	Log.print_log("* Start all types.")
 
 
-		yield(start_timer(5.0), "timeout")
+	for type_index in range(_object_templates.size()):
+		yield(_start_type(type_index), "completed")
 		if is_timer_canceled():
 		if is_timer_canceled():
 			return
 			return
 
 
-		_despawn_objects()
-
-	Log.print_log("* Done.")
+	Log.print_log("* Done all types.")
 
 
 
 
 func _spawn_objects(type_index):
 func _spawn_objects(type_index):
@@ -128,34 +153,35 @@ func _spawn_objects(type_index):
 
 
 		Log.print_log("* Spawning: " + template_node.name)
 		Log.print_log("* Spawning: " + template_node.name)
 
 
-		for _index in range(spawn_multiplier):
-			for _node_index in spawn_count / spawn_multiplier:
-				var node = template_node.duplicate() as Spatial
-				node.transform.origin = Vector3.ZERO
-				spawn_parent.add_child(node)
+		for _node_index in range(spawn_count):
+			# Create a new object and shape every time to avoid the overhead of connecting many bodies to the same shape.
+			var collision = template_node.get_child(0) as CollisionShape
+			var shape = collision.shape.duplicate()
+			var body = create_rigidbody(shape, false, collision.transform)
+			body.set_sleeping(true)
+			spawn_parent.add_child(body)
 
 
 
 
 func _activate_objects():
 func _activate_objects():
-	var spawn_parent = $SpawnTarget1
+	for spawn in spawns:
+		var spawn_parent = get_node(spawn)
 
 
-	Log.print_log("* Activating")
+		Log.print_log("* Activating")
 
 
-	for node_index in spawn_parent.get_child_count():
-		var node = spawn_parent.get_child(node_index) as RigidBody
-		node.set_sleeping(false)
+		for node_index in range(spawn_parent.get_child_count()):
+			var node = spawn_parent.get_child(node_index) as RigidBody
+			node.set_sleeping(false)
 
 
 
 
 func _despawn_objects():
 func _despawn_objects():
 	for spawn in spawns:
 	for spawn in spawns:
 		var spawn_parent = get_node(spawn)
 		var spawn_parent = get_node(spawn)
 
 
-		if spawn_parent.get_child_count() == 0:
-			return
-
 		Log.print_log("* Despawning")
 		Log.print_log("* Despawning")
 
 
-		while spawn_parent.get_child_count():
-			var node_index = spawn_parent.get_child_count() - 1
-			var node = spawn_parent.get_child(node_index)
+		# Remove objects in reversed order to avoid the overhead of changing children index in parent.
+		var object_count = spawn_parent.get_child_count()
+		for object_index in range(object_count):
+			var node = spawn_parent.get_child(object_count - object_index - 1)
 			spawn_parent.remove_child(node)
 			spawn_parent.remove_child(node)
 			node.queue_free()
 			node.queue_free()

+ 7 - 6
3d/physics_tests/tests/performance/test_perf_contacts.tscn

@@ -17,12 +17,13 @@ points = PoolVector3Array( -0.7, 0, -0.7, -0.3, 0, 0.8, 0.8, 0, -0.3, 0, -1, 0 )
 
 
 [sub_resource type="SphereShape" id=5]
 [sub_resource type="SphereShape" id=5]
 
 
-[sub_resource type="ConcavePolygonShape" id=6]
-data = PoolVector3Array( -1, 0, 1, 1, 0, -1, 1, 0, 1, -1, 0, 1, -1, 0, -1, 1, 0, -1 )
+[sub_resource type="PlaneShape" id=6]
 
 
 [node name="Test" type="Spatial"]
 [node name="Test" type="Spatial"]
 script = ExtResource( 2 )
 script = ExtResource( 2 )
+_enable_debug_collision = false
 spawns = [ "SpawnTarget1" ]
 spawns = [ "SpawnTarget1" ]
+spawn_count = 500
 
 
 [node name="Options" parent="." instance=ExtResource( 4 )]
 [node name="Options" parent="." instance=ExtResource( 4 )]
 
 
@@ -65,19 +66,19 @@ shape = SubResource( 5 )
 [node name="StaticBodyWalls" type="StaticBody" parent="."]
 [node name="StaticBodyWalls" type="StaticBody" parent="."]
 
 
 [node name="CollisionShape1" type="CollisionShape" parent="StaticBodyWalls"]
 [node name="CollisionShape1" type="CollisionShape" parent="StaticBodyWalls"]
-transform = Transform( -1.62921e-05, 1, 0, -100, -1.62921e-07, 0, 0, 0, 100, -5, 0, 0 )
+transform = Transform( -1.62921e-07, 1, 0, -1, -1.62921e-07, 0, 0, 0, 1, -5, 0, 0 )
 shape = SubResource( 6 )
 shape = SubResource( 6 )
 
 
 [node name="CollisionShape2" type="CollisionShape" parent="StaticBodyWalls"]
 [node name="CollisionShape2" type="CollisionShape" parent="StaticBodyWalls"]
-transform = Transform( -1.62921e-05, -1, 0, 100, -1.62921e-07, 0, 0, 0, 100, 5, 0, 0 )
+transform = Transform( -1.62921e-07, -1, 0, 1, -1.62921e-07, 0, 0, 0, 1, 5, 0, 0 )
 shape = SubResource( 6 )
 shape = SubResource( 6 )
 
 
 [node name="CollisionShape3" type="CollisionShape" parent="StaticBodyWalls"]
 [node name="CollisionShape3" type="CollisionShape" parent="StaticBodyWalls"]
-transform = Transform( 2.65431e-12, 1.62921e-07, 100, 100, -1.62921e-07, 0, 1.62921e-05, 1, -1.62921e-05, 0, 0, -5 )
+transform = Transform( 2.6543e-14, 1.62921e-07, 1, 1, -1.62921e-07, 1.56125e-19, 1.62921e-07, 1, -1.62921e-07, 0, 0, -5 )
 shape = SubResource( 6 )
 shape = SubResource( 6 )
 
 
 [node name="CollisionShape4" type="CollisionShape" parent="StaticBodyWalls"]
 [node name="CollisionShape4" type="CollisionShape" parent="StaticBodyWalls"]
-transform = Transform( 2.65431e-12, 1.62921e-07, -100, 100, -1.62921e-07, 0, -1.62921e-05, -1, -1.62921e-05, 0, 0, 5 )
+transform = Transform( 2.6543e-14, 1.62921e-07, -1, 1, -1.62921e-07, -1.56125e-19, -1.62921e-07, -1, -1.62921e-07, 0, 0, 5 )
 shape = SubResource( 6 )
 shape = SubResource( 6 )
 
 
 [node name="StaticScene" parent="." instance=ExtResource( 1 )]
 [node name="StaticScene" parent="." instance=ExtResource( 1 )]

+ 9 - 8
3d/physics_tests/tests/performance/test_perf_contacts_extended.tscn

@@ -17,12 +17,13 @@ points = PoolVector3Array( -0.7, 0, -0.7, -0.3, 0, 0.8, 0.8, 0, -0.3, 0, -1, 0 )
 
 
 [sub_resource type="SphereShape" id=5]
 [sub_resource type="SphereShape" id=5]
 
 
-[sub_resource type="ConcavePolygonShape" id=6]
-data = PoolVector3Array( -1, 0, 1, 1, 0, -1, 1, 0, 1, -1, 0, 1, -1, 0, -1, 1, 0, -1 )
+[sub_resource type="PlaneShape" id=6]
 
 
 [node name="Test" type="Spatial"]
 [node name="Test" type="Spatial"]
 script = ExtResource( 1 )
 script = ExtResource( 1 )
+_enable_debug_collision = false
 spawns = [ "SpawnTarget1", "SpawnTarget2", "SpawnTarget3", "SpawnTarget4", "SpawnTarget5", "SpawnTarget6", "SpawnTarget7", "SpawnTarget8", "SpawnTarget9", "SpawnTarget10", "SpawnTarget11", "SpawnTarget12", "SpawnTarget13", "SpawnTarget14", "SpawnTarget15", "SpawnTarget16" ]
 spawns = [ "SpawnTarget1", "SpawnTarget2", "SpawnTarget3", "SpawnTarget4", "SpawnTarget5", "SpawnTarget6", "SpawnTarget7", "SpawnTarget8", "SpawnTarget9", "SpawnTarget10", "SpawnTarget11", "SpawnTarget12", "SpawnTarget13", "SpawnTarget14", "SpawnTarget15", "SpawnTarget16" ]
+spawn_count = 50
 
 
 [node name="Options" parent="." instance=ExtResource( 4 )]
 [node name="Options" parent="." instance=ExtResource( 4 )]
 
 
@@ -107,26 +108,26 @@ transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0 )
 [node name="CollisionShape" type="CollisionShape" parent="DynamicShapes/RigidBodySphere"]
 [node name="CollisionShape" type="CollisionShape" parent="DynamicShapes/RigidBodySphere"]
 shape = SubResource( 5 )
 shape = SubResource( 5 )
 
 
+[node name="StaticScene" parent="." instance=ExtResource( 5 )]
+
 [node name="StaticBodyWalls" type="StaticBody" parent="."]
 [node name="StaticBodyWalls" type="StaticBody" parent="."]
 
 
 [node name="CollisionShape1" type="CollisionShape" parent="StaticBodyWalls"]
 [node name="CollisionShape1" type="CollisionShape" parent="StaticBodyWalls"]
-transform = Transform( -1.62921e-05, 1, 0, -100, -1.62921e-07, 0, 0, 0, 100, -5, 0, 0 )
+transform = Transform( -1.62921e-07, 1, 0, -1, -1.62921e-07, 0, 0, 0, 1, -50, 0, 0 )
 shape = SubResource( 6 )
 shape = SubResource( 6 )
 
 
 [node name="CollisionShape2" type="CollisionShape" parent="StaticBodyWalls"]
 [node name="CollisionShape2" type="CollisionShape" parent="StaticBodyWalls"]
-transform = Transform( -1.62921e-05, -1, 0, 100, -1.62921e-07, 0, 0, 0, 100, 5, 0, 0 )
+transform = Transform( -1.62921e-07, -1, 0, 1, -1.62921e-07, 0, 0, 0, 1, 50, 0, 0 )
 shape = SubResource( 6 )
 shape = SubResource( 6 )
 
 
 [node name="CollisionShape3" type="CollisionShape" parent="StaticBodyWalls"]
 [node name="CollisionShape3" type="CollisionShape" parent="StaticBodyWalls"]
-transform = Transform( 2.65431e-12, 1.62921e-07, 100, 100, -1.62921e-07, 0, 1.62921e-05, 1, -1.62921e-05, 0, 0, -5 )
+transform = Transform( 2.6543e-14, 1.62921e-07, 1, 1, -1.62921e-07, 1.56125e-19, 1.62921e-07, 1, -1.62921e-07, 0, 0, -50 )
 shape = SubResource( 6 )
 shape = SubResource( 6 )
 
 
 [node name="CollisionShape4" type="CollisionShape" parent="StaticBodyWalls"]
 [node name="CollisionShape4" type="CollisionShape" parent="StaticBodyWalls"]
-transform = Transform( 2.65431e-12, 1.62921e-07, -100, 100, -1.62921e-07, 0, -1.62921e-05, -1, -1.62921e-05, 0, 0, 5 )
+transform = Transform( 2.6543e-14, 1.62921e-07, -1, 1, -1.62921e-07, -1.56125e-19, -1.62921e-07, -1, -1.62921e-07, 0, 0, 50 )
 shape = SubResource( 6 )
 shape = SubResource( 6 )
 
 
-[node name="StaticScene" parent="." instance=ExtResource( 5 )]
-
 [node name="Camera" type="Camera" parent="."]
 [node name="Camera" type="Camera" parent="."]
 transform = Transform( 1, 0, 0, 0, 0.881757, 0.471705, 0, -0.471705, 0.881757, 0, 20.4125, 41.0426 )
 transform = Transform( 1, 0, 0, 0, 0.881757, 0.471705, 0, -0.471705, 0.881757, 0, 20.4125, 41.0426 )
 script = ExtResource( 3 )
 script = ExtResource( 3 )

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

@@ -13,7 +13,7 @@ func add_menu_item(item_path, checkbox = false, checked = false):
 
 
 	var path = ""
 	var path = ""
 	var popup = get_popup()
 	var popup = get_popup()
-	for element_index in path_element_count - 1:
+	for element_index in range(path_element_count - 1):
 		var popup_label = path_elements[element_index]
 		var popup_label = path_elements[element_index]
 		path += popup_label + "/"
 		path += popup_label + "/"
 		popup = _add_popup(popup, path, popup_label)
 		popup = _add_popup(popup, path, popup_label)