Godot pathfinding / 2026-07-14 / 16 min read
Verified as of Godot 4.7 stable
Weighted Terrain Pathfinding in Godot
Make AStarGrid2D prefer roads over mud and swamp with verified terrain weights, correct route-cost math, safe heuristics, and per-unit profiles.

The direct answer
To make AStarGrid2D prefer roads over mud, swamp, or expensive terrain in Godot 4.7, use three rules:
- Keep the cheapest terrain at
1.0. Give normal ground or roads a weight of1.0, then express mud, swamp, and hazards as larger multipliers. Values below1.0can make the built-in estimate overstate the true remaining cost and return a suboptimal route. - Disable Jump Point Search. Set
jumping_enabled = false; Godot currently ignores weight scaling while jumping is enabled. - Charge the cell being entered. The destination point's weight multiplies the step cost. The starting cell's weight is not added to the route.
Start from the gameplay problem
A longer road wins when its accumulated entry cost is lower. If the game truly needs discounts below 1.0, override _estimate_cost() with a safe minimum-weight-scaled estimate or return 0.0 for Dijkstra-style search.
| What your game needs | Route-cost decision |
|---|---|
| The character crosses swamp instead of following the road | Keep the road at 1.0, raise the swamp above 1.0, turn jumping off, and compare the complete route costs. |
| AI should avoid cells under enemy fire | Combine authored terrain cost with a shared danger penalty. If danger depends on faction or unit, use a profile grid or custom query layer. |
| A tractor is fast on dirt but a sports car prefers asphalt | One native grid cannot accept different weights per query. Use one prepared grid per stable vehicle profile or a profile-aware solver. |
| A diagonal route clips a blocked or dangerous corner | Choose an explicit diagonal_mode. It checks adjacent solid cells but doesn't charge adjacent expensive cells that the route never enters. |
| The route looks right but its AP total is wrong | Add the base step cost multiplied by each entered cell's weight. Don't charge the starting cell. |
| A discounted road below 1.0 is ignored | Scale the estimate by the minimum legal weight or return 0.0; the built-in estimate can otherwise return a suboptimal route. |
Route cost is not cell count
The AStarGrid2D complete reference covers coordinate conversion, grid construction, and the broader API. This page stays on weighted route choice.
An influence map can generate danger values. A danger layer shared by every unit can be folded into one grid's point weights; faction- or unit-specific danger belongs in the profile architecture later in this article.
Suppose the direct route enters four swamp cells at cost 3.0 each. Its total is 12.0. A six-cell road costs 6.0 when every entered road cell is 1.0. The road is geometrically longer, but it is the correct weighted route.
For the four-way baseline, route cost = sum(weight_scale of every path cell after the start). AStarGrid2D compares that accumulated value during search. It doesn't find a shortest path first and add terrain preference afterward.
Build a safe four-way grid
Every helper snippet below comes from the same verified weighted_terrain_pathfinder.gd. The complete file, runnable scene, and headless verifier are in the download.
This baseline has one unit of geometric cost per cardinal step. The cheapest traversable terrain is 1.0, expensive terrain is greater than 1.0, and Jump Point Search is off.
Apply the complete weight batch after update()
Keep walkability and cost as separate data. A tile may be walkable and still be expensive. Validate a batch before the first write so one invalid cell doesn't leave a partially updated grid.
Direct weight writes take effect without another update(). If a later change to region, cell_size, offset, or cell_shape dirties the grid, the runtime AStarGrid2D update guide shows how to rebuild and replay the authored layer.
The TileMapLayer walkability guide keeps blocked-cell metadata separate. A future movement-cost authoring article will own the TileSet editor workflow; this page consumes normalized weights.
Diagonal movement can cut a corner
diagonal_mode decides whether a point agent may connect two diagonal cells when side-adjacent cells are solid.
The corner rule doesn't inspect the side cells' weights. If a side cell is walkable but costs 5.0, a diagonal can pass beside it without paying 5.0 because the route didn't enter that cell. The diagonal destination is charged: _compute_cost(from, destination) * destination_weight.
Use ONLY_IF_NO_OBSTACLES when a point unit must not squeeze past either solid corner. If fire exposure should make the diagonal edge itself expensive, put the penalty on its destination or implement a documented _compute_cost() transition rule. A multi-cell body needs the separate footprint-transition contract; point-agent diagonal modes don't prove that a 2x2 unit fits.
| Mode | Corner rule |
|---|---|
| DIAGONAL_MODE_ALWAYS | Allows the diagonal without checking the two side neighbors. |
| DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE | Allows it while at least one side neighbor is open. |
| DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES | Requires both side neighbors to be open. |
| DIAGONAL_MODE_NEVER | Uses cardinal movement only. |
The starting-cell paradox: leaving mud is not charged
When a route moves from A to B, Godot applies B's weight. It doesn't charge A again.
For [start, road, mud, goal] with weights [9.0, 1.0, 3.0, 1.0], the four-way route cost is 1.0 + 3.0 + 1.0 = 5.0. The 9.0 stored on start is not part of that route.
If a unit begins in mud and its first step enters a road, that step pays the road's weight. A UI that charges the starting mud again will disagree with the pathfinder, and an AP confirmation layer may reject a route that the preview called affordable. The action-points guide uses the same entry-cost convention for preview, confirm, and undo.
Calculate the cost returned by get_id_path()
get_id_path() returns cells, not a total. With Jump Point Search disabled, the verified four-way helper requires adjacent cardinal steps and sums every entered destination.
Don't use this four-way helper for diagonal movement. A diagonal-cost display must use the same geometric _compute_cost() rule as the configured grid. It also deliberately rejects a JPS path that omits intermediate cells; this weighted baseline keeps jumping disabled.
Why a road below 1.0 can return the wrong route
A* needs an estimate that doesn't overstate the cheapest remaining cost. Manhattan distance assumes every remaining cardinal step costs at least 1.0. A road weight of 0.25 breaks that assumption.
The Godot 4.7 proof uses a 5 x 2 grid. The direct top route costs 4.0; the six-step discounted road costs 2.25. Built-in Manhattan returns 4.0. A zero or correctly 0.25-scaled estimate returns 2.25.
The easiest correction is authoring: keep roads at 1.0 and raise terrain the route should avoid. If an existing cost set has a minimum below 1.0, scale the whole set upward until its new minimum is at least 1.0. That preserves route ordering and restores admissibility for the built-in geometric estimate. Multiplying every weight by an arbitrary positive constant preserves the mathematically cheapest route, but it doesn't preserve search correctness when the resulting minimum remains below 1.0.
When discounts are a real game rule, subclass AStarGrid2D and scale the estimate by the smallest legal weight. Setting the scale to 0.0 gives Dijkstra-style search.
Godot 4.7 does call both GDScript overrides; the exact observed call counts stay in the receipt because they're implementation detail. Don't set default_estimate_heuristic = 0 expecting a disabled estimate: enum value 0 is Euclidean.
A zero estimate may expand materially more cells than an informed admissible estimate. This article makes no performance claim about that trade-off.
| Estimate | Returned cost | Optimal cost |
|---|---|---|
| Built-in Manhattan | 4.0 | 2.25 |
| Zero estimate | 2.25 | 2.25 |
| Manhattan scaled by 0.25 | 2.25 | 2.25 |
Pick the smallest profile architecture that fits
get_id_path() doesn't accept a terrain profile or per-query weight callback. Native point weights are shared state on one AStarGrid2D instance.
Five stable soldier or vehicle types can reasonably use five prepared grids. A tractor profile may treat dirt as 1.0; a sports-car profile can give dirt a higher multiplier. Query the matching grid instead of rewriting one shared weight layer before every request. Remember that runtime blockers and structural rebuilds must stay synchronized across those grids.
Avoid per-query mutation of one shared grid when queries can overlap, defer, or feed a path cache. For tactical reachable cells rather than one destination, use the movement-range guide.
| Terrain rule | Architecture |
|---|---|
| Every unit reads the same costs | One AStarGrid2D and one weight layer. |
| A few stable profiles read terrain differently | One prepared grid per profile, with shared structural updates replayed to each grid. |
| Many or dynamic profiles use masks and multipliers | A custom query layer that reads the profile during expansion and returns total cost. |
| The player needs every cell within an AP budget | A budgeted Dijkstra/range search, then reconstruct the preview from that result. |
What the verification covers
The downloadable Godot 4.7 project contains the assembled helper, runnable comparison scene, verifier, and README. Its machine-readable receipt reports 17/17 named checks and zero failures after a clean extracted-package rerun.
The receipt covers route choice, entry-cost reconstruction, the starting cell, runtime weight edits, JPS, the 4.0 versus 2.25 counterexample, GDScript overrides, diagonal corners, and profile-specific grids. It doesn't cover search speed, memory, TileSet authoring, influence-map generation, AP transactions, or multi-cell movement.
The ZIP is 9,531 bytes with SHA-256 d7abad3952b97987a0432b2e277b78a2e8a52dfbf1368e2207ccb9e96778bf9c. Measurement-gap: this is a deterministic correctness result, not a runtime, throughput, memory, asymptotic, every-project, or released-product benchmark.

Debug weighted terrain in this order
The order matters because it separates coordinate mistakes, JPS, route math, heuristic admissibility, diagonal policy, rebuild loss, and profile selection.
- Confirm the weighted cells use grid IDs, not world positions.
- Confirm
jumping_enabledis false. - Compare the total cost of the returned route with the intended detour.
- Check whether any legal weight is below
1.0and whether the estimate is scaled accordingly. - Check
diagonal_modeand remember that side-cell weights aren't charged. - Check whether a structural
update()recreated the point layer. - Check that the query used the intended unit-profile grid.
Where PathForge goes further
PathForge applies terrain masks and fixed-point cost multipliers from the query's agent profile without rewriting an AStarGrid2D weight layer before each request, and returns total cost with each path.
That capability matters when infantry, vehicles, flying units, and large footprints interpret the same board differently. The standalone download above proves the native Godot baseline; PathForge extends the profile and workflow layer.
Frequently asked questions
How do I make AStarGrid2D prefer roads?
Keep roads at the cheapest safe baseline, normally 1.0, and give mud, swamp, slopes, or hazards larger weights. Disable jumping_enabled, then compare total route costs rather than cell count.
Why is the shortest route not always the cheapest route?
Weighted pathfinding minimizes accumulated traversal cost. A six-cell road can correctly beat a four-cell swamp route when the six entered road cells cost less in total.
Why can a road below weight scale 1.0 be ignored?
The built-in estimate can overstate the remaining cost when legal steps cost less than 1.0. Keep the minimum at 1.0, scale an override by the minimum legal weight, or return 0.0 for Dijkstra-style search.
How do I calculate the cost returned by get_id_path()?
Starting at path index one, add each step's geometric cost multiplied by the weight of the destination cell. Use the same cardinal or diagonal distance rule as _compute_cost().
Can each unit type have different terrain costs?
Yes, but one AStarGrid2D stores one weight per point. Use one prepared grid per stable profile when there are few profiles, or a custom profile-aware solver for dynamic masks and multipliers.
When do I reapply terrain weights?
Direct set_point_weight_scale() writes apply without update(). Reapply the authored layer after a structural change dirties the grid and update() rebuilds its points.