Browse Source

Add physics tests for 2D character controller

Two tests for character controller, with options to use RigidBody2D,
KinematicBody2D or KinematicBody2D with RayShape2D.

Tilemap: Tests for moving and jumping within tilemap blocks, with a
specific one-way collision test case scenario based on Block Climb Test
from https://github.com/madmiraal/godot-gym.

Slopes: Tests for moving and jumping in slopes, with different cases
based on snap and stop-on-slope parameters for kinematic bodies.
PouleyKetchoupp 4 years ago
parent
commit
cdf0ed3be9

+ 0 - 0
2d/physics_tests/assets/godot-head.png → 2d/physics_tests/assets/texture/godot-head.png


+ 3 - 3
2d/physics_tests/assets/godot-head.png.import → 2d/physics_tests/assets/texture/godot-head.png.import

@@ -2,15 +2,15 @@
 
 importer="texture"
 type="StreamTexture"
-path="res://.import/godot-head.png-cc6844293d74c4f45ec6c4ab08de67ee.stex"
+path="res://.import/godot-head.png-6a90da7ab6a8c80b4170f240c8e33e70.stex"
 metadata={
 "vram_texture": false
 }
 
 [deps]
 
-source_file="res://assets/godot-head.png"
-dest_files=[ "res://.import/godot-head.png-cc6844293d74c4f45ec6c4ab08de67ee.stex" ]
+source_file="res://assets/texture/godot-head.png"
+dest_files=[ "res://.import/godot-head.png-6a90da7ab6a8c80b4170f240c8e33e70.stex" ]
 
 [params]
 

BIN
2d/physics_tests/assets/tileset/tiles_demo.png


+ 34 - 0
2d/physics_tests/assets/tileset/tiles_demo.png.import

@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/tiles_demo.png-4d398d5cc02bc85a2809dc13fbc9a3c2.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/tileset/tiles_demo.png"
+dest_files=[ "res://.import/tiles_demo.png-4d398d5cc02bc85a2809dc13fbc9a3c2.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

+ 335 - 0
2d/physics_tests/assets/tileset/tileset.tres

@@ -0,0 +1,335 @@
+[gd_resource type="TileSet" load_steps=14 format=2]
+
+[ext_resource path="res://assets/tileset/tiles_demo.png" type="Texture" id=1]
+
+[sub_resource type="ConvexPolygonShape2D" id=1]
+points = PoolVector2Array( 0, 6, 32, 6, 32, 32, 0, 32 )
+
+[sub_resource type="ConvexPolygonShape2D" id=2]
+points = PoolVector2Array( 0, 6, 28, 6, 28, 32, 0, 32 )
+
+[sub_resource type="ConvexPolygonShape2D" id=3]
+points = PoolVector2Array( 0, 0, 32, 0, 32, 32, 0, 32 )
+
+[sub_resource type="ConvexPolygonShape2D" id=4]
+points = PoolVector2Array( 0, 6, 32, 6, 32, 32, 0, 32 )
+
+[sub_resource type="ConvexPolygonShape2D" id=5]
+points = PoolVector2Array( 32, 38, 32, 64, 0, 64, 0, 6 )
+
+[sub_resource type="ConvexPolygonShape2D" id=6]
+points = PoolVector2Array( 0, 0, 28, 0, 28, 32, 0, 32 )
+
+[sub_resource type="ConvexPolygonShape2D" id=7]
+points = PoolVector2Array( 28, 6, 32, 6, 32, 32, 0, 32, 0, 0, 28, 0 )
+
+[sub_resource type="ConvexPolygonShape2D" id=8]
+points = PoolVector2Array( 0, 6, 32, 6, 32, 32, 0, 32 )
+
+[sub_resource type="ConvexPolygonShape2D" id=9]
+points = PoolVector2Array( 0, 6, 28, 6, 28, 32, 0, 32 )
+
+[sub_resource type="ConvexPolygonShape2D" id=10]
+points = PoolVector2Array( 0, 0, 32, 0, 32, 32, 0, 32 )
+
+[sub_resource type="ConvexPolygonShape2D" id=11]
+points = PoolVector2Array( 0, 0, 32, 0, 32, 24, 0, 24 )
+
+[sub_resource type="ConvexPolygonShape2D" id=12]
+points = PoolVector2Array( 0, 0, 28, 0, 28, 24, 0, 24 )
+
+[resource]
+0/name = "ground"
+0/texture = ExtResource( 1 )
+0/tex_offset = Vector2( 0, 0 )
+0/modulate = Color( 0, 0, 1, 1 )
+0/region = Rect2( 0, 0, 32, 32 )
+0/tile_mode = 0
+0/occluder_offset = Vector2( 0, 0 )
+0/navigation_offset = Vector2( 0, 0 )
+0/shape_offset = Vector2( 0, 0 )
+0/shape_transform = Transform2D( 1, 0, 0, 1, 0, 0 )
+0/shape = SubResource( 1 )
+0/shape_one_way = false
+0/shape_one_way_margin = 1.0
+0/shapes = [ {
+"autotile_coord": Vector2( 0, 0 ),
+"one_way": false,
+"one_way_margin": 1.0,
+"shape": SubResource( 1 ),
+"shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 )
+} ]
+0/z_index = 0
+1/name = "ground_end"
+1/texture = ExtResource( 1 )
+1/tex_offset = Vector2( 0, 0 )
+1/modulate = Color( 1, 1, 1, 1 )
+1/region = Rect2( 32, 0, 32, 32 )
+1/tile_mode = 0
+1/occluder_offset = Vector2( 0, 0 )
+1/navigation_offset = Vector2( 0, 0 )
+1/shape_offset = Vector2( 0, 0 )
+1/shape_transform = Transform2D( 1, 0, 0, 1, 0, 0 )
+1/shape = SubResource( 2 )
+1/shape_one_way = false
+1/shape_one_way_margin = 1.0
+1/shapes = [ {
+"autotile_coord": Vector2( 0, 0 ),
+"one_way": false,
+"one_way_margin": 1.0,
+"shape": SubResource( 2 ),
+"shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 )
+} ]
+1/z_index = 0
+2/name = "slope"
+2/texture = ExtResource( 1 )
+2/tex_offset = Vector2( 0, 0 )
+2/modulate = Color( 1, 1, 1, 1 )
+2/region = Rect2( 64, 64, 32, 64 )
+2/tile_mode = 0
+2/occluder_offset = Vector2( 0, 0 )
+2/navigation_offset = Vector2( 0, 0 )
+2/shape_offset = Vector2( 0, 0 )
+2/shape_transform = Transform2D( 1, 0, 0, 1, 0, 0 )
+2/shape = SubResource( 5 )
+2/shape_one_way = false
+2/shape_one_way_margin = 1.0
+2/shapes = [ {
+"autotile_coord": Vector2( 0, 0 ),
+"one_way": false,
+"one_way_margin": 1.0,
+"shape": SubResource( 5 ),
+"shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 )
+} ]
+2/z_index = 0
+3/name = "wall"
+3/texture = ExtResource( 1 )
+3/tex_offset = Vector2( 0, 0 )
+3/modulate = Color( 1, 1, 1, 1 )
+3/region = Rect2( 32, 32, 32, 32 )
+3/tile_mode = 0
+3/occluder_offset = Vector2( 0, 0 )
+3/navigation_offset = Vector2( 0, 0 )
+3/shape_offset = Vector2( 0, 0 )
+3/shape_transform = Transform2D( 1, 0, 0, 1, 0, 0 )
+3/shape = SubResource( 6 )
+3/shape_one_way = false
+3/shape_one_way_margin = 1.0
+3/shapes = [ {
+"autotile_coord": Vector2( 0, 0 ),
+"one_way": false,
+"one_way_margin": 1.0,
+"shape": SubResource( 6 ),
+"shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 )
+} ]
+3/z_index = 0
+4/name = "slope_top"
+4/texture = ExtResource( 1 )
+4/tex_offset = Vector2( 0, 0 )
+4/modulate = Color( 1, 1, 1, 1 )
+4/region = Rect2( 32, 64, 32, 32 )
+4/tile_mode = 0
+4/occluder_offset = Vector2( 0, 0 )
+4/navigation_offset = Vector2( 0, 0 )
+4/shape_offset = Vector2( 0, 0 )
+4/shape_transform = Transform2D( 1, 0, 0, 1, 0, 0 )
+4/shape = SubResource( 7 )
+4/shape_one_way = false
+4/shape_one_way_margin = 1.0
+4/shapes = [ {
+"autotile_coord": Vector2( 0, 0 ),
+"one_way": false,
+"one_way_margin": 1.0,
+"shape": SubResource( 7 ),
+"shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 )
+} ]
+4/z_index = 0
+5/name = "one_way"
+5/texture = ExtResource( 1 )
+5/tex_offset = Vector2( 0, 0 )
+5/modulate = Color( 1, 1, 1, 1 )
+5/region = Rect2( 64, 0, 32, 32 )
+5/tile_mode = 0
+5/occluder_offset = Vector2( 0, 0 )
+5/navigation_offset = Vector2( 0, 0 )
+5/shape_offset = Vector2( 0, 0 )
+5/shape_transform = Transform2D( 1, 0, 0, 1, 0, 0 )
+5/shape = SubResource( 8 )
+5/shape_one_way = true
+5/shape_one_way_margin = 1.0
+5/shapes = [ {
+"autotile_coord": Vector2( 0, 0 ),
+"one_way": true,
+"one_way_margin": 1.0,
+"shape": SubResource( 8 ),
+"shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 )
+} ]
+5/z_index = 0
+6/name = "one_way_end"
+6/texture = ExtResource( 1 )
+6/tex_offset = Vector2( 0, 0 )
+6/modulate = Color( 1, 1, 1, 1 )
+6/region = Rect2( 96, 0, 32, 32 )
+6/tile_mode = 0
+6/occluder_offset = Vector2( 0, 0 )
+6/navigation_offset = Vector2( 0, 0 )
+6/shape_offset = Vector2( 0, 0 )
+6/shape_transform = Transform2D( 1, 0, 0, 1, 0, 0 )
+6/shape = SubResource( 9 )
+6/shape_one_way = true
+6/shape_one_way_margin = 1.0
+6/shapes = [ {
+"autotile_coord": Vector2( 0, 0 ),
+"one_way": true,
+"one_way_margin": 1.0,
+"shape": SubResource( 9 ),
+"shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 )
+} ]
+6/z_index = 0
+7/name = "rock"
+7/texture = ExtResource( 1 )
+7/tex_offset = Vector2( 0, 0 )
+7/modulate = Color( 1, 1, 1, 1 )
+7/region = Rect2( 0, 32, 32, 32 )
+7/tile_mode = 0
+7/occluder_offset = Vector2( 0, 0 )
+7/navigation_offset = Vector2( 0, 0 )
+7/shape_offset = Vector2( 0, 0 )
+7/shape_transform = Transform2D( 1, 0, 0, 1, 0, 0 )
+7/shape = SubResource( 10 )
+7/shape_one_way = false
+7/shape_one_way_margin = 1.0
+7/shapes = [ {
+"autotile_coord": Vector2( 0, 0 ),
+"one_way": false,
+"one_way_margin": 1.0,
+"shape": SubResource( 10 ),
+"shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 )
+} ]
+7/z_index = 0
+8/name = "bottom"
+8/texture = ExtResource( 1 )
+8/tex_offset = Vector2( 0, 0 )
+8/modulate = Color( 1, 1, 1, 1 )
+8/region = Rect2( 192, 32, 32, 32 )
+8/tile_mode = 0
+8/occluder_offset = Vector2( 0, 0 )
+8/navigation_offset = Vector2( 0, 0 )
+8/shape_offset = Vector2( 0, 0 )
+8/shape_transform = Transform2D( 1, 0, 0, 1, 0, 0 )
+8/shape = SubResource( 11 )
+8/shape_one_way = false
+8/shape_one_way_margin = 1.0
+8/shapes = [ {
+"autotile_coord": Vector2( 0, 0 ),
+"one_way": false,
+"one_way_margin": 1.0,
+"shape": SubResource( 11 ),
+"shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 )
+} ]
+8/z_index = 0
+9/name = "bottom_end"
+9/texture = ExtResource( 1 )
+9/tex_offset = Vector2( 0, 0 )
+9/modulate = Color( 1, 1, 1, 1 )
+9/region = Rect2( 224, 32, 32, 32 )
+9/tile_mode = 0
+9/occluder_offset = Vector2( 0, 0 )
+9/navigation_offset = Vector2( 0, 0 )
+9/shape_offset = Vector2( 0, 0 )
+9/shape_transform = Transform2D( 1, 0, 0, 1, 0, 0 )
+9/shape = SubResource( 12 )
+9/shape_one_way = false
+9/shape_one_way_margin = 1.0
+9/shapes = [ {
+"autotile_coord": Vector2( 0, 0 ),
+"one_way": false,
+"one_way_margin": 1.0,
+"shape": SubResource( 12 ),
+"shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 )
+} ]
+9/z_index = 0
+10/name = "bottom_corner"
+10/texture = ExtResource( 1 )
+10/tex_offset = Vector2( 0, 0 )
+10/modulate = Color( 1, 1, 1, 1 )
+10/region = Rect2( 160, 32, 32, 32 )
+10/tile_mode = 0
+10/occluder_offset = Vector2( 0, 0 )
+10/navigation_offset = Vector2( 0, 0 )
+10/shape_offset = Vector2( 0, 0 )
+10/shape_transform = Transform2D( 1, 0, 0, 1, 0, 0 )
+10/shape = SubResource( 3 )
+10/shape_one_way = false
+10/shape_one_way_margin = 1.0
+10/shapes = [ {
+"autotile_coord": Vector2( 0, 0 ),
+"one_way": false,
+"one_way_margin": 1.0,
+"shape": SubResource( 3 ),
+"shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 )
+} ]
+10/z_index = 0
+11/name = "tree_trunk_0"
+11/texture = ExtResource( 1 )
+11/tex_offset = Vector2( 0, 0 )
+11/modulate = Color( 1, 1, 1, 1 )
+11/region = Rect2( 128, 64, 32, 32 )
+11/tile_mode = 0
+11/occluder_offset = Vector2( 0, 0 )
+11/navigation_offset = Vector2( 0, 0 )
+11/shape_offset = Vector2( 0, 0 )
+11/shape_transform = Transform2D( 1, 0, 0, 1, 0, 0 )
+11/shape_one_way = false
+11/shape_one_way_margin = 0.0
+11/shapes = [  ]
+11/z_index = 0
+12/name = "tree_trunk_1"
+12/texture = ExtResource( 1 )
+12/tex_offset = Vector2( 0, 0 )
+12/modulate = Color( 1, 1, 1, 1 )
+12/region = Rect2( 128, 32, 32, 32 )
+12/tile_mode = 0
+12/occluder_offset = Vector2( 0, 0 )
+12/navigation_offset = Vector2( 0, 0 )
+12/shape_offset = Vector2( 0, 0 )
+12/shape_transform = Transform2D( 1, 0, 0, 1, 0, 0 )
+12/shape_one_way = false
+12/shape_one_way_margin = 0.0
+12/shapes = [  ]
+12/z_index = 0
+13/name = "tree_base"
+13/texture = ExtResource( 1 )
+13/tex_offset = Vector2( 0, 0 )
+13/modulate = Color( 1, 1, 1, 1 )
+13/region = Rect2( 128, 96, 32, 32 )
+13/tile_mode = 0
+13/occluder_offset = Vector2( 0, 0 )
+13/navigation_offset = Vector2( 0, 0 )
+13/shape_offset = Vector2( 0, 0 )
+13/shape_transform = Transform2D( 1, 0, 0, 1, 0, 0 )
+13/shape = SubResource( 4 )
+13/shape_one_way = false
+13/shape_one_way_margin = 1.0
+13/shapes = [ {
+"autotile_coord": Vector2( 0, 0 ),
+"one_way": false,
+"one_way_margin": 1.0,
+"shape": SubResource( 4 ),
+"shape_transform": Transform2D( 1, 0, 0, 1, 0, 0 )
+} ]
+13/z_index = 0
+14/name = "tree_top"
+14/texture = ExtResource( 1 )
+14/tex_offset = Vector2( 0, 0 )
+14/modulate = Color( 1, 1, 1, 1 )
+14/region = Rect2( 128, 0, 32, 32 )
+14/tile_mode = 0
+14/occluder_offset = Vector2( 0, 0 )
+14/navigation_offset = Vector2( 0, 0 )
+14/shape_offset = Vector2( 0, 0 )
+14/shape_transform = Transform2D( 1, 0, 0, 1, 0, 0 )
+14/shape_one_way = false
+14/shape_one_way_margin = 0.0
+14/shapes = [  ]
+14/z_index = 0

+ 25 - 1
2d/physics_tests/project.godot

@@ -18,10 +18,16 @@ _global_script_classes=[ {
 "class": "Test",
 "language": "GDScript",
 "path": "res://test.gd"
+}, {
+"base": "Test",
+"class": "TestCharacter",
+"language": "GDScript",
+"path": "res://tests/functional/test_character.gd"
 } ]
 _global_script_class_icons={
 "OptionMenu": "",
-"Test": ""
+"Test": "",
+"TestCharacter": ""
 }
 
 [application]
@@ -72,6 +78,24 @@ toggle_pause={
 "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":80,"unicode":0,"echo":false,"script":null)
  ]
 }
+character_left={
+"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":16777231,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":65,"unicode":0,"echo":false,"script":null)
+ ]
+}
+character_right={
+"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":16777233,"unicode":0,"echo":false,"script":null)
+, 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)
+ ]
+}
+character_jump={
+"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":16777232,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":87,"unicode":0,"echo":false,"script":null)
+ ]
+}
 
 [memory]
 

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

@@ -18,6 +18,14 @@ var _tests = [
 		"id": "Functional Tests/Collision Pairs",
 		"path": "res://tests/functional/test_collision_pairs.tscn",
 	},
+	{
+		"id": "Functional Tests/Character - Slopes",
+		"path": "res://tests/functional/test_character_slopes.tscn",
+	},
+	{
+		"id": "Functional Tests/Character - Tilemap",
+		"path": "res://tests/functional/test_character_tilemap.tscn",
+	},
 	{
 		"id": "Functional Tests/One Way Collision",
 		"path": "res://tests/functional/test_one_way_collision.tscn",

+ 149 - 0
2d/physics_tests/tests/functional/test_character.gd

@@ -0,0 +1,149 @@
+extends Test
+class_name TestCharacter
+
+
+enum E_BodyType {
+	RIGID_BODY,
+	KINEMATIC_BODY,
+	KINEMATIC_BODY_RAY_SHAPE,
+}
+
+const OPTION_OBJECT_TYPE_RIGIDBODY = "Object type/Rigid body (1)"
+const OPTION_OBJECT_TYPE_KINEMATIC = "Object type/Kinematic body (2)"
+const OPTION_OBJECT_TYPE_KINEMATIC_RAYSHAPE = "Object type/Kinematic body with ray shape (3)"
+
+const OPTION_MOVE_KINEMATIC_SNAP = "Move Options/Use snap (Kinematic only)"
+const OPTION_MOVE_KINEMATIC_STOP_ON_SLOPE = "Move Options/Use stop on slope (Kinematic only)"
+
+export(Vector2) var _initial_velocity = Vector2.ZERO
+export(Vector2) var _constant_velocity = Vector2.ZERO
+export(float) var _snap_distance = 0.0
+export(float) var _floor_max_angle = 45.0
+export(E_BodyType) var _body_type = 0
+
+var _use_snap = true
+var _use_stop_on_slope = true
+
+var _rigid_body_template = null
+var _kinematic_body_template = null
+var _kinematic_body_ray_template = null
+var _moving_body = null
+
+
+func _ready():
+	$Options.connect("option_selected", self, "_on_option_selected")
+	$Options.connect("option_changed", self, "_on_option_changed")
+
+	_rigid_body_template = find_node("RigidBody2D")
+	if _rigid_body_template:
+		remove_child(_rigid_body_template)
+		var enabled = _body_type == E_BodyType.RIGID_BODY
+		$Options.add_menu_item(OPTION_OBJECT_TYPE_RIGIDBODY, true, enabled, true)
+
+	_kinematic_body_template = find_node("KinematicBody2D")
+	if _kinematic_body_template:
+		remove_child(_kinematic_body_template)
+		var enabled = _body_type == E_BodyType.KINEMATIC_BODY
+		$Options.add_menu_item(OPTION_OBJECT_TYPE_KINEMATIC, true, enabled, true)
+
+	_kinematic_body_ray_template = find_node("KinematicBodyRay2D")
+	if _kinematic_body_ray_template:
+		remove_child(_kinematic_body_ray_template)
+		var enabled = _body_type == E_BodyType.KINEMATIC_BODY_RAY_SHAPE
+		$Options.add_menu_item(OPTION_OBJECT_TYPE_KINEMATIC_RAYSHAPE, true, enabled, true)
+
+	$Options.add_menu_item(OPTION_MOVE_KINEMATIC_SNAP, true, _use_snap)
+	$Options.add_menu_item(OPTION_MOVE_KINEMATIC_STOP_ON_SLOPE, true, _use_stop_on_slope)
+
+	_start_test()
+
+
+func _process(_delta):
+	if _moving_body:
+		if _moving_body.is_on_floor():
+			$LabelFloor.text = "ON FLOOR"
+			$LabelFloor.self_modulate = Color.green
+		else:
+			$LabelFloor.text = "OFF FLOOR"
+			$LabelFloor.self_modulate = Color.red
+	else:
+		$LabelFloor.visible = false
+
+
+func _input(event):
+	var key_event = event as InputEventKey
+	if key_event and not key_event.pressed:
+		if key_event.scancode == KEY_1:
+			if _rigid_body_template:
+				_on_option_selected(OPTION_OBJECT_TYPE_RIGIDBODY)
+		elif key_event.scancode == KEY_2:
+			if _kinematic_body_template:
+				_on_option_selected(OPTION_OBJECT_TYPE_KINEMATIC)
+		elif key_event.scancode == KEY_3:
+			if _kinematic_body_ray_template:
+				_on_option_selected(OPTION_OBJECT_TYPE_KINEMATIC_RAYSHAPE)
+
+
+func _exit_tree():
+	if _rigid_body_template:
+		_rigid_body_template.free()
+	if _kinematic_body_template:
+		_kinematic_body_template.free()
+	if _kinematic_body_ray_template:
+		_kinematic_body_ray_template.free()
+
+
+func _on_option_selected(option):
+	match option:
+		OPTION_OBJECT_TYPE_RIGIDBODY:
+			_body_type = E_BodyType.RIGID_BODY
+			_start_test()
+		OPTION_OBJECT_TYPE_KINEMATIC:
+			_body_type = E_BodyType.KINEMATIC_BODY
+			_start_test()
+		OPTION_OBJECT_TYPE_KINEMATIC_RAYSHAPE:
+			_body_type = E_BodyType.KINEMATIC_BODY_RAY_SHAPE
+			_start_test()
+
+
+func _on_option_changed(option, checked):
+	match option:
+		OPTION_MOVE_KINEMATIC_SNAP:
+			_use_snap = checked
+			_start_test()
+		OPTION_MOVE_KINEMATIC_STOP_ON_SLOPE:
+			_use_stop_on_slope = checked
+			_start_test()
+
+
+func _start_test():
+	if _moving_body:
+		remove_child(_moving_body)
+		_moving_body.queue_free()
+		_moving_body = null
+
+	var test_label = "Testing: "
+
+	var template = null
+	match _body_type:
+		E_BodyType.RIGID_BODY:
+			template = _rigid_body_template
+		E_BodyType.KINEMATIC_BODY:
+			template = _kinematic_body_template
+		E_BodyType.KINEMATIC_BODY_RAY_SHAPE:
+			template = _kinematic_body_ray_template
+
+	test_label += template.name
+	_moving_body = template.duplicate()
+	add_child(_moving_body)
+
+	_moving_body._initial_velocity = _initial_velocity
+	_moving_body._constant_velocity = _constant_velocity
+
+	if _moving_body is KinematicBody2D:
+		if _use_snap:
+			_moving_body._snap = Vector2(0, _snap_distance)
+		_moving_body._stop_on_slope = _use_stop_on_slope
+		_moving_body._floor_max_angle = _floor_max_angle
+
+	$LabelTestType.text = test_label

+ 103 - 0
2d/physics_tests/tests/functional/test_character_slopes.tscn

@@ -0,0 +1,103 @@
+[gd_scene load_steps=11 format=2]
+
+[ext_resource path="res://tests/functional/test_character.gd" type="Script" id=1]
+[ext_resource path="res://tests/test_options.tscn" type="PackedScene" id=3]
+[ext_resource path="res://tests/static_scene_flat.tscn" type="PackedScene" id=4]
+[ext_resource path="res://utils/rigidbody_controller.gd" type="Script" id=6]
+[ext_resource path="res://utils/kinematicbody_controller.gd" type="Script" id=7]
+
+[sub_resource type="PhysicsMaterial" id=1]
+friction = 0.0
+
+[sub_resource type="CapsuleShape2D" id=2]
+radius = 32.0
+height = 32.0
+
+[sub_resource type="CapsuleShape2D" id=3]
+radius = 32.0
+height = 32.0
+
+[sub_resource type="CircleShape2D" id=4]
+radius = 32.0
+
+[sub_resource type="RayShape2D" id=5]
+length = 64.0
+
+[node name="Test" type="Node2D"]
+script = ExtResource( 1 )
+_snap_distance = 32.0
+_floor_max_angle = 60.0
+
+[node name="LabelTestType" type="Label" parent="."]
+margin_left = 14.0
+margin_top = 79.0
+margin_right = 145.0
+margin_bottom = 93.0
+text = "Testing: "
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Options" parent="." instance=ExtResource( 3 )]
+
+[node name="LabelFloor" type="Label" parent="."]
+margin_left = 14.0
+margin_top = 237.929
+margin_right = 145.0
+margin_bottom = 251.929
+text = "ON FLOOR"
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="LabelControls" type="Label" parent="."]
+margin_left = 14.0
+margin_top = 263.291
+margin_right = 145.0
+margin_bottom = 294.291
+text = "LEFT/RIGHT - MOVE
+UP - JUMP"
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="RigidBody2D" type="RigidBody2D" parent="."]
+position = Vector2( 100, 450 )
+collision_mask = 2147483649
+mode = 2
+physics_material_override = SubResource( 1 )
+contacts_reported = 4
+contact_monitor = true
+script = ExtResource( 6 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="RigidBody2D"]
+shape = SubResource( 2 )
+
+[node name="KinematicBody2D" type="KinematicBody2D" parent="."]
+position = Vector2( 100, 450 )
+collision_mask = 2147483649
+script = ExtResource( 7 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="KinematicBody2D"]
+shape = SubResource( 3 )
+
+[node name="KinematicBodyRay2D" type="KinematicBody2D" parent="."]
+position = Vector2( 100, 450 )
+collision_mask = 2147483649
+script = ExtResource( 7 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="KinematicBodyRay2D"]
+position = Vector2( 0, -16 )
+shape = SubResource( 4 )
+
+[node name="CollisionShapeRay2D" type="CollisionShape2D" parent="KinematicBodyRay2D"]
+position = Vector2( 0, -16 )
+shape = SubResource( 5 )
+
+[node name="StaticSceneFlat" parent="." instance=ExtResource( 4 )]
+position = Vector2( 0, 12 )
+
+[node name="StaticBody2D" type="StaticBody2D" parent="."]
+
+[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="StaticBody2D"]
+polygon = PoolVector2Array( 171.04, 529.248, 379.275, 294.316, 506.084, 429.135, 648.26, 322.058, 868.746, 322.058, 985.282, 36.6919, 1242.91, 531.917 )

+ 26 - 0
2d/physics_tests/tests/functional/test_character_tilemap.gd

@@ -0,0 +1,26 @@
+extends TestCharacter
+
+
+const OPTION_TEST_CASE_JUMP_ONE_WAY = "Test Cases/Jump through one-way tiles"
+
+var _test_jump_one_way = false
+
+
+func _ready():
+	$Options.add_menu_item(OPTION_TEST_CASE_JUMP_ONE_WAY, true, false)
+
+
+func _on_option_changed(option, checked):
+	match option:
+		OPTION_TEST_CASE_JUMP_ONE_WAY:
+			_test_jump_one_way = checked
+			_start_test()
+
+	._on_option_changed(option, checked)
+
+
+func _start_test():
+	._start_test()
+
+	if _test_jump_one_way:
+		_moving_body._initial_velocity = Vector2(600, -1000)

+ 106 - 0
2d/physics_tests/tests/functional/test_character_tilemap.tscn

@@ -0,0 +1,106 @@
+[gd_scene load_steps=11 format=2]
+
+[ext_resource path="res://tests/functional/test_character_tilemap.gd" type="Script" id=1]
+[ext_resource path="res://tests/test_options.tscn" type="PackedScene" id=3]
+[ext_resource path="res://tests/static_scene_flat.tscn" type="PackedScene" id=4]
+[ext_resource path="res://assets/tileset/tileset.tres" type="TileSet" id=5]
+[ext_resource path="res://utils/rigidbody_controller.gd" type="Script" id=6]
+[ext_resource path="res://utils/kinematicbody_controller.gd" type="Script" id=7]
+
+[sub_resource type="PhysicsMaterial" id=1]
+friction = 0.0
+
+[sub_resource type="RectangleShape2D" id=2]
+extents = Vector2( 16, 32 )
+
+[sub_resource type="RectangleShape2D" id=3]
+extents = Vector2( 16, 24 )
+
+[sub_resource type="RayShape2D" id=4]
+length = 24.0
+
+[node name="Test" type="Node2D"]
+script = ExtResource( 1 )
+
+[node name="LabelTestType" type="Label" parent="."]
+margin_left = 14.0
+margin_top = 79.0
+margin_right = 145.0
+margin_bottom = 93.0
+text = "Testing: "
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Options" parent="." instance=ExtResource( 3 )]
+
+[node name="LabelFloor" type="Label" parent="."]
+margin_left = 14.0
+margin_top = 237.929
+margin_right = 145.0
+margin_bottom = 251.929
+text = "ON FLOOR"
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="LabelControls" type="Label" parent="."]
+margin_left = 14.0
+margin_top = 263.291
+margin_right = 145.0
+margin_bottom = 277.291
+text = "LEFT/RIGHT - MOVE
+UP - JUMP"
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="RigidBody2D" type="RigidBody2D" parent="."]
+position = Vector2( 250, 460 )
+collision_mask = 2147483649
+mode = 2
+physics_material_override = SubResource( 1 )
+contacts_reported = 4
+contact_monitor = true
+script = ExtResource( 6 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="RigidBody2D"]
+shape = SubResource( 2 )
+
+[node name="KinematicBody2D" type="KinematicBody2D" parent="."]
+position = Vector2( 250, 460 )
+collision_mask = 2147483649
+script = ExtResource( 7 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="KinematicBody2D"]
+shape = SubResource( 2 )
+
+[node name="KinematicBodyRay2D" type="KinematicBody2D" parent="."]
+position = Vector2( 250, 460 )
+collision_mask = 2147483649
+script = ExtResource( 7 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="KinematicBodyRay2D"]
+position = Vector2( 0, -8 )
+shape = SubResource( 3 )
+
+[node name="CollisionShapeRay2D" type="CollisionShape2D" parent="KinematicBodyRay2D"]
+position = Vector2( 0, 8 )
+shape = SubResource( 4 )
+
+[node name="CollisionShapeRay2DLeft" type="CollisionShape2D" parent="KinematicBodyRay2D"]
+position = Vector2( -16, 8 )
+shape = SubResource( 4 )
+
+[node name="CollisionShapeRay2DRight" type="CollisionShape2D" parent="KinematicBodyRay2D"]
+position = Vector2( 16, 8 )
+shape = SubResource( 4 )
+
+[node name="StaticSceneFlat" parent="." instance=ExtResource( 4 )]
+position = Vector2( 0, 12 )
+
+[node name="TileMap" type="TileMap" parent="."]
+tile_set = ExtResource( 5 )
+cell_size = Vector2( 32, 32 )
+format = 1
+tile_data = PoolIntArray( 458764, 5, 0, 458765, 5, 0, 458766, 5, 0, 458767, 5, 0, 458768, 5, 0, 458769, 5, 0, 458770, 5, 0, 458771, 5, 0, 524300, 5, 0, 524301, 5, 0, 524302, 5, 0, 524303, 5, 0, 524304, 5, 0, 524305, 5, 0, 524306, 5, 0, 524307, 5, 0, 589836, 5, 0, 589837, 5, 0, 589838, 5, 0, 589839, 5, 0, 589840, 5, 0, 589841, 5, 0, 589842, 5, 0, 589843, 5, 0, 655372, 5, 0, 655373, 5, 0, 655374, 5, 0, 655375, 5, 0, 655376, 5, 0, 655377, 5, 0, 655378, 5, 0, 655379, 5, 0, 720908, 5, 0, 720909, 5, 0, 720910, 5, 0, 720911, 5, 0, 720912, 5, 0, 720913, 5, 0, 720914, 5, 0, 720915, 5, 0, 720922, 0, 0, 720923, 0, 0, 720924, 0, 0, 720925, 0, 0, 786438, 5, 0, 786439, 5, 0, 786440, 5, 0, 786441, 5, 0, 786444, 5, 0, 786445, 5, 0, 786446, 5, 0, 786447, 5, 0, 786448, 5, 0, 786449, 5, 0, 786450, 5, 0, 786451, 5, 0, 851980, 5, 0, 851981, 5, 0, 851982, 5, 0, 851983, 5, 0, 851984, 5, 0, 851985, 5, 0, 851986, 5, 0, 851987, 5, 0, 851992, 0, 0, 851993, 0, 0, 851994, 0, 0, 851995, 0, 0, 917516, 5, 0, 917517, 5, 0, 917518, 5, 0, 917519, 5, 0, 917520, 5, 0, 917521, 5, 0, 917522, 5, 0, 917523, 5, 0, 983052, 5, 0, 983053, 5, 0, 983054, 5, 0, 983055, 5, 0, 983056, 5, 0, 983057, 5, 0, 983058, 5, 0, 983059, 5, 0 )

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

@@ -1,7 +1,7 @@
 [gd_scene load_steps=8 format=2]
 
 [ext_resource path="res://tests/functional/test_collision_pairs.gd" type="Script" id=1]
-[ext_resource path="res://assets/godot-head.png" type="Texture" id=2]
+[ext_resource path="res://assets/texture/godot-head.png" type="Texture" id=2]
 [ext_resource path="res://tests/test_options.tscn" type="PackedScene" id=3]
 
 [sub_resource type="RectangleShape2D" id=1]

+ 1 - 0
2d/physics_tests/tests/functional/test_one_way_collision.gd

@@ -1,6 +1,7 @@
 extends Test
 tool
 
+
 const OPTION_OBJECT_TYPE_RIGIDBODY = "Object type/Rigid body (1)"
 const OPTION_OBJECT_TYPE_KINEMATIC = "Object type/Kinematic body (2)"
 

+ 1 - 1
2d/physics_tests/tests/functional/test_raycasting.tscn

@@ -1,6 +1,6 @@
 [gd_scene load_steps=7 format=2]
 
-[ext_resource path="res://assets/godot-head.png" type="Texture" id=1]
+[ext_resource path="res://assets/texture/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]

+ 1 - 1
2d/physics_tests/tests/functional/test_shapes.tscn

@@ -1,6 +1,6 @@
 [gd_scene load_steps=7 format=2]
 
-[ext_resource path="res://assets/godot-head.png" type="Texture" id=1]
+[ext_resource path="res://assets/texture/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]
 

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

@@ -2,7 +2,7 @@
 
 [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://assets/texture/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]

+ 1 - 1
2d/physics_tests/tests/static_scene_flat.tscn

@@ -1,7 +1,7 @@
 [gd_scene load_steps=2 format=2]
 
 [sub_resource type="RectangleShape2D" id=1]
-extents = Vector2( 512, 50 )
+extents = Vector2( 800, 50 )
 
 [node name="StaticSceneFlat" type="Node2D"]
 

+ 52 - 0
2d/physics_tests/utils/kinematicbody_controller.gd

@@ -0,0 +1,52 @@
+extends KinematicBody2D
+
+
+var _initial_velocity = Vector2.ZERO
+var _constant_velocity = Vector2.ZERO
+var _velocity = Vector2.ZERO
+var _snap = Vector2.ZERO
+var _floor_max_angle = 45.0
+var _stop_on_slope = false
+var _jumping = false
+var _keep_velocity = false
+
+
+func _physics_process(_delta):
+	if _initial_velocity != Vector2.ZERO:
+		_velocity = _initial_velocity
+		_initial_velocity = Vector2.ZERO
+		_keep_velocity = true
+	elif _constant_velocity != Vector2.ZERO:
+		_velocity = _constant_velocity
+	elif not _keep_velocity:
+		_velocity.x = 0.0
+
+	# Handle horizontal controls.
+	if Input.is_action_pressed("character_left"):
+		if position.x > 0.0:
+			_velocity.x = -400.0
+			_keep_velocity = false
+			_constant_velocity = Vector2.ZERO
+	elif Input.is_action_pressed("character_right"):
+		if position.x < 1024.0:
+			_velocity.x = 400.0
+			_keep_velocity = false
+			_constant_velocity = Vector2.ZERO
+
+	# Handle jump controls and gravity.
+	if is_on_floor():
+		if not _jumping and Input.is_action_just_pressed("character_jump"):
+			# Start jumping.
+			_jumping = true
+			_velocity.y = -1000.0
+		elif not _jumping:
+			# Apply velocity when standing for floor detection.
+			_velocity.y = 10.0
+	else:
+		# Apply gravity and get jump ready.
+		_jumping = false
+		_velocity.y += 50.0
+
+	var snap = _snap if not _jumping else Vector2.ZERO
+	var max_angle = deg2rad(_floor_max_angle)
+	move_and_slide_with_snap(_velocity, snap, Vector2.UP, _stop_on_slope, 4, max_angle)

+ 62 - 0
2d/physics_tests/utils/rigidbody_controller.gd

@@ -0,0 +1,62 @@
+extends RigidBody2D
+
+
+var _initial_velocity = Vector2.ZERO
+var _constant_velocity = Vector2.ZERO
+var _velocity = Vector2.ZERO
+var _on_floor = false
+var _jumping = false
+var _keep_velocity = false
+
+
+func _physics_process(_delta):
+	if _initial_velocity != Vector2.ZERO:
+		_velocity = _initial_velocity
+		_initial_velocity = Vector2.ZERO
+		_keep_velocity = true
+	elif _constant_velocity != Vector2.ZERO:
+		_velocity = _constant_velocity
+	elif not _keep_velocity:
+		_velocity.x = 0.0
+
+	# Handle horizontal controls.
+	if Input.is_action_pressed("character_left"):
+		if position.x > 0.0:
+			_velocity.x = -400.0
+			_keep_velocity = false
+			_constant_velocity = Vector2.ZERO
+	elif Input.is_action_pressed("character_right"):
+		if position.x < 1024.0:
+			_velocity.x = 400.0
+			_keep_velocity = false
+			_constant_velocity = Vector2.ZERO
+
+	# Handle jump controls and gravity.
+	if is_on_floor():
+		if not _jumping and Input.is_action_just_pressed("character_jump"):
+			# Start jumping.
+			_jumping = true
+			_velocity.y = -1000.0
+		elif not _jumping:
+			# Apply velocity when standing for floor detection.
+			_velocity.y = 10.0
+	else:
+		# Apply gravity and get jump ready.
+		_jumping = false
+		_velocity.y += 50.0
+
+	linear_velocity = _velocity
+
+
+func _integrate_forces(state):
+	_on_floor = false
+
+	var contacts = state.get_contact_count()
+	for i in contacts:
+		var pos = state.get_contact_collider_position(i)
+		if pos.y > position.y:
+			_on_floor = true
+
+
+func is_on_floor():
+	return _on_floor