Browse Source

Add demo: Physics Tests 2D

Similar to its 3D counterpart, with tests using 2D physics.
PouleyKetchoupp 4 years ago
parent
commit
8241be5817
37 changed files with 1465 additions and 0 deletions
  1. 18 0
      2d/physics_tests/README.md
  2. BIN
      2d/physics_tests/assets/godot-head.png
  3. 34 0
      2d/physics_tests/assets/godot-head.png.import
  4. BIN
      2d/physics_tests/icon.png
  5. 34 0
      2d/physics_tests/icon.png.import
  6. 174 0
      2d/physics_tests/main.tscn
  7. 75 0
      2d/physics_tests/project.godot
  8. 0 0
      2d/physics_tests/screenshots/.gdignore
  9. BIN
      2d/physics_tests/screenshots/screenshot.png
  10. 92 0
      2d/physics_tests/test.gd
  11. 35 0
      2d/physics_tests/tests.gd
  12. 10 0
      2d/physics_tests/tests/dynamic_box.tscn
  13. 41 0
      2d/physics_tests/tests/functional/test_pyramid.gd
  14. 12 0
      2d/physics_tests/tests/functional/test_pyramid.tscn
  15. 69 0
      2d/physics_tests/tests/functional/test_raycasting.gd
  16. 68 0
      2d/physics_tests/tests/functional/test_raycasting.tscn
  17. 64 0
      2d/physics_tests/tests/functional/test_shapes.tscn
  18. 39 0
      2d/physics_tests/tests/functional/test_stack.gd
  19. 12 0
      2d/physics_tests/tests/functional/test_stack.tscn
  20. 149 0
      2d/physics_tests/tests/performance/test_perf_broadphase.gd
  21. 9 0
      2d/physics_tests/tests/performance/test_perf_broadphase.tscn
  22. 161 0
      2d/physics_tests/tests/performance/test_perf_contacts.gd
  23. 70 0
      2d/physics_tests/tests/performance/test_perf_contacts.tscn
  24. 10 0
      2d/physics_tests/tests/static_scene.tscn
  25. 12 0
      2d/physics_tests/tests/static_scene_flat.tscn
  26. 16 0
      2d/physics_tests/tests/test_options.tscn
  27. 54 0
      2d/physics_tests/tests_menu.gd
  28. 41 0
      2d/physics_tests/utils/container_log.gd
  29. 12 0
      2d/physics_tests/utils/label_engine.gd
  30. 5 0
      2d/physics_tests/utils/label_fps.gd
  31. 13 0
      2d/physics_tests/utils/label_test.gd
  32. 5 0
      2d/physics_tests/utils/label_version.gd
  33. 47 0
      2d/physics_tests/utils/option_menu.gd
  34. 14 0
      2d/physics_tests/utils/scroll_log.gd
  35. 50 0
      2d/physics_tests/utils/system.gd
  36. 20 0
      2d/physics_tests/utils/system_log.gd
  37. 0 0
      3d/physics_tests/tests/functional/test_raycasting.gd

+ 18 - 0
2d/physics_tests/README.md

@@ -0,0 +1,18 @@
+# 2D Physics Tests
+
+This demo contains a series of tests for the 2D
+physics engine.
+
+They can be used for different purpose:
+- Functional tests to check for regressions and
+  behavior of the 2D physics engine
+- Performance tests to evaluate performance
+  of the 2D physics engine
+
+Language: GDScript
+
+Renderer: GLES 2
+
+## Screenshots
+
+![Screenshot](screenshots/screenshot.png)

BIN
2d/physics_tests/assets/godot-head.png


+ 34 - 0
2d/physics_tests/assets/godot-head.png.import

@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/godot-head.png-cc6844293d74c4f45ec6c4ab08de67ee.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/godot-head.png"
+dest_files=[ "res://.import/godot-head.png-cc6844293d74c4f45ec6c4ab08de67ee.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0

BIN
2d/physics_tests/icon.png


+ 34 - 0
2d/physics_tests/icon.png.import

@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.png"
+dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0

+ 174 - 0
2d/physics_tests/main.tscn

@@ -0,0 +1,174 @@
+[gd_scene load_steps=10 format=2]
+
+[ext_resource path="res://utils/label_fps.gd" type="Script" id=1]
+[ext_resource path="res://utils/label_version.gd" type="Script" id=2]
+[ext_resource path="res://utils/label_engine.gd" type="Script" id=3]
+[ext_resource path="res://tests_menu.gd" type="Script" id=4]
+[ext_resource path="res://utils/label_test.gd" type="Script" id=5]
+[ext_resource path="res://utils/container_log.gd" type="Script" id=10]
+[ext_resource path="res://utils/scroll_log.gd" type="Script" id=11]
+[ext_resource path="res://tests.gd" type="Script" id=12]
+
+[sub_resource type="StyleBoxFlat" id=1]
+bg_color = Color( 0, 0, 0, 0.176471 )
+
+[node name="Main" type="Control"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+mouse_filter = 2
+script = ExtResource( 12 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="TestsMenu" type="MenuButton" parent="."]
+margin_left = 10.0
+margin_top = 10.0
+margin_right = 125.0
+margin_bottom = 30.0
+text = "TESTS"
+flat = false
+script = ExtResource( 4 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="LabelControls" type="Label" parent="."]
+margin_left = 157.0
+margin_top = 13.0
+margin_right = 646.0
+margin_bottom = 27.0
+text = "R - RESTART / D - TOGGLE COLLISION / F - TOGGLE FULL SCREEN / ESC - QUIT"
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="LabelFPS" type="Label" parent="."]
+anchor_top = 1.0
+anchor_bottom = 1.0
+margin_left = 10.0
+margin_top = -19.0
+margin_right = 50.0
+margin_bottom = -5.0
+text = "FPS: 0"
+script = ExtResource( 1 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="LabelEngine" type="Label" parent="."]
+anchor_top = 1.0
+anchor_bottom = 1.0
+margin_left = 10.0
+margin_top = -39.0
+margin_right = 50.0
+margin_bottom = -25.0
+text = "Physics engine:"
+script = ExtResource( 3 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="LabelVersion" type="Label" parent="."]
+anchor_top = 1.0
+anchor_bottom = 1.0
+margin_left = 10.0
+margin_top = -59.0
+margin_right = 50.0
+margin_bottom = -45.0
+text = "Godot Version:"
+script = ExtResource( 2 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="LabelTest" type="Label" parent="."]
+anchor_top = 1.0
+anchor_bottom = 1.0
+margin_left = 10.0
+margin_top = -79.0
+margin_right = 50.0
+margin_bottom = -65.0
+text = "Test:"
+script = ExtResource( 5 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="PanelLog" type="Panel" parent="."]
+anchor_left = 1.0
+anchor_top = 1.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+margin_left = -428.0
+margin_top = -125.0
+custom_styles/panel = SubResource( 1 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="ButtonClear" type="Button" parent="PanelLog"]
+anchor_left = 1.0
+anchor_top = 1.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+margin_left = -48.0
+margin_top = -25.0
+margin_right = -5.0
+margin_bottom = -5.0
+focus_mode = 0
+enabled_focus_mode = 0
+text = "clear"
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="CheckBoxScroll" type="CheckBox" parent="PanelLog"]
+anchor_left = 1.0
+anchor_top = 1.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+margin_left = -150.0
+margin_top = -27.0
+margin_right = -54.0
+margin_bottom = -3.0
+focus_mode = 0
+pressed = true
+enabled_focus_mode = 0
+text = "auto-scroll"
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="ScrollLog" type="ScrollContainer" parent="PanelLog"]
+margin_left = 10.0
+margin_top = 5.0
+margin_right = 418.0
+margin_bottom = 94.0
+scroll_horizontal_enabled = false
+script = ExtResource( 11 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+auto_scroll = true
+
+[node name="VBoxLog" type="VBoxContainer" parent="PanelLog/ScrollLog"]
+margin_right = 408.0
+margin_bottom = 89.0
+size_flags_horizontal = 3
+size_flags_vertical = 3
+alignment = 2
+script = ExtResource( 10 )
+
+[node name="LabelLog" type="Label" parent="PanelLog/ScrollLog/VBoxLog"]
+margin_top = 75.0
+margin_right = 408.0
+margin_bottom = 89.0
+text = "Log start"
+valign = 2
+max_lines_visible = 5
+__meta__ = {
+"_edit_use_anchors_": false
+}
+[connection signal="pressed" from="PanelLog/ButtonClear" to="PanelLog/ScrollLog/VBoxLog" method="clear"]
+[connection signal="toggled" from="PanelLog/CheckBoxScroll" to="PanelLog/ScrollLog" method="set_auto_scroll"]

+ 75 - 0
2d/physics_tests/project.godot

@@ -0,0 +1,75 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+;   [section] ; section goes between []
+;   param=value ; assign values to parameters
+
+config_version=4
+
+_global_script_classes=[ {
+"base": "MenuButton",
+"class": "OptionMenu",
+"language": "GDScript",
+"path": "res://utils/option_menu.gd"
+}, {
+"base": "Node2D",
+"class": "Test",
+"language": "GDScript",
+"path": "res://test.gd"
+} ]
+_global_script_class_icons={
+"OptionMenu": "",
+"Test": ""
+}
+
+[application]
+
+config/name="2D Physics Tests"
+run/main_scene="res://main.tscn"
+config/icon="res://icon.png"
+
+[autoload]
+
+Log="*res://utils/system_log.gd"
+System="*res://utils/system.gd"
+
+[debug]
+
+gdscript/warnings/return_value_discarded=false
+
+[display]
+
+window/dpi/allow_hidpi=true
+window/stretch/mode="2d"
+window/stretch/aspect="expand"
+
+[input]
+
+toggle_full_screen={
+"deadzone": 0.5,
+"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":70,"unicode":0,"echo":false,"script":null)
+ ]
+}
+exit={
+"deadzone": 0.5,
+"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777217,"unicode":0,"echo":false,"script":null)
+ ]
+}
+toggle_debug_collision={
+"deadzone": 0.5,
+"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":68,"unicode":0,"echo":false,"script":null)
+ ]
+}
+restart_test={
+"deadzone": 0.5,
+"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":82,"unicode":0,"echo":false,"script":null)
+ ]
+}
+
+[rendering]
+
+quality/driver/driver_name="GLES2"
+environment/default_clear_color=Color( 0.184314, 0.184314, 0.184314, 1 )
+quality/filters/msaa=2

+ 0 - 0
2d/physics_tests/screenshots/.gdignore


BIN
2d/physics_tests/screenshots/screenshot.png


+ 92 - 0
2d/physics_tests/test.gd

@@ -0,0 +1,92 @@
+class_name Test
+extends Node2D
+
+
+signal wait_done()
+
+var _timer
+var _timer_started = false
+
+var _wait_physics_ticks_counter = 0
+
+
+class Line:
+	var pos_start
+	var pos_end
+	var color
+
+var _lines = []
+
+
+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 _draw():
+	for line in _lines:
+		draw_line(line.pos_start, line.pos_end, line.color, 1.5)
+
+
+func add_line(pos_start, pos_end, color):
+	var line = Line.new()
+	line.pos_start = pos_start
+	line.pos_end = pos_end
+	line.color = color
+	_lines.push_back(line)
+	update()
+
+
+func clear_lines():
+	_lines.clear()
+	update()
+
+
+func create_rigidbody_box(size):
+	var template_shape = RectangleShape2D.new()
+	template_shape.extents = 0.5 * size
+
+	var template_collision = CollisionShape2D.new()
+	template_collision.shape = template_shape
+
+	var template_body = RigidBody2D.new()
+	template_body.add_child(template_collision)
+
+	return template_body
+
+
+func start_timer(timeout):
+	if _timer == null:
+		_timer = Timer.new()
+		_timer.one_shot = true
+		add_child(_timer)
+		_timer.connect("timeout", self, "_on_timer_done")
+	else:
+		cancel_timer()
+
+	_timer.start(timeout)
+	_timer_started = true
+
+	return _timer
+
+
+func cancel_timer():
+	if _timer_started:
+		_timer.paused = true
+		_timer.emit_signal("timeout")
+		_timer.paused = false
+
+
+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

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

@@ -0,0 +1,35 @@
+extends Node
+
+
+var _tests = [
+	{
+		"id": "Functional Tests/Shapes",
+		"path": "res://tests/functional/test_shapes.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",
+	},
+]
+
+
+func _ready():
+	var test_menu = $TestsMenu
+	for test in _tests:
+		test_menu.add_test(test.id, test.path)

+ 10 - 0
2d/physics_tests/tests/dynamic_box.tscn

@@ -0,0 +1,10 @@
+[gd_scene load_steps=2 format=2]
+
+[sub_resource type="RectangleShape2D" id=1]
+extents = Vector2( 20, 20 )
+
+[node name="StackBox" type="RigidBody2D"]
+position = Vector2( -180, -20 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
+shape = SubResource( 1 )

+ 41 - 0
2d/physics_tests/tests/functional/test_pyramid.gd

@@ -0,0 +1,41 @@
+extends Test
+
+
+export(int, 1, 100) var height = 10
+export(Vector2) var box_size = Vector2(40.0, 40.0)
+export(Vector2) var box_spacing =  Vector2(0.0, 0.0)
+
+
+func _ready():
+	_create_pyramid()
+
+
+func _create_pyramid():
+	var root_node = $Pyramid
+
+	var template_body = create_rigidbody_box(box_size)
+
+	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 row_node = Node2D.new()
+		row_node.position = Vector2(0.0, pos_y)
+		row_node.name = "Row%02d" % (level + 1)
+		root_node.add_child(row_node)
+
+		var pos_x = -0.5 * (num_boxes - 1) * (box_size.x + box_spacing.x)
+
+		for box_index in num_boxes:
+			var box = template_body.duplicate()
+			box.position = Vector2(pos_x, 0.0)
+			box.name = "Box%02d" % (box_index + 1)
+			row_node.add_child(box)
+
+			pos_x += box_size.x + box_spacing.x
+
+		pos_y -= box_size.y + box_spacing.y
+
+	template_body.queue_free()

+ 12 - 0
2d/physics_tests/tests/functional/test_pyramid.tscn

@@ -0,0 +1,12 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://tests/functional/test_pyramid.gd" type="Script" id=1]
+[ext_resource path="res://tests/static_scene_flat.tscn" type="PackedScene" id=2]
+
+[node name="Test" type="Node2D"]
+script = ExtResource( 1 )
+
+[node name="Pyramid" type="Node2D" parent="."]
+position = Vector2( 512, 500 )
+
+[node name="StaticSceneFlat" parent="." instance=ExtResource( 2 )]

+ 69 - 0
2d/physics_tests/tests/functional/test_raycasting.gd

@@ -0,0 +1,69 @@
+extends Test
+
+
+var _do_raycasts = false
+
+
+func _ready():
+	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...")
+
+	clear_lines()
+
+	for shape in $Shapes.get_children():
+		var body = shape as PhysicsBody2D
+		var space_state = body.get_world_2d().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 - Vector2(0, 100), center)
+		Log.print_log("Raycast in: %s" % ("HIT" if res else "NO HIT"))
+
+		# Raycast exiting from inside.
+		center.x -= 20
+		res = _add_raycast(space_state, center, center + Vector2(0, 200))
+		Log.print_log("Raycast out: %s" % ("HIT" if res else "NO HIT"))
+
+		# Raycast all inside.
+		center.x += 40
+		res = _add_raycast(space_state, center, center + Vector2(0, 40))
+		Log.print_log("Raycast inside: %s" % ("HIT" if res else "NO HIT"))
+
+		if body.name.ends_with("ConcavePolygon"):
+			# Raycast inside an internal face.
+			center.x += 20
+			res = _add_raycast(space_state, center, center + Vector2(0, 40))
+			Log.print_log("Raycast inside face: %s" % ("HIT" if res else "NO HIT"))
+
+
+func _add_raycast(space_state, pos_start, pos_end):
+	var result = space_state.intersect_ray(pos_start, pos_end)
+	var color
+	if result:
+		color = Color.green
+	else:
+		color = Color.red.darkened(0.5)
+
+	# Draw raycast line.
+	add_line(pos_start, pos_end, color)
+
+	# Draw raycast arrow.
+	add_line(pos_end, pos_end + Vector2(-5, -10), color)
+	add_line(pos_end, pos_end + Vector2(5, -10), color)
+
+	return result

+ 68 - 0
2d/physics_tests/tests/functional/test_raycasting.tscn

@@ -0,0 +1,68 @@
+[gd_scene load_steps=6 format=2]
+
+[ext_resource path="res://assets/godot-head.png" type="Texture" id=1]
+[ext_resource path="res://tests/functional/test_raycasting.gd" type="Script" id=2]
+
+[sub_resource type="RectangleShape2D" id=1]
+extents = Vector2( 40, 60 )
+
+[sub_resource type="CapsuleShape2D" id=2]
+radius = 30.0
+height = 50.0
+
+[sub_resource type="CircleShape2D" id=3]
+radius = 60.0
+
+[node name="Test" type="Node2D"]
+script = ExtResource( 2 )
+
+[node name="Shapes" type="Node2D" parent="."]
+z_index = -1
+z_as_relative = false
+
+[node name="RigidBodyRectangle" type="RigidBody2D" parent="Shapes"]
+position = Vector2( 114.877, 248.76 )
+mode = 3
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Shapes/RigidBodyRectangle"]
+rotation = -1.19206
+scale = Vector2( 1.5, 1.5 )
+shape = SubResource( 1 )
+
+[node name="RigidBodyCapsule" type="RigidBody2D" parent="Shapes"]
+position = Vector2( 313.583, 261.204 )
+mode = 3
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Shapes/RigidBodyCapsule"]
+rotation = -0.202458
+scale = Vector2( 1.5, 1.5 )
+shape = SubResource( 2 )
+
+[node name="RigidBodyConcavePolygon" type="RigidBody2D" parent="Shapes"]
+position = Vector2( 514.899, 252.771 )
+mode = 3
+
+[node name="GodotIcon" type="Sprite" parent="Shapes/RigidBodyConcavePolygon"]
+modulate = Color( 1, 1, 1, 0.392157 )
+texture = ExtResource( 1 )
+
+[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="Shapes/RigidBodyConcavePolygon"]
+polygon = PoolVector2Array( -5.93512, -43.2195, 6.44476, -42.9695, 11.127, -54.3941, 26.9528, -49.4309, 26.2037, -36.508, 37.5346, -28.1737, 47.6282, -34.3806, 58.0427, -20.9631, 51.113, -10.2876, 50.9869, 35.2694, 38.8, 47.5, 15.9852, 54.3613, -14.9507, 54.1845, -36.5, 48.1, -50.4828, 36.33, -51.3668, -9.98545, -57.8889, -20.5885, -46.9473, -34.7342, -37.4014, -28.547, -26.0876, -37.0323, -26.9862, -49.15, -11.4152, -54.5332 )
+
+[node name="RigidBodyConvexPolygon" type="RigidBody2D" parent="Shapes"]
+position = Vector2( 738.975, 252.771 )
+mode = 3
+
+[node name="GodotIcon" type="Sprite" parent="Shapes/RigidBodyConvexPolygon"]
+modulate = Color( 1, 1, 1, 0.392157 )
+texture = ExtResource( 1 )
+
+[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="Shapes/RigidBodyConvexPolygon"]
+polygon = PoolVector2Array( 10.7, -54.5, 28.3596, -49.4067, 47.6282, -34.3806, 57.9717, -20.9447, 50.9869, 35.2694, 38.8, 47.5, 15.9852, 54.3613, -14.9507, 54.1845, -36.5, 48.1, -50.4828, 36.33, -58.0115, -20.515, -46.9473, -34.7342, -26.0876, -50.1138, -11.4152, -54.5332 )
+
+[node name="RigidBodySphere" type="RigidBody2D" parent="Shapes"]
+position = Vector2( 917.136, 270.868 )
+mode = 3
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Shapes/RigidBodySphere"]
+shape = SubResource( 3 )

+ 64 - 0
2d/physics_tests/tests/functional/test_shapes.tscn

@@ -0,0 +1,64 @@
+[gd_scene load_steps=7 format=2]
+
+[ext_resource path="res://assets/godot-head.png" type="Texture" id=1]
+[ext_resource path="res://test.gd" type="Script" id=2]
+[ext_resource path="res://tests/static_scene.tscn" type="PackedScene" id=6]
+
+[sub_resource type="RectangleShape2D" id=1]
+extents = Vector2( 20, 30 )
+
+[sub_resource type="CapsuleShape2D" id=2]
+radius = 20.0
+height = 30.0
+
+[sub_resource type="CircleShape2D" id=3]
+radius = 30.0
+
+[node name="Test" type="Node2D"]
+script = ExtResource( 2 )
+
+[node name="DynamicShapes" type="Node2D" parent="."]
+
+[node name="RigidBodyRectangle" type="RigidBody2D" parent="DynamicShapes"]
+position = Vector2( 96, 127 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="DynamicShapes/RigidBodyRectangle"]
+rotation = 0.675442
+shape = SubResource( 1 )
+
+[node name="RigidBodyCapsule" type="RigidBody2D" parent="DynamicShapes"]
+position = Vector2( 270.165, 139.444 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="DynamicShapes/RigidBodyCapsule"]
+rotation = -0.202458
+shape = SubResource( 2 )
+
+[node name="RigidBodyConcavePolygon" type="RigidBody2D" parent="DynamicShapes"]
+position = Vector2( 683.614, 132.749 )
+
+[node name="GodotIcon" type="Sprite" parent="DynamicShapes/RigidBodyConcavePolygon"]
+scale = Vector2( 0.5, 0.5 )
+texture = ExtResource( 1 )
+
+[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="DynamicShapes/RigidBodyConcavePolygon"]
+scale = Vector2( 0.5, 0.5 )
+polygon = PoolVector2Array( -5.93512, -43.2195, 6.44476, -42.9695, 11.127, -54.3941, 26.9528, -49.4309, 26.2037, -36.508, 37.5346, -28.1737, 47.6282, -34.3806, 58.0427, -20.9631, 51.113, -10.2876, 50.9869, 35.2694, 38.8, 47.5, 15.9852, 54.3613, -14.9507, 54.1845, -36.5, 48.1, -50.4828, 36.33, -51.3668, -9.98545, -57.8889, -20.5885, -46.9473, -34.7342, -37.4014, -28.547, -26.0876, -37.0323, -26.9862, -49.15, -11.4152, -54.5332 )
+
+[node name="RigidBodyConvexPolygon" type="RigidBody2D" parent="DynamicShapes"]
+position = Vector2( 473.536, 134.336 )
+
+[node name="GodotIcon" type="Sprite" parent="DynamicShapes/RigidBodyConvexPolygon"]
+scale = Vector2( 0.5, 0.5 )
+texture = ExtResource( 1 )
+
+[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="DynamicShapes/RigidBodyConvexPolygon"]
+scale = Vector2( 0.5, 0.5 )
+polygon = PoolVector2Array( 10.7, -54.5, 28.3596, -49.4067, 47.6282, -34.3806, 57.9717, -20.9447, 50.9869, 35.2694, 38.8, 47.5, 15.9852, 54.3613, -14.9507, 54.1845, -36.5, 48.1, -50.4828, 36.33, -58.0115, -20.515, -46.9473, -34.7342, -26.0876, -50.1138, -11.4152, -54.5332 )
+
+[node name="RigidBodySphere" type="RigidBody2D" parent="DynamicShapes"]
+position = Vector2( 919.968, 115.129 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="DynamicShapes/RigidBodySphere"]
+shape = SubResource( 3 )
+
+[node name="StaticScene" parent="." instance=ExtResource( 6 )]

+ 39 - 0
2d/physics_tests/tests/functional/test_stack.gd

@@ -0,0 +1,39 @@
+extends Test
+
+
+export(int) var height = 10
+export(int) var width = 1
+export(Vector2) var box_size = Vector2(40.0, 40.0)
+export(Vector2) var box_spacing =  Vector2(0.0, 0.0)
+
+
+func _ready():
+	_create_stack()
+
+
+func _create_stack():
+	var root_node = $Stack
+
+	var template_body = create_rigidbody_box(box_size)
+
+	var pos_y = -0.5 * box_size.y - box_spacing.y
+
+	for level in height:
+		var row_node = Node2D.new()
+		row_node.position = Vector2(0.0, pos_y)
+		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 in width:
+			var box = template_body.duplicate()
+			box.position = Vector2(pos_x, 0.0)
+			box.name = "Box%02d" % (box_index + 1)
+			row_node.add_child(box)
+
+			pos_x += box_size.x + box_spacing.x
+
+		pos_y -= box_size.y + box_spacing.y
+
+	template_body.queue_free()

+ 12 - 0
2d/physics_tests/tests/functional/test_stack.tscn

@@ -0,0 +1,12 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://tests/functional/test_stack.gd" type="Script" id=1]
+[ext_resource path="res://tests/static_scene_flat.tscn" type="PackedScene" id=2]
+
+[node name="Test" type="Node2D"]
+script = ExtResource( 1 )
+
+[node name="Stack" type="Node2D" parent="."]
+position = Vector2( 512, 500 )
+
+[node name="StaticSceneFlat" parent="." instance=ExtResource( 2 )]

+ 149 - 0
2d/physics_tests/tests/performance/test_perf_broadphase.gd

@@ -0,0 +1,149 @@
+extends Test
+
+
+const BOX_SIZE = Vector2(40, 40)
+const BOX_SPACE = Vector2(50, 50)
+
+export(int, 1, 1000) var row_size = 100
+export(int, 1, 1000) var column_size = 100
+
+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 box = template_body.duplicate()
+			box.position = Vector2(pos_x, pos_y)
+			box.name = "Box%03d" % (row * column + 1)
+			_objects.push_back(box)
+
+			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.position += 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))

+ 9 - 0
2d/physics_tests/tests/performance/test_perf_broadphase.tscn

@@ -0,0 +1,9 @@
+[gd_scene load_steps=2 format=2]
+
+[ext_resource path="res://tests/performance/test_perf_broadphase.gd" type="Script" id=1]
+
+[node name="Test" type="Node2D"]
+script = ExtResource( 1 )
+
+[node name="Objects" type="Node2D" parent="."]
+position = Vector2( 512, 300 )

+ 161 - 0
2d/physics_tests/tests/performance/test_perf_contacts.gd

@@ -0,0 +1,161 @@
+extends Test
+
+
+const OPTION_TYPE_ALL = "Shape type/All"
+const OPTION_TYPE_RECTANGLE = "Shape type/Rectangle"
+const OPTION_TYPE_SPHERE = "Shape type/Sphere"
+const OPTION_TYPE_CAPSULE = "Shape type/Capsule"
+const OPTION_TYPE_CONVEX_POLYGON = "Shape type/Convex Polygon"
+const OPTION_TYPE_CONCAVE_POLYGON = "Shape type/Concave Polygon"
+export(Array) var spawns = Array()
+
+export(int) var spawn_count = 100
+export(int, 1, 10) var spawn_multiplier = 5
+
+var _object_templates = []
+
+
+func _ready():
+	yield(start_timer(0.5), "timeout")
+	if is_timer_canceled():
+		return
+
+	while $DynamicShapes.get_child_count():
+		var type_node = $DynamicShapes.get_child(0)
+		type_node.position = Vector2.ZERO
+		_object_templates.push_back(type_node)
+		$DynamicShapes.remove_child(type_node)
+
+	$Options.add_menu_item(OPTION_TYPE_ALL)
+	$Options.add_menu_item(OPTION_TYPE_RECTANGLE)
+	$Options.add_menu_item(OPTION_TYPE_SPHERE)
+	$Options.add_menu_item(OPTION_TYPE_CAPSULE)
+	$Options.add_menu_item(OPTION_TYPE_CONVEX_POLYGON)
+	$Options.add_menu_item(OPTION_TYPE_CONCAVE_POLYGON)
+	$Options.connect("option_selected", self, "_on_option_selected")
+
+	_start_all_types()
+
+
+func _exit_tree():
+	for object_template in _object_templates:
+		object_template.free()
+
+
+func _on_option_selected(option):
+	cancel_timer()
+
+	_despawn_objects()
+
+	match option:
+		OPTION_TYPE_ALL:
+			_start_all_types()
+		OPTION_TYPE_RECTANGLE:
+			_start_type(_find_type_index("Rectangle"))
+		OPTION_TYPE_SPHERE:
+			_start_type(_find_type_index("Sphere"))
+		OPTION_TYPE_CAPSULE:
+			_start_type(_find_type_index("Capsule"))
+		OPTION_TYPE_CONVEX_POLYGON:
+			_start_type(_find_type_index("ConvexPolygon"))
+		OPTION_TYPE_CONCAVE_POLYGON:
+			_start_type(_find_type_index("ConcavePolygon"))
+
+
+func _find_type_index(type_name):
+	for type_index in _object_templates.size():
+		var type_node = _object_templates[type_index]
+		if type_node.name.find(type_name) > -1:
+			return type_index
+
+	Log.print_error("Invalid shape type: " + type_name)
+	return -1
+
+
+func _start_type(type_index):
+	if type_index < 0:
+		return
+	if type_index >= _object_templates.size():
+		return
+
+	yield(start_timer(1.0), "timeout")
+	if is_timer_canceled():
+		return
+
+	_spawn_objects(type_index)
+
+	yield(start_timer(1.0), "timeout")
+	if is_timer_canceled():
+		return
+
+	_activate_objects()
+
+	yield(start_timer(5.0), "timeout")
+	if is_timer_canceled():
+		return
+
+	_despawn_objects()
+
+	Log.print_log("* Done.")
+
+
+func _start_all_types():
+	for type_index in _object_templates.size():
+		yield(start_timer(1.0), "timeout")
+		if is_timer_canceled():
+			return
+
+		_spawn_objects(type_index)
+
+		yield(start_timer(1.0), "timeout")
+		if is_timer_canceled():
+			return
+
+		_activate_objects()
+
+		yield(start_timer(5.0), "timeout")
+		if is_timer_canceled():
+			return
+
+		_despawn_objects()
+
+	Log.print_log("* Done.")
+
+
+func _spawn_objects(type_index):
+	var template_node = _object_templates[type_index]
+	for spawn in spawns:
+		var spawn_parent = get_node(spawn)
+
+		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)
+
+
+func _activate_objects():
+	var spawn_parent = $SpawnTarget1
+
+	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)
+
+
+func _despawn_objects():
+	for spawn in spawns:
+		var spawn_parent = get_node(spawn)
+
+		if spawn_parent.get_child_count() == 0:
+			return
+
+		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)
+			spawn_parent.remove_child(node)
+			node.queue_free()

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

@@ -0,0 +1,70 @@
+[gd_scene load_steps=8 format=2]
+
+[ext_resource path="res://tests/static_scene.tscn" type="PackedScene" id=1]
+[ext_resource path="res://tests/performance/test_perf_contacts.gd" type="Script" id=2]
+[ext_resource path="res://assets/godot-head.png" type="Texture" id=3]
+[ext_resource path="res://tests/test_options.tscn" type="PackedScene" id=4]
+
+[sub_resource type="RectangleShape2D" id=1]
+extents = Vector2( 20, 30 )
+
+[sub_resource type="CircleShape2D" id=2]
+radius = 30.0
+
+[sub_resource type="CapsuleShape2D" id=3]
+radius = 20.0
+height = 30.0
+
+[node name="Test" type="Node2D"]
+script = ExtResource( 2 )
+spawns = [ NodePath("SpawnTarget1") ]
+
+[node name="Options" parent="." instance=ExtResource( 4 )]
+
+[node name="SpawnTarget1" type="Node2D" parent="."]
+position = Vector2( 512, 400 )
+
+[node name="StaticScene" parent="." instance=ExtResource( 1 )]
+position = Vector2( 0, 125.017 )
+
+[node name="DynamicShapes" type="Node2D" parent="."]
+
+[node name="RigidBodyRectangle" type="RigidBody2D" parent="DynamicShapes"]
+position = Vector2( 0, 1024 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="DynamicShapes/RigidBodyRectangle"]
+shape = SubResource( 1 )
+
+[node name="RigidBodySphere" type="RigidBody2D" parent="DynamicShapes"]
+position = Vector2( 100, 1024 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="DynamicShapes/RigidBodySphere"]
+shape = SubResource( 2 )
+
+[node name="RigidBodyCapsule" type="RigidBody2D" parent="DynamicShapes"]
+position = Vector2( 200, 1024 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="DynamicShapes/RigidBodyCapsule"]
+shape = SubResource( 3 )
+
+[node name="RigidBodyConvexPolygon" type="RigidBody2D" parent="DynamicShapes"]
+position = Vector2( 300, 1024 )
+
+[node name="GodotIcon" type="Sprite" parent="DynamicShapes/RigidBodyConvexPolygon"]
+scale = Vector2( 0.5, 0.5 )
+texture = ExtResource( 3 )
+
+[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="DynamicShapes/RigidBodyConvexPolygon"]
+scale = Vector2( 0.5, 0.5 )
+polygon = PoolVector2Array( 10.7, -54.5, 28.3596, -49.4067, 47.6282, -34.3806, 57.9717, -20.9447, 50.9869, 35.2694, 38.8, 47.5, 15.9852, 54.3613, -14.9507, 54.1845, -36.5, 48.1, -50.4828, 36.33, -58.0115, -20.515, -46.9473, -34.7342, -26.0876, -50.1138, -11.4152, -54.5332 )
+
+[node name="RigidBodyConcavePolygon" type="RigidBody2D" parent="DynamicShapes"]
+position = Vector2( 400, 1024 )
+
+[node name="GodotIcon" type="Sprite" parent="DynamicShapes/RigidBodyConcavePolygon"]
+scale = Vector2( 0.5, 0.5 )
+texture = ExtResource( 3 )
+
+[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="DynamicShapes/RigidBodyConcavePolygon"]
+scale = Vector2( 0.5, 0.5 )
+polygon = PoolVector2Array( -5.93512, -43.2195, 6.44476, -42.9695, 11.127, -54.3941, 26.9528, -49.4309, 26.2037, -36.508, 37.5346, -28.1737, 47.6282, -34.3806, 58.0427, -20.9631, 51.113, -10.2876, 50.9869, 35.2694, 38.8, 47.5, 15.9852, 54.3613, -14.9507, 54.1845, -36.5, 48.1, -50.4828, 36.33, -51.3668, -9.98545, -57.8889, -20.5885, -46.9473, -34.7342, -37.4014, -28.547, -26.0876, -37.0323, -26.9862, -49.15, -11.4152, -54.5332 )

+ 10 - 0
2d/physics_tests/tests/static_scene.tscn

@@ -0,0 +1,10 @@
+[gd_scene format=2]
+
+[node name="StaticScene" type="Node2D"]
+
+[node name="StaticBodyPolygon" type="StaticBody2D" parent="."]
+position = Vector2( -7.85718, 399.596 )
+
+[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="StaticBodyPolygon"]
+build_mode = 1
+polygon = PoolVector2Array( 16.3331, -129.432, 154.006, -20.0078, 292.354, 30.3943, 447.054, 33.9161, 584.899, -14.7955, 751.156, -15.5179, 894.098, -65.4518, 1000.73, -209.127, 1037.77, -398.823, 1029.92, 253.327, 6.2309, 261.185, 7.35339, -398.823 )

+ 12 - 0
2d/physics_tests/tests/static_scene_flat.tscn

@@ -0,0 +1,12 @@
+[gd_scene load_steps=2 format=2]
+
+[sub_resource type="RectangleShape2D" id=1]
+extents = Vector2( 512, 50 )
+
+[node name="StaticSceneFlat" type="Node2D"]
+
+[node name="StaticBodyPolygon" type="StaticBody2D" parent="."]
+position = Vector2( 512, 550 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="StaticBodyPolygon"]
+shape = SubResource( 1 )

+ 16 - 0
2d/physics_tests/tests/test_options.tscn

@@ -0,0 +1,16 @@
+[gd_scene load_steps=2 format=2]
+
+[ext_resource path="res://utils/option_menu.gd" type="Script" id=1]
+
+[node name="Options" type="MenuButton"]
+margin_left = 10.0
+margin_top = 106.719
+margin_right = 125.0
+margin_bottom = 126.719
+text = "TEST OPTIONS"
+flat = false
+align = 0
+script = ExtResource( 1 )
+__meta__ = {
+"_edit_use_anchors_": false
+}

+ 54 - 0
2d/physics_tests/tests_menu.gd

@@ -0,0 +1,54 @@
+extends OptionMenu
+
+
+class TestData:
+	var id
+	var scene_path
+
+
+var _test_list = []
+
+var _current_test = null
+var _current_test_scene = null
+
+
+func _ready():
+	connect("option_selected", self, "_on_option_selected")
+
+
+func _process(_delta):
+	if Input.is_action_just_pressed("restart_test"):
+		if _current_test:
+			_start_test(_current_test)
+
+
+func add_test(id, scene_path):
+	var test_data = TestData.new()
+	test_data.id = id
+	test_data.scene_path = scene_path
+	_test_list.append(test_data)
+
+	add_menu_item(id)
+
+
+func _on_option_selected(item_path):
+	for test in _test_list:
+		if test.id == item_path:
+			_start_test(test)
+
+
+func _start_test(test):
+	_current_test = test
+
+	if _current_test_scene:
+		_current_test_scene.queue_free()
+		_current_test_scene = null
+
+	Log.print_log("*** STARTING TEST: " + test.id)
+	var scene = load(test.scene_path)
+	_current_test_scene = scene.instance()
+	get_tree().root.add_child(_current_test_scene)
+	get_tree().root.move_child(_current_test_scene, 0)
+
+	var label_test = get_node("../LabelTest")
+	label_test.test_name = test.id

+ 41 - 0
2d/physics_tests/utils/container_log.gd

@@ -0,0 +1,41 @@
+extends Control
+
+
+const MAX_ENTRIES = 100
+
+var _entry_template
+
+
+func _enter_tree():
+	Log.connect("entry_logged", self, "_on_log_entry")
+
+	_entry_template = get_child(0) as Label
+	remove_child(_entry_template)
+
+
+func _exit_tree():
+	_entry_template.free()
+
+
+func clear():
+	while get_child_count():
+		var entry = get_child(get_child_count() - 1)
+		remove_child(entry)
+		entry.queue_free()
+
+
+func _on_log_entry(message, type):
+	var new_entry = _entry_template.duplicate() as Label
+
+	new_entry.set_text(message)
+	if type == Log.LogType.ERROR:
+		new_entry.modulate = Color.red
+	else:
+		new_entry.modulate = Color.white
+
+	if get_child_count() >= MAX_ENTRIES:
+		var first_entry = get_child(0) as Label
+		remove_child(first_entry)
+		first_entry.queue_free()
+
+	add_child(new_entry)

+ 12 - 0
2d/physics_tests/utils/label_engine.gd

@@ -0,0 +1,12 @@
+extends Label
+
+
+func _process(_delta):
+	var engine_name = ""
+	match System.get_physics_engine():
+		System.PhysicsEngine.GODOT_PHYSICS:
+			engine_name = "Godot Physics"
+		System.PhysicsEngine.OTHER:
+			var engine_setting = ProjectSettings.get_setting("physics/2d/physics_engine")
+			engine_name = "Other (%s)" % engine_setting
+	set_text("Physics engine: %s" % engine_name)

+ 5 - 0
2d/physics_tests/utils/label_fps.gd

@@ -0,0 +1,5 @@
+extends Label
+
+
+func _process(_delta):
+	set_text("FPS: %d" % Engine.get_frames_per_second())

+ 13 - 0
2d/physics_tests/utils/label_test.gd

@@ -0,0 +1,13 @@
+extends Label
+
+
+var test_name setget _set_test_name
+
+
+func _ready():
+	set_text("Select a test from the menu to start it")
+
+
+func _set_test_name(value):
+	test_name = value
+	set_text("Test: %s" % test_name)

+ 5 - 0
2d/physics_tests/utils/label_version.gd

@@ -0,0 +1,5 @@
+extends Label
+
+
+func _process(_delta):
+	set_text("Godot Version: %s" % Engine.get_version_info().string)

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

@@ -0,0 +1,47 @@
+class_name OptionMenu
+extends MenuButton
+
+
+signal option_selected(item_path)
+
+
+func add_menu_item(item_path):
+	var path_elements = item_path.split("/", false)
+	var path_element_count = path_elements.size()
+	assert(path_element_count > 0)
+
+	var path = ""
+	var popup = get_popup()
+	for element_index in path_element_count - 1:
+		var popup_label = path_elements[element_index]
+		path += popup_label + "/"
+		popup = _add_popup(popup, path, popup_label)
+
+	_add_item(popup, path_elements[path_element_count - 1])
+
+
+func _add_item(parent_popup, label):
+	parent_popup.add_item(label)
+
+
+func _add_popup(parent_popup, path, label):
+	if parent_popup.has_node(label):
+		var popup_node = parent_popup.get_node(label)
+		var popup_menu = popup_node as PopupMenu
+		assert(popup_menu)
+		return popup_menu
+
+	var popup_menu = PopupMenu.new()
+	popup_menu.name = label
+
+	parent_popup.add_child(popup_menu)
+	parent_popup.add_submenu_item(label, label)
+
+	popup_menu.connect("index_pressed", self, "_on_item_pressed", [popup_menu, path])
+
+	return popup_menu
+
+
+func _on_item_pressed(item_index, popup_menu, path):
+	var item_path = path + popup_menu.get_item_text(item_index)
+	emit_signal("option_selected", item_path)

+ 14 - 0
2d/physics_tests/utils/scroll_log.gd

@@ -0,0 +1,14 @@
+extends ScrollContainer
+
+
+export(bool) var auto_scroll = false setget set_auto_scroll
+
+
+func _process(_delta):
+	if auto_scroll:
+		var scrollbar = get_v_scrollbar()
+		scrollbar.value = scrollbar.max_value
+
+
+func set_auto_scroll(value):
+	auto_scroll = value

+ 50 - 0
2d/physics_tests/utils/system.gd

@@ -0,0 +1,50 @@
+extends Node
+
+
+enum PhysicsEngine {
+	GODOT_PHYSICS,
+	OTHER,
+}
+
+var _engine = PhysicsEngine.OTHER
+
+
+func _enter_tree():
+	get_tree().debug_collisions_hint = true
+
+	var engine_string = ProjectSettings.get_setting("physics/2d/physics_engine")
+	match engine_string:
+		"DEFAULT":
+			_engine = PhysicsEngine.GODOT_PHYSICS
+		"GodotPhysics":
+			_engine = PhysicsEngine.GODOT_PHYSICS
+		_:
+			_engine = PhysicsEngine.OTHER
+
+
+func _process(_delta):
+	if Input.is_action_just_pressed("toggle_full_screen"):
+		OS.window_fullscreen = not OS.window_fullscreen
+
+	if Input.is_action_just_pressed("toggle_debug_collision"):
+		var debug_collision_enabled = not _is_debug_collision_enabled()
+		_set_debug_collision_enabled(debug_collision_enabled)
+		if debug_collision_enabled:
+			Log.print_log("Debug Collision ON")
+		else:
+			Log.print_log("Debug Collision OFF")
+
+	if Input.is_action_just_pressed("exit"):
+		get_tree().quit()
+
+
+func get_physics_engine():
+	return _engine
+
+
+func _set_debug_collision_enabled(enabled):
+	get_tree().debug_collisions_hint = enabled
+
+
+func _is_debug_collision_enabled():
+	return get_tree().debug_collisions_hint

+ 20 - 0
2d/physics_tests/utils/system_log.gd

@@ -0,0 +1,20 @@
+extends Node
+
+
+enum LogType {
+	LOG,
+	ERROR,
+}
+
+signal entry_logged(message, type)
+
+
+func print_log(message):
+	print(message)
+	emit_signal("entry_logged", message, LogType.LOG)
+
+
+func print_error(message):
+	push_error(message)
+	printerr(message)
+	emit_signal("entry_logged", message, LogType.ERROR)

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