Vav Labs
Back to blog

Godot pathfinding / 2026-06-29 / 8 min read

Verified as of Godot 4.7 stable docs checked on 2026-06-29

TileMapLayer Custom Data for Walkability in Godot

Use a Godot TileSet custom data layer as the AStarGrid2D walkability source: read TileData, handle empty cells, and avoid navigation-layer confusion.

Godot TileMapLayer painted map converted into AStarGrid2D solid cells from custom data walkability.

The grid needs a rule

Your AStarGrid2D does not know that a wall tile is blocked.

It does not know that water is blocked for a ground unit, that a bridge is walkable even if it has collision, or that empty cells should be treated as solid void in this one level. AStarGrid2D only knows about grid points. If a point should be blocked, you have to write that point with set_point_solid().

The clean source for that decision is usually a TileSet custom data layer.

The short answer

Use a TileSet custom data layer named walkable, with type bool. Paint that value on the tiles in your TileSet. Then read the tile data from your TileMapLayer and convert it into AStarGrid2D solid points.

func is_cell_solid(tile_layer: TileMapLayer, cell: Vector2i) -> bool:
    var data := tile_layer.get_cell_tile_data(cell)
    if data == null:
        return true # empty-cell policy: solid void

    return not bool(data.get_custom_data("walkable"))

Custom data is not a navigation layer

Godot gives tiles more than one kind of metadata, and the names are easy to mix up.

TileSet navigation layers describe navigation polygons for Godot's navigation system. They are useful when your pathfinding is based on NavigationServer2D, NavigationRegion2D, and navigation maps.

TileSet custom data layers are different. They are typed game metadata that you define. A walkable: bool layer can mean "ground units may stand here" because that is your game rule.

For AStarGrid2D, the custom data layer is the useful one. AStarGrid2D does not inspect your tile map's navigation polygons and it does not automatically convert TileSet navigation layers into solid cells. You read the rule yourself and write the result into the grid.

Tile featureFeedsUse it for
Navigation layerNavigationServer2D / navigation polygonsNavmesh-style 2D navigation
Custom data layerYour codeWalkability, terrain type, movement tags, faction rules
Physics collisionPhysics queries and collisionBodies, projectiles, placement checks, hit shapes

Set up a walkable custom data layer

In the TileSet editor, add a custom data layer and make the movement rule explicit.

  1. Open the TileSet used by your TileMapLayer.
  2. Add a custom data layer.
  3. Name it walkable.
  4. Set its type to bool.
  5. Select each movement-relevant tile and set its walkable value deliberately.
Tile<code>walkable</code>
Floortrue
Wallfalse
Waterfalse for ground units
Bridgetrue

Read TileData from TileMapLayer

The normal read path is small: get the cell's TileData, handle the empty-cell case, then read the custom data value.

If the cell is empty, or if the placed cell does not resolve to atlas tile data, data can be null. Decide what that means for your game. In many authored dungeon or tactics maps, empty means solid void.

func is_cell_solid(tile_layer: TileMapLayer, cell: Vector2i) -> bool:
    var data := tile_layer.get_cell_tile_data(cell)
    if data == null:
        return true

    var walkable := bool(data.get_custom_data("walkable"))
    return not walkable

The has_custom_data trap

TileData.has_custom_data("walkable") checks whether a custom data layer with that name exists. It is not a per-cell "did I paint this value?" check.

For pathfinding code, handle data == null first, then read the custom data value. If your game needs a stricter audit, validate the TileSet setup separately instead of hiding that check inside every cell read.

Where the rule plugs in

Keep the full grid construction in TileMap to Navigation Grid in Godot. This article owns the walkability rule, so the important handoff is the line that converts custom data into point solidity.

In the grid-building step, configure grid.region, grid.cell_size, grid.offset, and call the one shape update(). Then apply the walkability rule.

# grid.region, grid.cell_size, grid.offset, and grid.update()
# are configured in the grid-building step.
for y in range(used_rect.position.y, used_rect.end.y):
    for x in range(used_rect.position.x, used_rect.end.x):
        var cell := Vector2i(x, y)
        grid.set_point_solid(cell, is_cell_solid(tile_layer, cell))

Call update() for shape, not every solid cell

Call grid.update() after the grid shape is configured. Then write point solidity.

Do not call update() after every set_point_solid() write. update() is for shape changes such as region, cell_size, offset, and cell shape. Solid points are point data.

Why not use physics collision?

Physics-derived walkability can work in a prototype where "has collision" and "cannot walk here" are the same rule.

That stops being true quickly. A tile can have collision because projectiles should hit it. Another tile can have collision because it is cover. A bridge can have collision for placement, selection, or decoration while still being walkable. Water can have no collision at all while still being blocked for ground units.

Physics is a useful system, but it is not the same thing as movement design.

return not bool(data.get_custom_data("walkable"))

Why not use atlas coordinates?

Atlas IDs are tempting because they are visible in code.

if tile_atlas_coords == Vector2i(3, 1):
    return true

Atlas IDs are brittle

Hard-coded atlas coordinates are fine for a tiny throwaway prototype. They are brittle for production content.

Art changes. Tiles get reorganized. Several visual tiles may share the same movement rule. One tile may get an alternate version later. If your pathfinding rule is tied to atlas coordinates, every art cleanup becomes a gameplay risk.

Custom data keeps the rule attached to the tile definition, not to a hard-coded coordinate in your script.

Beyond one boolean

Start with walkable: bool. It is the clearest rule and it maps directly to AStarGrid2D.set_point_solid().

Once the project grows, custom data can carry richer movement rules.

Custom data fieldTypeMeaning
walkableboolCan a normal ground unit stand here?
swimmableboolCan swimming units use this tile?
flyableboolCan flying units pass through this tile?
movement_maskintBitmask for multiple movement profiles
requires_factionStringGate a tile by team or owner

Walkability is not movement cost

Keep this boundary clean. This page is about "can I enter this cell?" Movement cost is a separate question. A mud tile may be walkable but expensive.

Use a separate custom data field for cost, such as movement_cost or the value you eventually map into weight_scale. Do not make one field answer both questions.

Cache the layer id in hot loops

Name-based custom data reads are clear and are the right first version.

data.get_custom_data("walkable")

Use a cached id when the loop is hot

If you are rebuilding a large grid often, cache the custom data layer id once and use get_custom_data_by_layer_id() in the cell-read helper.

func get_walkable_layer_id(tile_layer: TileMapLayer) -> int:
    var layer_id := tile_layer.tile_set.get_custom_data_layer_by_name("walkable")
    assert(layer_id >= 0)
    return layer_id

func is_cell_solid_fast(
        tile_layer: TileMapLayer,
        cell: Vector2i,
        walkable_layer_id: int) -> bool:
    var data := tile_layer.get_cell_tile_data(cell)
    return data == null or not bool(data.get_custom_data_by_layer_id(walkable_layer_id))

Runtime walkability, light version

Custom data is best treated as authored tile definition data. Every placed instance of the same tile shares the same custom data value unless you use an alternative tile or a runtime override.

For runtime changes, keep the model simple.

  1. If the terrain really changed, swap the tile or alternative tile with set_cell(), then rewrite the affected grid point.
  2. If a temporary blocker changed, keep that blocker state outside the TileSet and update AStarGrid2D point data directly.
  3. If you need advanced per-cell tile data overrides, Godot exposes _tile_data_runtime_update(), but that is a heavier TileMapLayer workflow and should stay out of this article.
func refresh_cell_walkability(
        tile_layer: TileMapLayer,
        grid: AStarGrid2D,
        cell: Vector2i) -> void:
    if not grid.is_in_boundsv(cell):
        return

    grid.set_point_solid(cell, is_cell_solid(tile_layer, cell))

What the reused proof demo shows

This article does not need a new demo. The TileMap to Navigation Grid proof already shows the important case: custom-data walkability and physics-layer walkability can disagree.

Open the TileMap to Navigation Grid demo and toggle the source. A new custom-data visualizer would only slow the cadence without adding a new technical claim.

Measurement-gap: this article does not publish a new benchmark dataset. The reused artifact is a correctness proof for the walkability source, not a timing claim.

  1. The map is painted with TileMapLayer.
  2. The grid is derived from tile data.
  3. The demo can switch between custom-data walkability and physics-layer walkability.
  4. Water and bridge tiles show why those two rules are not the same.
  5. The verification JSON records the expected solid cells and disagreement cases.
Animated Godot demo showing TileMapLayer custom-data walkability and physics-layer walkability producing different AStarGrid2D paths.
The reused TileMap-to-navigation-grid proof shows why custom-data walkability and physics-layer walkability are different rules.

Common failure modes

These are the boring checks that catch most broken TileMapLayer walkability setups.

The path ignores walls

You probably built the grid shape but did not write point solidity after grid.update().

Fix: call set_point_solid(cell, true) for blocked cells after the shape setup.

Empty cells behave strangely

get_cell_tile_data(cell) can return null. If you do not choose a policy, empty cells become accidental behavior.

Fix: decide whether empty cells are solid or walkable and make that branch explicit.

Navigation layers do nothing

TileSet navigation layers are not automatically consumed by AStarGrid2D.

Fix: use a custom data layer for the grid rule, or use the NavigationServer2D workflow if your game is navmesh-shaped.

Every floor tile is blocked

The walkable custom data value may be missing, defaulting in the wrong direction, or assigned only on some tile variants.

Fix: audit the TileSet and set the walkable value deliberately on every movement-relevant tile and alternative tile.

A runtime blocker changed but paths did not

The TileSet custom data is only the authored base rule. A new tower, door, or temporary obstacle must also update the grid point state.

Fix: rewrite affected cells with set_point_solid() or use a dedicated runtime blocker system.

Frequently asked questions

Should I use a Navigation Layer or a Custom Data Layer for walkability?

Use a custom data layer for AStarGrid2D or custom grid pathfinding. Navigation layers are for Godot's navigation server workflow, not for automatically feeding solid cells into AStarGrid2D.

How do I read a tile's custom data in Godot 4?

Call tile_layer.get_cell_tile_data(cell) to get TileData, handle the null case, then call data.get_custom_data("walkable").

Why is my custom data null or missing?

The cell may be empty, the layer name may be wrong, or the tile may not be the tile variant you edited. Handle data == null separately from custom data lookup.

Is custom data per placed cell?

Treat custom data as TileSet tile-definition data. Placed instances of the same tile share the value unless you use an alternative tile or a runtime override.

Is get_custom_data("walkable") slow?

It is fine for clear code and normal builds. For hot loops, cache the layer id with TileSet.get_custom_data_layer_by_name("walkable") and use get_custom_data_by_layer_id(id).

Should walkability and movement cost be the same field?

No. Walkability answers whether a cell can be entered. Cost answers how expensive it is. Keep walkable: bool separate from fields such as movement_cost or weight_scale.