<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vav Labs</title>
    <description>The latest articles on DEV Community by Vav Labs (@vav_labs).</description>
    <link>https://dev.to/vav_labs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3986932%2F34593eb7-f7c5-49ff-9381-e86c74adb4e9.jpg</url>
      <title>DEV Community: Vav Labs</title>
      <link>https://dev.to/vav_labs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vav_labs"/>
    <language>en</language>
    <item>
      <title>3D Pathfinding in Godot 4: NavigationAgent3D, the Three Radii, and When You Actually Need AStar3D</title>
      <dc:creator>Vav Labs</dc:creator>
      <pubDate>Fri, 07 Aug 2026 08:26:42 +0000</pubDate>
      <link>https://dev.to/vav_labs/3d-pathfinding-in-godot-4-navigationagent3d-the-three-radii-and-when-you-actually-need-astar3d-4nn1</link>
      <guid>https://dev.to/vav_labs/3d-pathfinding-in-godot-4-navigationagent3d-the-three-radii-and-when-you-actually-need-astar3d-4nn1</guid>
      <description>&lt;p&gt;Originally published on &lt;a href="https://vav-labs.com/blog/3d-pathfinding-in-godot/" rel="noopener noreferrer"&gt;vav-labs.com&lt;/a&gt;. Everything below is Godot 4.7.1 stable, and there's a runnable MIT-licensed project with a verification receipt at the end.&lt;/p&gt;

&lt;p&gt;You add a &lt;code&gt;NavigationRegion3D&lt;/code&gt;, bake a mesh, drop a &lt;code&gt;NavigationAgent3D&lt;/code&gt; on your character, set &lt;code&gt;target_position&lt;/code&gt;, hit Play, and the character stands there doing nothing at all.&lt;/p&gt;

&lt;p&gt;That's the normal first run. All of it is documented, and people still lose an evening to it (me included). The reason is that 3D navigation in Godot is really four separate systems that have to agree with each other: the baked mesh, the physics body, the agent, and your own movement code. Any one of them can be wrong on its own terms while looking fine in the inspector.&lt;/p&gt;

&lt;p&gt;So here's the whole contract in one place, in the order you'd actually build it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Navmesh or AStar3D? Decide by movement space
&lt;/h2&gt;

&lt;p&gt;Before any setup, pick the representation. This is the choice people get wrong most often, and it's not really about 2D versus 3D.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your movement space&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Grounded movement over authored 3D geometry&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NavigationMesh&lt;/code&gt; + &lt;code&gt;NavigationAgent3D&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voxels, stacked tiles, waypoints, other predefined positions&lt;/td&gt;
&lt;td&gt;An explicit &lt;code&gt;AStar3D&lt;/code&gt; point graph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fully volumetric flight or swimming&lt;/td&gt;
&lt;td&gt;An explicit 3D graph or something purpose-built&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A navmesh describes continuous walkable space. One polygon can cover a whole floor, and the bake follows your authored geometry instead of chopping the level into cubes. That's the ordinary answer for a character walking through a scene.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AStar3D&lt;/code&gt; describes points and connections, and it's the better fit when the positions themselves are the rules: a voxel that can be occupied, a flying unit hopping between lattice nodes, a turn-based actor paying for one exact step. The catch is that &lt;code&gt;AStar3D&lt;/code&gt; won't inspect a &lt;code&gt;GridMap&lt;/code&gt; and discover the graph for you. You add every point, connect every legal pair, and keep the thing in sync when the world changes.&lt;/p&gt;

&lt;p&gt;This post builds the first case.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scene, and the order to build it
&lt;/h2&gt;

&lt;p&gt;Nothing exotic. Everything here is authored before Play.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Main (Node3D)
├── NavigationRegion3D
│   └── LevelGeometry (floor, ramp, platform, walls)
├── Player (CharacterBody3D)
│   ├── CollisionShape3D
│   ├── MeshInstance3D
│   └── NavigationAgent3D
├── MovingObstacle (AnimatableBody3D)
│   └── NavigationObstacle3D
├── TargetMarker
├── PathLine
└── Camera3D
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build it in this order and you get a working result at every step, which makes it obvious where things broke:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;NavigationRegion3D&lt;/code&gt; with the level geometry under it. The floor, ramp, platform and walls are visible in the editor.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;NavigationMesh&lt;/code&gt; resource on the region, baked and saved. You now have a walkable surface.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CharacterBody3D&lt;/code&gt; with a &lt;code&gt;NavigationAgent3D&lt;/code&gt; child. A grounded body can follow one target across the ramp.&lt;/li&gt;
&lt;li&gt;Physics-step mouse raycast. Left click produces a world-space target.&lt;/li&gt;
&lt;li&gt;Closest-point clamping and a path line. The target snaps to the navmesh and the route is visible.&lt;/li&gt;
&lt;li&gt;Optional avoidance obstacle. Local steering reacts to motion without touching the global path.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Baking the NavigationMesh
&lt;/h2&gt;

&lt;p&gt;Put the walkable geometry under &lt;code&gt;NavigationRegion3D&lt;/code&gt;, create a &lt;code&gt;NavigationMesh&lt;/code&gt; on the region, bake. Turn on the navigation debug view while you're adjusting things — the colored overlay should connect the lower floor to the upper platform and keep visible clearance around dividers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;NavigationMesh property&lt;/th&gt;
&lt;th&gt;Starter value&lt;/th&gt;
&lt;th&gt;What it changes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;agent_radius&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.50&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Erodes clearance around walls and ledges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;agent_height&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minimum vertical space the body needs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;agent_max_climb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.50&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Joins surfaces across small height changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;agent_max_slope&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;35.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keeps the 20-degree ramp walkable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cell_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.25&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Horizontal bake resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cell_height&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.25&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Vertical bake resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things that bite here. &lt;code&gt;cell_size&lt;/code&gt; and &lt;code&gt;cell_height&lt;/code&gt; have to match the navigation map, and Godot rounds the bake radius &lt;strong&gt;up&lt;/strong&gt; to a multiple of &lt;code&gt;cell_size&lt;/code&gt;. So a coarse cell size can quietly erode more space than the radius field suggests, and your doorway disappears for reasons the inspector never shows you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the three radii separate
&lt;/h2&gt;

&lt;p&gt;This is the one I'd put on a sticky note. Three different settings all read like "the agent radius" during a quick inspector pass, and they don't do the same job.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Starter value&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CapsuleShape3D.radius&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.48&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Physics collision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NavigationMesh.agent_radius&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.50&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bake-time wall and ledge clearance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NavigationAgent3D.radius&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.55&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Local avoidance only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The bake radius should cover the physical body plus a small margin. The avoidance radius can run a little wider, because it controls preferred spacing between agents, not whether a doorway is legal.&lt;/p&gt;

&lt;p&gt;Get the relationship backwards and you get two distinct bugs. Bake for a body smaller than the collision capsule and the path will happily route around a corner that physics won't let you take. Bake too large and narrow doors vanish from the mesh entirely. When something looks wrong at a doorway, debug the baked surface and the collision shape together, not one at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wait until the navigation map is usable
&lt;/h2&gt;

&lt;p&gt;The empty-first-path case is documented: the map hasn't synchronized the region yet. For a small scene, deferring setup and waiting one physics frame is the normal baseline.&lt;/p&gt;

&lt;p&gt;The starter is stricter than that, because it assigns an automatic target the moment the scene is ready. It waits for a map iteration, a registered region, and a closest point near the authored spawn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_ready&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;_spawn_transform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;global_transform&lt;/span&gt;
    &lt;span class="n"&gt;floor_snap_length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.45&lt;/span&gt;
    &lt;span class="n"&gt;floor_max_angle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deg_to_rad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;45.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;velocity_computed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_on_velocity_computed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_finish_navigation_setup&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call_deferred&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_finish_navigation_setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;map_rid&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_navigation_map&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;map_has_data&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NavigationServer3D&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map_get_iteration_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;map_rid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; \
            &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;NavigationServer3D&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map_get_regions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;map_rid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;closest_point&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NavigationServer3D&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map_get_closest_point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;map_rid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;global_position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;map_has_data&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="kt"&gt;Vector3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ZERO&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;map_has_data&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;closest_point&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distance_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;global_position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;get_tree&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;physics_frame&lt;/span&gt;
    &lt;span class="n"&gt;_navigation_is_ready&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;navigation_ready&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't copy the two-metre check into a reusable library. It belongs to this authored spawn point. In your scene, pick a readiness assertion that matches where your character is actually supposed to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Raycast the click during the physics step
&lt;/h2&gt;

&lt;p&gt;Mouse input arrives outside the physics callback, but &lt;code&gt;direct_space_state&lt;/code&gt; wants to be queried during physics processing. Store the screen position, consume it in &lt;code&gt;_physics_process()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_unhandled_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;InputEventMouseButton&lt;/span&gt; \
            &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;button_index&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;MOUSE_BUTTON_LEFT&lt;/span&gt; \
            &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pressed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_pending_click&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;
        &lt;span class="n"&gt;get_viewport&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_input_as_handled&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_physics_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_pending_click&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;click_position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Vector2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_pending_click&lt;/span&gt;
    &lt;span class="n"&gt;_pending_click&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt;
    &lt;span class="n"&gt;_raycast_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;click_position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_raycast_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;screen_position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ray_origin&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;camera&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_ray_origin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;screen_position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ray_end&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ray_origin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;camera&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_ray_normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;screen_position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;200.0&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PhysicsRayQueryParameters3D&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ray_origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ray_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exclude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_rid&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_world_3d&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;direct_space_state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;intersect_ray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_empty&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;_set_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"No level surface under that click."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;_set_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A physics hit is not a navigation target
&lt;/h2&gt;

&lt;p&gt;A click can land on the side of a wall, on geometry outside the baked region, or next to a disconnected surface. Normalize it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;set_navigation_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requested_position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;_navigation_is_ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;global_position&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reachable_target&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NavigationServer3D&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map_get_closest_point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_navigation_map&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;requested_position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target_position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reachable_target&lt;/span&gt;
    &lt;span class="n"&gt;_has_target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;_last_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PackedVector3Array&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;reachable_target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep both values around if your UI ever needs to explain a correction — the starter displays the requested world position next to the clamped navmesh target. When a character stops short of where you clicked, &lt;code&gt;get_final_position()&lt;/code&gt; gives you the reachable end of the current path and &lt;code&gt;is_target_reachable()&lt;/code&gt; tells you what the agent thinks of the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  NavigationAgent3D does not move your character
&lt;/h2&gt;

&lt;p&gt;It computes path information. That's it. Your controller asks for one next point per physics frame, flattens the steering onto the ground plane, applies acceleration, and calls &lt;code&gt;move_and_slide()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_physics_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;is_on_floor&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;velocity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;_gravity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;velocity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;velocity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;desired_velocity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Vector3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ZERO&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_navigation_is_ready&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;_has_target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_navigation_finished&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;_has_target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;next_path_position&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_next_path_position&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;global_position&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;direction_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next_path_position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length_squared&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.0001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;desired_velocity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;move_speed&lt;/span&gt;
            &lt;span class="n"&gt;_emit_path_if_changed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;current_horizontal&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;velocity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;velocity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;desired_velocity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_horizontal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;move_toward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;desired_velocity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acceleration&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;avoidance_enabled&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;_navigation_is_ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;velocity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;desired_velocity&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_on_velocity_computed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;desired_velocity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep &lt;code&gt;get_next_path_position()&lt;/code&gt; in the physics loop. Calling it from signals like &lt;code&gt;waypoint_reached&lt;/code&gt; can retrigger path updates and recurse on you.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ramp that ate an evening
&lt;/h3&gt;

&lt;p&gt;The first version of this starter had a route that was completely valid and completely unwalkable. The navmesh connected the floor to the upper platform, the path line drew a clean cyan arc up the ramp, and the capsule walked to the bottom of the ramp and stopped.&lt;/p&gt;

&lt;p&gt;Nothing was wrong with the pathfinding. The baked route approached the &lt;em&gt;vertical side face&lt;/em&gt; of the ramp collider, and no combination of &lt;code&gt;floor_snap_length&lt;/code&gt;, gravity, or &lt;code&gt;floor_max_angle&lt;/code&gt; was going to get a &lt;code&gt;CharacterBody3D&lt;/code&gt; up a wall. Making both ramp transitions physically flush is what fixed it.&lt;/p&gt;

&lt;p&gt;A valid path is not a promise that the body can execute it. When movement stalls and the path looks correct, stop reading navigation code and go inspect the collider.&lt;/p&gt;

&lt;p&gt;The other tuning result worth stealing: &lt;code&gt;path_desired_distance = 0.65&lt;/code&gt;. Smaller values made this particular accelerated body overshoot each waypoint and then curve back toward it, forever. Derive that number from your controller's speed and stopping behaviour rather than copying mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoidance changes velocity, not the path
&lt;/h2&gt;

&lt;p&gt;The moving obstacle in the starter is a &lt;code&gt;NavigationObstacle3D&lt;/code&gt; that publishes its velocity every physics frame so the avoidance server can predict it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_physics_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;previous_position&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;global_position&lt;/span&gt;
    &lt;span class="n"&gt;_phase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;fmod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_phase&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="bp"&gt;TAU&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cycles_per_second&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;TAU&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;next_position&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_spawn_position&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;travel_axis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; \
        &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_phase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;travel_distance&lt;/span&gt;
    &lt;span class="n"&gt;last_reported_velocity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next_position&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;previous_position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;maxf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0001&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;navigation_obstacle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;velocity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last_reported_velocity&lt;/span&gt;
    &lt;span class="n"&gt;global_position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next_position&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The player sends its desired horizontal velocity through &lt;code&gt;agent.velocity&lt;/code&gt;, and &lt;code&gt;velocity_computed&lt;/code&gt; hands back a locally safer one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_on_velocity_computed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;safe_velocity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;velocity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;safe_velocity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;
    &lt;span class="n"&gt;velocity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;safe_velocity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;
    &lt;span class="n"&gt;move_and_slide&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While all of that happens, the cyan path line doesn't move. That's correct behaviour, and it surprises people. Avoidance doesn't rebake the mesh, doesn't pick a new route, and doesn't know your physics collider exists. It nudges velocity and nothing else.&lt;/p&gt;

&lt;p&gt;Which also means avoidance can pin an agent against a wall if you park a moving obstacle in a narrow corridor. If an object is supposed to make a route &lt;em&gt;illegal&lt;/em&gt;, you need an actual navigation change, not steering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Symptom table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Check first&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First path comes back empty&lt;/td&gt;
&lt;td&gt;Wait for map and region data to synchronize&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path computes, character doesn't move&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NavigationAgent3D&lt;/code&gt; doesn't move its parent; call your controller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Body clips or sticks at a doorway&lt;/td&gt;
&lt;td&gt;Compare &lt;code&gt;CollisionShape3D&lt;/code&gt; against the baked &lt;code&gt;agent_radius&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A moving object gets ignored&lt;/td&gt;
&lt;td&gt;Enable avoidance and supply the obstacle's velocity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Character stops short of the click&lt;/td&gt;
&lt;td&gt;Compare the request with &lt;code&gt;get_final_position()&lt;/code&gt; and &lt;code&gt;is_target_reachable()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path crosses the ramp, body stops at its foot&lt;/td&gt;
&lt;td&gt;Inspect the collider for a vertical lip or side-face approach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Body circles a waypoint&lt;/td&gt;
&lt;td&gt;Raise &lt;code&gt;path_desired_distance&lt;/code&gt;, or retune speed and acceleration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path line changes when you only expected steering&lt;/td&gt;
&lt;td&gt;Something is assigning a new target. Avoidance alone doesn't reroute&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When AStar3D is the right answer
&lt;/h2&gt;

&lt;p&gt;Reach for it when the legal positions and edges are explicit game data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A voxel world where each block position can be occupied or disabled&lt;/li&gt;
&lt;li&gt;A dungeon built from stacked tile layers&lt;/li&gt;
&lt;li&gt;Fixed 3D waypoints with one-way or authored connections&lt;/li&gt;
&lt;li&gt;A flying or swimming lattice with discrete neighbours&lt;/li&gt;
&lt;li&gt;Turn-based 3D movement with exact per-node costs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You own that graph completely. Add each point, connect the legal pairs, disable and reconnect points when the world changes, then move the actor along the returned point path. Nothing discovers it for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the verified starter
&lt;/h2&gt;

&lt;p&gt;The download is the exact authored scene and scripts from this article, MIT-licensed: &lt;a href="https://vav-labs.com/downloads/pathfinding/3d-pathfinding-starter-godot-source.zip" rel="noopener noreferrer"&gt;Godot 4.7.1 source project&lt;/a&gt;, 13,833 bytes, SHA-256 &lt;code&gt;bcfb590993cfd025b810f745bf0303d39061cdf6c177f0c27bf66be637104d1c&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Beside it is a &lt;a href="https://vav-labs.com/downloads/pathfinding/3d-pathfinding-starter-godot-verification.json" rel="noopener noreferrer"&gt;machine-readable verification receipt&lt;/a&gt;: 21 named checks, 21 passed, 0 failed, on Godot 4.7.1-stable (official, engine hash &lt;code&gt;a13da4fe&lt;/code&gt;). It records the ZIP hash, per-file hashes, a clean import of the extracted project, and a scene smoke test that reports 27 navmesh polygons and 26 vertices from the saved bake.&lt;/p&gt;

&lt;p&gt;Honest boundary, straight from the receipt: this is deterministic tutorial correctness. It makes no claim about FPS, throughput, crowd capacity, path optimality, production-readiness, or dynamic blockers. If you want a number for "how many agents can I run," you'll have to profile your own project.&lt;/p&gt;




&lt;p&gt;The full version is on &lt;a href="https://vav-labs.com/blog/3d-pathfinding-in-godot/" rel="noopener noreferrer"&gt;vav-labs.com&lt;/a&gt;, and it embeds the scene as a playable web export so you can click around before downloading anything. It also carries the FAQ and the links out to the related guides.&lt;/p&gt;

&lt;p&gt;If any of this doesn't match what you're hitting in your own scene, I'd genuinely like to hear about it. The failure modes here are more varied than one post can cover.&lt;/p&gt;

</description>
      <category>pathfinding</category>
      <category>godot</category>
      <category>navigationagent3d</category>
    </item>
    <item>
      <title>Why NavigationAgent2D Lags With Hundreds of Units</title>
      <dc:creator>Vav Labs</dc:creator>
      <pubDate>Wed, 15 Jul 2026 15:05:59 +0000</pubDate>
      <link>https://dev.to/vav_labs/why-navigationagent2d-lags-with-hundreds-of-units-1pp2</link>
      <guid>https://dev.to/vav_labs/why-navigationagent2d-lags-with-hundreds-of-units-1pp2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F84334ychfgghlag83qo3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F84334ychfgghlag83qo3.gif" alt=" " width="760" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Originally published on &lt;a href="https://vav-labs.com/blog/why-navigationagent2d-lags-with-hundreds-of-units/" rel="noopener noreferrer"&gt;vav-labs.com&lt;/a&gt;. Everything here is against Godot 4.7 stable, and there's a runnable source project with a verification receipt at the end.&lt;/p&gt;

&lt;p&gt;If your game runs fine with a dozen units and then hitches the moment you box-select a hundred and click "move," the instinct is to blame the pathfinding algorithm. Usually it isn't the algorithm.&lt;/p&gt;

&lt;p&gt;It's how many path requests you started in one physics tick.&lt;/p&gt;

&lt;p&gt;This one's in the docs, and people still hit it constantly (me included, the first time I built an RTS selection). Setting &lt;code&gt;NavigationAgent2D.target_position&lt;/code&gt; requests a new path. Do that for 100 selected units in a single frame and you've just queued 100 path queries in that frame. One query is cheap. A hundred of them landing on the same tick is a scheduling problem wearing an algorithm costume.&lt;/p&gt;

&lt;p&gt;So before you change anything, count.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, confirm navigation actually owns the hitch
&lt;/h2&gt;

&lt;p&gt;Profile before you theorize. If the profiler says the frame time is going to physics or rendering or a gameplay script, none of the fixes below apply. Nail the attribution first.&lt;/p&gt;

&lt;p&gt;Once you know it's navigation, the next question is whether it's the &lt;em&gt;request&lt;/em&gt; side (too many queries starting at once) or the &lt;em&gt;search&lt;/em&gt; side (one query that's genuinely expensive). They have different fixes, so don't guess.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;First action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One hitch when a group gets a move order&lt;/td&gt;
&lt;td&gt;Same-tick request burst&lt;/td&gt;
&lt;td&gt;Update groups or a per-tick request budget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Steady stutter while units chase a moving target&lt;/td&gt;
&lt;td&gt;Per-frame &lt;code&gt;target_position&lt;/code&gt; resets&lt;/td&gt;
&lt;td&gt;Distance + time repath thresholds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lag only where the crowd bunches up&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;path_max_distance&lt;/code&gt; recalculations&lt;/td&gt;
&lt;td&gt;Measure deviation, tune tolerance, freeze arrivals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every query is slow, even for one unit&lt;/td&gt;
&lt;td&gt;Search-side polygon/edge count&lt;/td&gt;
&lt;td&gt;Optimize the nav mesh — scheduling isn't the fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A squad is smooth but hundreds share one goal badly&lt;/td&gt;
&lt;td&gt;Per-agent paths hit their ceiling&lt;/td&gt;
&lt;td&gt;Move to a shared field&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One thing worth internalizing: path-search cost tracks navigation-mesh polygons and edges, not the physical size of your world. And an unreachable target can force a much longer search. If a single unit is already slow, that's a mesh problem, and no amount of request scheduling will save you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Count the requests before you tune them
&lt;/h2&gt;

&lt;p&gt;Don't tune budgets and thresholds blind. Add a few custom monitors and watch real numbers in the Debugger's Monitor tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Battle-scene wiring. The receipt covers the governor, not this glue.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_ready&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;Performance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_custom_monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"repath/requests_per_tick"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_requests_this_tick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Performance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_custom_monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"repath/queue_depth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_governor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queue_depth&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;Performance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_custom_monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"repath/oldest_request_ticks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_governor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oldest_request_age_ticks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_physics_frames&lt;/span&gt;&lt;span class="p"&gt;())))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each one tells you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Requests per physics tick&lt;/strong&gt; — a one-frame group-order burst shows up here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Requests per unit per second&lt;/strong&gt; — sustained every-tick target resets show up here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue depth and oldest-request age&lt;/strong&gt; — tells you your budget is too tight for acceptable response latency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One caveat: &lt;code&gt;path_changed&lt;/code&gt; fires with no reason payload, so you can't ask the engine &lt;em&gt;why&lt;/em&gt; it repathed. Track your own reasons where you queue the request (&lt;code&gt;GROUP_ORDER&lt;/code&gt;, &lt;code&gt;TARGET_DRIFT&lt;/code&gt;, &lt;code&gt;PERIODIC_REFRESH&lt;/code&gt;), and only infer "probably knocked off-path" when the target and map revision both stayed fixed but the agent drifted past its tolerance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause A: one click, a hundred requests
&lt;/h2&gt;

&lt;p&gt;This is the common one. The fix is the explicit version of Godot's own "split agents into update groups" advice: route every target change through a single governor that dispatches a budgeted number of live requests per tick.&lt;/p&gt;

&lt;p&gt;The nice thing is you can predict the latency before you even run the game. With &lt;code&gt;N&lt;/code&gt; pending requests and a budget of &lt;code&gt;B&lt;/code&gt; per tick, draining takes &lt;code&gt;ceil(N / B)&lt;/code&gt; ticks. At 60 Hz, 500 requests with &lt;code&gt;B = 8&lt;/code&gt; means the last unit gets its path about a second after the order. If that's too slow, raise the budget for small groups, prioritize the visible/player units, or stop issuing one path per unit entirely.&lt;/p&gt;

&lt;p&gt;The dispatch loop is bounded two ways — a query ceiling and a &lt;em&gt;separate&lt;/em&gt; scan ceiling, so that cleaning up cancelled or superseded entries can't itself blow the tick:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;drain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;is_alive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;current_map_revision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;now_tick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dispatched&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scanned&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;query_limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maxi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queries_per_tick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scan_limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maxi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_entries_scanned_per_tick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;dispatched&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;query_limit&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;scanned&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;scan_limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_take_next_raw_entry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_empty&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="n"&gt;scanned&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="c1"&gt;# ...skip stale/dead entries, then run the query and record it...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few properties that matter once you're at scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latest target wins.&lt;/strong&gt; A repeated request for a unit still in the queue updates its target in place, instead of piling on more work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dead and cancelled units stop before the callback.&lt;/strong&gt; Their queued entries never reach the agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request and scan budgets are separate.&lt;/strong&gt; Stale cleanup can't quietly consume an unbounded tick.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two small gotchas: keep the governor on the controller that owns the unit registry (one queue per agent just recreates the burst with more objects), and don't reach for &lt;code&gt;Array.pop_front()&lt;/code&gt; on a big queue — it shifts every remaining index. Head cursors are cheaper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause B: the target moved one pixel
&lt;/h2&gt;

&lt;p&gt;A chasing unit tends to reset &lt;code&gt;target_position&lt;/code&gt; every time its target moves, and every reset is a new path request. The docs even attribute the classic "unit dances between two spots" bug to path updates that are too frequent.&lt;/p&gt;

&lt;p&gt;Gate it. Ignore drift smaller than a distance threshold, and refresh a slowly-moving target on a time threshold instead of every frame. Long intervals are fine at range; tighten them near the destination. And treat arrival as a hard stop — an arrived unit requests nothing and stops calling &lt;code&gt;get_next_path_position()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;repath_reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;planned_target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;live_target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Vector2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;repath_distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;now_tick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;next_repath_tick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;arrived&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;StringName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;arrived&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;REASON_NONE&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;planned_target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;distance_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;live_target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maxf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repath_distance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;REASON_TARGET_DRIFT&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now_tick&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;next_repath_tick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;REASON_PERIODIC_REFRESH&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;REASON_NONE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cause C: avoidance pushes, path_max_distance pulls
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;path_max_distance&lt;/code&gt; is how far an agent is allowed to stray from its ideal path. Godot documents that crossing it triggers a recalculation — including when collision avoidance is what shoved the agent out there.&lt;/p&gt;

&lt;p&gt;At crowd scale that chains: avoidance nudges one agent past its tolerance, its new route shifts the local pressure, a neighbor crosses &lt;em&gt;its&lt;/em&gt; tolerance, and so on. Before you blame this, correlate the &lt;code&gt;path_changed&lt;/code&gt; signal with an unchanged target and map revision plus measured deviation. Otherwise you're guessing.&lt;/p&gt;

&lt;p&gt;Three things help, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tune from measured deviation.&lt;/strong&gt; Raise &lt;code&gt;path_max_distance&lt;/code&gt; just enough that normal steering stays inside it, then recheck that a genuinely lost agent still recovers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freeze arrivals.&lt;/strong&gt; Units sitting inside their destination tolerance stop consuming path updates, so avoidance can't shove them into another recalculation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep steering separate from planning.&lt;/strong&gt; A momentary avoidance offset doesn't need a brand-new target assignment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Avoidance and pathfinding are separate systems, and avoidance has its own cost knobs (&lt;code&gt;neighbor_distance&lt;/code&gt;, &lt;code&gt;max_neighbors&lt;/code&gt;). That's a deeper rabbit hole than this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the proof yourself
&lt;/h2&gt;

&lt;p&gt;There's a standalone Godot 4.7 project with the full assembled governor, a runnable scene, a verifier, README, and license: &lt;a href="https://vav-labs.com/downloads/pathforge/why-navigationagent2d-lags-with-hundreds-of-units-source.zip" rel="noopener noreferrer"&gt;source ZIP here&lt;/a&gt;. Its &lt;a href="https://vav-labs.com/downloads/pathforge/why-navigationagent2d-lags-with-hundreds-of-units-verification.json" rel="noopener noreferrer"&gt;machine-readable receipt&lt;/a&gt; reports 20/20 named checks, zero failures.&lt;/p&gt;

&lt;p&gt;The checks cover both ceilings, FIFO within a priority, high-priority preemption without starving the normal queue, coalescing and promotion, dead/cancelled entries, the threshold policies, arrival freeze, and current-revision dispatch. The scene smoke stands up a real &lt;code&gt;NavigationServer2D&lt;/code&gt; region, gets both direct and &lt;code&gt;NavigationAgent2D&lt;/code&gt; paths, and drains ten requests as &lt;code&gt;3, 3, 3, 1&lt;/code&gt; under a three-query budget. The ZIP is 11,641 bytes, SHA-256 &lt;code&gt;5c58b4a53df42cf9b8aa81a0a75758bc64656435344fc0d44db36e92e6f94bef&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Honest boundary: this is scheduling-correctness and scene-integration evidence. It is &lt;strong&gt;not&lt;/strong&gt; an FPS, throughput, memory, or supported-unit-count benchmark. If you want a number for "how many units can I run," you'll have to profile your own project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debug order, short version
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Profile — confirm the hitch is navigation, not physics/rendering/scripts.&lt;/li&gt;
&lt;li&gt;Record requests per tick, per-unit rate, your own reasons, queue depth, oldest age.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GROUP_ORDER&lt;/code&gt; spikes → Cause A. Sustained &lt;code&gt;TARGET_DRIFT&lt;/code&gt; → Cause B.&lt;/li&gt;
&lt;li&gt;Unexplained &lt;code&gt;path_changed&lt;/code&gt; with a stable target/map + measured deviation → Cause C.&lt;/li&gt;
&lt;li&gt;Check whether arrived units are still consuming path updates.&lt;/li&gt;
&lt;li&gt;Change one budget/threshold/tolerance, then re-measure the same counters.&lt;/li&gt;
&lt;li&gt;Requests minimal but one query slow → it's the mesh, not scheduling.&lt;/li&gt;
&lt;li&gt;Still need separate routes to one shared goal for a huge crowd → shared field.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fix the cause you measured, not the one you assumed. If any of this doesn't match what you're seeing in your own project, I'd genuinely like to know — the failure modes are more varied than one post can cover.&lt;/p&gt;

&lt;p&gt;The full version, with the complete governor code and the internal links to the related failure-mode guides, is on &lt;a href="https://vav-labs.com/blog/why-navigationagent2d-lags-with-hundreds-of-units/" rel="noopener noreferrer"&gt;vav-labs.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>pathfinding</category>
      <category>performance</category>
    </item>
    <item>
      <title>Tower Defense Path Validation in Godot</title>
      <dc:creator>Vav Labs</dc:creator>
      <pubDate>Wed, 24 Jun 2026 15:15:50 +0000</pubDate>
      <link>https://dev.to/vav_labs/tower-defense-path-validation-in-godot-12e2</link>
      <guid>https://dev.to/vav_labs/tower-defense-path-validation-in-godot-12e2</guid>
      <description>&lt;p&gt;In a tower defense, a player can drop one tower that walls off the path and softlocks the wave. The fix isn't to place it and hope pathfinding recovers — it's to validate first: treat the tower as a temporary blocker, ask if a route still exists, revert if it doesn't, and only then commit.&lt;/p&gt;

&lt;p&gt;I built a playable Godot 4.7 demo for it: place towers, watch the path reroute, try to seal it and get rejected. The guide also measures the cheap vs expensive way to run that check, with a runnable project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vav-labs.com/blog/tower-defense-path-validation-in-godot/" rel="noopener noreferrer"&gt;https://vav-labs.com/blog/tower-defense-path-validation-in-godot/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1llhjn6ghfoshlxo7xii.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1llhjn6ghfoshlxo7xii.gif" alt=" " width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>pathfinding</category>
    </item>
    <item>
      <title>Godot's NavigationObstacle2D has two modes (and other NavigationServer2D gotchas)</title>
      <dc:creator>Vav Labs</dc:creator>
      <pubDate>Tue, 23 Jun 2026 09:42:09 +0000</pubDate>
      <link>https://dev.to/vav_labs/godots-navigationobstacle2d-has-two-modes-and-other-navigationserver2d-gotchas-1l6</link>
      <guid>https://dev.to/vav_labs/godots-navigationobstacle2d-has-two-modes-and-other-navigationserver2d-gotchas-1l6</guid>
      <description>&lt;p&gt;Godot's NavigationServer2D is powerful and quietly confusing. Most of the pain isn't the API — it's a handful of distinctions that look similar and behave nothing alike. The ones that eat afternoons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NavigationObstacle2D has two modes, and people conflate them.&lt;/strong&gt; &lt;code&gt;avoidance_enabled&lt;/code&gt; makes your agents steer around an obstacle, but the path query still runs straight through it — so your tower-defense blocker doesn't block the route. &lt;code&gt;affect_navigation_mesh&lt;/code&gt; is the other one: it carves the obstacle into the navmesh at bake time, and &lt;em&gt;then&lt;/em&gt; the path reroutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The empty path.&lt;/strong&gt; A query returns nothing even though the scene looks walkable — usually because the navmesh was never baked. It's the NavigationServer2D version of forgetting &lt;code&gt;AStarGrid2D.update()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map-sync timing.&lt;/strong&gt; NavigationServer changes apply at the end of the physics frame. Query in &lt;code&gt;_ready()&lt;/code&gt; or right after editing nav data and you get a stale or empty result — then blame the agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NavigationAgent2D isn't the map.&lt;/strong&gt; The agent is a node-level helper that consumes navigation data; it doesn't make the map valid by itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I wrote the full guide — regions, agents, direct path queries, both obstacle modes, links, and the map-sync trap — checked every claim against the docs and the C++ source, and built an interactive playground for the gotchas. (It's a browser illustration of how the system behaves, not the engine running, and there's no benchmark here — it's a guide, not a measurement.)&lt;/p&gt;

&lt;p&gt;Full guide + playground: &lt;strong&gt;&lt;a href="https://vav-labs.com/blog/navigationserver2d-godot-complete-guide/" rel="noopener noreferrer"&gt;https://vav-labs.com/blog/navigationserver2d-godot-complete-guide/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If any of it's wrong or out of date for your version, tell me — the navigation API has moved across Godot 4.x, and I'd rather fix it than leave you debugging my mistake.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0x6k3z4a31nv2736h8qk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0x6k3z4a31nv2736h8qk.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>AStarGrid2D in Godot 4: the gotchas, and an interactive sandbox</title>
      <dc:creator>Vav Labs</dc:creator>
      <pubDate>Sun, 21 Jun 2026 23:20:15 +0000</pubDate>
      <link>https://dev.to/vav_labs/astargrid2d-in-godot-4-the-gotchas-and-an-interactive-sandbox-3dl8</link>
      <guid>https://dev.to/vav_labs/astargrid2d-in-godot-4-the-gotchas-and-an-interactive-sandbox-3dl8</guid>
      <description>&lt;p&gt;Godot's &lt;code&gt;AStarGrid2D&lt;/code&gt; is a ten-minute read and a month of gotchas. The API page is small, you skim it, and then your path comes back empty, your units cut through wall corners, or your weighted swamp gets ignored. None of it is a bug. It's the gap between knowing the method names and knowing which handful of settings actually decide the behavior.&lt;/p&gt;

&lt;p&gt;The five that get almost everyone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You forget &lt;code&gt;update()&lt;/code&gt;.&lt;/strong&gt; Change the region or cell size, skip the rebuild, and you get an empty array with no error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;update()&lt;/code&gt; wipes your point data.&lt;/strong&gt; It clears every solid cell and weight, so you have to set those &lt;em&gt;after&lt;/em&gt; you call it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagonal corner-cutting.&lt;/strong&gt; &lt;code&gt;DIAGONAL_MODE_ALWAYS&lt;/code&gt; lets units squeeze diagonally between two walls. You usually want &lt;code&gt;ONLY_IF_NO_OBSTACLES&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;jumping_enabled&lt;/code&gt; silently ignores weight scale.&lt;/strong&gt; Turn on JPS and your weighted terrain stops mattering. It's in the docs. People still lose an afternoon to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manhattan overestimates with diagonals.&lt;/strong&gt; On an 8-direction grid it counts a diagonal as two steps, so your "shortest" path isn't. Reach for Octile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I wrote the reference I actually wanted (every property and method, the gotcha attached to each, version-honest across Godot 4.0 → 4.7, checked against the official docs and the C++ source), and built an interactive sandbox for the parts you have to &lt;em&gt;see&lt;/em&gt;: drag the goal, paint solid and weighted cells, flip diagonal modes, toggle jumping, and watch the path recompute live. (It's a browser illustration of how AStarGrid2D behaves, not the engine itself running in a tab.)&lt;/p&gt;

&lt;p&gt;Full reference and the sandbox: &lt;strong&gt;&lt;a href="https://vav-labs.com/blog/astargrid2d-complete-reference/" rel="noopener noreferrer"&gt;https://vav-labs.com/blog/astargrid2d-complete-reference/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If any of it's wrong or out of date for your version, tell me. The API moved across Godot 4.x, and I'd rather fix it than leave you debugging my mistake.&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>gdscript</category>
      <category>pathfinding</category>
    </item>
    <item>
      <title>Moving 10,000 agents in Godot without the frame spike</title>
      <dc:creator>Vav Labs</dc:creator>
      <pubDate>Tue, 16 Jun 2026 15:08:59 +0000</pubDate>
      <link>https://dev.to/vav_labs/moving-10000-agents-in-godot-without-the-frame-spike-1l9n</link>
      <guid>https://dev.to/vav_labs/moving-10000-agents-in-godot-without-the-frame-spike-1l9n</guid>
      <description>&lt;p&gt;I kept reading that Godot pathfinding falls apart with a lot of agents, so I actually measured it. 500 agents each calling AStarGrid2D.get_id_path() every frame gave me a 670 ms median frame on an 8-year-old desktop — a slideshow. Swap those 500 per-agent queries for one shared field the whole crowd reads, and the same agents drop to ~2 ms, nothing over the 16.6 ms budget. The fix was the shape of the work, not a faster solver — and the same approach holds 10,000 agents at 77 FPS in a browser tab, pure GDScript.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/4I9pNktBq1M"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Full write-up with the measured ladder out to 20,000 agents, a playable demo (drag the goal, flip naive vs scheduled, push the count yourself), and the benchmark JSON for every count: &lt;a href="https://vav-labs.com/blog/moving-10000-agents-in-godot/" rel="noopener noreferrer"&gt;https://vav-labs.com/blog/moving-10000-agents-in-godot/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>performance</category>
      <category>gdscript</category>
    </item>
  </channel>
</rss>
