Browse Source

Merge pull request #6049 from smix8/doc_navserver_n_navagent_n_avoidance_4.x

Add doc for NavigationServer, NavigationAgent and RVO agent avoidance
Max Hilbrunner 2 years ago
parent
commit
a8382b4bed

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.