Vav Labs
Back to blog

Godot pathfinding / 2026-06-21 / 15 min read

Verified as of Godot 4.7 stable docs on 2026-06-21

AStarGrid2D in Godot 4: complete reference

Complete AStarGrid2D reference for Godot 4: region, cell size, update(), solid cells, weight scale, diagonals, heuristics, jumping, and partial paths.

AStarGrid2D is simple. The edge cases aren't.

AStarGrid2D is Godot's grid-based A* pathfinder for 2D games. You give it a rectangular cell region, call update(), mark blocked cells as solid, assign terrain costs with weight_scale, and ask for a path in grid coordinates or world positions. It's the right default for tile-based movement. It isn't a replacement for NavigationServer2D, crowd planning, clearance maps, or a production navigation workflow.

AStarGrid2D is a small class. You can read the whole API page in ten minutes, and most people do. Then they ship a grid where the path comes back empty, units cut through wall corners, or the route ignores the road and walks through the swamp. None of that is a Godot conspiracy. It's the gap between knowing the method names and knowing which small handful of settings decide the behavior.

So this is the page I'd keep open in a second tab. It walks through every property and method that changes behavior, with the gotcha attached to each one, and it ends with the question people actually came for: do I even want AStarGrid2D, or did I want NavigationServer2D?

If something here is wrong or out of date for your Godot version, I want to know. The API has moved across Godot 4.x, and stale navigation advice turns quickly into a debugging session.

Version notes for Godot 4.x

Godot's AStarGrid2D API has stayed recognizable, but a few details changed enough that older tutorials can mislead you.

If you're publishing code meant for current Godot, use region, not size. If you're reading an older tutorial that calls update() after marking obstacles, read the setup section twice.

Godot versionWhat matters for AStarGrid2D
4.0The docs use size = Vector2i(...) in the basic setup.
4.1+region: Rect2i is the preferred grid shape property; size is deprecated.
4.2+fill_solid_region() and fill_weight_scale_region() are available in the docs.
4.3+cell_shape adds square, isometric-right, and isometric-down cell placement. get_id_path() and get_point_path() support allow_partial_path.
4.7 stableCurrent stable docs as of 2026-06-21. Use these for final publish verification.

The setup that runs - and the order that matters

Here is the smallest setup that works. The order matters:

var astar := AStarGrid2D.new()
astar.region = Rect2i(0, 0, 64, 64)  # 64x64 cells, origin at (0, 0)
astar.cell_size = Vector2(16, 16)    # world units per cell
astar.update()                       # builds the grid

var cells: Array[Vector2i] = astar.get_id_path(Vector2i(0, 0), Vector2i(10, 8))
var points: PackedVector2Array = astar.get_point_path(Vector2i(0, 0), Vector2i(10, 8))

Apply solids and weights after update()

update() is the line everyone forgets. AStarGrid2D doesn't rebuild itself after shape changes until you ask it to. Set the region, set the cell size, call update(), then query. Forget it and you'll usually get an empty array with no useful gameplay explanation.

But the more expensive mistake is the opposite one: calling update() after you mark obstacles. update() rebuilds the grid and clears point data, including solidity and weight scales. The safe setup pattern is:

astar.region = Rect2i(0, 0, 64, 64)
astar.cell_size = Vector2(16, 16)
astar.update()

# Apply point data after update().
for cell in wall_cells:
    astar.set_point_solid(cell, true)

for cell in mud_cells:
    astar.set_point_weight_scale(cell, 4.0)

The rule worth memorizing

Grid shape changes need update(); point data is applied after update(). region, cell_size, offset, and cell_shape are shape or placement changes. Solids and weights are point data. If you change the shape later, replay the solids and weights from your own source of truth.

AStarGrid2D API cheat sheet

Use this as the quick reference before the longer explanations below.

APIWhat it doesNeeds update()?Gotcha
regionDefines the rectangular cell areaYesPreferred over deprecated size
cell_sizeMaps cell coordinates to world positionsYesAffects get_point_path(), not which cells are chosen
offsetShifts returned world positionsYesUseful for visual alignment
cell_shapeSquare, isometric-right, or isometric-down point placementYesPresent in 4.3+ docs
set_point_solid()Blocks or unblocks one cellNoCall after update(); guard bounds
fill_solid_region()Blocks or unblocks a rectangle of cellsNoPresent in 4.2+ docs
set_point_weight_scale()Sets terrain traversal cost multiplierNo0.0 is allowed; negative values are rejected
fill_weight_scale_region()Sets weight over a rectangleNoPresent in 4.2+ docs
diagonal_modeControls diagonal neighbors and corner cuttingNoSet before querying; pair with the right heuristic
jumping_enabledEnables Jump Point SearchNoIgnores weight scaling when enabled
get_id_path()Returns cell coordinates (Array[Vector2i])NoBest for tile logic
get_point_path()Returns world positions (PackedVector2Array)NoDocs warn this method is not thread-safe
allow_partial_pathReturns closest reachable path when possibleNoSolid targets can make the search slow
is_dirty()Reports shape changes needing update()NoTreat a dirty grid as setup failure
is_in_boundsv()Checks if a cell is inside regionNoUse before gameplay-driven point writes
get_point_data_in_region()Reads point data in a rectangleNoUseful for debugging overlays
clear()Clears grid stateN/AResets the grid shape and data

What AStarGrid2D actually is

AStarGrid2D is the grid-shaped specialization of Godot's A* search. Plain AStar2D makes you add every point and every connection by hand. That's useful when your graph is irregular: waypoints, a road network, a hand-built node mesh. AStarGrid2D skips all of that. You hand it a rectangular region and a cell size, and the grid is the graph: every cell is a point, neighbors are connected for you, and you spend your time marking which cells are solid and which are expensive.

That's the trade. You give up arbitrary graph shapes and get a dense uniform grid with almost no setup. For tile-based games - strategy, roguelikes, tactics, tower defense, most top-down movement - that's usually the shape you want.

What it isn't: it isn't NavigationServer2D. The navigation server works with baked navigation mesh (navmesh) regions and polygons, agent avoidance, and continuous space. AStarGrid2D works in whole cells. Different tool, different problem. See the Godot pathfinding glossary for the short version of the terms.

Region, cell size, offset, and cell shape

Four properties place the grid in your world:

  • region: Rect2i - the rectangle of cells, in cell coordinates. Rect2i(0, 0, 64, 64) is a 64x64 grid starting at origin. This is the current property to use.
  • size: Vector2i - old shape property from early Godot 4 docs. It still exists, but it's deprecated. Prefer region, especially when your grid doesn't begin at (0, 0).
  • cell_size: Vector2 - how big one cell is in world units. This affects the positions returned by get_point_path().
  • offset: Vector2 - a world-space shift added to returned point positions. Use it when your visual grid and logical grid need a consistent offset.
  • cell_shape - square, isometric-right, or isometric-down point placement. It affects how positions are placed in the grid; when changed, call update() before querying again.

Inspect alignment instead of guessing

get_point_position(id) gives you the world position of a single cell if you need to inspect alignment by hand. If your path starts one tile off, don't guess. Print the cell, print the region, and print the point position.

Solid cells and disabled cells: your obstacles

This is how walls happen:

astar.set_point_solid(Vector2i(5, 5), true)  # one obstacle cell
var blocked := astar.is_point_solid(Vector2i(5, 5))

astar.fill_solid_region(Rect2i(8, 0, 1, 30), true)  # a 1x30 wall

Solidity doesn't need another update()

A solid cell is disabled for pathfinding. Nothing routes through it.

Three things matter here. First, solidity changes don't need update(). They apply on the next query. Second, update() clears solidity, so apply solids after the grid has been updated. Third, single-cell reads and writes outside the region fail, so guard gameplay-driven IDs with is_in_boundsv(id).

fill_solid_region() is the rectangle version. Use it when you're stamping a wall, clearing a room, or applying tilemap collision data in blocks. It's present in the Godot 4.2 docs and current stable docs.

For the part where changing blockers too often starts costing frames, see dynamic blockers without a full rebuild.

Weight scale: making terrain cost something

Solid is binary. A cell is in or out. Weight is the dial in between.

set_point_weight_scale() multiplies the cost of traveling from a neighboring point into this cell:

astar.set_point_weight_scale(Vector2i(5, 6), 4.0)  # swamp: 4x as costly to enter

The weight floor is 0.0, not 1.0

Default is 1.0. Higher values are more expensive. Values between 0.0 and 1.0 make a cell cheaper than normal. The source guard rejects values below 0.0, so 0.0 itself is allowed. Treat that as a free-to-enter cell: occasionally useful, usually a sign that the cost model needs a second look. In most game code, keep it boring: 1.0 for normal terrain, > 1.0 for mud, water, danger, or rough ground.

Like solidity, weight changes apply after update() and don't need another update call.

One caveat: weights and heuristics talk to each other. For A* to remain optimal, the estimate must not overestimate the real remaining cost. If your weighted routes look strange, check the heuristic pairing before blaming the weight values.

Diagonal modes: corner cutting and how to stop it

diagonal_mode decides whether units can move diagonally and whether they can slip diagonally past obstacles. It has four values:

  • DIAGONAL_MODE_ALWAYS - diagonals are allowed even when adjacent solid cells would make the move look like corner cutting.
  • DIAGONAL_MODE_NEVER - orthogonal movement only. Four directions.
  • DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE - a diagonal is allowed if at least one of the two side-adjacent cells is walkable.
  • DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES - a diagonal is allowed only when both side-adjacent cells are walkable. This is the usual choice when walls should feel solid.

Diagonal mode doesn't dirty the grid

Changing diagonal_mode doesn't require update() in current source; set it before querying and pair it with a matching heuristic. If you want no diagonals, use DIAGONAL_MODE_NEVER and usually HEURISTIC_MANHATTAN.

Heuristics: compute vs estimate

AStarGrid2D exposes two heuristic settings:

  • default_compute_heuristic - used for the cost between connected cells if _compute_cost() is not overridden.
  • default_estimate_heuristic - used for the estimate from a point to the goal if _estimate_cost() is not overridden.

Heuristic pairings to start with

Both heuristic settings default to HEURISTIC_EUCLIDEAN. The options are EUCLIDEAN, MANHATTAN, OCTILE, and CHEBYSHEV.

MovementEstimate heuristic to start with
4-direction gridHEURISTIC_MANHATTAN
8-direction grid with diagonal costHEURISTIC_OCTILE
8-direction grid where diagonal and straight are treated similarlyHEURISTIC_CHEBYSHEV
Unsure / conservative defaultHEURISTIC_EUCLIDEAN

Don't overestimate the real remaining cost

On a square grid with diagonal movement, Manhattan overestimates because it counts diagonal progress as if it needed two orthogonal moves. That can make the search faster-looking and less correct. If your path is almost right but not quite, heuristic plus diagonal mode is the first pair to inspect.

Jumping (JPS): fast path, sharp caveat

jumping_enabled = true switches on Jump Point Search. On a large, open, uniform-cost grid it can reduce how many intermediate points the search expands. It can also change the shape of returned paths because the algorithm jumps over long runs of open cells.

The current docs include the important warning: when jumping is enabled, weight scaling isn't considered in pathfinding. So this isn't a free speed button for weighted terrain.

Use it for open, uniform-cost grids. Turn it off when terrain weights are part of the design.

get_id_path vs get_point_path, and allow_partial_path

Two methods ask for a route:

  • get_id_path(from, to) returns Array[Vector2i] / Vector2i[] of cell coordinates. Use this for tile logic, movement ranges, turn-based steps, and debugging.
  • get_point_path(from, to) returns a PackedVector2Array of world positions. Use this to drive a sprite, draw a line, or feed a simple follow loop.

Partial paths aren't a validation substitute

In current Godot 4.x docs, both path methods can take allow_partial_path. With it true, an unreachable goal can return a path to the closest reachable cell instead of an empty array.

Two catches: if the start cell is solid, current docs say the result is empty even when from_id == to_id; and if allow_partial_path is true while the target cell is solid, the search may take unusually long.

So don't use partial path as a substitute for validation. Validate bounds and solidity first. Then query.

func safe_id_path(astar: AStarGrid2D, start: Vector2i, goal: Vector2i) -> Array[Vector2i]:
    if astar.is_dirty():
        push_warning("AStarGrid2D parameters changed; call update() and replay point data.")
        return []

    if not astar.is_in_boundsv(start) or not astar.is_in_boundsv(goal):
        return []

    if astar.is_point_solid(start):
        return []

    if astar.is_point_solid(goal):
        # Pick your own policy here: reject, choose a neighbor, or allow a partial path knowingly.
        return []

    return astar.get_id_path(start, goal)

TileMapLayer alignment: the quiet source of wrong paths

AStarGrid2D uses grid IDs. Gameplay code often starts from world positions. Tile maps add another space in between.

For TileMapLayer, the safe conversion is usually:

# World position -> map cell.
var cell: Vector2i = tile_layer.local_to_map(tile_layer.to_local(global_position))

# Map cell -> world position.
var local_center: Vector2 = tile_layer.map_to_local(cell)
var world_center: Vector2 = tile_layer.to_global(local_center)

Print converted cells before inspecting the solver

If your TileMapLayer is at (0, 0), skipping to_local() may appear to work. Then someone moves the layer under a parent and every path starts lying. Print the converted cells before you inspect the solver.

AStarGrid2D vs AStar2D vs NavigationServer2D

Pick the tool by world shape, not by habit.

You have...UseWhy
Tile grid, whole-cell movementAStarGrid2DThe grid is the graph. Solid and weighted cells are built in.
Irregular graph: waypoints, roads, hand-placed nodesAStar2DYou define points and connections yourself.
Continuous movement, baked geometry, avoidanceNavigationServer2D / navigation mesh (navmesh)Polygon regions and server-backed navigation, not cell rules.

Grid or navigation mesh?

If you're weighing grid against a navigation mesh (navmesh) specifically, read grid or navmesh in Godot. Short version: grids win when movement is tile-shaped and you want cell-level control; navmesh wins when movement is continuous and the geometry isn't naturally a tile board.

Where AStarGrid2D stops being enough

Everything above is the tool working as designed. The next question is usually: how many agents can it move? The honest answer is that the solver is rarely the whole problem. The query pattern is.

If 500 agents ask for full paths in the same frame, the frame goes bad. In our measured Godot benchmark, the naive per-agent query pattern produced a 670 ms median frame and a 5.7-second worst sampled frame. A scheduled shared-field approach moved far more agents while keeping sampled frames under budget. That's not an argument that AStarGrid2D is bad. It's an argument that repeated per-agent path solves are the wrong shape for crowds with shared goals.

See moving 10,000 agents in Godot for the measured version.

The other edges - multi-size agents, dynamic blockers at scale, diagnostics for why a route failed, and repeated query scheduling - are where a grid project turns into a navigation system. That boundary is covered in where AStarGrid2D stops being enough.

Use the built-in while it keeps the project legible. When the scaffolding around it outgrows it, admit what you're building.

Frequently asked questions

What is AStarGrid2D in Godot?

AStarGrid2D is Godot's grid-based A* pathfinder. You give it a rectangular region and a cell size, call update(), then mark cells as solid or weighted. It's built for tile-based 2D movement.

AStarGrid2D vs AStar2D - what is the difference?

AStar2D is a general graph where you add points and connections yourself. AStarGrid2D assumes a rectangular grid and wires the neighboring cells for you. Use AStar2D for irregular waypoint graphs; use AStarGrid2D for tile maps.

AStarGrid2D vs NavigationServer2D - which should I use?

Use AStarGrid2D for whole-cell, tile-based movement where you want per-cell control. Use NavigationServer2D for continuous movement over baked navigation mesh regions, especially when your world isn't naturally a grid.

Does AStarGrid2D.update() clear solid cells and weight scales?

Yes. update() rebuilds the grid and clears point data, including solidity and weight scale. Call update() after shape changes, then apply solids and weights again from your own data.

Why does my AStarGrid2D path come back empty?

The usual causes are a dirty grid that needs update(), start or goal outside region, a solid start cell, a solid or unreachable goal, wrong world-to-cell conversion, or disconnected walkable areas.

What does get_id_path return?

get_id_path() returns cell coordinates: Array[Vector2i] / Vector2i[]. Use it when gameplay logic cares about tiles.

What does get_point_path return?

get_point_path() returns world positions as a PackedVector2Array, with cell_size, offset, and cell_shape applied. Use it for drawing or simple movement following.

How do I make some terrain cost more?

Use set_point_weight_scale(cell, value). 1.0 is normal, values above 1.0 are more expensive, 0.0 is allowed as a free-to-enter cell, and negative values are rejected. Weight changes don't need update(), but they're cleared by a later update().

Does jumping_enabled ignore weight_scale?

Yes. Current docs state that enabling jumping disables consideration of weight scaling. Use jumping for open uniform-cost grids; turn it off for weighted terrain.

Does AStarGrid2D support isometric or hex grids?

Current Godot has cell_shape for square, CELL_SHAPE_ISOMETRIC_RIGHT, and CELL_SHAPE_ISOMETRIC_DOWN placement. Hex grids aren't a native AStarGrid2D mode; you need your own coordinate mapping/neighbor logic or a different graph approach.

How many agents can AStarGrid2D handle?

The class isn't usually the limit by itself. Repeated per-agent path queries in the same frame are the common frame killer. See the measured crowd benchmark in moving 10,000 agents in Godot.