Przeglądaj źródła

Fixes and adjustments in 3D physics tests

Add Functional Test / Stack & Pyramid
For testing stack stability.

Add Functional Test / Raycasts
Visually test raycast on different shapes.

Add Performance Test / Broadphase
Add/move/remove lots of non-colliding objects and measure time.

Fix leaks on exit
Some Nodes are copied and removed from the scene to be used as templates,
they need to be freed manually on exit.

Fix Performance Test / Contacts
Positions adjusted, some shape types were not created at the center.
PouleyKetchoupp 4 lat temu
rodzic
commit
6dd09308fa

+ 2 - 2
3d/physics_tests/project.godot

@@ -26,7 +26,7 @@ _global_script_class_icons={
 
 [application]
 
-config/name="Physics Tests"
+config/name="3D Physics Tests"
 run/main_scene="res://main.tscn"
 config/icon="res://icon.png"
 
@@ -71,5 +71,5 @@ restart_test={
 [rendering]
 
 quality/driver/driver_name="GLES2"
-environment/default_clear_color=Color( 0, 0, 0, 1 )
+environment/default_clear_color=Color( 0.184314, 0.184314, 0.184314, 1 )
 quality/filters/msaa=2

+ 28 - 0
3d/physics_tests/test.gd

@@ -1,10 +1,33 @@
 class_name Test
 extends Node
 
+signal wait_done()
 
 var _timer
 var _timer_started = false
 
+var _wait_physics_ticks_counter = 0
+
+
+func _physics_process(_delta):
+	if (_wait_physics_ticks_counter > 0):
+		_wait_physics_ticks_counter -= 1
+		if (_wait_physics_ticks_counter == 0):
+			emit_signal("wait_done")
+
+
+func create_rigidbody_box(size):
+	var template_shape = BoxShape.new()
+	template_shape.extents = 0.5 * size
+
+	var template_collision = CollisionShape.new()
+	template_collision.shape = template_shape
+
+	var template_body = RigidBody.new()
+	template_body.add_child(template_collision)
+
+	return template_body
+
 
 func start_timer(timeout):
 	if _timer == null:
@@ -32,5 +55,10 @@ func is_timer_canceled():
 	return _timer.paused
 
 
+func wait_for_physics_ticks(tick_count):
+	_wait_physics_ticks_counter = tick_count
+	return self
+
+
 func _on_timer_done():
 	_timer_started = false

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

@@ -14,6 +14,22 @@ var _tests = [
 		"id": "Functional Tests/Friction",
 		"path": "res://tests/functional/test_friction.tscn",
 	},
+	{
+		"id": "Functional Tests/Box Stack",
+		"path": "res://tests/functional/test_stack.tscn",
+	},
+	{
+		"id": "Functional Tests/Box Pyramid",
+		"path": "res://tests/functional/test_pyramid.tscn",
+	},
+	{
+		"id": "Functional Tests/Raycasting",
+		"path": "res://tests/functional/test_raycasting.tscn",
+	},
+	{
+		"id": "Performance Tests/Broadphase",
+		"path": "res://tests/performance/test_perf_broadphase.tscn",
+	},
 	{
 		"id": "Performance Tests/Contacts",
 		"path": "res://tests/performance/test_perf_contacts.tscn",

+ 58 - 0
3d/physics_tests/tests/functional/test_pyramid.gd

@@ -0,0 +1,58 @@
+extends Test
+
+
+export(int, 1, 100) var height = 10
+export(int, 1, 100) var width_max = 100
+export(int, 1, 100) var depth_max = 1
+export(Vector3) var box_size = Vector3(1.0, 1.0, 1.0)
+export(Vector3) var box_spacing = Vector3(0.0, 0.0, 0.0)
+
+
+func _ready():
+	_create_pyramid()
+
+
+func _create_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 pos_y = 0.5 * box_size.y + box_spacing.y
+
+	for level in height:
+		var level_index = height - level - 1
+		var num_boxes = 2 * level_index + 1
+		var num_boxes_width = min(num_boxes, width_max)
+		var num_boxes_depth = min(num_boxes, depth_max)
+
+		var row_node = Spatial.new()
+		row_node.transform.origin = Vector3(0.0, pos_y, 0.0)
+		row_node.name = "Row%02d" % (level + 1)
+		root_node.add_child(row_node)
+
+		var pos_x = -0.5 * (num_boxes_width - 1) * (box_size.x + box_spacing.x)
+
+		for box_index_x in num_boxes_width:
+			var pos_z = -0.5 * (num_boxes_depth - 1) * (box_size.z + box_spacing.z)
+
+			for box_index_z in num_boxes_depth:
+				var box_index = box_index_x * box_index_z
+				var box = template_body.duplicate()
+				box.transform.origin = Vector3(pos_x, 0.0, pos_z)
+				box.name = "Box%02d" % (box_index + 1)
+				row_node.add_child(box)
+
+				pos_z += box_size.z + box_spacing.z
+
+			pos_x += box_size.x + box_spacing.x
+
+		pos_y += box_size.y + box_spacing.y
+
+	template_body.queue_free()

+ 16 - 0
3d/physics_tests/tests/functional/test_pyramid.tscn

@@ -0,0 +1,16 @@
+[gd_scene load_steps=4 format=2]
+
+[ext_resource path="res://tests/functional/test_pyramid.gd" type="Script" id=1]
+[ext_resource path="res://tests/static_scene_plane.tscn" type="PackedScene" id=2]
+[ext_resource path="res://utils/camera_orbit.gd" type="Script" id=4]
+
+[node name="Test" type="Spatial"]
+script = ExtResource( 1 )
+
+[node name="Pyramid" type="Spatial" parent="."]
+
+[node name="StaticBodyPlane" parent="." instance=ExtResource( 2 )]
+
+[node name="Camera" type="Camera" parent="."]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6.62348, 22.9474 )
+script = ExtResource( 4 )

+ 78 - 0
3d/physics_tests/tests/functional/test_raycasting.gd

@@ -0,0 +1,78 @@
+extends Test
+
+
+var _do_raycasts = false
+
+onready var _raycast_visuals = ImmediateGeometry.new()
+
+
+func _ready():
+	var material = SpatialMaterial.new()
+	material.flags_unshaded = true
+	material.vertex_color_use_as_albedo = true
+	_raycast_visuals.material_override = material
+
+	add_child(_raycast_visuals)
+	move_child(_raycast_visuals, get_child_count())
+
+	yield(start_timer(0.5), "timeout")
+	if is_timer_canceled():
+		return
+
+	_do_raycasts = true
+
+
+func _physics_process(_delta):
+	if !_do_raycasts:
+		return
+
+	_do_raycasts = false
+
+	Log.print_log("* Start Raycasting...")
+
+	_raycast_visuals.clear()
+	_raycast_visuals.begin(Mesh.PRIMITIVE_LINES)
+
+	for shape in $Shapes.get_children():
+		var body = shape as PhysicsBody
+		var space_state = body.get_world().direct_space_state
+
+		Log.print_log("* Testing: %s" % body.name)
+
+		var center = body.global_transform.origin
+
+		# Raycast entering from the top.
+		var res = _add_raycast(space_state, center + Vector3(0.0, 2.0, 0.0), center)
+		Log.print_log("Raycast in: %s" % ("HIT" if res else "NO HIT"))
+
+		# Raycast exiting from inside.
+		center.x -= 0.2
+		res = _add_raycast(space_state, center, center - Vector3(0.0, 3.0, 0.0))
+		Log.print_log("Raycast out: %s" % ("HIT" if res else "NO HIT"))
+
+		# Raycast all inside.
+		center.x += 0.4
+		res = _add_raycast(space_state, center, center - Vector3(0.0, 0.8, 0.0))
+		Log.print_log("Raycast inside: %s" % ("HIT" if res else "NO HIT"))
+
+	_raycast_visuals.end()
+
+
+func _add_raycast(space_state, pos_start, pos_end):
+	var result = space_state.intersect_ray(pos_start, pos_end)
+	if result:
+		_raycast_visuals.set_color(Color.green)
+	else:
+		_raycast_visuals.set_color(Color.red.darkened(0.5))
+
+	# Draw raycast line.
+	_raycast_visuals.add_vertex(pos_start)
+	_raycast_visuals.add_vertex(pos_end)
+
+	# Draw raycast arrow.
+	_raycast_visuals.add_vertex(pos_end)
+	_raycast_visuals.add_vertex(pos_end + Vector3(-0.05, 0.1, 0.0))
+	_raycast_visuals.add_vertex(pos_end)
+	_raycast_visuals.add_vertex(pos_end + Vector3(0.05, 0.1, 0.0))
+
+	return result

+ 74 - 0
3d/physics_tests/tests/functional/test_raycasting.tscn

@@ -0,0 +1,74 @@
+[gd_scene load_steps=10 format=2]
+
+[ext_resource path="res://assets/robot_head/godot3_robot_head_collision.tres" type="Shape" id=1]
+[ext_resource path="res://tests/functional/test_raycasting.gd" type="Script" id=2]
+[ext_resource path="res://utils/exception_cylinder.gd" type="Script" id=3]
+[ext_resource path="res://utils/camera_orbit.gd" type="Script" id=4]
+
+[sub_resource type="BoxShape" id=1]
+
+[sub_resource type="CapsuleShape" id=2]
+
+[sub_resource type="CylinderShape" id=3]
+
+[sub_resource type="ConvexPolygonShape" id=4]
+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]
+
+[node name="Test" type="Spatial"]
+script = ExtResource( 2 )
+
+[node name="Shapes" type="Spatial" parent="."]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 9.35591, 0 )
+
+[node name="RigidBodyBox" type="RigidBody" parent="Shapes"]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0, 0 )
+mode = 3
+
+[node name="CollisionShape" type="CollisionShape" parent="Shapes/RigidBodyBox"]
+transform = Transform( 0.579556, 0.0885213, 0.145926, 0, 0.939693, -0.205212, -0.155291, 0.330366, 0.544604, 0, 0, 0 )
+shape = SubResource( 1 )
+
+[node name="RigidBodyCapsule" type="RigidBody" parent="Shapes"]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 0 )
+mode = 3
+
+[node name="CollisionShape" type="CollisionShape" parent="Shapes/RigidBodyCapsule"]
+transform = Transform( 0.8, 0, 0, 0, -1.30337e-07, -0.8, 0, 0.8, -1.30337e-07, 0, 0, 0 )
+shape = SubResource( 2 )
+
+[node name="RigidBodyCylinder" type="RigidBody" parent="Shapes"]
+mode = 3
+script = ExtResource( 3 )
+
+[node name="CollisionShape" type="CollisionShape" parent="Shapes/RigidBodyCylinder"]
+transform = Transform( 0.772741, -0.258819, 2.59821e-08, 0.2, 0.933013, -0.207055, 0.0535898, 0.25, 0.772741, 0, 0, 0 )
+shape = SubResource( 3 )
+
+[node name="RigidBodyConvex" type="RigidBody" parent="Shapes"]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 3, -0.210678, 0 )
+mode = 3
+
+[node name="CollisionShape" type="CollisionShape" parent="Shapes/RigidBodyConvex"]
+transform = Transform( 2, 0, 0, 0, 2.89766, -0.517939, 0, 0.776908, 1.93177, 0, 0.3533, 0 )
+shape = SubResource( 4 )
+
+[node name="RigidBodySphere" type="RigidBody" parent="Shapes"]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 6, 0, 0 )
+mode = 3
+
+[node name="CollisionShape" type="CollisionShape" parent="Shapes/RigidBodySphere"]
+transform = Transform( 1.2, 0, 0, 0, 1.2, 0, 0, 0, 1.2, 0, 0, 0 )
+shape = SubResource( 5 )
+
+[node name="StaticBodyHead" type="StaticBody" parent="Shapes"]
+transform = Transform( 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, -6, 3.93357 )
+
+[node name="CollisionShape" type="CollisionShape" parent="Shapes/StaticBodyHead"]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0 )
+shape = ExtResource( 1 )
+
+[node name="Camera" type="Camera" parent="."]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5.8667, 11.8164 )
+script = ExtResource( 4 )

+ 53 - 0
3d/physics_tests/tests/functional/test_stack.gd

@@ -0,0 +1,53 @@
+extends Test
+
+
+export(int, 1, 100) var height = 10
+export(int, 1, 100) var width = 1
+export(int, 1, 100) var depth = 1
+export(Vector3) var box_size = Vector3(1.0, 1.0, 1.0)
+export(Vector3) var box_spacing = Vector3(0.0, 0.0, 0.0)
+
+
+func _ready():
+	_create_stack()
+
+
+func _create_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 pos_y = 0.5 * box_size.y + box_spacing.y
+
+	for level in height:
+		var row_node = Spatial.new()
+		row_node.transform.origin = Vector3(0.0, pos_y, 0.0)
+		row_node.name = "Row%02d" % (level + 1)
+		root_node.add_child(row_node)
+
+		var pos_x = -0.5 * (width - 1) * (box_size.x + box_spacing.x)
+
+		for box_index_x in width:
+			var pos_z = -0.5 * (depth - 1) * (box_size.z + box_spacing.z)
+
+			for box_index_z in depth:
+				var box_index = box_index_x * box_index_z
+				var box = template_body.duplicate()
+				box.transform.origin = Vector3(pos_x, 0.0, pos_z)
+				box.name = "Box%02d" % (box_index + 1)
+				row_node.add_child(box)
+
+				pos_z += box_size.z + box_spacing.z
+
+			pos_x += box_size.x + box_spacing.x
+
+		pos_y += box_size.y + box_spacing.y
+
+	template_body.queue_free()

+ 16 - 0
3d/physics_tests/tests/functional/test_stack.tscn

@@ -0,0 +1,16 @@
+[gd_scene load_steps=4 format=2]
+
+[ext_resource path="res://tests/functional/test_stack.gd" type="Script" id=1]
+[ext_resource path="res://tests/static_scene_plane.tscn" type="PackedScene" id=2]
+[ext_resource path="res://utils/camera_orbit.gd" type="Script" id=4]
+
+[node name="Test" type="Spatial"]
+script = ExtResource( 1 )
+
+[node name="Stack" type="Spatial" parent="."]
+
+[node name="StaticBodyPlane" parent="." instance=ExtResource( 2 )]
+
+[node name="Camera" type="Camera" parent="."]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.53602, 12.2684 )
+script = ExtResource( 4 )

+ 155 - 0
3d/physics_tests/tests/performance/test_perf_broadphase.gd

@@ -0,0 +1,155 @@
+extends Test
+
+
+const BOX_SIZE = Vector3(0.8, 0.8, 0.8)
+const BOX_SPACE = Vector3(1.0, 1.0, 1.0)
+
+export(int, 1, 1000) var row_size = 20
+export(int, 1, 1000) var column_size = 20
+export(int, 1, 1000) var depth_size = 20
+
+var _objects = []
+
+var _log_physics = false
+var _log_physics_time = 0
+var _log_physics_time_start = 0
+
+
+func _ready():
+	_create_objects()
+
+	_log_physics_start()
+	yield(wait_for_physics_ticks(5), "wait_done")
+	_log_physics_stop()
+
+	yield(start_timer(1.0), "timeout")
+	if is_timer_canceled():
+		return
+
+	_add_objects()
+
+	_log_physics_start()
+	yield(wait_for_physics_ticks(5), "wait_done")
+	_log_physics_stop()
+
+	yield(start_timer(1.0), "timeout")
+	if is_timer_canceled():
+		return
+
+	_move_objects()
+
+	_log_physics_start()
+	yield(wait_for_physics_ticks(5), "wait_done")
+	_log_physics_stop()
+
+	yield(start_timer(1.0), "timeout")
+	if is_timer_canceled():
+		return
+
+	_remove_objects()
+
+	_log_physics_start()
+	yield(wait_for_physics_ticks(5), "wait_done")
+	_log_physics_stop()
+
+	yield(start_timer(1.0), "timeout")
+	if is_timer_canceled():
+		return
+
+	Log.print_log("* Done.")
+
+
+func _exit_tree():
+	for object in _objects:
+		object.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 _create_objects():
+	_objects.clear()
+
+	var template_body = create_rigidbody_box(BOX_SIZE)
+	template_body.gravity_scale = 0.0
+
+	Log.print_log("* Creating objects...")
+	var timer = OS.get_ticks_usec()
+
+	var pos_x = -0.5 * (row_size - 1) * BOX_SPACE.x
+
+	for row in row_size:
+		var pos_y = -0.5 * (column_size - 1) * BOX_SPACE.y
+
+		for column in column_size:
+			var pos_z = -0.5 * (depth_size - 1) * BOX_SPACE.z
+
+			for depth in depth_size:
+				var box = template_body.duplicate()
+				box.transform.origin = Vector3(pos_x, pos_y, pos_z)
+				box.name = "Box%03d" % (row * column + 1)
+				_objects.push_back(box)
+
+				pos_z += BOX_SPACE.z
+
+			pos_y += BOX_SPACE.y
+
+		pos_x += BOX_SPACE.x
+
+	timer = OS.get_ticks_usec() - timer
+	Log.print_log("  Create Time: %.3f ms" % (0.001 * timer))
+
+	template_body.queue_free()
+
+
+func _add_objects():
+	var root_node = $Objects
+
+	Log.print_log("* Adding objects...")
+	var timer = OS.get_ticks_usec()
+
+	for object in _objects:
+		root_node.add_child(object)
+
+	timer = OS.get_ticks_usec() - timer
+	Log.print_log("  Add Time: %.3f ms" % (0.001 * timer))
+
+
+func _move_objects():
+	Log.print_log("* Moving objects...")
+	var timer = OS.get_ticks_usec()
+
+	for object in _objects:
+		object.transform.origin += BOX_SPACE
+
+	timer = OS.get_ticks_usec() - timer
+	Log.print_log("  Move Time: %.3f ms" % (0.001 * timer))
+
+
+func _remove_objects():
+	var root_node = $Objects
+
+	Log.print_log("* Removing objects...")
+	var timer = OS.get_ticks_usec()
+
+	for object in _objects:
+		root_node.remove_child(object)
+
+	timer = OS.get_ticks_usec() - timer
+	Log.print_log("  Remove Time: %.3f ms" % (0.001 * timer))

+ 13 - 0
3d/physics_tests/tests/performance/test_perf_broadphase.tscn

@@ -0,0 +1,13 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://tests/performance/test_perf_broadphase.gd" type="Script" id=1]
+[ext_resource path="res://utils/camera_orbit.gd" type="Script" id=5]
+
+[node name="Test" type="Spatial"]
+script = ExtResource( 1 )
+
+[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, 29.8407 )
+script = ExtResource( 5 )

+ 9 - 3
3d/physics_tests/tests/performance/test_perf_contacts.gd

@@ -10,7 +10,7 @@ const OPTION_TYPE_SPHERE = "Shape type/Sphere"
 export(Array) var spawns = Array()
 
 export(int) var spawn_count = 100
-export(int, 1, 10) var spawn_multipiler = 5
+export(int, 1, 10) var spawn_multiplier = 5
 
 var _object_templates = []
 
@@ -36,6 +36,11 @@ func _ready():
 	_start_all_types()
 
 
+func _exit_tree():
+	for object_template in _object_templates:
+		object_template.free()
+
+
 func _on_option_selected(option):
 	cancel_timer()
 
@@ -123,9 +128,10 @@ func _spawn_objects(type_index):
 
 		Log.print_log("* Spawning: " + template_node.name)
 
-		for _index in range(spawn_multipiler):
-			for _node_index in spawn_count / spawn_multipiler:
+		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)
 
 

+ 3 - 16
3d/physics_tests/tests/static_scene.tscn

@@ -1,25 +1,12 @@
-[gd_scene load_steps=5 format=2]
+[gd_scene load_steps=4 format=2]
 
 [ext_resource path="res://assets/robot_head/godot3_robot_head_collision.tres" type="Shape" id=1]
 [ext_resource path="res://assets/robot_head/godot3_robot_head.mesh" type="ArrayMesh" id=2]
-
-[sub_resource type="PlaneMesh" id=1]
-
-[sub_resource type="ConcavePolygonShape" id=2]
-data = PoolVector3Array( -1, 0, 1, 1, 0, -1, 1, 0, 1, -1, 0, 1, -1, 0, -1, 1, 0, -1 )
+[ext_resource path="res://tests/static_scene_plane.tscn" type="PackedScene" id=3]
 
 [node name="StaticScene" type="Spatial"]
 
-[node name="StaticBodyPlane" type="StaticBody" parent="."]
-
-[node name="MeshInstance" type="MeshInstance" parent="StaticBodyPlane"]
-transform = Transform( 50, 0, 0, 0, 1, 0, 0, 0, 50, 0, 0, 0 )
-mesh = SubResource( 1 )
-material/0 = null
-
-[node name="CollisionShape" type="CollisionShape" parent="StaticBodyPlane"]
-transform = Transform( 50, 0, 0, 0, 1, 0, 0, 0, 50, 0, 0, 0 )
-shape = SubResource( 2 )
+[node name="StaticBodyPlane" parent="." instance=ExtResource( 3 )]
 
 [node name="StaticBodyHead" type="StaticBody" parent="."]
 transform = Transform( 10, 0, 0, 0, 8.66025, 5, 0, -5, 8.66025, 0, -11.1389, 2.29332 )

+ 17 - 0
3d/physics_tests/tests/static_scene_plane.tscn

@@ -0,0 +1,17 @@
+[gd_scene load_steps=3 format=2]
+
+[sub_resource type="PlaneMesh" id=1]
+
+[sub_resource type="ConcavePolygonShape" id=2]
+data = PoolVector3Array( -1, 0, 1, 1, 0, -1, 1, 0, 1, -1, 0, 1, -1, 0, -1, 1, 0, -1 )
+
+[node name="StaticBodyPlane" type="StaticBody"]
+
+[node name="MeshInstance" type="MeshInstance" parent="."]
+transform = Transform( 50, 0, 0, 0, 1, 0, 0, 0, 50, 0, 0, 0 )
+mesh = SubResource( 1 )
+material/0 = null
+
+[node name="CollisionShape" type="CollisionShape" parent="."]
+transform = Transform( 50, 0, 0, 0, 1, 0, 0, 0, 50, 0, 0, 0 )
+shape = SubResource( 2 )

+ 4 - 0
3d/physics_tests/utils/container_log.gd

@@ -13,6 +13,10 @@ func _enter_tree():
 	remove_child(_entry_template)
 
 
+func _exit_tree():
+	_entry_template.free()
+
+
 func clear():
 	while get_child_count():
 		var entry = get_child(get_child_count() - 1)