Browse Source

Add doc for NavigationServer, NavigationAgent and RVO agent avoidance

Adds documentation for NavigationServer with focus on topics not explained in the class documentation.
Adds documentation for NavigationAgent node use and caveats.
Adds documentation for RVO agent avoidance.
smix8 3 years ago
parent
commit
594911cf33

BIN
tutorials/navigation/img/agent_avoidance_enabled.png


BIN
tutorials/navigation/img/agent_safevelocity_signal.png


+ 3 - 0
tutorials/navigation/index.rst

@@ -12,3 +12,6 @@ Navigation
    navigation_different_actor_locomotion
    navigation_using_navigationobstacles
    navigation_using_navigationmeshes
+   navigation_using_navigationservers
+   navigation_using_navigationagents
+   navigation_using_agent_avoidance

+ 74 - 0
tutorials/navigation/navigation_using_agent_avoidance.rst

@@ -0,0 +1,74 @@
+.. _doc_navigation_using_agent_avoidance:
+
+Using Agent Avoidance
+=====================
+
+This section is about how to use agent avoidance with the NavigationServer and 
+documents how agent avoidance is implemented in Godot.
+
+For avoidance with NavigationAgents see :ref:`doc_navigation_using_navigationagents`.
+
+Agent avoidance helps to prevent direct collision with other agents or moving obstacles
+while the agents still follow their original velocity as best as possible.
+
+Avoidance in Godot is implemented with the help of the RVO library (Reciprocal Velocity Obstacle).
+RVO places agents on a flat RVO map and gives each agent a ``radius`` and a ``position``.
+Agents with overlapping radius compute a ``safe_velocity`` from their
+current ``velocity``. The ``safe_velocity`` then needs to replace the original 
+submitted ``velocity`` to move the actor behind the agent with custom movement code.
+
+.. note::
+
+    RVO avoidance is not involved in regular pathfinding, it is a completely separate system.
+    If used inappropriately, the RVO avoidance can actively harm the perceived pathfinding quality.
+
+Creating Avoidance Agents with Scripts
+--------------------------------------
+
+Agents and obstacles share the same NavigationServer API functions.
+
+Creating agents on the NavigationServer is only required for avoidance but not for normal pathfinding.
+Pathfinding is map and region navmesh based while avoidance is purely map and agent based.
+
+.. tabs::
+ .. code-tab:: gdscript GDScript
+    
+    extends Node3D
+    
+    var new_agent_rid : RID = NavigationServer3D.agent_create()
+    var default_3d_map_rid : RID = get_world_3d().get_navigation_map()
+    
+    NavigationServer3D.agent_set_map(new_agent_rid, default_3d_map_rid)
+    NavigationServer3D.agent_set_radius(new_agent_rid, 0.5)
+    NavigationServer3D.agent_set_position(new_agent_rid, global_transform.origin)
+
+To receive safe_velocity signals for avoidance for the agent a callback needs to be registered on the NavigationServer.
+
+.. tabs::
+ .. code-tab:: gdscript GDScript
+    
+    extends Node3D
+    
+    var agent_rid : RID = NavigationServer3D.agent_create()
+    var agent_node3d : Node3D = self
+    var callback_function_name : StringName = "on_safe_velocity_computed"
+    NavigationServer3D.agent_set_callback(agent_rid, agent_node3d, callback_function_name)
+    
+    func on_safe_velocity_computed(safe_velocity : Vector3):
+        # do your avoidance movement
+
+After the current and new calculated velocity needs to be passed to the NavigationServer each physics frame to trigger the safe_velocity callback when the avoidance processing is finished.
+
+.. tabs::
+ .. code-tab:: gdscript GDScript
+    
+    func _physics_process(delta):
+        
+        NavigationServer3D.agent_set_velocity(current_velocity)
+        NavigationServer3D.agent_set_target_velocity(new_velocity)
+
+.. warning::
+
+    If _process() is used instead of _physics_process() at a higher framerate 
+    than physics the agent velocity should not be updated more than ones each 
+    physics frame e.g. by tracking the Engine.get_physics_frames().

+ 231 - 0
tutorials/navigation/navigation_using_navigationagents.rst

@@ -0,0 +1,231 @@
+.. _doc_navigation_using_navigationagents:
+
+Using NavigationAgents
+======================
+
+NavigationsAgents are helper nodes to facilitate common calls to the NavigationServer API 
+on behalf of the parent actor node in a more convenient manner for beginners.
+
+2D and 3D version of NavigationAgents are available as 
+:ref:`NavigationAgent2D<class_NavigationAgent2D>` and 
+:ref:`NavigationAgent3D<class_NavigationAgent3D>` respectively.
+
+NavigationsAgents are entirely optional for navigation pathfinding. 
+The functionality of NavigationsAgents can be recreated with scripts and direct 
+calls to the NavigationServer API. If the default NavigationsAgent does not do what you want 
+for your game feel free to design your own NavigationsAgent with scripts.
+
+.. warning::
+
+    NavigationsAgent nodes and NavigationServer ``agents`` are not the same. 
+    The later is an RVO avoidance agent and solely used for avoidance. 
+    RVO avoidance agents are not involved in regular pathfinding.
+
+NavigationAgent Pathfinding
+---------------------------
+
+To use NavigationAgents for pathfinding, place a NavigationAgent2D/3D Node below a Node2D/3D inheriting parent node.
+
+To have the agent query a path to a target position use the ``set_target_location()`` method.
+Once the target has been set, the next position to follow in the path 
+can be retrieved with the ``get_next_location()`` function. Move the parent actor node 
+to this position with your own movement code. On the next ``physics_frame``, call 
+``get_next_location()`` again for the next position and repeat this until the path ends.
+
+NavigationAgents have their own internal logic to proceed with the current path and call for updates.
+NavigationAgents recognize by distance when a path point or the final target is reached.
+NavigationAgents refresh a path automatically when too far away from the current pathpoint.
+The important updates are all triggered with the ``get_next_location()`` function 
+when called in ``_physics_process()``.
+
+Be careful calling other NavigationAgent functions not required for path movement 
+while the actor is following a path, as many function trigger a full path refresh.
+
+.. note::
+
+    New NavigationAgents will automatically join the 
+    default navigation map for their 2D/3D dimension.
+
+.. warning::
+
+    Resetting the path every frame (by accident) might get the actor to stutter or spin around in place.
+
+NavigationAgents were designed with ``_physics_process()`` in mind to keep in sync with both :ref:`NavigationServer3D<class_NavigationServer3D>` and :ref:`PhysicsServer3D<class_PhysicsServer3D>`.
+
+They work well out of the box with :ref:`CharacterBody2D<class_CharacterBody2D>` and :ref:`CharacterBody3D<class_CharacterBody3D>` as well as any rigid bodies.
+
+.. warning::
+
+    The important restriction for non-physics characters is that the NavigationAgent node only accepts a single update each ``physics_frame`` as further updates will be blocked.
+
+.. warning::
+
+    If a NavigationAgent is used with ``_process()`` at high framerate make sure to accumulate the values of multiple frames and call the NavigationAgent function only once each ``physics_frame``.
+
+.. _doc_navigation_script_templates:
+
+
+NavigationAgent Avoidance
+-------------------------
+
+This section explains how to use the built-in avoidance specific 
+to NavigationAgent nodes. For general avoidance use and more technical details 
+on RVO avoidance see :ref:`doc_navigation_using_agent_avoidance`.
+
+
+In order for NavigationAgents to use the avoidance feature the ``enable_avoidance`` property must be set to ``true``.
+
+.. image:: img/agent_avoidance_enabled.png
+
+.. note::
+
+    Only other agents on the same map that are registered for avoidance themself will be considered in the avoidance calculation.
+
+The following NavigationAgent properties are relevant for avoidance:
+
+  - The property ``radius`` controls the size of the avoidance circle around the agent. This area describes the agents body and not the avoidance maneuver distance.
+  - The property ``neighbor_distance`` controls the search radius of the agent when searching for other agents that should be avoided. A lower value reduces processing cost.
+  - The property ``max_neighbors`` controls how many other agents are considered in the avoidance calculation if they all have overlapping radius.
+    A lower value reduces processing cost but a too low value may result in agents ignoring the avoidance.
+  - The property ``time_horizion`` controls the avoidance maneuver start and end distance. 
+    How early and for how long an agents reacts to other agents within the ``neighbor_distance`` radius to correct its own velocity. 
+    A lower value results in avoidance kicking in with a very intense velocity change at a short distance while a high value results in very early but small velocity changes.
+  - The property ``max_speed`` controls the maximum velocity assumed for the agents avoidance calculation.
+    If the agents parents moves faster than this value the avoidance ``safe_velocity`` might not be accurate enough to avoid collision.
+
+The ``velocity_computed`` signal of the agent node must be connected to receive the ``safe_velocity`` calculation result.
+
+.. image:: img/agent_safevelocity_signal.png
+
+Additional the current velocity of the agents parent must be set for the agent in ``_physics_process()`` with ``set_velocity()``.
+
+After a short wait for processing the avoidance (still in the same frame) the ``safe_velocity`` vector will be received with the signal. 
+This velocity vector should be used to move the NavigationAgent's parent node in order to avoidance collision with other avoidance registered agents in proximity.
+
+RVO exists in its own space and has no information from navigation meshes or physics collision.
+Behind the scene avoidance agents are just circles with different radius on a flat plane.
+In narrow places obstructed with collision objects, the avoidance maneuver radius needs to be 
+reduced considerably or disabled else the avoidance velocity will get actors stuck on collision easily.
+
+.. note::
+
+    Avoidance should be seen as a last resort option for constantly moving objects that cannot be re(baked) to a navigationmesh efficiently in order to move around them.
+
+.. warning::
+
+    Actors that move according to their avoidance agent velocity will not move at 
+    full speed, can leave the navigation mesh bounds and can make movement 
+    pauses when the avoidance simulation becomes unsolvable.
+
+Using the NavigationAgent ``enable_avoidance`` property is the preferred option 
+to toggle avoidance but the following scripts for NavigationAgents can be 
+used to create or delete avoidance callbacks for the agent RID.
+
+.. tabs::
+ .. code-tab:: gdscript GDScript
+
+    extends NavigationAgent2D
+    
+    var agent : RID = get_rid()
+    NavigationServer2D::get_singleton()->agent_set_callback(agent, self, "_avoidance_done")
+    NavigationServer2D::get_singleton()->agent_set_callback(agent, null, "_avoidance_done")
+
+.. tabs::
+ .. code-tab:: gdscript GDScript
+
+    extends NavigationAgent3D
+    
+    var agent : RID = get_rid()
+    NavigationServer3D::get_singleton()->agent_set_callback(agent, self, "_avoidance_done")
+    NavigationServer3D::get_singleton()->agent_set_callback(agent, null, "_avoidance_done")
+
+NavigationAgent Script Templates
+--------------------------------
+
+The following sections provides script templates for nodes commonly used with NavigationAgents.
+
+Actor as Node3D
+~~~~~~~~~~~~~~~
+
+This script adds basic navigation movement to a Node3D with a NavigationAgent3D child node.
+
+.. tabs::
+ .. code-tab:: gdscript GDScript
+
+    extends Node3D
+    # script on agent parent node, connect the agent 'velocity_computed' signal for collision avoidance
+
+    @export var movement_speed : float = 4.0
+    @onready var navigation_agent : NavigationAgent3D = get_node("NavigationAgent3D")
+    var movement_delta : float
+
+    func set_movement_target(movement_target : Vector3):
+        navigation_agent.set_target_location(movement_target)
+
+    func _physics_process(delta):
+
+        movement_delta = move_speed * delta
+        var next_path_position : Vector3 = navigation_agent.get_next_location()
+        var current_agent_position : Vector3 = global_transform.origin
+        var new_velocity : Vector3 = (next_path_position - current_agent_position).normalized() * movement_delta
+        navigation_agent.set_velocity(new_velocity)
+
+    func _on_NavigationAgent3D_velocity_computed(safe_velocity : Vector3):
+        # Move Node3D with the computed `safe_velocity` to avoid dynamic obstacles.
+        global_transform.origin = global_transform.origin.move_toward(global_transform.origin + safe_velocity, movement_delta)
+
+Actor as CharacterBody3D
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+This script adds basic navigation movement to a CharacterBody3D with a NavigationAgent3D child node.
+
+.. tabs::
+ .. code-tab:: gdscript GDScript
+
+    extends CharacterBody3D
+    # script on agent parent node, connect the agent 'velocity_computed' signal for collision avoidance
+
+    @export var movement_speed : float = 4.0
+    @onready var navigation_agent : NavigationAgent3D = get_node("NavigationAgent3D")
+
+    func set_movement_target(movement_target : Vector3):
+        navigation_agent.set_target_location(movement_target)
+
+    func _physics_process(delta):
+        
+        var next_path_position : Vector3 = navigation_agent.get_next_location()
+        var current_agent_position : Vector3 = global_transform.origin
+        var new_velocity : Vector3 = (next_path_position - current_agent_position).normalized() * movement_speed
+        navigation_agent.set_velocity(new_velocity)
+
+    func _on_NavigationAgent3D_velocity_computed(safe_velocity : Vector3):
+        # Move KinematicBody3D with the computed `safe_velocity` to avoid dynamic obstacles.
+        velocity = safe_velocity
+        move_and_slide()
+
+Actor as RigidBody3D
+~~~~~~~~~~~~~~~~~~~~
+
+This script adds basic navigation movement to a RigidBody3D with a NavigationAgent3D child node.
+
+.. tabs::
+ .. code-tab:: gdscript GDScript
+
+    extends RigidBody3D
+    # script on agent parent node, connect the agent 'velocity_computed' signal for collision avoidance
+
+    @onready var navigation_agent : NavigationAgent3D = get_node("NavigationAgent3D")
+
+    func set_movement_target(movement_target : Vector3):
+        navigation_agent.set_target_location(movement_target)
+
+    func _physics_process(delta):
+        
+        var next_path_position : Vector3 = navigation_agent.get_next_location()
+        var current_agent_position : Vector3 = global_transform.origin
+        var new_velocity : Vector3 = (next_path_position - current_agent_position).normalized() * velocity
+        navigation_agent.set_velocity(new_velocity)
+        
+    func _on_NavigationAgent3D_velocity_computed(safe_velocity : Vector3):
+        # Move RigidBody3D with the computed `safe_velocity` to avoid dynamic obstacles.
+        set_linear_velocity(safe_velocity)

+ 189 - 0
tutorials/navigation/navigation_using_navigationservers.rst

@@ -0,0 +1,189 @@
+.. _doc_navigation_using_navigationservers:
+
+Using NavigationServer
+======================
+
+2D and 3D version of the NavigationServer are available as 
+:ref:`NavigationServer2D<class_NavigationServer2D>` and 
+:ref:`NavigationServer3D<class_NavigationServer3D>` respectively.
+
+Both 2D and 3D use the same NavigationServer with NavigationServer3D being the primary server. The NavigationServer2D is a frontend that converts 2D positions into 3D positions and back.
+Hence it is entirely possible (if not a little cumbersome) to exclusively use the NavigationServer3D API for 2D navigation.
+
+Communicating with the NavigationServer
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To work with the NavigationServer means to prepare parameters for a ``query`` that can be send to the NavigationServer for updates or requesting data.
+
+To reference the internal NavigationServer objects like maps, regions and agents RIDs are used as identification numbers.
+Every navigation related node in the SceneTree has a function that returns the RID for this node.
+
+Threading and Synchronisation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The NavigationServer does not update every change immediately but waits until 
+the end of the ``physics_frame`` to synchronise all the changes together.
+
+Waiting for synchronisation is required to apply changes to all maps, regions and agents. 
+Synchronisation is done because some updates like a recalculation of the entire navigation map are very expensive and require updated data from all other objects. 
+Also the NavigationServer uses a ``threadpool`` by default for some functionality like avoidance calculation between agents. 
+
+Waiting is not required for most ``get()`` functions that only request data from the NavigationServer without making changes. 
+Note that not all data will account for changes made in the same frame. 
+E.g. if an avoidance ``agent`` changed the navigation ``map`` this frame the ``agent_get_map()`` function will still return the old map before the synchronisation.
+The exception to this are nodes that store their values internally before sending the update to the NavigationServer. 
+When a getter on a node is used for a value that was updated in the same frame it will return the already updated value stored on the node.
+
+The NavigationServer is ``thread-safe`` as it places all API calls that want to make changes in a queue to be executed in the synchronisation phase.
+Synchronisation for the NavigationServer happens in the middle of the physics frame after scene input from scripts and nodes are all done. 
+
+.. note::
+    The important takeaway is that most NavigationServer changes take effect after the next physics frame and not immediately.
+    This includes all changes made by navigation related nodes in the SceneTree or through scripts.
+
+The following functions will be executed in the synchronisation phase only:
+
+- map_set_active()
+- map_set_up()
+- map_set_cell_size()
+- map_set_edge_connection_margin()
+- region_set_map()
+- region_set_transform()
+- region_set_enter_cost()
+- region_set_travel_cost()
+- region_set_navigation_layers()
+- region_set_navmesh()
+- agent_set_map()
+- agent_set_neighbor_dist()
+- agent_set_max_neighbors()
+- agent_set_time_horizon()
+- agent_set_radius()
+- agent_set_max_speed()
+- agent_set_velocity()
+- agent_set_target_velocity()
+- agent_set_position()
+- agent_set_ignore_y()
+- agent_set_callback()
+- free()
+
+2D and 3D NavigationServer differences
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+NavigationServer2D and NavigationServer3D are equivalent in functionality 
+for their dimension and both use the same NavigationServer behind the scene.
+
+Strictly technical a NavigationServer2D is a myth. 
+The NavigationServer2D is a frontend to facilitate conversions of Vector2(x, y) to 
+Vector3(x, 0.0, z) and back for the NavigationServer3D API. 2D uses a flat 3D mesh 
+pathfinding and the NavigationServer2D facilitates the conversions.
+When a guide uses just NavigationServer without the 2D or 3D suffix it usually works for both servers 
+by exchange Vector2(x, y) with Vector3(x, 0.0, z) or reverse.
+
+Technically it is possible to use the tools for creating navigationmesh for the other 
+dimension, e..g. baking 2D navigationmesh with the 3D NavigationMesh when using 
+flat 3D source geometry or creating 3D flat navigationmesh with the 
+polygon outline drawtools of NavigationRegion2D and NavigationPolygons.
+
+Any RID created with the NavigationServer2D API works on the NavigationServer3D API 
+as well and both 2D and 3D avoidance agents can exist on the same map.
+
+.. note::
+    Regions created in 2D and 3D will merge their navigationmeshes when placed on the same map and merge conditions apply.
+    The NavigationServer does not discriminate between NavigationRegion2D and NavigationRegion3D nodes as both are regions on the server.
+    By default those nodes register on different navigation maps so this merge can only happen when maps are changed manually e.g. with scripts.
+
+    Actors with avoidance enabled will avoid both 2D and 3D avoidance agents when placed on the same map.
+
+.. warning::
+    It is not possible to use NavigationServer2D while disabling 3D on a Godot custom build.
+
+Waiting for synchronisation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+At the start of the game, a new scene or procedual navigation changes any path query to a NavigationServer will return empty or wrong.
+
+The navigation map is still empty or not updated at this point.
+All nodes from the SceneTree need to first upload their navigation related data to the NavigationServer.
+Each added or changed map, region or agent need to be registered with the NavigationServer.
+Afterward the NavigationServer requires a ``physics_frame`` for synchronisation to update the maps, regions and agents.
+
+One workaround is to make a deferred call to a custom setup function (so all nodes are ready).
+The setup function makes all the navigation changes, e.g. adding procedual stuff.
+Afterwards the function waits for the next physics_frame before continuing with path queries.
+
+.. tabs::
+ .. code-tab:: gdscript GDScript
+
+    extends Node3D
+    
+    func _ready():
+        # use call deferred to make sure the entire SceneTree Nodes are setup
+        # else await / yield on 'physics_frame' in a _ready() might get stuck
+        call_deferred("custom_setup")
+
+    func custom_setup():
+        
+        # create a new navigation map
+        var map : RID = NavigationServer3D.map_create()
+        NavigationServer3D.map_set_up(map, Vector3.UP)
+        NavigationServer3D.map_set_active(map, true)
+        
+        # create a new navigation region and add it to the map
+        var region : RID = NavigationServer3D.region_create()
+        NavigationServer3D.region_set_transform(region, Transform())
+        NavigationServer3D.region_set_map(region, map)
+        
+        # create a procedual navmesh for the region
+        var navmesh : NavigationMesh = NavigationMesh.new()
+        var vertices : PackedVector3Array = PoolVector3Array([
+            Vector3(0,0,0),
+            Vector3(9.0,0,0),
+            Vector3(0,0,9.0)
+        ])
+        navmesh.set_vertices(vertices)
+        var polygon : PackedInt32Array = PackedInt32Array([0, 1, 2])
+        navmesh.add_polygon(polygon)
+        NavigationServer3D.region_set_navmesh(region, navmesh)
+        
+        # wait for NavigationServer sync to adapt to made changes
+        await get_tree().physics_frame
+        
+        # query the path from the navigationserver
+        var start_position : Vector3 = Vector3(0.1, 0.0, 0.1)
+        var target_position : Vector3 = Vector3(1.0, 0.0, 1.0)
+        var optimize_path : bool = true
+        
+        var path : PackedVector3Array = NavigationServer3D.map_get_path(
+            map,
+            start_position,
+            target_position,
+            optimize_path
+        )
+        
+        print("Found a path!")
+        print(path)
+
+Server Avoidance Callbacks
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If RVO avoidance agents are registered for avoidance callbacks the NavigationServer dispatches 
+their ``safe_velocity`` signals just before the PhysicsServer synchronisation.
+
+To learn more about NavigationAgents see :ref:`doc_navigation_using_navigationagents`.
+To learn more about RVO Avoidance see :ref:`doc_navigation_using_agent_avoidance`.
+
+The simplified order of execution for NavigationAgents that use avoidance:
+
+- physics frame starts.
+- _physics_process(delta).
+- set_velocity() on NavigationAgent Node.
+- Agent sends velocity and position to NavigationServer.
+- NavigationServer waits for synchronisation.
+- NavigationServer synchronises and computes avoidance velocities for all registered avoidance agents.
+- NavigationServer sends safe_velocity vector with signals for each registered avoidance agents.
+- Agents receive the signal and move their parent e.g. with move_and_slide or linear_velocity.
+- PhysicsServer synchronises.
+- physics frame ends.
+
+Therefore moving a physicsbody actor in the callback function with the safe_velocity is perfectly thread- and physics-safe 
+as all happens inside the same physics_frame before the PhysicsServer commits to changes and does its own calculations.