Răsfoiți Sursa

Merge pull request #63479 from DarkKilauea/nav-link

Rémi Verschelde 3 ani în urmă
părinte
comite
2e0cffdb6f
34 a modificat fișierele cu 2563 adăugiri și 49 ștergeri
  1. 55 0
      doc/classes/NavigationLink2D.xml
  2. 55 0
      doc/classes/NavigationLink3D.xml
  3. 133 0
      doc/classes/NavigationServer2D.xml
  4. 133 0
      doc/classes/NavigationServer3D.xml
  5. 18 0
      doc/classes/ProjectSettings.xml
  6. 2 0
      editor/editor_node.cpp
  7. 4 0
      editor/icons/NavigationLink2D.svg
  8. 4 0
      editor/icons/NavigationLink3D.svg
  9. 191 0
      editor/plugins/navigation_link_2d_editor_plugin.cpp
  10. 83 0
      editor/plugins/navigation_link_2d_editor_plugin.h
  11. 170 0
      editor/plugins/node_3d_editor_gizmos.cpp
  12. 17 0
      editor/plugins/node_3d_editor_gizmos.h
  13. 1 0
      editor/plugins/node_3d_editor_plugin.cpp
  14. 171 0
      modules/navigation/godot_navigation_server.cpp
  15. 22 0
      modules/navigation/godot_navigation_server.h
  16. 56 0
      modules/navigation/nav_base.h
  17. 60 0
      modules/navigation/nav_link.cpp
  18. 69 0
      modules/navigation/nav_link.h
  19. 154 10
      modules/navigation/nav_map.cpp
  20. 20 1
      modules/navigation/nav_map.h
  21. 0 8
      modules/navigation/nav_region.cpp
  22. 2 19
      modules/navigation/nav_region.h
  23. 14 8
      modules/navigation/nav_utils.h
  24. 284 0
      scene/2d/navigation_link_2d.cpp
  25. 88 0
      scene/2d/navigation_link_2d.h
  26. 389 0
      scene/3d/navigation_link_3d.cpp
  27. 90 0
      scene/3d/navigation_link_3d.h
  28. 4 0
      scene/register_scene_types.cpp
  29. 1 0
      scene/resources/world_2d.cpp
  30. 1 0
      scene/resources/world_3d.cpp
  31. 63 0
      servers/navigation_server_2d.cpp
  32. 44 0
      servers/navigation_server_2d.h
  33. 101 0
      servers/navigation_server_3d.cpp
  34. 64 3
      servers/navigation_server_3d.h

+ 55 - 0
doc/classes/NavigationLink2D.xml

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="NavigationLink2D" inherits="Node2D" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
+	<brief_description>
+		Creates a link between two locations that [NavigationServer2D] can route agents through.
+	</brief_description>
+	<description>
+		Creates a link between two locations that [NavigationServer2D] can route agents through.  Links can be used to express navigation methods that aren't just traveling along the surface of the navigation mesh, like zip-lines, teleporters, or jumping across gaps.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_navigation_layer_value" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="layer_number" type="int" />
+			<description>
+				Returns whether or not the specified layer of the [member navigation_layers] bitmask is enabled, given a [code]layer_number[/code] between 1 and 32.
+			</description>
+		</method>
+		<method name="set_navigation_layer_value">
+			<return type="void" />
+			<param index="0" name="layer_number" type="int" />
+			<param index="1" name="value" type="bool" />
+			<description>
+				Based on [code]value[/code], enables or disables the specified layer in the [member navigation_layers] bitmask, given a [code]layer_number[/code] between 1 and 32.
+			</description>
+		</method>
+	</methods>
+	<members>
+		<member name="bidirectional" type="bool" setter="set_bidirectional" getter="is_bidirectional" default="true">
+			Whether this link can be traveled in both directions or only from [member start_location] to [member end_location].
+		</member>
+		<member name="enabled" type="bool" setter="set_enabled" getter="is_enabled" default="true">
+			Whether this link is currently active. If [code]false[/code], [method NavigationServer2D.map_get_path] will ignore this link.
+		</member>
+		<member name="end_location" type="Vector2" setter="set_end_location" getter="get_end_location" default="Vector2(0, 0)">
+			Ending position of the link.
+			This position will search out the nearest polygon in the navigation mesh to attach to.
+			The distance the link will search is controlled by [method NavigationServer2D.map_set_link_connection_radius].
+		</member>
+		<member name="enter_cost" type="float" setter="set_enter_cost" getter="get_enter_cost" default="0.0">
+			When pathfinding enters this link from another regions navmesh the [code]enter_cost[/code] value is added to the path distance for determining the shortest path.
+		</member>
+		<member name="navigation_layers" type="int" setter="set_navigation_layers" getter="get_navigation_layers" default="1">
+			A bitfield determining all navigation layers the link belongs to. These navigation layers will be checked when requesting a path with [method NavigationServer2D.map_get_path].
+		</member>
+		<member name="start_location" type="Vector2" setter="set_start_location" getter="get_start_location" default="Vector2(0, 0)">
+			Starting position of the link.
+			This position will search out the nearest polygon in the navigation mesh to attach to.
+			The distance the link will search is controlled by [method NavigationServer2D.map_set_link_connection_radius].
+		</member>
+		<member name="travel_cost" type="float" setter="set_travel_cost" getter="get_travel_cost" default="1.0">
+			When pathfinding moves along the link the traveled distance is multiplied with [code]travel_cost[/code] for determining the shortest path.
+		</member>
+	</members>
+</class>

+ 55 - 0
doc/classes/NavigationLink3D.xml

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="NavigationLink3D" inherits="Node3D" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
+	<brief_description>
+		Creates a link between two locations that [NavigationServer3D] can route agents through.
+	</brief_description>
+	<description>
+		Creates a link between two locations that [NavigationServer3D] can route agents through.  Links can be used to express navigation methods that aren't just traveling along the surface of the navigation mesh, like zip-lines, teleporters, or jumping across gaps.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_navigation_layer_value" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="layer_number" type="int" />
+			<description>
+				Returns whether or not the specified layer of the [member navigation_layers] bitmask is enabled, given a [code]layer_number[/code] between 1 and 32.
+			</description>
+		</method>
+		<method name="set_navigation_layer_value">
+			<return type="void" />
+			<param index="0" name="layer_number" type="int" />
+			<param index="1" name="value" type="bool" />
+			<description>
+				Based on [code]value[/code], enables or disables the specified layer in the [member navigation_layers] bitmask, given a [code]layer_number[/code] between 1 and 32.
+			</description>
+		</method>
+	</methods>
+	<members>
+		<member name="bidirectional" type="bool" setter="set_bidirectional" getter="is_bidirectional" default="true">
+			Whether this link can be traveled in both directions or only from [member start_location] to [member end_location].
+		</member>
+		<member name="enabled" type="bool" setter="set_enabled" getter="is_enabled" default="true">
+			Whether this link is currently active. If [code]false[/code], [method NavigationServer3D.map_get_path] will ignore this link.
+		</member>
+		<member name="end_location" type="Vector3" setter="set_end_location" getter="get_end_location" default="Vector3(0, 0, 0)">
+			Ending position of the link.
+			This position will search out the nearest polygon in the navigation mesh to attach to.
+			The distance the link will search is controlled by [method NavigationServer3D.map_set_link_connection_radius].
+		</member>
+		<member name="enter_cost" type="float" setter="set_enter_cost" getter="get_enter_cost" default="0.0">
+			When pathfinding enters this link from another regions navmesh the [code]enter_cost[/code] value is added to the path distance for determining the shortest path.
+		</member>
+		<member name="navigation_layers" type="int" setter="set_navigation_layers" getter="get_navigation_layers" default="1">
+			A bitfield determining all navigation layers the link belongs to. These navigation layers will be checked when requesting a path with [method NavigationServer3D.map_get_path].
+		</member>
+		<member name="start_location" type="Vector3" setter="set_start_location" getter="get_start_location" default="Vector3(0, 0, 0)">
+			Starting position of the link.
+			This position will search out the nearest polygon in the navigation mesh to attach to.
+			The distance the link will search is controlled by [method NavigationServer3D.map_set_link_connection_radius].
+		</member>
+		<member name="travel_cost" type="float" setter="set_travel_cost" getter="get_travel_cost" default="1.0">
+			When pathfinding moves along the link the traveled distance is multiplied with [code]travel_cost[/code] for determining the shortest path.
+		</member>
+	</members>
+</class>

+ 133 - 0
doc/classes/NavigationServer2D.xml

@@ -133,6 +133,117 @@
 				Returns all created navigation map [RID]s on the NavigationServer. This returns both 2D and 3D created navigation maps as there is technically no distinction between them.
 			</description>
 		</method>
+		<method name="link_create" qualifiers="const">
+			<return type="RID" />
+			<description>
+				Create a new link between two locations on a map.
+			</description>
+		</method>
+		<method name="link_get_end_location" qualifiers="const">
+			<return type="Vector2" />
+			<param index="0" name="link" type="RID" />
+			<description>
+				Returns the ending location of this [code]link[/code].
+			</description>
+		</method>
+		<method name="link_get_enter_cost" qualifiers="const">
+			<return type="float" />
+			<param index="0" name="link" type="RID" />
+			<description>
+				Returns the [code]enter_cost[/code] of this [code]link[/code].
+			</description>
+		</method>
+		<method name="link_get_map" qualifiers="const">
+			<return type="RID" />
+			<param index="0" name="link" type="RID" />
+			<description>
+				Returns the navigation map [RID] the requested [code]link[/code] is currently assigned to.
+			</description>
+		</method>
+		<method name="link_get_navigation_layers" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="link" type="RID" />
+			<description>
+				Returns the navigation layers for this [code]link[/code].
+			</description>
+		</method>
+		<method name="link_get_start_location" qualifiers="const">
+			<return type="Vector2" />
+			<param index="0" name="link" type="RID" />
+			<description>
+				Returns the starting location of this [code]link[/code].
+			</description>
+		</method>
+		<method name="link_get_travel_cost" qualifiers="const">
+			<return type="float" />
+			<param index="0" name="link" type="RID" />
+			<description>
+				Returns the [code]travel_cost[/code] of this [code]link[/code].
+			</description>
+		</method>
+		<method name="link_is_bidirectional" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="link" type="RID" />
+			<description>
+				Returns whether this [code]link[/code] can be travelled in both directions.
+			</description>
+		</method>
+		<method name="link_set_bidirectional" qualifiers="const">
+			<return type="void" />
+			<param index="0" name="link" type="RID" />
+			<param index="1" name="bidirectional" type="bool" />
+			<description>
+				Sets whether this [code]link[/code] can be travelled in both directions.
+			</description>
+		</method>
+		<method name="link_set_end_location" qualifiers="const">
+			<return type="void" />
+			<param index="0" name="link" type="RID" />
+			<param index="1" name="location" type="Vector2" />
+			<description>
+				Sets the exit location for the [code]link[/code].
+			</description>
+		</method>
+		<method name="link_set_enter_cost" qualifiers="const">
+			<return type="void" />
+			<param index="0" name="link" type="RID" />
+			<param index="1" name="enter_cost" type="float" />
+			<description>
+				Sets the [code]enter_cost[/code] for this [code]link[/code].
+			</description>
+		</method>
+		<method name="link_set_map" qualifiers="const">
+			<return type="void" />
+			<param index="0" name="link" type="RID" />
+			<param index="1" name="map" type="RID" />
+			<description>
+				Sets the navigation map [RID] for the link.
+			</description>
+		</method>
+		<method name="link_set_navigation_layers" qualifiers="const">
+			<return type="void" />
+			<param index="0" name="link" type="RID" />
+			<param index="1" name="navigation_layers" type="int" />
+			<description>
+				Set the links's navigation layers. This allows selecting links from a path request (when using [method NavigationServer2D.map_get_path]).
+			</description>
+		</method>
+		<method name="link_set_start_location" qualifiers="const">
+			<return type="void" />
+			<param index="0" name="link" type="RID" />
+			<param index="1" name="location" type="Vector2" />
+			<description>
+				Sets the entry location for this [code]link[/code].
+			</description>
+		</method>
+		<method name="link_set_travel_cost" qualifiers="const">
+			<return type="void" />
+			<param index="0" name="link" type="RID" />
+			<param index="1" name="travel_cost" type="float" />
+			<description>
+				Sets the [code]travel_cost[/code] for this [code]link[/code].
+			</description>
+		</method>
 		<method name="map_create" qualifiers="const">
 			<return type="RID" />
 			<description>
@@ -186,6 +297,20 @@
 				Returns the edge connection margin of the map. The edge connection margin is a distance used to connect two regions.
 			</description>
 		</method>
+		<method name="map_get_link_connection_radius" qualifiers="const">
+			<return type="float" />
+			<param index="0" name="map" type="RID" />
+			<description>
+				Returns the link connection radius of the map. This distance is the maximum range any link will search for navigation mesh polygons to connect to.
+			</description>
+		</method>
+		<method name="map_get_links" qualifiers="const">
+			<return type="RID[]" />
+			<param index="0" name="map" type="RID" />
+			<description>
+				Returns all navigation link [RID]s that are currently assigned to the requested navigation [code]map[/code].
+			</description>
+		</method>
 		<method name="map_get_path" qualifiers="const">
 			<return type="PackedVector2Array" />
 			<param index="0" name="map" type="RID" />
@@ -235,6 +360,14 @@
 				Set the map edge connection margin used to weld the compatible region edges.
 			</description>
 		</method>
+		<method name="map_set_link_connection_radius" qualifiers="const">
+			<return type="void" />
+			<param index="0" name="map" type="RID" />
+			<param index="1" name="radius" type="float" />
+			<description>
+				Set the map's link connection radius used to connect links to navigation polygons.
+			</description>
+		</method>
 		<method name="region_create" qualifiers="const">
 			<return type="RID" />
 			<description>

+ 133 - 0
doc/classes/NavigationServer3D.xml

@@ -133,6 +133,117 @@
 				Returns all created navigation map [RID]s on the NavigationServer. This returns both 2D and 3D created navigation maps as there is technically no distinction between them.
 			</description>
 		</method>
+		<method name="link_create" qualifiers="const">
+			<return type="RID" />
+			<description>
+				Create a new link between two locations on a map.
+			</description>
+		</method>
+		<method name="link_get_end_location" qualifiers="const">
+			<return type="Vector3" />
+			<param index="0" name="link" type="RID" />
+			<description>
+				Returns the ending location of this [code]link[/code].
+			</description>
+		</method>
+		<method name="link_get_enter_cost" qualifiers="const">
+			<return type="float" />
+			<param index="0" name="link" type="RID" />
+			<description>
+				Returns the [code]enter_cost[/code] of this [code]link[/code].
+			</description>
+		</method>
+		<method name="link_get_map" qualifiers="const">
+			<return type="RID" />
+			<param index="0" name="link" type="RID" />
+			<description>
+				Returns the navigation map [RID] the requested [code]link[/code] is currently assigned to.
+			</description>
+		</method>
+		<method name="link_get_navigation_layers" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="link" type="RID" />
+			<description>
+				Returns the navigation layers for this [code]link[/code].
+			</description>
+		</method>
+		<method name="link_get_start_location" qualifiers="const">
+			<return type="Vector3" />
+			<param index="0" name="link" type="RID" />
+			<description>
+				Returns the starting location of this [code]link[/code].
+			</description>
+		</method>
+		<method name="link_get_travel_cost" qualifiers="const">
+			<return type="float" />
+			<param index="0" name="link" type="RID" />
+			<description>
+				Returns the [code]travel_cost[/code] of this [code]link[/code].
+			</description>
+		</method>
+		<method name="link_is_bidirectional" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="link" type="RID" />
+			<description>
+				Returns whether this [code]link[/code] can be travelled in both directions.
+			</description>
+		</method>
+		<method name="link_set_bidirectional" qualifiers="const">
+			<return type="void" />
+			<param index="0" name="link" type="RID" />
+			<param index="1" name="bidirectional" type="bool" />
+			<description>
+				Sets whether this [code]link[/code] can be travelled in both directions.
+			</description>
+		</method>
+		<method name="link_set_end_location" qualifiers="const">
+			<return type="void" />
+			<param index="0" name="link" type="RID" />
+			<param index="1" name="location" type="Vector3" />
+			<description>
+				Sets the exit location for the [code]link[/code].
+			</description>
+		</method>
+		<method name="link_set_enter_cost" qualifiers="const">
+			<return type="void" />
+			<param index="0" name="link" type="RID" />
+			<param index="1" name="enter_cost" type="float" />
+			<description>
+				Sets the [code]enter_cost[/code] for this [code]link[/code].
+			</description>
+		</method>
+		<method name="link_set_map" qualifiers="const">
+			<return type="void" />
+			<param index="0" name="link" type="RID" />
+			<param index="1" name="map" type="RID" />
+			<description>
+				Sets the navigation map [RID] for the link.
+			</description>
+		</method>
+		<method name="link_set_navigation_layers" qualifiers="const">
+			<return type="void" />
+			<param index="0" name="link" type="RID" />
+			<param index="1" name="navigation_layers" type="int" />
+			<description>
+				Set the links's navigation layers. This allows selecting links from a path request (when using [method NavigationServer3D.map_get_path]).
+			</description>
+		</method>
+		<method name="link_set_start_location" qualifiers="const">
+			<return type="void" />
+			<param index="0" name="link" type="RID" />
+			<param index="1" name="location" type="Vector3" />
+			<description>
+				Sets the entry location for this [code]link[/code].
+			</description>
+		</method>
+		<method name="link_set_travel_cost" qualifiers="const">
+			<return type="void" />
+			<param index="0" name="link" type="RID" />
+			<param index="1" name="travel_cost" type="float" />
+			<description>
+				Sets the [code]travel_cost[/code] for this [code]link[/code].
+			</description>
+		</method>
 		<method name="map_create" qualifiers="const">
 			<return type="RID" />
 			<description>
@@ -204,6 +315,20 @@
 				Returns the edge connection margin of the map. This distance is the minimum vertex distance needed to connect two edges from different regions.
 			</description>
 		</method>
+		<method name="map_get_link_connection_radius" qualifiers="const">
+			<return type="float" />
+			<param index="0" name="map" type="RID" />
+			<description>
+				Returns the link connection radius of the map. This distance is the maximum range any link will search for navigation mesh polygons to connect to.
+			</description>
+		</method>
+		<method name="map_get_links" qualifiers="const">
+			<return type="RID[]" />
+			<param index="0" name="map" type="RID" />
+			<description>
+				Returns all navigation link [RID]s that are currently assigned to the requested navigation [code]map[/code].
+			</description>
+		</method>
 		<method name="map_get_path" qualifiers="const">
 			<return type="PackedVector3Array" />
 			<param index="0" name="map" type="RID" />
@@ -260,6 +385,14 @@
 				Set the map edge connection margin used to weld the compatible region edges.
 			</description>
 		</method>
+		<method name="map_set_link_connection_radius" qualifiers="const">
+			<return type="void" />
+			<param index="0" name="map" type="RID" />
+			<param index="1" name="radius" type="float" />
+			<description>
+				Set the map's link connection radius used to connect links to navigation polygons.
+			</description>
+		</method>
 		<method name="map_set_up" qualifiers="const">
 			<return type="void" />
 			<param index="0" name="map" type="RID" />

+ 18 - 0
doc/classes/ProjectSettings.xml

@@ -497,6 +497,12 @@
 		<member name="debug/shapes/navigation/enable_geometry_face_random_color" type="bool" setter="" getter="" default="true">
 			If enabled, colorizes each navigation mesh polygon face with a random color when "Visible Navigation" is enabled in the Debug menu.
 		</member>
+		<member name="debug/shapes/navigation/enable_link_connections" type="bool" setter="" getter="" default="true">
+			If enabled, displays navigation link connections when "Visible Navigation" is enabled in the Debug menu.
+		</member>
+		<member name="debug/shapes/navigation/enable_link_connections_xray" type="bool" setter="" getter="" default="true">
+			If enabled, displays navigation link connections through geometry when "Visible Navigation" is enabled in the Debug menu.
+		</member>
 		<member name="debug/shapes/navigation/geometry_color" type="Color" setter="" getter="" default="Color(0.1, 1, 0.7, 0.4)">
 			Color of the navigation geometry, visible when "Visible Navigation" is enabled in the Debug menu.
 		</member>
@@ -512,6 +518,12 @@
 		<member name="debug/shapes/navigation/geometry_face_disabled_color" type="Color" setter="" getter="" default="Color(0.5, 0.5, 0.5, 0.4)">
 			Color to display disabled navigation mesh polygon faces, visible when "Visible Navigation" is enabled in the Debug menu.
 		</member>
+		<member name="debug/shapes/navigation/link_connection_color" type="Color" setter="" getter="" default="Color(1, 0.5, 1, 1)">
+			Color to use to display navigation link connections, visible when "Visible Navigation" is enabled in the Debug menu.
+		</member>
+		<member name="debug/shapes/navigation/link_connection_disabled_color" type="Color" setter="" getter="" default="Color(0.5, 0.5, 0.5, 1)">
+			Color to use to display disabled navigation link connections, visible when "Visible Navigation" is enabled in the Debug menu.
+		</member>
 		<member name="debug/shapes/paths/geometry_color" type="Color" setter="" getter="" default="Color(0.1, 1, 0.7, 0.4)">
 			Color of the curve path geometry, visible when "Visible Paths" is enabled in the Debug menu.
 		</member>
@@ -1439,12 +1451,18 @@
 		<member name="navigation/2d/default_edge_connection_margin" type="int" setter="" getter="" default="1">
 			Default edge connection margin for 2D navigation maps. See [method NavigationServer2D.map_set_edge_connection_margin].
 		</member>
+		<member name="navigation/2d/default_link_connection_radius" type="int" setter="" getter="" default="4">
+			Default link connection radius for 2D navigation maps. See [method NavigationServer2D.map_set_link_connection_radius].
+		</member>
 		<member name="navigation/3d/default_cell_size" type="float" setter="" getter="" default="0.25">
 			Default cell size for 3D navigation maps. See [method NavigationServer3D.map_set_cell_size].
 		</member>
 		<member name="navigation/3d/default_edge_connection_margin" type="float" setter="" getter="" default="0.25">
 			Default edge connection margin for 3D navigation maps. See [method NavigationServer3D.map_set_edge_connection_margin].
 		</member>
+		<member name="navigation/3d/default_link_connection_radius" type="float" setter="" getter="" default="1.0">
+			Default link connection radius for 3D navigation maps. See [method NavigationServer3D.map_set_link_connection_radius].
+		</member>
 		<member name="network/limits/debugger/max_chars_per_second" type="int" setter="" getter="" default="32768">
 			Maximum number of characters allowed to send as output from the debugger. Over this value, content is dropped. This helps not to stall the debugger connection.
 		</member>

+ 2 - 0
editor/editor_node.cpp

@@ -170,6 +170,7 @@
 #include "editor/plugins/mesh_instance_3d_editor_plugin.h"
 #include "editor/plugins/mesh_library_editor_plugin.h"
 #include "editor/plugins/multimesh_editor_plugin.h"
+#include "editor/plugins/navigation_link_2d_editor_plugin.h"
 #include "editor/plugins/navigation_polygon_editor_plugin.h"
 #include "editor/plugins/node_3d_editor_plugin.h"
 #include "editor/plugins/occluder_instance_3d_editor_plugin.h"
@@ -7349,6 +7350,7 @@ EditorNode::EditorNode() {
 	add_editor_plugin(memnew(GPUParticles2DEditorPlugin));
 	add_editor_plugin(memnew(LightOccluder2DEditorPlugin));
 	add_editor_plugin(memnew(Line2DEditorPlugin));
+	add_editor_plugin(memnew(NavigationLink2DEditorPlugin));
 	add_editor_plugin(memnew(NavigationPolygonEditorPlugin));
 	add_editor_plugin(memnew(Path2DEditorPlugin));
 	add_editor_plugin(memnew(Polygon2DEditorPlugin));

+ 4 - 0
editor/icons/NavigationLink2D.svg

@@ -0,0 +1,4 @@
+<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+<path d="m12.386 5.3097c-0.69157-0.021112-1.3071 0.36382-1.7492 0.86685-0.58 0.58-1.16 1.16-1.74 1.74 0.4588-0.28502 1.0599-0.064948 1.4771-0.037996 0.45549-0.44357 0.89024-0.91006 1.3596-1.3383 0.56256-0.44564 1.4906-0.15731 1.7028 0.52802 0.18967 0.4871-0.049221 1.0098-0.43284 1.3208-0.70048 0.68896-1.3789 1.4022-2.0935 2.0755-0.47999 0.3725-1.2044 0.226-1.5679-0.24034-0.38763-0.38194-1.0641 0.16031-0.78317 0.6241 0.6767 0.94379 2.1573 1.1282 3.0411 0.36751 0.80287-0.7704 1.5793-1.5696 2.3665-2.3564 0.79925-0.83719 0.70104-2.3112-0.19552-3.0393-0.38108-0.32877-0.8822-0.5119-1.385-0.51049zm-3.051 3.051c-0.69157-0.021106-1.3071 0.36382-1.7492 0.86685-0.67513 0.68452-1.37 1.3506-2.0319 2.0474-0.75433 0.87744-0.58087 2.3428 0.34933 3.0252 0.84748 0.68613 2.192 0.54839 2.8998-0.27341 0.63032-0.63031 1.2606-1.2606 1.8909-1.8909-0.4587 0.28554-1.0602 0.0659-1.477 0.038069-0.45445 0.44348-0.88773 0.91034-1.3564 1.3383-0.56256 0.44565-1.4906 0.15731-1.7028-0.52802-0.18967-0.4871 0.049229-1.0098 0.43284-1.3208 0.70048-0.68896 1.3789-1.4022 2.0935-2.0755 0.48-0.3725 1.2044-0.22601 1.5679 0.24036 0.38733 0.38325 1.064-0.16067 0.78313-0.6241-0.39353-0.52481-1.0429-0.84871-1.7002-0.8434z" fill="#8ea6f4" fill-opacity=".99608" stroke-linecap="round" stroke-linejoin="round" stroke-width=".013911"/>
+<path d="m2 1c-0.61942-0.0066969-1.0877 0.60314-1 1.198 0.00345 3.968-0.006897 7.9364 0.00517 11.904 0.043388 0.62851 0.69346 0.98513 1.272 0.89776h2.5896c-0.77174-0.5015-1.2078-1.2613-1.3143-2.3356-0.11601-1.1701 0.63729-2.024 1.6748-3.1566 0.65335-0.71326 1.4757-1.5822 2.3587-2.3316 0.76308-0.64765 1.7509-1.679 2.9376-2.578 0.91259-0.69136 2.2893-0.74691 3.1014-0.33143 0.91184 0.46649 1.2635 1.1209 1.4067 1.3826-0.0052-2.335-0.02135-1.3526-0.03955-3.6863 5e-3 -0.64349-0.67497-1.0568-1.2694-0.96289z" fill="#8ea6f4" fill-opacity=".99608"/>
+</svg>

+ 4 - 0
editor/icons/NavigationLink3D.svg

@@ -0,0 +1,4 @@
+<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+<path d="m12.386 5.3097c-0.69157-0.021112-1.3071 0.36382-1.7492 0.86685-0.58 0.58-1.16 1.16-1.74 1.74 0.4588-0.28502 1.0599-0.064948 1.4771-0.037996 0.45549-0.44357 0.89024-0.91006 1.3596-1.3383 0.56256-0.44564 1.4906-0.15731 1.7028 0.52802 0.18967 0.4871-0.049221 1.0098-0.43284 1.3208-0.70048 0.68896-1.3789 1.4022-2.0935 2.0755-0.47999 0.3725-1.2044 0.226-1.5679-0.24034-0.38763-0.38194-1.0641 0.16031-0.78317 0.6241 0.6767 0.94379 2.1573 1.1282 3.0411 0.36751 0.80287-0.7704 1.5793-1.5696 2.3665-2.3564 0.79925-0.83719 0.70104-2.3112-0.19552-3.0393-0.38108-0.32877-0.8822-0.5119-1.385-0.51049zm-3.051 3.051c-0.69157-0.021106-1.3071 0.36382-1.7492 0.86685-0.67513 0.68452-1.37 1.3506-2.0319 2.0474-0.75433 0.87744-0.58087 2.3428 0.34933 3.0252 0.84748 0.68613 2.192 0.54839 2.8998-0.27341 0.63032-0.63031 1.2606-1.2606 1.8909-1.8909-0.4587 0.28554-1.0602 0.0659-1.477 0.038069-0.45445 0.44348-0.88773 0.91034-1.3564 1.3383-0.56256 0.44565-1.4906 0.15731-1.7028-0.52802-0.18967-0.4871 0.049229-1.0098 0.43284-1.3208 0.70048-0.68896 1.3789-1.4022 2.0935-2.0755 0.48-0.3725 1.2044-0.22601 1.5679 0.24036 0.38733 0.38325 1.064-0.16067 0.78313-0.6241-0.39353-0.52481-1.0429-0.84871-1.7002-0.8434z" fill="#fc7e7e" fill-opacity=".99608" stroke-linecap="round" stroke-linejoin="round" stroke-width=".013911"/>
+<path d="m2 1c-0.61942-0.0066969-1.0877 0.60314-1 1.198 0.00345 3.968-0.006897 7.9364 0.00517 11.904 0.043388 0.62851 0.69346 0.98513 1.272 0.89776h2.5896c-0.77174-0.5015-1.2078-1.2613-1.3143-2.3356-0.11601-1.1701 0.63729-2.024 1.6748-3.1566 0.65335-0.71326 1.4757-1.5822 2.3587-2.3316 0.76308-0.64765 1.7509-1.679 2.9376-2.578 0.91259-0.69136 2.2893-0.74691 3.1014-0.33143 0.91184 0.46649 1.2635 1.1209 1.4067 1.3826-0.0052-2.335-0.02135-1.3526-0.03955-3.6863 5e-3 -0.64349-0.67497-1.0568-1.2694-0.96289z" fill="#fc7d7d" fill-opacity=".99608"/>
+</svg>

+ 191 - 0
editor/plugins/navigation_link_2d_editor_plugin.cpp

@@ -0,0 +1,191 @@
+/*************************************************************************/
+/*  navigation_link_2d_editor_plugin.cpp                                 */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#include "navigation_link_2d_editor_plugin.h"
+
+#include "canvas_item_editor_plugin.h"
+#include "editor/editor_node.h"
+#include "editor/editor_settings.h"
+#include "editor/editor_undo_redo_manager.h"
+#include "servers/navigation_server_3d.h"
+
+void NavigationLink2DEditor::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_ENTER_TREE: {
+			get_tree()->connect("node_removed", callable_mp(this, &NavigationLink2DEditor::_node_removed));
+		} break;
+
+		case NOTIFICATION_EXIT_TREE: {
+			get_tree()->disconnect("node_removed", callable_mp(this, &NavigationLink2DEditor::_node_removed));
+		} break;
+	}
+}
+
+void NavigationLink2DEditor::_node_removed(Node *p_node) {
+	if (p_node == node) {
+		node = nullptr;
+	}
+}
+
+bool NavigationLink2DEditor::forward_canvas_gui_input(const Ref<InputEvent> &p_event) {
+	if (!node || !node->is_visible_in_tree()) {
+		return false;
+	}
+
+	real_t grab_threshold = EDITOR_GET("editors/polygon_editor/point_grab_radius");
+	Transform2D xform = canvas_item_editor->get_canvas_transform() * node->get_global_transform();
+
+	Ref<InputEventMouseButton> mb = p_event;
+	if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT) {
+		if (mb->is_pressed()) {
+			// Start location
+			if (xform.xform(node->get_start_location()).distance_to(mb->get_position()) < grab_threshold) {
+				start_grabbed = true;
+				original_start_location = node->get_start_location();
+
+				return true;
+			} else {
+				start_grabbed = false;
+			}
+
+			// End location
+			if (xform.xform(node->get_end_location()).distance_to(mb->get_position()) < grab_threshold) {
+				end_grabbed = true;
+				original_end_location = node->get_end_location();
+
+				return true;
+			} else {
+				end_grabbed = false;
+			}
+		} else {
+			if (start_grabbed) {
+				undo_redo->create_action(TTR("Set start_location"));
+				undo_redo->add_do_method(node, "set_start_location", node->get_start_location());
+				undo_redo->add_do_method(canvas_item_editor, "update_viewport");
+				undo_redo->add_undo_method(node, "set_start_location", original_start_location);
+				undo_redo->add_undo_method(canvas_item_editor, "update_viewport");
+				undo_redo->commit_action();
+
+				start_grabbed = false;
+
+				return true;
+			}
+
+			if (end_grabbed) {
+				undo_redo->create_action(TTR("Set end_location"));
+				undo_redo->add_do_method(node, "set_end_location", node->get_end_location());
+				undo_redo->add_do_method(canvas_item_editor, "update_viewport");
+				undo_redo->add_undo_method(node, "set_end_location", original_end_location);
+				undo_redo->add_undo_method(canvas_item_editor, "update_viewport");
+				undo_redo->commit_action();
+
+				end_grabbed = false;
+
+				return true;
+			}
+		}
+	}
+
+	Ref<InputEventMouseMotion> mm = p_event;
+	if (mm.is_valid()) {
+		Vector2 point = canvas_item_editor->snap_point(canvas_item_editor->get_canvas_transform().affine_inverse().xform(mm->get_position()));
+		point = node->get_global_transform().affine_inverse().xform(point);
+
+		if (start_grabbed) {
+			node->set_start_location(point);
+			canvas_item_editor->update_viewport();
+
+			return true;
+		}
+
+		if (end_grabbed) {
+			node->set_end_location(point);
+			canvas_item_editor->update_viewport();
+
+			return true;
+		}
+	}
+
+	return false;
+}
+
+void NavigationLink2DEditor::forward_canvas_draw_over_viewport(Control *p_overlay) {
+	if (!node || !node->is_visible_in_tree()) {
+		return;
+	}
+
+	Transform2D gt = canvas_item_editor->get_canvas_transform() * node->get_global_transform();
+	Vector2 global_start_location = gt.xform(node->get_start_location());
+	Vector2 global_end_location = gt.xform(node->get_end_location());
+
+	// Only drawing the handles here, since the debug rendering will fill in the rest.
+	const Ref<Texture2D> handle = get_theme_icon(SNAME("EditorHandle"), SNAME("EditorIcons"));
+	p_overlay->draw_texture(handle, global_start_location - handle->get_size() / 2);
+	p_overlay->draw_texture(handle, global_end_location - handle->get_size() / 2);
+}
+
+void NavigationLink2DEditor::edit(NavigationLink2D *p_node) {
+	if (!canvas_item_editor) {
+		canvas_item_editor = CanvasItemEditor::get_singleton();
+	}
+
+	if (p_node) {
+		node = p_node;
+	} else {
+		node = nullptr;
+	}
+
+	canvas_item_editor->update_viewport();
+}
+
+NavigationLink2DEditor::NavigationLink2DEditor() {
+	undo_redo = EditorNode::get_undo_redo();
+}
+
+///////////////////////
+
+void NavigationLink2DEditorPlugin::edit(Object *p_object) {
+	editor->edit(Object::cast_to<NavigationLink2D>(p_object));
+}
+
+bool NavigationLink2DEditorPlugin::handles(Object *p_object) const {
+	return Object::cast_to<NavigationLink2D>(p_object) != nullptr;
+}
+
+void NavigationLink2DEditorPlugin::make_visible(bool p_visible) {
+	if (!p_visible) {
+		edit(nullptr);
+	}
+}
+
+NavigationLink2DEditorPlugin::NavigationLink2DEditorPlugin() {
+	editor = memnew(NavigationLink2DEditor);
+	EditorNode::get_singleton()->get_gui_base()->add_child(editor);
+}

+ 83 - 0
editor/plugins/navigation_link_2d_editor_plugin.h

@@ -0,0 +1,83 @@
+/*************************************************************************/
+/*  navigation_link_2d_editor_plugin.h                                   */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#ifndef NAVIGATION_LINK_2D_EDITOR_PLUGIN_H
+#define NAVIGATION_LINK_2D_EDITOR_PLUGIN_H
+
+#include "editor/editor_plugin.h"
+#include "scene/2d/navigation_link_2d.h"
+
+class CanvasItemEditor;
+class EditorUndoRedoManager;
+
+class NavigationLink2DEditor : public Control {
+	GDCLASS(NavigationLink2DEditor, Control);
+
+	Ref<EditorUndoRedoManager> undo_redo;
+	CanvasItemEditor *canvas_item_editor = nullptr;
+	NavigationLink2D *node;
+
+	bool start_grabbed = false;
+	Vector2 original_start_location;
+
+	bool end_grabbed = false;
+	Vector2 original_end_location;
+
+protected:
+	void _notification(int p_what);
+	void _node_removed(Node *p_node);
+
+public:
+	bool forward_canvas_gui_input(const Ref<InputEvent> &p_event);
+	void forward_canvas_draw_over_viewport(Control *p_overlay);
+	void edit(NavigationLink2D *p_node);
+
+	NavigationLink2DEditor();
+};
+
+class NavigationLink2DEditorPlugin : public EditorPlugin {
+	GDCLASS(NavigationLink2DEditorPlugin, EditorPlugin);
+
+	NavigationLink2DEditor *editor = nullptr;
+
+public:
+	virtual bool forward_canvas_gui_input(const Ref<InputEvent> &p_event) override { return editor->forward_canvas_gui_input(p_event); }
+	virtual void forward_canvas_draw_over_viewport(Control *p_overlay) override { editor->forward_canvas_draw_over_viewport(p_overlay); }
+
+	virtual String get_name() const override { return "NavigationLink2D"; }
+	bool has_main_screen() const override { return false; }
+	virtual void edit(Object *p_object) override;
+	virtual bool handles(Object *p_object) const override;
+	virtual void make_visible(bool p_visible) override;
+
+	NavigationLink2DEditorPlugin();
+};
+
+#endif // NAVIGATION_LINK_2D_EDITOR_PLUGIN_H

+ 170 - 0
editor/plugins/node_3d_editor_gizmos.cpp

@@ -54,6 +54,7 @@
 #include "scene/3d/lightmap_probe.h"
 #include "scene/3d/marker_3d.h"
 #include "scene/3d/mesh_instance_3d.h"
+#include "scene/3d/navigation_link_3d.h"
 #include "scene/3d/navigation_region_3d.h"
 #include "scene/3d/occluder_instance_3d.h"
 #include "scene/3d/ray_cast_3d.h"
@@ -4999,6 +5000,175 @@ void NavigationRegion3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) {
 	}
 }
 
+////
+
+NavigationLink3DGizmoPlugin::NavigationLink3DGizmoPlugin() {
+	create_material("navigation_link_material", NavigationServer3D::get_singleton()->get_debug_navigation_link_connection_color());
+	create_material("navigation_link_material_disabled", NavigationServer3D::get_singleton()->get_debug_navigation_link_connection_disabled_color());
+	create_handle_material("handles");
+}
+
+bool NavigationLink3DGizmoPlugin::has_gizmo(Node3D *p_spatial) {
+	return Object::cast_to<NavigationLink3D>(p_spatial) != nullptr;
+}
+
+String NavigationLink3DGizmoPlugin::get_gizmo_name() const {
+	return "NavigationLink3D";
+}
+
+int NavigationLink3DGizmoPlugin::get_priority() const {
+	return -1;
+}
+
+void NavigationLink3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) {
+	NavigationLink3D *link = Object::cast_to<NavigationLink3D>(p_gizmo->get_spatial_node());
+
+	RID nav_map = link->get_world_3d()->get_navigation_map();
+	real_t search_radius = NavigationServer3D::get_singleton()->map_get_link_connection_radius(nav_map);
+	Vector3 up_vector = NavigationServer3D::get_singleton()->map_get_up(nav_map);
+	Vector3::Axis up_axis = up_vector.max_axis_index();
+
+	Vector3 start_location = link->get_start_location();
+	Vector3 end_location = link->get_end_location();
+
+	Ref<Material> link_material = get_material("navigation_link_material", p_gizmo);
+	Ref<Material> link_material_disabled = get_material("navigation_link_material_disabled", p_gizmo);
+	Ref<Material> handles_material = get_material("handles");
+
+	p_gizmo->clear();
+
+	// Draw line between the points.
+	Vector<Vector3> lines;
+	lines.append(start_location);
+	lines.append(end_location);
+
+	// Draw start location search radius
+	for (int i = 0; i < 30; i++) {
+		// Create a circle
+		const float ra = Math::deg_to_rad((float)(i * 12));
+		const float rb = Math::deg_to_rad((float)((i + 1) * 12));
+		const Point2 a = Vector2(Math::sin(ra), Math::cos(ra)) * search_radius;
+		const Point2 b = Vector2(Math::sin(rb), Math::cos(rb)) * search_radius;
+
+		// Draw axis-aligned circle
+		switch (up_axis) {
+			case Vector3::AXIS_X:
+				lines.append(start_location + Vector3(0, a.x, a.y));
+				lines.append(start_location + Vector3(0, b.x, b.y));
+				break;
+			case Vector3::AXIS_Y:
+				lines.append(start_location + Vector3(a.x, 0, a.y));
+				lines.append(start_location + Vector3(b.x, 0, b.y));
+				break;
+			case Vector3::AXIS_Z:
+				lines.append(start_location + Vector3(a.x, a.y, 0));
+				lines.append(start_location + Vector3(b.x, b.y, 0));
+				break;
+		}
+	}
+
+	// Draw end location search radius
+	for (int i = 0; i < 30; i++) {
+		// Create a circle
+		const float ra = Math::deg_to_rad((float)(i * 12));
+		const float rb = Math::deg_to_rad((float)((i + 1) * 12));
+		const Point2 a = Vector2(Math::sin(ra), Math::cos(ra)) * search_radius;
+		const Point2 b = Vector2(Math::sin(rb), Math::cos(rb)) * search_radius;
+
+		// Draw axis-aligned circle
+		switch (up_axis) {
+			case Vector3::AXIS_X:
+				lines.append(end_location + Vector3(0, a.x, a.y));
+				lines.append(end_location + Vector3(0, b.x, b.y));
+				break;
+			case Vector3::AXIS_Y:
+				lines.append(end_location + Vector3(a.x, 0, a.y));
+				lines.append(end_location + Vector3(b.x, 0, b.y));
+				break;
+			case Vector3::AXIS_Z:
+				lines.append(end_location + Vector3(a.x, a.y, 0));
+				lines.append(end_location + Vector3(b.x, b.y, 0));
+				break;
+		}
+	}
+
+	p_gizmo->add_lines(lines, link->is_enabled() ? link_material : link_material_disabled);
+	p_gizmo->add_collision_segments(lines);
+
+	Vector<Vector3> handles;
+	handles.append(start_location);
+	handles.append(end_location);
+	p_gizmo->add_handles(handles, handles_material);
+}
+
+String NavigationLink3DGizmoPlugin::get_handle_name(const EditorNode3DGizmo *p_gizmo, int p_id, bool p_secondary) const {
+	return p_id == 0 ? TTR("Start Location") : TTR("End Location");
+}
+
+Variant NavigationLink3DGizmoPlugin::get_handle_value(const EditorNode3DGizmo *p_gizmo, int p_id, bool p_secondary) const {
+	NavigationLink3D *link = Object::cast_to<NavigationLink3D>(p_gizmo->get_spatial_node());
+	return p_id == 0 ? link->get_start_location() : link->get_end_location();
+}
+
+void NavigationLink3DGizmoPlugin::set_handle(const EditorNode3DGizmo *p_gizmo, int p_id, bool p_secondary, Camera3D *p_camera, const Point2 &p_point) {
+	NavigationLink3D *link = Object::cast_to<NavigationLink3D>(p_gizmo->get_spatial_node());
+
+	Transform3D gt = link->get_global_transform();
+	Transform3D gi = gt.affine_inverse();
+
+	Transform3D ct = p_camera->get_global_transform();
+	Vector3 cam_dir = ct.basis.get_column(Vector3::AXIS_Z);
+
+	Vector3 ray_from = p_camera->project_ray_origin(p_point);
+	Vector3 ray_dir = p_camera->project_ray_normal(p_point);
+
+	Vector3 location = p_id == 0 ? link->get_start_location() : link->get_end_location();
+	Plane move_plane = Plane(cam_dir, gt.xform(location));
+
+	Vector3 intersection;
+	if (!move_plane.intersects_ray(ray_from, ray_dir, &intersection)) {
+		return;
+	}
+
+	if (Node3DEditor::get_singleton()->is_snap_enabled()) {
+		double snap = Node3DEditor::get_singleton()->get_translate_snap();
+		intersection.snap(Vector3(snap, snap, snap));
+	}
+
+	location = gi.xform(intersection);
+	if (p_id == 0) {
+		link->set_start_location(location);
+	} else if (p_id == 1) {
+		link->set_end_location(location);
+	}
+}
+
+void NavigationLink3DGizmoPlugin::commit_handle(const EditorNode3DGizmo *p_gizmo, int p_id, bool p_secondary, const Variant &p_restore, bool p_cancel) {
+	NavigationLink3D *link = Object::cast_to<NavigationLink3D>(p_gizmo->get_spatial_node());
+
+	if (p_cancel) {
+		if (p_id == 0) {
+			link->set_start_location(p_restore);
+		} else {
+			link->set_end_location(p_restore);
+		}
+		return;
+	}
+
+	Ref<EditorUndoRedoManager> &ur = EditorNode::get_undo_redo();
+	if (p_id == 0) {
+		ur->create_action(TTR("Change Start Location"));
+		ur->add_do_method(link, "set_start_location", link->get_start_location());
+		ur->add_undo_method(link, "set_start_location", p_restore);
+	} else {
+		ur->create_action(TTR("Change End Location"));
+		ur->add_do_method(link, "set_end_location", link->get_end_location());
+		ur->add_undo_method(link, "set_end_location", p_restore);
+	}
+
+	ur->commit_action();
+}
+
 //////
 
 #define BODY_A_RADIUS 0.25

+ 17 - 0
editor/plugins/node_3d_editor_gizmos.h

@@ -631,6 +631,23 @@ public:
 	NavigationRegion3DGizmoPlugin();
 };
 
+class NavigationLink3DGizmoPlugin : public EditorNode3DGizmoPlugin {
+	GDCLASS(NavigationLink3DGizmoPlugin, EditorNode3DGizmoPlugin);
+
+public:
+	bool has_gizmo(Node3D *p_spatial) override;
+	String get_gizmo_name() const override;
+	int get_priority() const override;
+	void redraw(EditorNode3DGizmo *p_gizmo) override;
+
+	String get_handle_name(const EditorNode3DGizmo *p_gizmo, int p_id, bool p_secondary) const override;
+	Variant get_handle_value(const EditorNode3DGizmo *p_gizmo, int p_id, bool p_secondary) const override;
+	void set_handle(const EditorNode3DGizmo *p_gizmo, int p_id, bool p_secondary, Camera3D *p_camera, const Point2 &p_point) override;
+	void commit_handle(const EditorNode3DGizmo *p_gizmo, int p_id, bool p_secondary, const Variant &p_restore, bool p_cancel = false) override;
+
+	NavigationLink3DGizmoPlugin();
+};
+
 class JointGizmosDrawer {
 public:
 	static Basis look_body(const Transform3D &p_joint_transform, const Transform3D &p_body_transform);

+ 1 - 0
editor/plugins/node_3d_editor_plugin.cpp

@@ -7523,6 +7523,7 @@ void Node3DEditor::_register_all_gizmos() {
 	add_gizmo_plugin(Ref<CollisionObject3DGizmoPlugin>(memnew(CollisionObject3DGizmoPlugin)));
 	add_gizmo_plugin(Ref<CollisionShape3DGizmoPlugin>(memnew(CollisionShape3DGizmoPlugin)));
 	add_gizmo_plugin(Ref<CollisionPolygon3DGizmoPlugin>(memnew(CollisionPolygon3DGizmoPlugin)));
+	add_gizmo_plugin(Ref<NavigationLink3DGizmoPlugin>(memnew(NavigationLink3DGizmoPlugin)));
 	add_gizmo_plugin(Ref<NavigationRegion3DGizmoPlugin>(memnew(NavigationRegion3DGizmoPlugin)));
 	add_gizmo_plugin(Ref<Joint3DGizmoPlugin>(memnew(Joint3DGizmoPlugin)));
 	add_gizmo_plugin(Ref<PhysicalBone3DGizmoPlugin>(memnew(PhysicalBone3DGizmoPlugin)));

+ 171 - 0
modules/navigation/godot_navigation_server.cpp

@@ -210,6 +210,20 @@ real_t GodotNavigationServer::map_get_edge_connection_margin(RID p_map) const {
 	return map->get_edge_connection_margin();
 }
 
+COMMAND_2(map_set_link_connection_radius, RID, p_map, real_t, p_connection_radius) {
+	NavMap *map = map_owner.get_or_null(p_map);
+	ERR_FAIL_COND(map == nullptr);
+
+	map->set_link_connection_radius(p_connection_radius);
+}
+
+real_t GodotNavigationServer::map_get_link_connection_radius(RID p_map) const {
+	const NavMap *map = map_owner.get_or_null(p_map);
+	ERR_FAIL_COND_V(map == nullptr, 0);
+
+	return map->get_link_connection_radius();
+}
+
 Vector<Vector3> GodotNavigationServer::map_get_path(RID p_map, Vector3 p_origin, Vector3 p_destination, bool p_optimize, uint32_t p_navigation_layers) const {
 	const NavMap *map = map_owner.get_or_null(p_map);
 	ERR_FAIL_COND_V(map == nullptr, Vector<Vector3>());
@@ -245,6 +259,20 @@ RID GodotNavigationServer::map_get_closest_point_owner(RID p_map, const Vector3
 	return map->get_closest_point_owner(p_point);
 }
 
+TypedArray<RID> GodotNavigationServer::map_get_links(RID p_map) const {
+	TypedArray<RID> link_rids;
+	const NavMap *map = map_owner.get_or_null(p_map);
+	ERR_FAIL_COND_V(map == nullptr, link_rids);
+
+	const LocalVector<NavLink *> links = map->get_links();
+	link_rids.resize(links.size());
+
+	for (uint32_t i = 0; i < links.size(); i++) {
+		link_rids[i] = links[i]->get_self();
+	}
+	return link_rids;
+}
+
 TypedArray<RID> GodotNavigationServer::map_get_regions(RID p_map) const {
 	TypedArray<RID> regions_rids;
 	const NavMap *map = map_owner.get_or_null(p_map);
@@ -417,6 +445,131 @@ Vector3 GodotNavigationServer::region_get_connection_pathway_end(RID p_region, i
 	return region->get_connection_pathway_end(p_connection_id);
 }
 
+RID GodotNavigationServer::link_create() const {
+	GodotNavigationServer *mut_this = const_cast<GodotNavigationServer *>(this);
+	MutexLock lock(mut_this->operations_mutex);
+	RID rid = link_owner.make_rid();
+	NavLink *link = link_owner.get_or_null(rid);
+	link->set_self(rid);
+	return rid;
+}
+
+COMMAND_2(link_set_map, RID, p_link, RID, p_map) {
+	NavLink *link = link_owner.get_or_null(p_link);
+	ERR_FAIL_COND(link == nullptr);
+
+	if (link->get_map() != nullptr) {
+		if (link->get_map()->get_self() == p_map) {
+			return; // Pointless
+		}
+
+		link->get_map()->remove_link(link);
+		link->set_map(nullptr);
+	}
+
+	if (p_map.is_valid()) {
+		NavMap *map = map_owner.get_or_null(p_map);
+		ERR_FAIL_COND(map == nullptr);
+
+		map->add_link(link);
+		link->set_map(map);
+	}
+}
+
+RID GodotNavigationServer::link_get_map(const RID p_link) const {
+	const NavLink *link = link_owner.get_or_null(p_link);
+	ERR_FAIL_COND_V(link == nullptr, RID());
+
+	if (link->get_map()) {
+		return link->get_map()->get_self();
+	}
+	return RID();
+}
+
+COMMAND_2(link_set_bidirectional, RID, p_link, bool, p_bidirectional) {
+	NavLink *link = link_owner.get_or_null(p_link);
+	ERR_FAIL_COND(link == nullptr);
+
+	link->set_bidirectional(p_bidirectional);
+}
+
+bool GodotNavigationServer::link_is_bidirectional(RID p_link) const {
+	const NavLink *link = link_owner.get_or_null(p_link);
+	ERR_FAIL_COND_V(link == nullptr, false);
+
+	return link->is_bidirectional();
+}
+
+COMMAND_2(link_set_navigation_layers, RID, p_link, uint32_t, p_navigation_layers) {
+	NavLink *link = link_owner.get_or_null(p_link);
+	ERR_FAIL_COND(link == nullptr);
+
+	link->set_navigation_layers(p_navigation_layers);
+}
+
+uint32_t GodotNavigationServer::link_get_navigation_layers(const RID p_link) const {
+	const NavLink *link = link_owner.get_or_null(p_link);
+	ERR_FAIL_COND_V(link == nullptr, 0);
+
+	return link->get_navigation_layers();
+}
+
+COMMAND_2(link_set_start_location, RID, p_link, Vector3, p_location) {
+	NavLink *link = link_owner.get_or_null(p_link);
+	ERR_FAIL_COND(link == nullptr);
+
+	link->set_start_location(p_location);
+}
+
+Vector3 GodotNavigationServer::link_get_start_location(RID p_link) const {
+	const NavLink *link = link_owner.get_or_null(p_link);
+	ERR_FAIL_COND_V(link == nullptr, Vector3());
+
+	return link->get_start_location();
+}
+
+COMMAND_2(link_set_end_location, RID, p_link, Vector3, p_location) {
+	NavLink *link = link_owner.get_or_null(p_link);
+	ERR_FAIL_COND(link == nullptr);
+
+	link->set_end_location(p_location);
+}
+
+Vector3 GodotNavigationServer::link_get_end_location(RID p_link) const {
+	const NavLink *link = link_owner.get_or_null(p_link);
+	ERR_FAIL_COND_V(link == nullptr, Vector3());
+
+	return link->get_end_location();
+}
+
+COMMAND_2(link_set_enter_cost, RID, p_link, real_t, p_enter_cost) {
+	NavLink *link = link_owner.get_or_null(p_link);
+	ERR_FAIL_COND(link == nullptr);
+
+	link->set_enter_cost(p_enter_cost);
+}
+
+real_t GodotNavigationServer::link_get_enter_cost(const RID p_link) const {
+	const NavLink *link = link_owner.get_or_null(p_link);
+	ERR_FAIL_COND_V(link == nullptr, 0);
+
+	return link->get_enter_cost();
+}
+
+COMMAND_2(link_set_travel_cost, RID, p_link, real_t, p_travel_cost) {
+	NavLink *link = link_owner.get_or_null(p_link);
+	ERR_FAIL_COND(link == nullptr);
+
+	link->set_travel_cost(p_travel_cost);
+}
+
+real_t GodotNavigationServer::link_get_travel_cost(const RID p_link) const {
+	const NavLink *link = link_owner.get_or_null(p_link);
+	ERR_FAIL_COND_V(link == nullptr, 0);
+
+	return link->get_travel_cost();
+}
+
 RID GodotNavigationServer::agent_create() const {
 	GodotNavigationServer *mut_this = const_cast<GodotNavigationServer *>(this);
 	MutexLock lock(mut_this->operations_mutex);
@@ -549,6 +702,13 @@ COMMAND_1(free, RID, p_object) {
 			regions[i]->set_map(nullptr);
 		}
 
+		// Removes any assigned links
+		LocalVector<NavLink *> links = map->get_links();
+		for (uint32_t i = 0; i < links.size(); i++) {
+			map->remove_link(links[i]);
+			links[i]->set_map(nullptr);
+		}
+
 		// Remove any assigned agent
 		LocalVector<RvoAgent *> agents = map->get_agents();
 		for (uint32_t i = 0; i < agents.size(); i++) {
@@ -572,6 +732,17 @@ COMMAND_1(free, RID, p_object) {
 
 		region_owner.free(p_object);
 
+	} else if (link_owner.owns(p_object)) {
+		NavLink *link = link_owner.get_or_null(p_object);
+
+		// Removes this link from the map if assigned
+		if (link->get_map() != nullptr) {
+			link->get_map()->remove_link(link);
+			link->set_map(nullptr);
+		}
+
+		link_owner.free(p_object);
+
 	} else if (agent_owner.owns(p_object)) {
 		RvoAgent *agent = agent_owner.get_or_null(p_object);
 

+ 22 - 0
modules/navigation/godot_navigation_server.h

@@ -36,6 +36,7 @@
 #include "core/templates/rid_owner.h"
 #include "servers/navigation_server_3d.h"
 
+#include "nav_link.h"
 #include "nav_map.h"
 #include "nav_region.h"
 #include "rvo_agent.h"
@@ -71,6 +72,7 @@ class GodotNavigationServer : public NavigationServer3D {
 
 	LocalVector<SetCommand *> commands;
 
+	mutable RID_Owner<NavLink> link_owner;
 	mutable RID_Owner<NavMap> map_owner;
 	mutable RID_Owner<NavRegion> region_owner;
 	mutable RID_Owner<RvoAgent> agent_owner;
@@ -100,6 +102,9 @@ public:
 	COMMAND_2(map_set_edge_connection_margin, RID, p_map, real_t, p_connection_margin);
 	virtual real_t map_get_edge_connection_margin(RID p_map) const override;
 
+	COMMAND_2(map_set_link_connection_radius, RID, p_map, real_t, p_connection_radius);
+	virtual real_t map_get_link_connection_radius(RID p_map) const override;
+
 	virtual Vector<Vector3> map_get_path(RID p_map, Vector3 p_origin, Vector3 p_destination, bool p_optimize, uint32_t p_navigation_layers = 1) const override;
 
 	virtual Vector3 map_get_closest_point_to_segment(RID p_map, const Vector3 &p_from, const Vector3 &p_to, const bool p_use_collision = false) const override;
@@ -107,6 +112,7 @@ public:
 	virtual Vector3 map_get_closest_point_normal(RID p_map, const Vector3 &p_point) const override;
 	virtual RID map_get_closest_point_owner(RID p_map, const Vector3 &p_point) const override;
 
+	virtual TypedArray<RID> map_get_links(RID p_map) const override;
 	virtual TypedArray<RID> map_get_regions(RID p_map) const override;
 	virtual TypedArray<RID> map_get_agents(RID p_map) const override;
 
@@ -132,6 +138,22 @@ public:
 	virtual Vector3 region_get_connection_pathway_start(RID p_region, int p_connection_id) const override;
 	virtual Vector3 region_get_connection_pathway_end(RID p_region, int p_connection_id) const override;
 
+	virtual RID link_create() const override;
+	COMMAND_2(link_set_map, RID, p_link, RID, p_map);
+	virtual RID link_get_map(RID p_link) const override;
+	COMMAND_2(link_set_bidirectional, RID, p_link, bool, p_bidirectional);
+	virtual bool link_is_bidirectional(RID p_link) const override;
+	COMMAND_2(link_set_navigation_layers, RID, p_link, uint32_t, p_navigation_layers);
+	virtual uint32_t link_get_navigation_layers(RID p_link) const override;
+	COMMAND_2(link_set_start_location, RID, p_link, Vector3, p_location);
+	virtual Vector3 link_get_start_location(RID p_link) const override;
+	COMMAND_2(link_set_end_location, RID, p_link, Vector3, p_location);
+	virtual Vector3 link_get_end_location(RID p_link) const override;
+	COMMAND_2(link_set_enter_cost, RID, p_link, real_t, p_enter_cost);
+	virtual real_t link_get_enter_cost(RID p_link) const override;
+	COMMAND_2(link_set_travel_cost, RID, p_link, real_t, p_travel_cost);
+	virtual real_t link_get_travel_cost(RID p_link) const override;
+
 	virtual RID agent_create() const override;
 	COMMAND_2(agent_set_map, RID, p_agent, RID, p_map);
 	virtual RID agent_get_map(RID p_agent) const override;

+ 56 - 0
modules/navigation/nav_base.h

@@ -0,0 +1,56 @@
+/*************************************************************************/
+/*  nav_base.h                                                           */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#ifndef NAV_BASE_H
+#define NAV_BASE_H
+
+#include "nav_rid.h"
+#include "nav_utils.h"
+
+class NavMap;
+
+class NavBase : public NavRid {
+protected:
+	uint32_t navigation_layers = 1;
+	float enter_cost = 0.0;
+	float travel_cost = 1.0;
+
+public:
+	void set_navigation_layers(uint32_t p_navigation_layers) { navigation_layers = p_navigation_layers; }
+	uint32_t get_navigation_layers() const { return navigation_layers; }
+
+	void set_enter_cost(float p_enter_cost) { enter_cost = MAX(p_enter_cost, 0.0); }
+	float get_enter_cost() const { return enter_cost; }
+
+	void set_travel_cost(float p_travel_cost) { travel_cost = MAX(p_travel_cost, 0.0); }
+	float get_travel_cost() const { return travel_cost; }
+};
+
+#endif // NAV_BASE_H

+ 60 - 0
modules/navigation/nav_link.cpp

@@ -0,0 +1,60 @@
+/*************************************************************************/
+/*  nav_link.cpp                                                         */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#include "nav_link.h"
+
+#include "nav_map.h"
+
+void NavLink::set_map(NavMap *p_map) {
+	map = p_map;
+	link_dirty = true;
+}
+
+void NavLink::set_bidirectional(bool p_bidirectional) {
+	bidirectional = p_bidirectional;
+	link_dirty = true;
+}
+
+void NavLink::set_start_location(const Vector3 p_location) {
+	start_location = p_location;
+	link_dirty = true;
+}
+
+void NavLink::set_end_location(const Vector3 p_location) {
+	end_location = p_location;
+	link_dirty = true;
+}
+
+bool NavLink::check_dirty() {
+	const bool was_dirty = link_dirty;
+
+	link_dirty = false;
+	return was_dirty;
+}

+ 69 - 0
modules/navigation/nav_link.h

@@ -0,0 +1,69 @@
+/*************************************************************************/
+/*  nav_link.h                                                           */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#ifndef NAV_LINK_H
+#define NAV_LINK_H
+
+#include "nav_base.h"
+#include "nav_utils.h"
+
+class NavLink : public NavBase {
+	NavMap *map = nullptr;
+	bool bidirectional = true;
+	Vector3 start_location = Vector3();
+	Vector3 end_location = Vector3();
+
+	bool link_dirty = true;
+
+public:
+	void set_map(NavMap *p_map);
+	NavMap *get_map() const {
+		return map;
+	}
+
+	void set_bidirectional(bool p_bidirectional);
+	bool is_bidirectional() const {
+		return bidirectional;
+	}
+
+	void set_start_location(Vector3 p_location);
+	Vector3 get_start_location() const {
+		return start_location;
+	}
+
+	void set_end_location(Vector3 p_location);
+	Vector3 get_end_location() const {
+		return end_location;
+	}
+
+	bool check_dirty();
+};
+
+#endif // NAV_LINK_H

+ 154 - 10
modules/navigation/nav_map.cpp

@@ -31,6 +31,7 @@
 #include "nav_map.h"
 
 #include "core/object/worker_thread_pool.h"
+#include "nav_link.h"
 #include "nav_region.h"
 #include "rvo_agent.h"
 #include <algorithm>
@@ -52,6 +53,11 @@ void NavMap::set_edge_connection_margin(float p_edge_connection_margin) {
 	regenerate_links = true;
 }
 
+void NavMap::set_link_connection_radius(float p_link_connection_radius) {
+	link_connection_radius = p_link_connection_radius;
+	regenerate_links = true;
+}
+
 gd::PointKey NavMap::get_point_key(const Vector3 &p_pos) const {
 	const int x = int(Math::floor(p_pos.x / cell_size));
 	const int y = int(Math::floor(p_pos.y / cell_size));
@@ -158,17 +164,17 @@ Vector<Vector3> NavMap::get_path(Vector3 p_origin, Vector3 p_destination, bool p
 					continue;
 				}
 
-				float region_enter_cost = 0.0;
-				float region_travel_cost = least_cost_poly->poly->owner->get_travel_cost();
+				float poly_enter_cost = 0.0;
+				float poly_travel_cost = least_cost_poly->poly->owner->get_travel_cost();
 
-				if (prev_least_cost_poly != nullptr && !(prev_least_cost_poly->poly->owner->get_self() == least_cost_poly->poly->owner->get_self())) {
-					region_enter_cost = least_cost_poly->poly->owner->get_enter_cost();
+				if (prev_least_cost_poly != nullptr && (prev_least_cost_poly->poly->owner->get_self() != least_cost_poly->poly->owner->get_self())) {
+					poly_enter_cost = least_cost_poly->poly->owner->get_enter_cost();
 				}
 				prev_least_cost_poly = least_cost_poly;
 
 				Vector3 pathway[2] = { connection.pathway_start, connection.pathway_end };
 				const Vector3 new_entry = Geometry3D::get_closest_point_to_segment(least_cost_poly->entry, pathway);
-				const float new_distance = (least_cost_poly->entry.distance_to(new_entry) * region_travel_cost) + region_enter_cost + least_cost_poly->traveled_distance;
+				const float new_distance = (least_cost_poly->entry.distance_to(new_entry) * poly_travel_cost) + poly_enter_cost + least_cost_poly->traveled_distance;
 
 				int64_t already_visited_polygon_index = navigation_polys.find(gd::NavigationPoly(connection.polygon));
 
@@ -360,10 +366,15 @@ Vector<Vector3> NavMap::get_path(Vector3 p_origin, Vector3 p_destination, bool p
 		// Add mid points
 		int np_id = least_cost_id;
 		while (np_id != -1 && navigation_polys[np_id].back_navigation_poly_id != -1) {
-			int prev = navigation_polys[np_id].back_navigation_edge;
-			int prev_n = (navigation_polys[np_id].back_navigation_edge + 1) % navigation_polys[np_id].poly->points.size();
-			Vector3 point = (navigation_polys[np_id].poly->points[prev].pos + navigation_polys[np_id].poly->points[prev_n].pos) * 0.5;
-			path.push_back(point);
+			if (navigation_polys[np_id].back_navigation_edge != -1) {
+				int prev = navigation_polys[np_id].back_navigation_edge;
+				int prev_n = (navigation_polys[np_id].back_navigation_edge + 1) % navigation_polys[np_id].poly->points.size();
+				Vector3 point = (navigation_polys[np_id].poly->points[prev].pos + navigation_polys[np_id].poly->points[prev_n].pos) * 0.5;
+				path.push_back(point);
+			} else {
+				path.push_back(navigation_polys[np_id].entry);
+			}
+
 			np_id = navigation_polys[np_id].back_navigation_poly_id;
 		}
 
@@ -475,6 +486,19 @@ void NavMap::remove_region(NavRegion *p_region) {
 	}
 }
 
+void NavMap::add_link(NavLink *p_link) {
+	links.push_back(p_link);
+	regenerate_links = true;
+}
+
+void NavMap::remove_link(NavLink *p_link) {
+	int64_t link_index = links.find(p_link);
+	if (link_index != -1) {
+		links.remove_at_unordered(link_index);
+		regenerate_links = true;
+	}
+}
+
 bool NavMap::has_agent(RvoAgent *agent) const {
 	return (agents.find(agent) != -1);
 }
@@ -526,6 +550,12 @@ void NavMap::sync() {
 		}
 	}
 
+	for (uint32_t l = 0; l < links.size(); l++) {
+		if (links[l]->check_dirty()) {
+			regenerate_links = true;
+		}
+	}
+
 	if (regenerate_links) {
 		// Remove regions connections.
 		for (uint32_t r = 0; r < regions.size(); r++) {
@@ -651,7 +681,121 @@ void NavMap::sync() {
 				free_edge.polygon->edges[free_edge.edge].connections.push_back(new_connection);
 
 				// Add the connection to the region_connection map.
-				free_edge.polygon->owner->get_connections().push_back(new_connection);
+				((NavRegion *)free_edge.polygon->owner)->get_connections().push_back(new_connection);
+			}
+		}
+
+		uint32_t link_poly_idx = 0;
+		link_polygons.resize(links.size());
+
+		// Search for polygons within range of a nav link.
+		for (uint32_t l = 0; l < links.size(); l++) {
+			const NavLink *link = links[l];
+			const Vector3 start = link->get_start_location();
+			const Vector3 end = link->get_end_location();
+
+			gd::Polygon *closest_start_polygon = nullptr;
+			real_t closest_start_distance = link_connection_radius;
+			Vector3 closest_start_point;
+
+			gd::Polygon *closest_end_polygon = nullptr;
+			real_t closest_end_distance = link_connection_radius;
+			Vector3 closest_end_point;
+
+			// Create link to any polygons within the search radius of the start point.
+			for (uint32_t start_index = 0; start_index < polygons.size(); start_index++) {
+				gd::Polygon &start_poly = polygons[start_index];
+
+				// For each face check the distance to the start
+				for (uint32_t start_point_id = 2; start_point_id < start_poly.points.size(); start_point_id += 1) {
+					const Face3 start_face(start_poly.points[0].pos, start_poly.points[start_point_id - 1].pos, start_poly.points[start_point_id].pos);
+					const Vector3 start_point = start_face.get_closest_point_to(start);
+					const real_t start_distance = start_point.distance_to(start);
+
+					// Pick the polygon that is within our radius and is closer than anything we've seen yet.
+					if (start_distance <= link_connection_radius && start_distance < closest_start_distance) {
+						closest_start_distance = start_distance;
+						closest_start_point = start_point;
+						closest_start_polygon = &start_poly;
+					}
+				}
+			}
+
+			// Find any polygons within the search radius of the end point.
+			for (uint32_t end_index = 0; end_index < polygons.size(); end_index++) {
+				gd::Polygon &end_poly = polygons[end_index];
+
+				// For each face check the distance to the end
+				for (uint32_t end_point_id = 2; end_point_id < end_poly.points.size(); end_point_id += 1) {
+					const Face3 end_face(end_poly.points[0].pos, end_poly.points[end_point_id - 1].pos, end_poly.points[end_point_id].pos);
+					const Vector3 end_point = end_face.get_closest_point_to(end);
+					const real_t end_distance = end_point.distance_to(end);
+
+					// Pick the polygon that is within our radius and is closer than anything we've seen yet.
+					if (end_distance <= link_connection_radius && end_distance < closest_end_distance) {
+						closest_end_distance = end_distance;
+						closest_end_point = end_point;
+						closest_end_polygon = &end_poly;
+					}
+				}
+			}
+
+			// If we have both a start and end point, then create a synthetic polygon to route through.
+			if (closest_start_polygon && closest_end_polygon) {
+				gd::Polygon &new_polygon = link_polygons[link_poly_idx++];
+				new_polygon.owner = link;
+
+				new_polygon.edges.clear();
+				new_polygon.edges.resize(4);
+				new_polygon.points.clear();
+				new_polygon.points.reserve(4);
+
+				// Build a set of vertices that create a thin polygon going from the start to the end point.
+				new_polygon.points.push_back({ closest_start_point, get_point_key(closest_start_point) });
+				new_polygon.points.push_back({ closest_start_point, get_point_key(closest_start_point) });
+				new_polygon.points.push_back({ closest_end_point, get_point_key(closest_end_point) });
+				new_polygon.points.push_back({ closest_end_point, get_point_key(closest_end_point) });
+
+				Vector3 center;
+				for (int p = 0; p < 4; ++p) {
+					center += new_polygon.points[p].pos;
+				}
+				new_polygon.center = center / real_t(new_polygon.points.size());
+				new_polygon.clockwise = true;
+
+				// Setup connections to go forward in the link.
+				{
+					gd::Edge::Connection entry_connection;
+					entry_connection.polygon = &new_polygon;
+					entry_connection.edge = -1;
+					entry_connection.pathway_start = new_polygon.points[0].pos;
+					entry_connection.pathway_end = new_polygon.points[1].pos;
+					closest_start_polygon->edges[0].connections.push_back(entry_connection);
+
+					gd::Edge::Connection exit_connection;
+					exit_connection.polygon = closest_end_polygon;
+					exit_connection.edge = -1;
+					exit_connection.pathway_start = new_polygon.points[2].pos;
+					exit_connection.pathway_end = new_polygon.points[3].pos;
+					new_polygon.edges[2].connections.push_back(exit_connection);
+				}
+
+				// If the link is bi-directional, create connections from the end to the start.
+				if (link->is_bidirectional()) {
+					gd::Edge::Connection entry_connection;
+					entry_connection.polygon = &new_polygon;
+					entry_connection.edge = -1;
+					entry_connection.pathway_start = new_polygon.points[2].pos;
+					entry_connection.pathway_end = new_polygon.points[3].pos;
+					closest_end_polygon->edges[0].connections.push_back(entry_connection);
+
+					gd::Edge::Connection exit_connection;
+					exit_connection.polygon = closest_start_polygon;
+					exit_connection.edge = -1;
+					exit_connection.pathway_start = new_polygon.points[0].pos;
+					exit_connection.pathway_end = new_polygon.points[1].pos;
+					new_polygon.edges[0].connections.push_back(exit_connection);
+				}
 			}
 		}
 

+ 20 - 1
modules/navigation/nav_map.h

@@ -40,9 +40,9 @@
 
 #include <KdTree.h>
 
+class NavLink;
 class NavRegion;
 class RvoAgent;
-class NavRegion;
 
 class NavMap : public NavRid {
 	/// Map Up
@@ -55,11 +55,19 @@ class NavMap : public NavRid {
 	/// This value is used to detect the near edges to connect.
 	real_t edge_connection_margin = 0.25;
 
+	/// This value is used to limit how far links search to find polygons to connect to.
+	real_t link_connection_radius = 1.0;
+
 	bool regenerate_polygons = true;
 	bool regenerate_links = true;
 
+	/// Map regions
 	LocalVector<NavRegion *> regions;
 
+	/// Map links
+	LocalVector<NavLink *> links;
+	LocalVector<gd::Polygon> link_polygons;
+
 	/// Map polygons
 	LocalVector<gd::Polygon> polygons;
 
@@ -100,6 +108,11 @@ public:
 		return edge_connection_margin;
 	}
 
+	void set_link_connection_radius(float p_link_connection_radius);
+	float get_link_connection_radius() const {
+		return link_connection_radius;
+	}
+
 	gd::PointKey get_point_key(const Vector3 &p_pos) const;
 
 	Vector<Vector3> get_path(Vector3 p_origin, Vector3 p_destination, bool p_optimize, uint32_t p_navigation_layers = 1) const;
@@ -115,6 +128,12 @@ public:
 		return regions;
 	}
 
+	void add_link(NavLink *p_link);
+	void remove_link(NavLink *p_link);
+	const LocalVector<NavLink *> &get_links() const {
+		return links;
+	}
+
 	bool has_agent(RvoAgent *agent) const;
 	void add_agent(RvoAgent *agent);
 	void remove_agent(RvoAgent *agent);

+ 0 - 8
modules/navigation/nav_region.cpp

@@ -40,14 +40,6 @@ void NavRegion::set_map(NavMap *p_map) {
 	}
 }
 
-void NavRegion::set_navigation_layers(uint32_t p_navigation_layers) {
-	navigation_layers = p_navigation_layers;
-}
-
-uint32_t NavRegion::get_navigation_layers() const {
-	return navigation_layers;
-}
-
 void NavRegion::set_transform(Transform3D p_transform) {
 	transform = p_transform;
 	polygons_dirty = true;

+ 2 - 19
modules/navigation/nav_region.h

@@ -33,21 +33,13 @@
 
 #include "scene/resources/navigation_mesh.h"
 
-#include "nav_rid.h"
+#include "nav_base.h"
 #include "nav_utils.h"
 
-#include <vector>
-
-class NavMap;
-class NavRegion;
-
-class NavRegion : public NavRid {
+class NavRegion : public NavBase {
 	NavMap *map = nullptr;
 	Transform3D transform;
 	Ref<NavigationMesh> mesh;
-	uint32_t navigation_layers = 1;
-	float enter_cost = 0.0;
-	float travel_cost = 1.0;
 	Vector<gd::Edge::Connection> connections;
 
 	bool polygons_dirty = true;
@@ -67,15 +59,6 @@ public:
 		return map;
 	}
 
-	void set_enter_cost(float p_enter_cost) { enter_cost = MAX(p_enter_cost, 0.0); }
-	float get_enter_cost() const { return enter_cost; }
-
-	void set_travel_cost(float p_travel_cost) { travel_cost = MAX(p_travel_cost, 0.0); }
-	float get_travel_cost() const { return travel_cost; }
-
-	void set_navigation_layers(uint32_t p_navigation_layers);
-	uint32_t get_navigation_layers() const;
-
 	void set_transform(Transform3D transform);
 	const Transform3D &get_transform() const {
 		return transform;

+ 14 - 8
modules/navigation/nav_utils.h

@@ -35,9 +35,8 @@
 #include "core/templates/hash_map.h"
 #include "core/templates/hashfuncs.h"
 #include "core/templates/local_vector.h"
-#include <vector>
 
-class NavRegion;
+class NavBase;
 
 namespace gd {
 struct Polygon;
@@ -79,26 +78,33 @@ struct Point {
 };
 
 struct Edge {
-	/// This edge ID
-	int this_edge = -1;
-
 	/// The gateway in the edge, as, in some case, the whole edge might not be navigable.
 	struct Connection {
+		/// Polygon that this connection leads to.
 		Polygon *polygon = nullptr;
+
+		/// Edge of the source polygon where this connection starts from.
 		int edge = -1;
+
+		/// Point on the edge where the gateway leading to the poly starts.
 		Vector3 pathway_start;
+
+		/// Point on the edge where the gateway leading to the poly ends.
 		Vector3 pathway_end;
 	};
+
+	/// Connections from this edge to other polygons.
 	Vector<Connection> connections;
 };
 
 struct Polygon {
-	NavRegion *owner = nullptr;
+	/// Navigation region or link that contains this polygon.
+	const NavBase *owner = nullptr;
 
 	/// The points of this `Polygon`
 	LocalVector<Point> points;
 
-	/// Are the points clockwise ?
+	/// Are the points clockwise?
 	bool clockwise;
 
 	/// The edges of this `Polygon`
@@ -115,7 +121,7 @@ struct NavigationPoly {
 
 	/// Those 4 variables are used to travel the path backwards.
 	int back_navigation_poly_id = -1;
-	uint32_t back_navigation_edge = UINT32_MAX;
+	int back_navigation_edge = -1;
 	Vector3 back_navigation_edge_pathway_start;
 	Vector3 back_navigation_edge_pathway_end;
 

+ 284 - 0
scene/2d/navigation_link_2d.cpp

@@ -0,0 +1,284 @@
+/*************************************************************************/
+/*  navigation_link_2d.cpp                                               */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#include "navigation_link_2d.h"
+
+#include "core/math/geometry_2d.h"
+#include "scene/resources/world_2d.h"
+#include "servers/navigation_server_2d.h"
+#include "servers/navigation_server_3d.h"
+
+void NavigationLink2D::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("set_enabled", "enabled"), &NavigationLink2D::set_enabled);
+	ClassDB::bind_method(D_METHOD("is_enabled"), &NavigationLink2D::is_enabled);
+
+	ClassDB::bind_method(D_METHOD("set_bidirectional", "bidirectional"), &NavigationLink2D::set_bidirectional);
+	ClassDB::bind_method(D_METHOD("is_bidirectional"), &NavigationLink2D::is_bidirectional);
+
+	ClassDB::bind_method(D_METHOD("set_navigation_layers", "navigation_layers"), &NavigationLink2D::set_navigation_layers);
+	ClassDB::bind_method(D_METHOD("get_navigation_layers"), &NavigationLink2D::get_navigation_layers);
+
+	ClassDB::bind_method(D_METHOD("set_navigation_layer_value", "layer_number", "value"), &NavigationLink2D::set_navigation_layer_value);
+	ClassDB::bind_method(D_METHOD("get_navigation_layer_value", "layer_number"), &NavigationLink2D::get_navigation_layer_value);
+
+	ClassDB::bind_method(D_METHOD("set_start_location", "location"), &NavigationLink2D::set_start_location);
+	ClassDB::bind_method(D_METHOD("get_start_location"), &NavigationLink2D::get_start_location);
+
+	ClassDB::bind_method(D_METHOD("set_end_location", "location"), &NavigationLink2D::set_end_location);
+	ClassDB::bind_method(D_METHOD("get_end_location"), &NavigationLink2D::get_end_location);
+
+	ClassDB::bind_method(D_METHOD("set_enter_cost", "enter_cost"), &NavigationLink2D::set_enter_cost);
+	ClassDB::bind_method(D_METHOD("get_enter_cost"), &NavigationLink2D::get_enter_cost);
+
+	ClassDB::bind_method(D_METHOD("set_travel_cost", "travel_cost"), &NavigationLink2D::set_travel_cost);
+	ClassDB::bind_method(D_METHOD("get_travel_cost"), &NavigationLink2D::get_travel_cost);
+
+	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "enabled"), "set_enabled", "is_enabled");
+	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "bidirectional"), "set_bidirectional", "is_bidirectional");
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "navigation_layers", PROPERTY_HINT_LAYERS_2D_NAVIGATION), "set_navigation_layers", "get_navigation_layers");
+	ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "start_location"), "set_start_location", "get_start_location");
+	ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "end_location"), "set_end_location", "get_end_location");
+	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "enter_cost"), "set_enter_cost", "get_enter_cost");
+	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "travel_cost"), "set_travel_cost", "get_travel_cost");
+}
+
+void NavigationLink2D::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_ENTER_TREE: {
+			if (enabled) {
+				NavigationServer2D::get_singleton()->link_set_map(link, get_world_2d()->get_navigation_map());
+
+				// Update global positions for the link.
+				Transform2D gt = get_global_transform();
+				NavigationServer2D::get_singleton()->link_set_start_location(link, gt.xform(start_location));
+				NavigationServer2D::get_singleton()->link_set_end_location(link, gt.xform(end_location));
+			}
+		} break;
+		case NOTIFICATION_TRANSFORM_CHANGED: {
+			// Update global positions for the link.
+			Transform2D gt = get_global_transform();
+			NavigationServer2D::get_singleton()->link_set_start_location(link, gt.xform(start_location));
+			NavigationServer2D::get_singleton()->link_set_end_location(link, gt.xform(end_location));
+		} break;
+		case NOTIFICATION_EXIT_TREE: {
+			NavigationServer2D::get_singleton()->link_set_map(link, RID());
+		} break;
+		case NOTIFICATION_DRAW: {
+#ifdef DEBUG_ENABLED
+			if (is_inside_tree() && (Engine::get_singleton()->is_editor_hint() || NavigationServer2D::get_singleton()->get_debug_enabled())) {
+				Color color;
+				if (enabled) {
+					color = NavigationServer2D::get_singleton()->get_debug_navigation_link_connection_color();
+				} else {
+					color = NavigationServer2D::get_singleton()->get_debug_navigation_link_connection_disabled_color();
+				}
+
+				real_t radius = NavigationServer2D::get_singleton()->map_get_link_connection_radius(get_world_2d()->get_navigation_map());
+
+				draw_line(get_start_location(), get_end_location(), color);
+				draw_arc(get_start_location(), radius, 0, Math_TAU, 10, color);
+				draw_arc(get_end_location(), radius, 0, Math_TAU, 10, color);
+			}
+#endif // DEBUG_ENABLED
+		} break;
+	}
+}
+
+#ifdef TOOLS_ENABLED
+Rect2 NavigationLink2D::_edit_get_rect() const {
+	real_t radius = NavigationServer2D::get_singleton()->map_get_link_connection_radius(get_world_2d()->get_navigation_map());
+
+	Rect2 rect(get_start_location(), Size2());
+	rect.expand_to(get_end_location());
+	rect.grow_by(radius);
+	return rect;
+}
+
+bool NavigationLink2D::_edit_is_selected_on_click(const Point2 &p_point, double p_tolerance) const {
+	Point2 segment[2] = { get_start_location(), get_end_location() };
+
+	Vector2 closest_point = Geometry2D::get_closest_point_to_segment(p_point, segment);
+	return p_point.distance_to(closest_point) < p_tolerance;
+}
+#endif // TOOLS_ENABLED
+
+void NavigationLink2D::set_enabled(bool p_enabled) {
+	if (enabled == p_enabled) {
+		return;
+	}
+
+	enabled = p_enabled;
+
+	if (!is_inside_tree()) {
+		return;
+	}
+
+	if (!enabled) {
+		NavigationServer2D::get_singleton()->link_set_map(link, RID());
+	} else {
+		NavigationServer2D::get_singleton()->link_set_map(link, get_world_2d()->get_navigation_map());
+	}
+
+#ifdef DEBUG_ENABLED
+	if (Engine::get_singleton()->is_editor_hint() || NavigationServer2D::get_singleton()->get_debug_enabled()) {
+		update();
+	}
+#endif // DEBUG_ENABLED
+}
+
+void NavigationLink2D::set_bidirectional(bool p_bidirectional) {
+	if (bidirectional == p_bidirectional) {
+		return;
+	}
+
+	bidirectional = p_bidirectional;
+
+	NavigationServer2D::get_singleton()->link_set_bidirectional(link, bidirectional);
+}
+
+void NavigationLink2D::set_navigation_layers(uint32_t p_navigation_layers) {
+	if (navigation_layers == p_navigation_layers) {
+		return;
+	}
+
+	navigation_layers = p_navigation_layers;
+
+	NavigationServer2D::get_singleton()->link_set_navigation_layers(link, navigation_layers);
+}
+
+void NavigationLink2D::set_navigation_layer_value(int p_layer_number, bool p_value) {
+	ERR_FAIL_COND_MSG(p_layer_number < 1, "Navigation layer number must be between 1 and 32 inclusive.");
+	ERR_FAIL_COND_MSG(p_layer_number > 32, "Navigation layer number must be between 1 and 32 inclusive.");
+
+	uint32_t _navigation_layers = get_navigation_layers();
+
+	if (p_value) {
+		_navigation_layers |= 1 << (p_layer_number - 1);
+	} else {
+		_navigation_layers &= ~(1 << (p_layer_number - 1));
+	}
+
+	set_navigation_layers(_navigation_layers);
+}
+
+bool NavigationLink2D::get_navigation_layer_value(int p_layer_number) const {
+	ERR_FAIL_COND_V_MSG(p_layer_number < 1, false, "Navigation layer number must be between 1 and 32 inclusive.");
+	ERR_FAIL_COND_V_MSG(p_layer_number > 32, false, "Navigation layer number must be between 1 and 32 inclusive.");
+
+	return get_navigation_layers() & (1 << (p_layer_number - 1));
+}
+
+void NavigationLink2D::set_start_location(Vector2 p_location) {
+	if (start_location.is_equal_approx(p_location)) {
+		return;
+	}
+
+	start_location = p_location;
+
+	if (!is_inside_tree()) {
+		return;
+	}
+
+	Transform2D gt = get_global_transform();
+	NavigationServer2D::get_singleton()->link_set_start_location(link, gt.xform(start_location));
+
+	update_configuration_warnings();
+
+#ifdef DEBUG_ENABLED
+	if (Engine::get_singleton()->is_editor_hint() || NavigationServer2D::get_singleton()->get_debug_enabled()) {
+		update();
+	}
+#endif // DEBUG_ENABLED
+}
+
+void NavigationLink2D::set_end_location(Vector2 p_location) {
+	if (end_location.is_equal_approx(p_location)) {
+		return;
+	}
+
+	end_location = p_location;
+
+	if (!is_inside_tree()) {
+		return;
+	}
+
+	Transform2D gt = get_global_transform();
+	NavigationServer2D::get_singleton()->link_set_end_location(link, gt.xform(end_location));
+
+	update_configuration_warnings();
+
+#ifdef DEBUG_ENABLED
+	if (Engine::get_singleton()->is_editor_hint() || NavigationServer2D::get_singleton()->get_debug_enabled()) {
+		update();
+	}
+#endif // DEBUG_ENABLED
+}
+
+void NavigationLink2D::set_enter_cost(real_t p_enter_cost) {
+	ERR_FAIL_COND_MSG(p_enter_cost < 0.0, "The enter_cost must be positive.");
+	if (Math::is_equal_approx(enter_cost, p_enter_cost)) {
+		return;
+	}
+
+	enter_cost = p_enter_cost;
+
+	NavigationServer2D::get_singleton()->link_set_enter_cost(link, enter_cost);
+}
+
+void NavigationLink2D::set_travel_cost(real_t p_travel_cost) {
+	ERR_FAIL_COND_MSG(p_travel_cost < 0.0, "The travel_cost must be positive.");
+	if (Math::is_equal_approx(travel_cost, p_travel_cost)) {
+		return;
+	}
+
+	travel_cost = p_travel_cost;
+
+	NavigationServer2D::get_singleton()->link_set_travel_cost(link, travel_cost);
+}
+
+TypedArray<String> NavigationLink2D::get_configuration_warnings() const {
+	TypedArray<String> warnings = Node::get_configuration_warnings();
+
+	if (start_location.is_equal_approx(end_location)) {
+		warnings.push_back(RTR("NavigationLink2D start location should be different than the end location to be useful."));
+	}
+
+	return warnings;
+}
+
+NavigationLink2D::NavigationLink2D() {
+	link = NavigationServer2D::get_singleton()->link_create();
+	set_notify_transform(true);
+}
+
+NavigationLink2D::~NavigationLink2D() {
+	NavigationServer2D::get_singleton()->free(link);
+	link = RID();
+}

+ 88 - 0
scene/2d/navigation_link_2d.h

@@ -0,0 +1,88 @@
+/*************************************************************************/
+/*  navigation_link_2d.h                                                 */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#ifndef NAVIGATION_LINK_2D_H
+#define NAVIGATION_LINK_2D_H
+
+#include "scene/2d/node_2d.h"
+
+class NavigationLink2D : public Node2D {
+	GDCLASS(NavigationLink2D, Node2D);
+
+	bool enabled = true;
+	RID link = RID();
+	bool bidirectional = true;
+	uint32_t navigation_layers = 1;
+	Vector2 end_location = Vector2();
+	Vector2 start_location = Vector2();
+	real_t enter_cost = 0.0;
+	real_t travel_cost = 1.0;
+
+protected:
+	static void _bind_methods();
+	void _notification(int p_what);
+
+public:
+#ifdef TOOLS_ENABLED
+	virtual Rect2 _edit_get_rect() const override;
+	virtual bool _edit_is_selected_on_click(const Point2 &p_point, double p_tolerance) const override;
+#endif
+
+	void set_enabled(bool p_enabled);
+	bool is_enabled() const { return enabled; }
+
+	void set_bidirectional(bool p_bidirectional);
+	bool is_bidirectional() const { return bidirectional; }
+
+	void set_navigation_layers(uint32_t p_navigation_layers);
+	uint32_t get_navigation_layers() const { return navigation_layers; }
+
+	void set_navigation_layer_value(int p_layer_number, bool p_value);
+	bool get_navigation_layer_value(int p_layer_number) const;
+
+	void set_start_location(Vector2 p_location);
+	Vector2 get_start_location() const { return start_location; }
+
+	void set_end_location(Vector2 p_location);
+	Vector2 get_end_location() const { return end_location; }
+
+	void set_enter_cost(real_t p_enter_cost);
+	real_t get_enter_cost() const { return enter_cost; }
+
+	void set_travel_cost(real_t p_travel_cost);
+	real_t get_travel_cost() const { return travel_cost; }
+
+	TypedArray<String> get_configuration_warnings() const override;
+
+	NavigationLink2D();
+	~NavigationLink2D();
+};
+
+#endif // NAVIGATION_LINK_2D_H

+ 389 - 0
scene/3d/navigation_link_3d.cpp

@@ -0,0 +1,389 @@
+/*************************************************************************/
+/*  navigation_link_3d.cpp                                               */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#include "navigation_link_3d.h"
+
+#include "mesh_instance_3d.h"
+#include "servers/navigation_server_3d.h"
+
+#ifdef DEBUG_ENABLED
+void NavigationLink3D::_update_debug_mesh() {
+	if (!is_inside_tree()) {
+		return;
+	}
+
+	if (Engine::get_singleton()->is_editor_hint()) {
+		// don't update inside Editor as node 3d gizmo takes care of this
+		// as collisions and selections for Editor Viewport need to be updated
+		return;
+	}
+
+	if (!NavigationServer3D::get_singleton()->get_debug_enabled()) {
+		if (debug_instance.is_valid()) {
+			RS::get_singleton()->instance_set_visible(debug_instance, false);
+		}
+		return;
+	}
+
+	if (!debug_instance.is_valid()) {
+		debug_instance = RenderingServer::get_singleton()->instance_create();
+	}
+
+	if (!debug_mesh.is_valid()) {
+		debug_mesh = Ref<ArrayMesh>(memnew(ArrayMesh));
+	}
+
+	RID nav_map = get_world_3d()->get_navigation_map();
+	real_t search_radius = NavigationServer3D::get_singleton()->map_get_link_connection_radius(nav_map);
+	Vector3 up_vector = NavigationServer3D::get_singleton()->map_get_up(nav_map);
+	Vector3::Axis up_axis = up_vector.max_axis_index();
+
+	debug_mesh->clear_surfaces();
+
+	Vector<Vector3> lines;
+
+	// Draw line between the points.
+	lines.push_back(start_location);
+	lines.push_back(end_location);
+
+	// Draw start location search radius
+	for (int i = 0; i < 30; i++) {
+		// Create a circle
+		const float ra = Math::deg_to_rad((float)(i * 12));
+		const float rb = Math::deg_to_rad((float)((i + 1) * 12));
+		const Point2 a = Vector2(Math::sin(ra), Math::cos(ra)) * search_radius;
+		const Point2 b = Vector2(Math::sin(rb), Math::cos(rb)) * search_radius;
+
+		// Draw axis-aligned circle
+		switch (up_axis) {
+			case Vector3::AXIS_X:
+				lines.append(start_location + Vector3(0, a.x, a.y));
+				lines.append(start_location + Vector3(0, b.x, b.y));
+				break;
+			case Vector3::AXIS_Y:
+				lines.append(start_location + Vector3(a.x, 0, a.y));
+				lines.append(start_location + Vector3(b.x, 0, b.y));
+				break;
+			case Vector3::AXIS_Z:
+				lines.append(start_location + Vector3(a.x, a.y, 0));
+				lines.append(start_location + Vector3(b.x, b.y, 0));
+				break;
+		}
+	}
+
+	// Draw end location search radius
+	for (int i = 0; i < 30; i++) {
+		// Create a circle
+		const float ra = Math::deg_to_rad((float)(i * 12));
+		const float rb = Math::deg_to_rad((float)((i + 1) * 12));
+		const Point2 a = Vector2(Math::sin(ra), Math::cos(ra)) * search_radius;
+		const Point2 b = Vector2(Math::sin(rb), Math::cos(rb)) * search_radius;
+
+		// Draw axis-aligned circle
+		switch (up_axis) {
+			case Vector3::AXIS_X:
+				lines.append(end_location + Vector3(0, a.x, a.y));
+				lines.append(end_location + Vector3(0, b.x, b.y));
+				break;
+			case Vector3::AXIS_Y:
+				lines.append(end_location + Vector3(a.x, 0, a.y));
+				lines.append(end_location + Vector3(b.x, 0, b.y));
+				break;
+			case Vector3::AXIS_Z:
+				lines.append(end_location + Vector3(a.x, a.y, 0));
+				lines.append(end_location + Vector3(b.x, b.y, 0));
+				break;
+		}
+	}
+
+	Array mesh_array;
+	mesh_array.resize(Mesh::ARRAY_MAX);
+	mesh_array[Mesh::ARRAY_VERTEX] = lines;
+
+	debug_mesh->add_surface_from_arrays(Mesh::PRIMITIVE_LINES, mesh_array);
+
+	RS::get_singleton()->instance_set_base(debug_instance, debug_mesh->get_rid());
+	RS::get_singleton()->instance_set_scenario(debug_instance, get_world_3d()->get_scenario());
+	RS::get_singleton()->instance_set_visible(debug_instance, is_visible_in_tree());
+
+	Ref<StandardMaterial3D> link_material = NavigationServer3D::get_singleton_mut()->get_debug_navigation_link_connections_material();
+	Ref<StandardMaterial3D> disabled_link_material = NavigationServer3D::get_singleton_mut()->get_debug_navigation_link_connections_disabled_material();
+
+	if (enabled) {
+		RS::get_singleton()->instance_set_surface_override_material(debug_instance, 0, link_material->get_rid());
+	} else {
+		RS::get_singleton()->instance_set_surface_override_material(debug_instance, 0, disabled_link_material->get_rid());
+	}
+}
+#endif // DEBUG_ENABLED
+
+void NavigationLink3D::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("set_enabled", "enabled"), &NavigationLink3D::set_enabled);
+	ClassDB::bind_method(D_METHOD("is_enabled"), &NavigationLink3D::is_enabled);
+
+	ClassDB::bind_method(D_METHOD("set_bidirectional", "bidirectional"), &NavigationLink3D::set_bidirectional);
+	ClassDB::bind_method(D_METHOD("is_bidirectional"), &NavigationLink3D::is_bidirectional);
+
+	ClassDB::bind_method(D_METHOD("set_navigation_layers", "navigation_layers"), &NavigationLink3D::set_navigation_layers);
+	ClassDB::bind_method(D_METHOD("get_navigation_layers"), &NavigationLink3D::get_navigation_layers);
+
+	ClassDB::bind_method(D_METHOD("set_navigation_layer_value", "layer_number", "value"), &NavigationLink3D::set_navigation_layer_value);
+	ClassDB::bind_method(D_METHOD("get_navigation_layer_value", "layer_number"), &NavigationLink3D::get_navigation_layer_value);
+
+	ClassDB::bind_method(D_METHOD("set_start_location", "location"), &NavigationLink3D::set_start_location);
+	ClassDB::bind_method(D_METHOD("get_start_location"), &NavigationLink3D::get_start_location);
+
+	ClassDB::bind_method(D_METHOD("set_end_location", "location"), &NavigationLink3D::set_end_location);
+	ClassDB::bind_method(D_METHOD("get_end_location"), &NavigationLink3D::get_end_location);
+
+	ClassDB::bind_method(D_METHOD("set_enter_cost", "enter_cost"), &NavigationLink3D::set_enter_cost);
+	ClassDB::bind_method(D_METHOD("get_enter_cost"), &NavigationLink3D::get_enter_cost);
+
+	ClassDB::bind_method(D_METHOD("set_travel_cost", "travel_cost"), &NavigationLink3D::set_travel_cost);
+	ClassDB::bind_method(D_METHOD("get_travel_cost"), &NavigationLink3D::get_travel_cost);
+
+	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "enabled"), "set_enabled", "is_enabled");
+	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "bidirectional"), "set_bidirectional", "is_bidirectional");
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "navigation_layers", PROPERTY_HINT_LAYERS_3D_NAVIGATION), "set_navigation_layers", "get_navigation_layers");
+	ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "start_location"), "set_start_location", "get_start_location");
+	ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "end_location"), "set_end_location", "get_end_location");
+	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "enter_cost"), "set_enter_cost", "get_enter_cost");
+	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "travel_cost"), "set_travel_cost", "get_travel_cost");
+}
+
+void NavigationLink3D::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_ENTER_TREE: {
+			if (enabled) {
+				NavigationServer3D::get_singleton()->link_set_map(link, get_world_3d()->get_navigation_map());
+
+				// Update global positions for the link.
+				Transform3D gt = get_global_transform();
+				NavigationServer3D::get_singleton()->link_set_start_location(link, gt.xform(start_location));
+				NavigationServer3D::get_singleton()->link_set_end_location(link, gt.xform(end_location));
+			}
+
+#ifdef DEBUG_ENABLED
+			_update_debug_mesh();
+#endif // DEBUG_ENABLED
+		} break;
+		case NOTIFICATION_TRANSFORM_CHANGED: {
+			// Update global positions for the link.
+			Transform3D gt = get_global_transform();
+			NavigationServer3D::get_singleton()->link_set_start_location(link, gt.xform(start_location));
+			NavigationServer3D::get_singleton()->link_set_end_location(link, gt.xform(end_location));
+
+#ifdef DEBUG_ENABLED
+			if (is_inside_tree() && debug_instance.is_valid()) {
+				RS::get_singleton()->instance_set_transform(debug_instance, get_global_transform());
+			}
+#endif // DEBUG_ENABLED
+		} break;
+		case NOTIFICATION_EXIT_TREE: {
+			NavigationServer3D::get_singleton()->link_set_map(link, RID());
+
+#ifdef DEBUG_ENABLED
+			if (debug_instance.is_valid()) {
+				RS::get_singleton()->instance_set_scenario(debug_instance, RID());
+				RS::get_singleton()->instance_set_visible(debug_instance, false);
+			}
+#endif // DEBUG_ENABLED
+		} break;
+	}
+}
+
+NavigationLink3D::NavigationLink3D() {
+	link = NavigationServer3D::get_singleton()->link_create();
+	set_notify_transform(true);
+}
+
+NavigationLink3D::~NavigationLink3D() {
+	NavigationServer3D::get_singleton()->free(link);
+	link = RID();
+
+#ifdef DEBUG_ENABLED
+	if (debug_instance.is_valid()) {
+		RenderingServer::get_singleton()->free(debug_instance);
+	}
+	if (debug_mesh.is_valid()) {
+		RenderingServer::get_singleton()->free(debug_mesh->get_rid());
+	}
+#endif // DEBUG_ENABLED
+}
+
+void NavigationLink3D::set_enabled(bool p_enabled) {
+	if (enabled == p_enabled) {
+		return;
+	}
+
+	enabled = p_enabled;
+
+	if (!is_inside_tree()) {
+		return;
+	}
+
+	if (enabled) {
+		NavigationServer3D::get_singleton()->link_set_map(link, get_world_3d()->get_navigation_map());
+	} else {
+		NavigationServer3D::get_singleton()->link_set_map(link, RID());
+	}
+
+#ifdef DEBUG_ENABLED
+	if (debug_instance.is_valid() && debug_mesh.is_valid()) {
+		if (enabled) {
+			Ref<StandardMaterial3D> link_material = NavigationServer3D::get_singleton_mut()->get_debug_navigation_link_connections_material();
+			RS::get_singleton()->instance_set_surface_override_material(debug_instance, 0, link_material->get_rid());
+		} else {
+			Ref<StandardMaterial3D> disabled_link_material = NavigationServer3D::get_singleton_mut()->get_debug_navigation_link_connections_disabled_material();
+			RS::get_singleton()->instance_set_surface_override_material(debug_instance, 0, disabled_link_material->get_rid());
+		}
+	}
+#endif // DEBUG_ENABLED
+
+	update_gizmos();
+}
+
+void NavigationLink3D::set_bidirectional(bool p_bidirectional) {
+	if (bidirectional == p_bidirectional) {
+		return;
+	}
+
+	bidirectional = p_bidirectional;
+
+	NavigationServer3D::get_singleton()->link_set_bidirectional(link, bidirectional);
+}
+
+void NavigationLink3D::set_navigation_layers(uint32_t p_navigation_layers) {
+	if (navigation_layers == p_navigation_layers) {
+		return;
+	}
+
+	navigation_layers = p_navigation_layers;
+
+	NavigationServer3D::get_singleton()->link_set_navigation_layers(link, navigation_layers);
+}
+
+void NavigationLink3D::set_navigation_layer_value(int p_layer_number, bool p_value) {
+	ERR_FAIL_COND_MSG(p_layer_number < 1, "Navigation layer number must be between 1 and 32 inclusive.");
+	ERR_FAIL_COND_MSG(p_layer_number > 32, "Navigation layer number must be between 1 and 32 inclusive.");
+
+	uint32_t _navigation_layers = get_navigation_layers();
+
+	if (p_value) {
+		_navigation_layers |= 1 << (p_layer_number - 1);
+	} else {
+		_navigation_layers &= ~(1 << (p_layer_number - 1));
+	}
+
+	set_navigation_layers(_navigation_layers);
+}
+
+bool NavigationLink3D::get_navigation_layer_value(int p_layer_number) const {
+	ERR_FAIL_COND_V_MSG(p_layer_number < 1, false, "Navigation layer number must be between 1 and 32 inclusive.");
+	ERR_FAIL_COND_V_MSG(p_layer_number > 32, false, "Navigation layer number must be between 1 and 32 inclusive.");
+
+	return get_navigation_layers() & (1 << (p_layer_number - 1));
+}
+
+void NavigationLink3D::set_start_location(Vector3 p_location) {
+	if (start_location.is_equal_approx(p_location)) {
+		return;
+	}
+
+	start_location = p_location;
+
+	if (!is_inside_tree()) {
+		return;
+	}
+
+	Transform3D gt = get_global_transform();
+	NavigationServer3D::get_singleton()->link_set_start_location(link, gt.xform(start_location));
+
+#ifdef DEBUG_ENABLED
+	_update_debug_mesh();
+#endif // DEBUG_ENABLED
+
+	update_gizmos();
+	update_configuration_warnings();
+}
+
+void NavigationLink3D::set_end_location(Vector3 p_location) {
+	if (end_location.is_equal_approx(p_location)) {
+		return;
+	}
+
+	end_location = p_location;
+
+	if (!is_inside_tree()) {
+		return;
+	}
+
+	Transform3D gt = get_global_transform();
+	NavigationServer3D::get_singleton()->link_set_end_location(link, gt.xform(end_location));
+
+#ifdef DEBUG_ENABLED
+	_update_debug_mesh();
+#endif // DEBUG_ENABLED
+
+	update_gizmos();
+	update_configuration_warnings();
+}
+
+void NavigationLink3D::set_enter_cost(real_t p_enter_cost) {
+	ERR_FAIL_COND_MSG(p_enter_cost < 0.0, "The enter_cost must be positive.");
+	if (Math::is_equal_approx(enter_cost, p_enter_cost)) {
+		return;
+	}
+
+	enter_cost = p_enter_cost;
+
+	NavigationServer3D::get_singleton()->link_set_enter_cost(link, enter_cost);
+}
+
+void NavigationLink3D::set_travel_cost(real_t p_travel_cost) {
+	ERR_FAIL_COND_MSG(p_travel_cost < 0.0, "The travel_cost must be positive.");
+	if (Math::is_equal_approx(travel_cost, p_travel_cost)) {
+		return;
+	}
+
+	travel_cost = p_travel_cost;
+
+	NavigationServer3D::get_singleton()->link_set_travel_cost(link, travel_cost);
+}
+
+TypedArray<String> NavigationLink3D::get_configuration_warnings() const {
+	TypedArray<String> warnings = Node::get_configuration_warnings();
+
+	if (start_location.is_equal_approx(end_location)) {
+		warnings.push_back(RTR("NavigationLink3D start location should be different than the end location to be useful."));
+	}
+
+	return warnings;
+}

+ 90 - 0
scene/3d/navigation_link_3d.h

@@ -0,0 +1,90 @@
+/*************************************************************************/
+/*  navigation_link_3d.h                                                 */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#ifndef NAVIGATION_LINK_3D_H
+#define NAVIGATION_LINK_3D_H
+
+#include "scene/3d/node_3d.h"
+
+class NavigationLink3D : public Node3D {
+	GDCLASS(NavigationLink3D, Node3D);
+
+	bool enabled = true;
+	RID link = RID();
+	bool bidirectional = true;
+	uint32_t navigation_layers = 1;
+	Vector3 end_location = Vector3();
+	Vector3 start_location = Vector3();
+	real_t enter_cost = 0.0;
+	real_t travel_cost = 1.0;
+
+#ifdef DEBUG_ENABLED
+	RID debug_instance;
+	Ref<ArrayMesh> debug_mesh;
+
+	void _update_debug_mesh();
+#endif // DEBUG_ENABLED
+
+protected:
+	static void _bind_methods();
+	void _notification(int p_what);
+
+public:
+	NavigationLink3D();
+	~NavigationLink3D();
+
+	void set_enabled(bool p_enabled);
+	bool is_enabled() const { return enabled; }
+
+	void set_bidirectional(bool p_bidirectional);
+	bool is_bidirectional() const { return bidirectional; }
+
+	void set_navigation_layers(uint32_t p_navigation_layers);
+	uint32_t get_navigation_layers() const { return navigation_layers; }
+
+	void set_navigation_layer_value(int p_layer_number, bool p_value);
+	bool get_navigation_layer_value(int p_layer_number) const;
+
+	void set_start_location(Vector3 p_location);
+	Vector3 get_start_location() const { return start_location; }
+
+	void set_end_location(Vector3 p_location);
+	Vector3 get_end_location() const { return end_location; }
+
+	void set_enter_cost(real_t p_enter_cost);
+	real_t get_enter_cost() const { return enter_cost; }
+
+	void set_travel_cost(real_t p_travel_cost);
+	real_t get_travel_cost() const { return travel_cost; }
+
+	TypedArray<String> get_configuration_warnings() const override;
+};
+
+#endif // NAVIGATION_LINK_3D_H

+ 4 - 0
scene/register_scene_types.cpp

@@ -54,6 +54,7 @@
 #include "scene/2d/mesh_instance_2d.h"
 #include "scene/2d/multimesh_instance_2d.h"
 #include "scene/2d/navigation_agent_2d.h"
+#include "scene/2d/navigation_link_2d.h"
 #include "scene/2d/navigation_obstacle_2d.h"
 #include "scene/2d/parallax_background.h"
 #include "scene/2d/parallax_layer.h"
@@ -240,6 +241,7 @@
 #include "scene/3d/mesh_instance_3d.h"
 #include "scene/3d/multimesh_instance_3d.h"
 #include "scene/3d/navigation_agent_3d.h"
+#include "scene/3d/navigation_link_3d.h"
 #include "scene/3d/navigation_obstacle_3d.h"
 #include "scene/3d/navigation_region_3d.h"
 #include "scene/3d/node_3d.h"
@@ -577,6 +579,7 @@ void register_scene_types() {
 	GDREGISTER_CLASS(NavigationRegion3D);
 	GDREGISTER_CLASS(NavigationAgent3D);
 	GDREGISTER_CLASS(NavigationObstacle3D);
+	GDREGISTER_CLASS(NavigationLink3D);
 
 	OS::get_singleton()->yield(); // may take time to init
 #endif // _3D_DISABLED
@@ -934,6 +937,7 @@ void register_scene_types() {
 	GDREGISTER_CLASS(NavigationRegion2D);
 	GDREGISTER_CLASS(NavigationAgent2D);
 	GDREGISTER_CLASS(NavigationObstacle2D);
+	GDREGISTER_CLASS(NavigationLink2D);
 
 	OS::get_singleton()->yield(); // may take time to init
 

+ 1 - 0
scene/resources/world_2d.cpp

@@ -85,6 +85,7 @@ World2D::World2D() {
 	NavigationServer2D::get_singleton()->map_set_active(navigation_map, true);
 	NavigationServer2D::get_singleton()->map_set_cell_size(navigation_map, GLOBAL_DEF("navigation/2d/default_cell_size", 1));
 	NavigationServer2D::get_singleton()->map_set_edge_connection_margin(navigation_map, GLOBAL_DEF("navigation/2d/default_edge_connection_margin", 1));
+	NavigationServer2D::get_singleton()->map_set_link_connection_radius(navigation_map, GLOBAL_DEF("navigation/2d/default_link_connection_radius", 4));
 }
 
 World2D::~World2D() {

+ 1 - 0
scene/resources/world_3d.cpp

@@ -153,6 +153,7 @@ World3D::World3D() {
 	NavigationServer3D::get_singleton()->map_set_active(navigation_map, true);
 	NavigationServer3D::get_singleton()->map_set_cell_size(navigation_map, GLOBAL_DEF("navigation/3d/default_cell_size", 0.25));
 	NavigationServer3D::get_singleton()->map_set_edge_connection_margin(navigation_map, GLOBAL_DEF("navigation/3d/default_edge_connection_margin", 0.25));
+	NavigationServer3D::get_singleton()->map_set_link_connection_radius(navigation_map, GLOBAL_DEF("navigation/3d/default_link_connection_radius", 1.0));
 }
 
 World3D::~World3D() {

+ 63 - 0
servers/navigation_server_2d.cpp

@@ -53,6 +53,12 @@ NavigationServer2D *NavigationServer2D::singleton = nullptr;
 		return NavigationServer3D::get_singleton()->FUNC_NAME(CONV_0(D_0)); \
 	}
 
+#define FORWARD_1_R_C(CONV_R, FUNC_NAME, T_0, D_0, CONV_0)                          \
+	NavigationServer2D::FUNC_NAME(T_0 D_0)                                          \
+			const {                                                                 \
+		return CONV_R(NavigationServer3D::get_singleton()->FUNC_NAME(CONV_0(D_0))); \
+	}
+
 #define FORWARD_2_C(FUNC_NAME, T_0, D_0, T_1, D_1, CONV_0, CONV_1)                       \
 	NavigationServer2D::FUNC_NAME(T_0 D_0, T_1 D_1)                                      \
 			const {                                                                      \
@@ -190,6 +196,22 @@ Color NavigationServer2D::get_debug_navigation_geometry_face_disabled_color() co
 	return NavigationServer3D::get_singleton()->get_debug_navigation_geometry_face_disabled_color();
 }
 
+void NavigationServer2D::set_debug_navigation_link_connection_color(const Color &p_color) {
+	NavigationServer3D::get_singleton_mut()->set_debug_navigation_link_connection_color(p_color);
+}
+
+Color NavigationServer2D::get_debug_navigation_link_connection_color() const {
+	return NavigationServer3D::get_singleton()->get_debug_navigation_link_connection_color();
+}
+
+void NavigationServer2D::set_debug_navigation_link_connection_disabled_color(const Color &p_color) {
+	NavigationServer3D::get_singleton_mut()->set_debug_navigation_link_connection_disabled_color(p_color);
+}
+
+Color NavigationServer2D::get_debug_navigation_link_connection_disabled_color() const {
+	return NavigationServer3D::get_singleton()->get_debug_navigation_link_connection_disabled_color();
+}
+
 void NavigationServer2D::set_debug_navigation_enable_edge_connections(const bool p_value) {
 	NavigationServer3D::get_singleton_mut()->set_debug_navigation_enable_edge_connections(p_value);
 }
@@ -209,10 +231,13 @@ void NavigationServer2D::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("map_get_cell_size", "map"), &NavigationServer2D::map_get_cell_size);
 	ClassDB::bind_method(D_METHOD("map_set_edge_connection_margin", "map", "margin"), &NavigationServer2D::map_set_edge_connection_margin);
 	ClassDB::bind_method(D_METHOD("map_get_edge_connection_margin", "map"), &NavigationServer2D::map_get_edge_connection_margin);
+	ClassDB::bind_method(D_METHOD("map_set_link_connection_radius", "map", "radius"), &NavigationServer2D::map_set_link_connection_radius);
+	ClassDB::bind_method(D_METHOD("map_get_link_connection_radius", "map"), &NavigationServer2D::map_get_link_connection_radius);
 	ClassDB::bind_method(D_METHOD("map_get_path", "map", "origin", "destination", "optimize", "navigation_layers"), &NavigationServer2D::map_get_path, DEFVAL(1));
 	ClassDB::bind_method(D_METHOD("map_get_closest_point", "map", "to_point"), &NavigationServer2D::map_get_closest_point);
 	ClassDB::bind_method(D_METHOD("map_get_closest_point_owner", "map", "to_point"), &NavigationServer2D::map_get_closest_point_owner);
 
+	ClassDB::bind_method(D_METHOD("map_get_links", "map"), &NavigationServer2D::map_get_links);
 	ClassDB::bind_method(D_METHOD("map_get_regions", "map"), &NavigationServer2D::map_get_regions);
 	ClassDB::bind_method(D_METHOD("map_get_agents", "map"), &NavigationServer2D::map_get_agents);
 
@@ -234,6 +259,22 @@ void NavigationServer2D::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("region_get_connection_pathway_start", "region", "connection"), &NavigationServer2D::region_get_connection_pathway_start);
 	ClassDB::bind_method(D_METHOD("region_get_connection_pathway_end", "region", "connection"), &NavigationServer2D::region_get_connection_pathway_end);
 
+	ClassDB::bind_method(D_METHOD("link_create"), &NavigationServer2D::link_create);
+	ClassDB::bind_method(D_METHOD("link_set_map", "link", "map"), &NavigationServer2D::link_set_map);
+	ClassDB::bind_method(D_METHOD("link_get_map", "link"), &NavigationServer2D::link_get_map);
+	ClassDB::bind_method(D_METHOD("link_set_bidirectional", "link", "bidirectional"), &NavigationServer2D::link_set_bidirectional);
+	ClassDB::bind_method(D_METHOD("link_is_bidirectional", "link"), &NavigationServer2D::link_is_bidirectional);
+	ClassDB::bind_method(D_METHOD("link_set_navigation_layers", "link", "navigation_layers"), &NavigationServer2D::link_set_navigation_layers);
+	ClassDB::bind_method(D_METHOD("link_get_navigation_layers", "link"), &NavigationServer2D::link_get_navigation_layers);
+	ClassDB::bind_method(D_METHOD("link_set_start_location", "link", "location"), &NavigationServer2D::link_set_start_location);
+	ClassDB::bind_method(D_METHOD("link_get_start_location", "link"), &NavigationServer2D::link_get_start_location);
+	ClassDB::bind_method(D_METHOD("link_set_end_location", "link", "location"), &NavigationServer2D::link_set_end_location);
+	ClassDB::bind_method(D_METHOD("link_get_end_location", "link"), &NavigationServer2D::link_get_end_location);
+	ClassDB::bind_method(D_METHOD("link_set_enter_cost", "link", "enter_cost"), &NavigationServer2D::link_set_enter_cost);
+	ClassDB::bind_method(D_METHOD("link_get_enter_cost", "link"), &NavigationServer2D::link_get_enter_cost);
+	ClassDB::bind_method(D_METHOD("link_set_travel_cost", "link", "travel_cost"), &NavigationServer2D::link_set_travel_cost);
+	ClassDB::bind_method(D_METHOD("link_get_travel_cost", "link"), &NavigationServer2D::link_get_travel_cost);
+
 	ClassDB::bind_method(D_METHOD("agent_create"), &NavigationServer2D::agent_create);
 	ClassDB::bind_method(D_METHOD("agent_set_map", "agent", "map"), &NavigationServer2D::agent_set_map);
 	ClassDB::bind_method(D_METHOD("agent_get_map", "agent"), &NavigationServer2D::agent_get_map);
@@ -265,6 +306,8 @@ NavigationServer2D::~NavigationServer2D() {
 
 TypedArray<RID> FORWARD_0_C(get_maps);
 
+TypedArray<RID> FORWARD_1_C(map_get_links, RID, p_map, rid_to_rid);
+
 TypedArray<RID> FORWARD_1_C(map_get_regions, RID, p_map, rid_to_rid);
 
 TypedArray<RID> FORWARD_1_C(map_get_agents, RID, p_map, rid_to_rid);
@@ -289,6 +332,9 @@ real_t FORWARD_1_C(map_get_cell_size, RID, p_map, rid_to_rid);
 void FORWARD_2_C(map_set_edge_connection_margin, RID, p_map, real_t, p_connection_margin, rid_to_rid, real_to_real);
 real_t FORWARD_1_C(map_get_edge_connection_margin, RID, p_map, rid_to_rid);
 
+void FORWARD_2_C(map_set_link_connection_radius, RID, p_map, real_t, p_connection_radius, rid_to_rid, real_to_real);
+real_t FORWARD_1_C(map_get_link_connection_radius, RID, p_map, rid_to_rid);
+
 Vector<Vector2> FORWARD_5_R_C(vector_v3_to_v2, map_get_path, RID, p_map, Vector2, p_origin, Vector2, p_destination, bool, p_optimize, uint32_t, p_layers, rid_to_rid, v2_to_v3, v2_to_v3, bool_to_bool, uint32_to_uint32);
 
 Vector2 FORWARD_2_R_C(v3_to_v2, map_get_closest_point, RID, p_map, const Vector2 &, p_point, rid_to_rid, v2_to_v3);
@@ -315,6 +361,23 @@ int FORWARD_1_C(region_get_connections_count, RID, p_region, rid_to_rid);
 Vector2 FORWARD_2_R_C(v3_to_v2, region_get_connection_pathway_start, RID, p_region, int, p_connection_id, rid_to_rid, int_to_int);
 Vector2 FORWARD_2_R_C(v3_to_v2, region_get_connection_pathway_end, RID, p_region, int, p_connection_id, rid_to_rid, int_to_int);
 
+RID FORWARD_0_C(link_create);
+
+void FORWARD_2_C(link_set_map, RID, p_link, RID, p_map, rid_to_rid, rid_to_rid);
+RID FORWARD_1_C(link_get_map, RID, p_link, rid_to_rid);
+void FORWARD_2_C(link_set_bidirectional, RID, p_link, bool, p_bidirectional, rid_to_rid, bool_to_bool);
+bool FORWARD_1_C(link_is_bidirectional, RID, p_link, rid_to_rid);
+void FORWARD_2_C(link_set_navigation_layers, RID, p_link, uint32_t, p_navigation_layers, rid_to_rid, uint32_to_uint32);
+uint32_t FORWARD_1_C(link_get_navigation_layers, RID, p_link, rid_to_rid);
+void FORWARD_2_C(link_set_start_location, RID, p_link, Vector2, p_location, rid_to_rid, v2_to_v3);
+Vector2 FORWARD_1_R_C(v3_to_v2, link_get_start_location, RID, p_link, rid_to_rid);
+void FORWARD_2_C(link_set_end_location, RID, p_link, Vector2, p_location, rid_to_rid, v2_to_v3);
+Vector2 FORWARD_1_R_C(v3_to_v2, link_get_end_location, RID, p_link, rid_to_rid);
+void FORWARD_2_C(link_set_enter_cost, RID, p_link, real_t, p_enter_cost, rid_to_rid, real_to_real);
+real_t FORWARD_1_C(link_get_enter_cost, RID, p_link, rid_to_rid);
+void FORWARD_2_C(link_set_travel_cost, RID, p_link, real_t, p_travel_cost, rid_to_rid, real_to_real);
+real_t FORWARD_1_C(link_get_travel_cost, RID, p_link, rid_to_rid);
+
 RID NavigationServer2D::agent_create() const {
 	RID agent = NavigationServer3D::get_singleton()->agent_create();
 	NavigationServer3D::get_singleton()->agent_set_ignore_y(agent, true);

+ 44 - 0
servers/navigation_server_2d.h

@@ -76,12 +76,19 @@ public:
 	/// Returns the edge connection margin of this map.
 	virtual real_t map_get_edge_connection_margin(RID p_map) const;
 
+	/// Set the map link connection radius used to attach links to the nav mesh.
+	virtual void map_set_link_connection_radius(RID p_map, real_t p_connection_radius) const;
+
+	/// Returns the link connection radius of this map.
+	virtual real_t map_get_link_connection_radius(RID p_map) const;
+
 	/// Returns the navigation path to reach the destination from the origin.
 	virtual Vector<Vector2> map_get_path(RID p_map, Vector2 p_origin, Vector2 p_destination, bool p_optimize, uint32_t p_navigation_layers = 1) const;
 
 	virtual Vector2 map_get_closest_point(RID p_map, const Vector2 &p_point) const;
 	virtual RID map_get_closest_point_owner(RID p_map, const Vector2 &p_point) const;
 
+	virtual TypedArray<RID> map_get_links(RID p_map) const;
 	virtual TypedArray<RID> map_get_regions(RID p_map) const;
 	virtual TypedArray<RID> map_get_agents(RID p_map) const;
 
@@ -119,6 +126,37 @@ public:
 	virtual Vector2 region_get_connection_pathway_start(RID p_region, int p_connection_id) const;
 	virtual Vector2 region_get_connection_pathway_end(RID p_region, int p_connection_id) const;
 
+	/// Creates a new link between locations in the nav map.
+	virtual RID link_create() const;
+
+	/// Set the map of this link.
+	virtual void link_set_map(RID p_link, RID p_map) const;
+	virtual RID link_get_map(RID p_link) const;
+
+	/// Set whether this link travels in both directions.
+	virtual void link_set_bidirectional(RID p_link, bool p_bidirectional) const;
+	virtual bool link_is_bidirectional(RID p_link) const;
+
+	/// Set the link's layers.
+	virtual void link_set_navigation_layers(RID p_link, uint32_t p_navigation_layers) const;
+	virtual uint32_t link_get_navigation_layers(RID p_link) const;
+
+	/// Set the start location of the link.
+	virtual void link_set_start_location(RID p_link, Vector2 p_location) const;
+	virtual Vector2 link_get_start_location(RID p_link) const;
+
+	/// Set the end location of the link.
+	virtual void link_set_end_location(RID p_link, Vector2 p_location) const;
+	virtual Vector2 link_get_end_location(RID p_link) const;
+
+	/// Set the enter cost of the link.
+	virtual void link_set_enter_cost(RID p_link, real_t p_enter_cost) const;
+	virtual real_t link_get_enter_cost(RID p_link) const;
+
+	/// Set the travel cost of the link.
+	virtual void link_set_travel_cost(RID p_link, real_t p_travel_cost) const;
+	virtual real_t link_get_travel_cost(RID p_link) const;
+
 	/// Creates the agent.
 	virtual RID agent_create() const;
 
@@ -198,6 +236,12 @@ public:
 	void set_debug_navigation_geometry_face_disabled_color(const Color &p_color);
 	Color get_debug_navigation_geometry_face_disabled_color() const;
 
+	void set_debug_navigation_link_connection_color(const Color &p_color);
+	Color get_debug_navigation_link_connection_color() const;
+
+	void set_debug_navigation_link_connection_disabled_color(const Color &p_color);
+	Color get_debug_navigation_link_connection_disabled_color() const;
+
 	void set_debug_navigation_enable_edge_connections(const bool p_value);
 	bool get_debug_navigation_enable_edge_connections() const;
 #endif // DEBUG_ENABLED

+ 101 - 0
servers/navigation_server_3d.cpp

@@ -48,12 +48,15 @@ void NavigationServer3D::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("map_get_cell_size", "map"), &NavigationServer3D::map_get_cell_size);
 	ClassDB::bind_method(D_METHOD("map_set_edge_connection_margin", "map", "margin"), &NavigationServer3D::map_set_edge_connection_margin);
 	ClassDB::bind_method(D_METHOD("map_get_edge_connection_margin", "map"), &NavigationServer3D::map_get_edge_connection_margin);
+	ClassDB::bind_method(D_METHOD("map_set_link_connection_radius", "map", "radius"), &NavigationServer3D::map_set_link_connection_radius);
+	ClassDB::bind_method(D_METHOD("map_get_link_connection_radius", "map"), &NavigationServer3D::map_get_link_connection_radius);
 	ClassDB::bind_method(D_METHOD("map_get_path", "map", "origin", "destination", "optimize", "navigation_layers"), &NavigationServer3D::map_get_path, DEFVAL(1));
 	ClassDB::bind_method(D_METHOD("map_get_closest_point_to_segment", "map", "start", "end", "use_collision"), &NavigationServer3D::map_get_closest_point_to_segment, DEFVAL(false));
 	ClassDB::bind_method(D_METHOD("map_get_closest_point", "map", "to_point"), &NavigationServer3D::map_get_closest_point);
 	ClassDB::bind_method(D_METHOD("map_get_closest_point_normal", "map", "to_point"), &NavigationServer3D::map_get_closest_point_normal);
 	ClassDB::bind_method(D_METHOD("map_get_closest_point_owner", "map", "to_point"), &NavigationServer3D::map_get_closest_point_owner);
 
+	ClassDB::bind_method(D_METHOD("map_get_links", "map"), &NavigationServer3D::map_get_links);
 	ClassDB::bind_method(D_METHOD("map_get_regions", "map"), &NavigationServer3D::map_get_regions);
 	ClassDB::bind_method(D_METHOD("map_get_agents", "map"), &NavigationServer3D::map_get_agents);
 
@@ -76,6 +79,22 @@ void NavigationServer3D::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("region_get_connection_pathway_start", "region", "connection"), &NavigationServer3D::region_get_connection_pathway_start);
 	ClassDB::bind_method(D_METHOD("region_get_connection_pathway_end", "region", "connection"), &NavigationServer3D::region_get_connection_pathway_end);
 
+	ClassDB::bind_method(D_METHOD("link_create"), &NavigationServer3D::link_create);
+	ClassDB::bind_method(D_METHOD("link_set_map", "link", "map"), &NavigationServer3D::link_set_map);
+	ClassDB::bind_method(D_METHOD("link_get_map", "link"), &NavigationServer3D::link_get_map);
+	ClassDB::bind_method(D_METHOD("link_set_bidirectional", "link", "bidirectional"), &NavigationServer3D::link_set_bidirectional);
+	ClassDB::bind_method(D_METHOD("link_is_bidirectional", "link"), &NavigationServer3D::link_is_bidirectional);
+	ClassDB::bind_method(D_METHOD("link_set_navigation_layers", "link", "navigation_layers"), &NavigationServer3D::link_set_navigation_layers);
+	ClassDB::bind_method(D_METHOD("link_get_navigation_layers", "link"), &NavigationServer3D::link_get_navigation_layers);
+	ClassDB::bind_method(D_METHOD("link_set_start_location", "link", "location"), &NavigationServer3D::link_set_start_location);
+	ClassDB::bind_method(D_METHOD("link_get_start_location", "link"), &NavigationServer3D::link_get_start_location);
+	ClassDB::bind_method(D_METHOD("link_set_end_location", "link", "location"), &NavigationServer3D::link_set_end_location);
+	ClassDB::bind_method(D_METHOD("link_get_end_location", "link"), &NavigationServer3D::link_get_end_location);
+	ClassDB::bind_method(D_METHOD("link_set_enter_cost", "link", "enter_cost"), &NavigationServer3D::link_set_enter_cost);
+	ClassDB::bind_method(D_METHOD("link_get_enter_cost", "link"), &NavigationServer3D::link_get_enter_cost);
+	ClassDB::bind_method(D_METHOD("link_set_travel_cost", "link", "travel_cost"), &NavigationServer3D::link_set_travel_cost);
+	ClassDB::bind_method(D_METHOD("link_get_travel_cost", "link"), &NavigationServer3D::link_get_travel_cost);
+
 	ClassDB::bind_method(D_METHOD("agent_create"), &NavigationServer3D::agent_create);
 	ClassDB::bind_method(D_METHOD("agent_set_map", "agent", "map"), &NavigationServer3D::agent_set_map);
 	ClassDB::bind_method(D_METHOD("agent_get_map", "agent"), &NavigationServer3D::agent_get_map);
@@ -118,11 +137,16 @@ NavigationServer3D::NavigationServer3D() {
 	debug_navigation_geometry_face_color = GLOBAL_DEF("debug/shapes/navigation/geometry_face_color", Color(0.5, 1.0, 1.0, 0.4));
 	debug_navigation_geometry_edge_disabled_color = GLOBAL_DEF("debug/shapes/navigation/geometry_edge_disabled_color", Color(0.5, 0.5, 0.5, 1.0));
 	debug_navigation_geometry_face_disabled_color = GLOBAL_DEF("debug/shapes/navigation/geometry_face_disabled_color", Color(0.5, 0.5, 0.5, 0.4));
+	debug_navigation_link_connection_color = GLOBAL_DEF("debug/shapes/navigation/link_connection_color", Color(1.0, 0.5, 1.0, 1.0));
+	debug_navigation_link_connection_disabled_color = GLOBAL_DEF("debug/shapes/navigation/link_connection_disabled_color", Color(0.5, 0.5, 0.5, 1.0));
+
 	debug_navigation_enable_edge_connections = GLOBAL_DEF("debug/shapes/navigation/enable_edge_connections", true);
 	debug_navigation_enable_edge_connections_xray = GLOBAL_DEF("debug/shapes/navigation/enable_edge_connections_xray", true);
 	debug_navigation_enable_edge_lines = GLOBAL_DEF("debug/shapes/navigation/enable_edge_lines", true);
 	debug_navigation_enable_edge_lines_xray = GLOBAL_DEF("debug/shapes/navigation/enable_edge_lines_xray", true);
 	debug_navigation_enable_geometry_face_random_color = GLOBAL_DEF("debug/shapes/navigation/enable_geometry_face_random_color", true);
+	debug_navigation_enable_link_connections = GLOBAL_DEF("debug/shapes/navigation/enable_link_connections", true);
+	debug_navigation_enable_link_connections_xray = GLOBAL_DEF("debug/shapes/navigation/enable_link_connections_xray", true);
 
 	if (Engine::get_singleton()->is_editor_hint()) {
 		// enable NavigationServer3D when in Editor or else navmesh edge connections are invisible
@@ -261,6 +285,40 @@ Ref<StandardMaterial3D> NavigationServer3D::get_debug_navigation_edge_connection
 	return debug_navigation_edge_connections_material;
 }
 
+Ref<StandardMaterial3D> NavigationServer3D::get_debug_navigation_link_connections_material() {
+	if (debug_navigation_link_connections_material.is_valid()) {
+		return debug_navigation_link_connections_material;
+	}
+
+	Ref<StandardMaterial3D> material = Ref<StandardMaterial3D>(memnew(StandardMaterial3D));
+	material->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED);
+	material->set_albedo(debug_navigation_link_connection_color);
+	if (debug_navigation_enable_link_connections_xray) {
+		material->set_flag(StandardMaterial3D::FLAG_DISABLE_DEPTH_TEST, true);
+	}
+	material->set_render_priority(StandardMaterial3D::RENDER_PRIORITY_MAX - 2);
+
+	debug_navigation_link_connections_material = material;
+	return debug_navigation_link_connections_material;
+}
+
+Ref<StandardMaterial3D> NavigationServer3D::get_debug_navigation_link_connections_disabled_material() {
+	if (debug_navigation_link_connections_disabled_material.is_valid()) {
+		return debug_navigation_link_connections_disabled_material;
+	}
+
+	Ref<StandardMaterial3D> material = Ref<StandardMaterial3D>(memnew(StandardMaterial3D));
+	material->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED);
+	material->set_albedo(debug_navigation_link_connection_disabled_color);
+	if (debug_navigation_enable_link_connections_xray) {
+		material->set_flag(StandardMaterial3D::FLAG_DISABLE_DEPTH_TEST, true);
+	}
+	material->set_render_priority(StandardMaterial3D::RENDER_PRIORITY_MAX - 2);
+
+	debug_navigation_link_connections_disabled_material = material;
+	return debug_navigation_link_connections_disabled_material;
+}
+
 void NavigationServer3D::set_debug_navigation_edge_connection_color(const Color &p_color) {
 	debug_navigation_edge_connection_color = p_color;
 	if (debug_navigation_edge_connections_material.is_valid()) {
@@ -316,6 +374,28 @@ Color NavigationServer3D::get_debug_navigation_geometry_face_disabled_color() co
 	return debug_navigation_geometry_face_disabled_color;
 }
 
+void NavigationServer3D::set_debug_navigation_link_connection_color(const Color &p_color) {
+	debug_navigation_link_connection_color = p_color;
+	if (debug_navigation_link_connections_material.is_valid()) {
+		debug_navigation_link_connections_material->set_albedo(debug_navigation_link_connection_color);
+	}
+}
+
+Color NavigationServer3D::get_debug_navigation_link_connection_color() const {
+	return debug_navigation_link_connection_color;
+}
+
+void NavigationServer3D::set_debug_navigation_link_connection_disabled_color(const Color &p_color) {
+	debug_navigation_link_connection_disabled_color = p_color;
+	if (debug_navigation_link_connections_disabled_material.is_valid()) {
+		debug_navigation_link_connections_disabled_material->set_albedo(debug_navigation_link_connection_disabled_color);
+	}
+}
+
+Color NavigationServer3D::get_debug_navigation_link_connection_disabled_color() const {
+	return debug_navigation_link_connection_disabled_color;
+}
+
 void NavigationServer3D::set_debug_navigation_enable_edge_connections(const bool p_value) {
 	debug_navigation_enable_edge_connections = p_value;
 	debug_dirty = true;
@@ -368,6 +448,27 @@ bool NavigationServer3D::get_debug_navigation_enable_geometry_face_random_color(
 	return debug_navigation_enable_geometry_face_random_color;
 }
 
+void NavigationServer3D::set_debug_navigation_enable_link_connections(const bool p_value) {
+	debug_navigation_enable_link_connections = p_value;
+	debug_dirty = true;
+	call_deferred("_emit_navigation_debug_changed_signal");
+}
+
+bool NavigationServer3D::get_debug_navigation_enable_link_connections() const {
+	return debug_navigation_enable_link_connections;
+}
+
+void NavigationServer3D::set_debug_navigation_enable_link_connections_xray(const bool p_value) {
+	debug_navigation_enable_link_connections_xray = p_value;
+	if (debug_navigation_link_connections_material.is_valid()) {
+		debug_navigation_link_connections_material->set_flag(StandardMaterial3D::FLAG_DISABLE_DEPTH_TEST, debug_navigation_enable_link_connections_xray);
+	}
+}
+
+bool NavigationServer3D::get_debug_navigation_enable_link_connections_xray() const {
+	return debug_navigation_enable_link_connections_xray;
+}
+
 void NavigationServer3D::set_debug_enabled(bool p_enabled) {
 	if (debug_enabled != p_enabled) {
 		debug_dirty = true;

+ 64 - 3
servers/navigation_server_3d.h

@@ -85,6 +85,12 @@ public:
 	/// Returns the edge connection margin of this map.
 	virtual real_t map_get_edge_connection_margin(RID p_map) const = 0;
 
+	/// Set the map link connection radius used to attach links to the nav mesh.
+	virtual void map_set_link_connection_radius(RID p_map, real_t p_connection_radius) const = 0;
+
+	/// Returns the link connection radius of this map.
+	virtual real_t map_get_link_connection_radius(RID p_map) const = 0;
+
 	/// Returns the navigation path to reach the destination from the origin.
 	virtual Vector<Vector3> map_get_path(RID p_map, Vector3 p_origin, Vector3 p_destination, bool p_optimize, uint32_t p_navigation_layers = 1) const = 0;
 
@@ -93,6 +99,7 @@ public:
 	virtual Vector3 map_get_closest_point_normal(RID p_map, const Vector3 &p_point) const = 0;
 	virtual RID map_get_closest_point_owner(RID p_map, const Vector3 &p_point) const = 0;
 
+	virtual TypedArray<RID> map_get_links(RID p_map) const = 0;
 	virtual TypedArray<RID> map_get_regions(RID p_map) const = 0;
 	virtual TypedArray<RID> map_get_agents(RID p_map) const = 0;
 
@@ -133,6 +140,37 @@ public:
 	virtual Vector3 region_get_connection_pathway_start(RID p_region, int p_connection_id) const = 0;
 	virtual Vector3 region_get_connection_pathway_end(RID p_region, int p_connection_id) const = 0;
 
+	/// Creates a new link between locations in the nav map.
+	virtual RID link_create() const = 0;
+
+	/// Set the map of this link.
+	virtual void link_set_map(RID p_link, RID p_map) const = 0;
+	virtual RID link_get_map(RID p_link) const = 0;
+
+	/// Set whether this link travels in both directions.
+	virtual void link_set_bidirectional(RID p_link, bool p_bidirectional) const = 0;
+	virtual bool link_is_bidirectional(RID p_link) const = 0;
+
+	/// Set the link's layers.
+	virtual void link_set_navigation_layers(RID p_link, uint32_t p_navigation_layers) const = 0;
+	virtual uint32_t link_get_navigation_layers(RID p_link) const = 0;
+
+	/// Set the start location of the link.
+	virtual void link_set_start_location(RID p_link, Vector3 p_location) const = 0;
+	virtual Vector3 link_get_start_location(RID p_link) const = 0;
+
+	/// Set the end location of the link.
+	virtual void link_set_end_location(RID p_link, Vector3 p_location) const = 0;
+	virtual Vector3 link_get_end_location(RID p_link) const = 0;
+
+	/// Set the enter cost of the link.
+	virtual void link_set_enter_cost(RID p_link, real_t p_enter_cost) const = 0;
+	virtual real_t link_get_enter_cost(RID p_link) const = 0;
+
+	/// Set the travel cost of the link.
+	virtual void link_set_travel_cost(RID p_link, real_t p_travel_cost) const = 0;
+	virtual real_t link_get_travel_cost(RID p_link) const = 0;
+
 	/// Creates the agent.
 	virtual RID agent_create() const = 0;
 
@@ -209,29 +247,38 @@ public:
 	virtual ~NavigationServer3D();
 
 #ifdef DEBUG_ENABLED
+private:
 	bool debug_enabled = false;
 	bool debug_dirty = true;
 	void _emit_navigation_debug_changed_signal();
 
-	void set_debug_enabled(bool p_enabled);
-	bool get_debug_enabled() const;
-
 	Color debug_navigation_edge_connection_color = Color(1.0, 0.0, 1.0, 1.0);
 	Color debug_navigation_geometry_edge_color = Color(0.5, 1.0, 1.0, 1.0);
 	Color debug_navigation_geometry_face_color = Color(0.5, 1.0, 1.0, 0.4);
 	Color debug_navigation_geometry_edge_disabled_color = Color(0.5, 0.5, 0.5, 1.0);
 	Color debug_navigation_geometry_face_disabled_color = Color(0.5, 0.5, 0.5, 0.4);
+	Color debug_navigation_link_connection_color = Color(1.0, 0.5, 1.0, 1.0);
+	Color debug_navigation_link_connection_disabled_color = Color(0.5, 0.5, 0.5, 1.0);
+
 	bool debug_navigation_enable_edge_connections = true;
 	bool debug_navigation_enable_edge_connections_xray = true;
 	bool debug_navigation_enable_edge_lines = true;
 	bool debug_navigation_enable_edge_lines_xray = true;
 	bool debug_navigation_enable_geometry_face_random_color = true;
+	bool debug_navigation_enable_link_connections = true;
+	bool debug_navigation_enable_link_connections_xray = true;
 
 	Ref<StandardMaterial3D> debug_navigation_geometry_edge_material;
 	Ref<StandardMaterial3D> debug_navigation_geometry_face_material;
 	Ref<StandardMaterial3D> debug_navigation_geometry_edge_disabled_material;
 	Ref<StandardMaterial3D> debug_navigation_geometry_face_disabled_material;
 	Ref<StandardMaterial3D> debug_navigation_edge_connections_material;
+	Ref<StandardMaterial3D> debug_navigation_link_connections_material;
+	Ref<StandardMaterial3D> debug_navigation_link_connections_disabled_material;
+
+public:
+	void set_debug_enabled(bool p_enabled);
+	bool get_debug_enabled() const;
 
 	void set_debug_navigation_edge_connection_color(const Color &p_color);
 	Color get_debug_navigation_edge_connection_color() const;
@@ -248,6 +295,12 @@ public:
 	void set_debug_navigation_geometry_face_disabled_color(const Color &p_color);
 	Color get_debug_navigation_geometry_face_disabled_color() const;
 
+	void set_debug_navigation_link_connection_color(const Color &p_color);
+	Color get_debug_navigation_link_connection_color() const;
+
+	void set_debug_navigation_link_connection_disabled_color(const Color &p_color);
+	Color get_debug_navigation_link_connection_disabled_color() const;
+
 	void set_debug_navigation_enable_edge_connections(const bool p_value);
 	bool get_debug_navigation_enable_edge_connections() const;
 
@@ -263,11 +316,19 @@ public:
 	void set_debug_navigation_enable_geometry_face_random_color(const bool p_value);
 	bool get_debug_navigation_enable_geometry_face_random_color() const;
 
+	void set_debug_navigation_enable_link_connections(const bool p_value);
+	bool get_debug_navigation_enable_link_connections() const;
+
+	void set_debug_navigation_enable_link_connections_xray(const bool p_value);
+	bool get_debug_navigation_enable_link_connections_xray() const;
+
 	Ref<StandardMaterial3D> get_debug_navigation_geometry_face_material();
 	Ref<StandardMaterial3D> get_debug_navigation_geometry_edge_material();
 	Ref<StandardMaterial3D> get_debug_navigation_geometry_face_disabled_material();
 	Ref<StandardMaterial3D> get_debug_navigation_geometry_edge_disabled_material();
 	Ref<StandardMaterial3D> get_debug_navigation_edge_connections_material();
+	Ref<StandardMaterial3D> get_debug_navigation_link_connections_material();
+	Ref<StandardMaterial3D> get_debug_navigation_link_connections_disabled_material();
 #endif // DEBUG_ENABLED
 };