Godot pathfinding / 2026-09-22 / 12 min read
Verified as of Godot 4.7.2 stable
RTS Crowd Movement in Godot: From Right-Click to Arrival
Build a Godot RTS move order with reachable destination slots, shared claims, partial allocation, and honest arrival states. Includes a tested 2D demo.
Give each unit a place to stop
For an RTS move order in Godot, use the click as the center of a destination area. Give each selected unit a separate reachable position, reserve those positions across groups, and check the body's position before reporting arrival. Keep avoidance enabled for movement between them.
If every unit receives the same target_position, you've asked several bodies to occupy one point. Changing avoidance settings doesn't change that request. Godot's documentation already separates navigation from body movement. The group order still needs rules for who stops where. Using NavigationAgents
The accompanying 2D demo makes that distinction visible. In the captured eight-unit fixture, the shared-target mode finished with all eight units stalled. With destination allocation enabled, all eight arrived. These are observations from the included scene, not a capacity claim for your RTS.
Download the source project · Read the verification record · Open the clean-extraction receipt

The code was tested on Godot 4.7.2 stable, build ed1daf0bf. The official archive listed 4.7.2 as the latest stable release when checked on September 22, 2026. API links below use the 4.7 documentation. The fixture uses equal-size circular bodies, a static navmesh, and ordinary NavigationAgent2D movement.
What the move order owns
The controller snapshots the selected unit IDs when you click. Changing the selection later doesn't change an existing order. Each selected member receives a new revision, which follows its planning work and eventual target write.
There are two separate results to report. Allocation says how many members received destinations. Movement says what happened to each body afterward.
| State | Meaning in this demo |
|---|---|
PLANNING | Candidate pairs are still being checked |
QUEUED | A position is reserved, but the agent target hasn't been written yet |
MOVING | The member is following its assigned destination |
ARRIVED | The body is within tolerance and has settled |
WAITING | No assignment was accepted, or the plan became invalid |
BLOCKED | Navigation ended before the assigned destination |
STALLED | The body stopped making sufficient progress along its route |
An order can be PARTIAL while its assigned members are moving. A fully allocated order can still contain a stalled member later. Keeping those facts separate makes the UI considerably easier to trust.
This controller doesn't keep a formation while units travel. If you need an anchor, named slots, and reformation after a corridor, start with formation movement in Godot.
Give arrival tolerance some room
The demo starts with 25 candidate points, arranged in two square rings around the click. That's a deliberately small search area. A click near a wall may yield fewer usable positions.
For equal-radius bodies, candidate spacing is:
Both units can stop on the edge of their arrival tolerance, toward each other. Including both tolerances leaves the requested margin between their bodies even in that case. The fixture uses radius 10, tolerance 3, and margin 4, giving 30 units between slot centers. Those values belong to this scene.
Next, project candidates onto the navigation map. Reject a projection that moves more than 18 units, then check spacing again. Two well-separated proposals can collapse onto the same edge when snapped. Spacing before projection doesn't protect you afterward.
The projection pass greedily retains separated points. It can discard a point that a more sophisticated sampler would use. So a shortage means no full assignment in the checked candidate set, not proof that the surrounding terrain has no room.
Validate the route for each unit-slot pair
A point on the navmesh may belong to a disconnected region. map_get_closest_point() doesn't receive the unit's start position or a navigation-layer mask. Treat its result as a candidate. The subsequent path query uses the member's start and agent.navigation_layers. NavigationServer2D API
The acceptance check in rts_destination_core.gd is small:
Checking both ends rejects a route that begins too far from the body as well as one that stops short. The start check is intentionally conservative. A unit far outside the walkable area needs a recovery policy before this controller will assign it a destination.
An unreachable destination can produce a nonempty path ending at an accessible point. In the disconnected fixture, that endpoint was 162 units from the requested target. A simple not path.is_empty() check would have accepted it. Using navigation paths
Before making any queries, the map must be synchronized. The demo waits for a nonzero map iteration. It also records that iteration with the plan and compares it again at commit. For a runtime mutation, the map owner must lower the controller's navigation_ready flag before changing the map and raise it after the change has synchronized. A nonzero iteration alone only tells you that some synchronization has happened.
Match the feasible pairs
Suppose A can reach slots 1 and 2, while B can only reach slot 1. Assigning A to 1 and stopping there creates a false shortage. Move A to 2 and both members have a destination.
The artifact uses maximum-cardinality bipartite matching. Members are on one side, candidate positions on the other, and each accepted path adds an edge. An augmenting search can move an earlier assignment to make room for a constrained member. It prefers a free feasible slot before displacing an existing assignment.
Here's the entry point from rts_destination_core.gd:
The complete helper is in the ZIP. Its cardinality was checked against an independent brute-force search for all 512 possible three-by-three graphs. That checks the small matching routine. It doesn't prove that the candidate generator found every useful position.
Matching also doesn't minimize travel distance or prevent crossing routes. Those are additional objectives. For the one-slot, four-member fixture, the controller reserves that slot once and leaves three members waiting. It doesn't duplicate the assignment to make the count look better.
The default budget allows four direct validation queries per physics tick. Unchecked pairs stay pending. Only after the finite graph has been checked does the controller decide whether the order is fully allocated or partial.
Agent target writes have a separate cap of two per tick. The validation counter doesn't include NavigationAgent2D's internal path searches, and neither cap is a millisecond limit. The broader scheduling problem belongs in the many-agent request guide.
Replace the whole order, including pending movement
A second click can arrive while an old plan is incomplete or while a target write is queued. Every member revision must be checked at the point where that work would affect the body.
Starting a replacement order increments the revision and stops motion immediately. An old job or dispatch request can remain in its queue briefly, but it can't write a target for the new revision.
Guard the avoidance callback as well. This is the actual movement entry point in rts_order_member.gd:
stop_motion() clears the pending submission and sets velocity to zero. The verifier injects a callback during replacement and checks that it can't move the planning body. It also checks a callback against an arrived body.
This guard follows Godot's physics-loop callback lifecycle. It doesn't attach a unique request token to the engine signal. Don't reuse it as a claim that arbitrary out-of-order worker responses are safe.
Measure arrival on the body
is_navigation_finished() can mean that an unreachable route reached its final waypoint. It isn't sufficient evidence that the assigned destination was reached. The controller distinguishes that from actual body arrival. NavigationAgent2D completion methods
In the demo, the body must be within 3 units of its assigned slot and remain below 2 units per second for 0.15 seconds. A changed order resets that settling timer. Those checks run while the current order is moving.
The first implementation exposed a mundane problem near the goal. A member kept approaching too quickly and oscillated under avoidance. The final loop reduces requested speed as it approaches the slot:
If avoidance displaces a body after its route has finished, the loop can continue toward the assigned slot when the route endpoint matches it. If the route ends elsewhere, the member becomes BLOCKED. The disconnected baseline fixture exercises that distinction with a real agent.
Stall detection uses distance remaining along the current route. Straight-line distance to the goal can increase during a perfectly valid wall detour. The fixture resets its timer after at least 0.5 units of route progress and reports STALLED after three seconds without that improvement. A changed path resets the timer too. For the live stuck watchdog and recovery diagnostics, see why NavigationAgent gets stuck in Godot.
Three seconds without progress doesn't prove that a destination is unreachable. It gives the player an observable failure state and leaves room for another order. This static demo latches ARRIVED once it settles. Knockback, teleports, and moving platforms would need an explicit policy to invalidate arrival and update occupancy.
What the artifact proves
The verifier passed 31 named checks in three runs inside the canonical project, then passed again from a clean extraction of the source ZIP. These are correctness fixtures with fixed 60 Hz simulation steps. These checks don't measure frame times.
| Fixture | Recorded result |
|---|---|
| Four units, open yard | All arrived within the 3-unit tolerance |
| Four members, one candidate | One assignment, three waiting members |
| Disconnected destination | Partial path rejected before dispatch |
| Disconnected shared-target baseline | Agent finished its route, body reported blocked |
| Parked group followed by another group | Four arrivals, separate retained claims |
| Replacement order | Old planning and dispatch work couldn't restore the old target |
| Wall detour | Arrival without a false straight-line-distance stall |
| Two opposing groups at a narrow gate | Three arrived, one stalled in each canonical run |
The narrow-gate result is useful. Destination reservations don't reserve passage through the gate. You'll need a traffic policy, such as waiting areas or directional priority, if your game requires guaranteed passage. The demo doesn't provide one.
The screenshots use a separate eight-unit capture. Here's the shared-target result with the same bodies and click:

To reproduce the checks after extracting the ZIP, run:
Use the Godot console executable available on your machine. The verifier writes dist/rts-group-orders-verification.json and exits with a failing status if a check fails. The README includes capture commands and the exact fixture settings.
For interactive inspection, open project.godot, run the project, select A, B, or both, then right-click the yard. Try the disconnected layout and replace a move while units are still planning. Watch which state changes, and whether the old target ever comes back.