DEV Community

Cover image for TileMapLayer Custom Data for Movement Costs in Godot
Vav Labs
Vav Labs

Posted on Originally published at vav-labs.com

TileMapLayer Custom Data for Movement Costs in Godot

Your units keep preferring a strip of tiles you never gave a movement cost.
There's no error anywhere. The path is legal. The route is wrong.

Here's the chain that produces it in Godot 4.7.2. A typed float custom-data
layer defaults to 0.0. TileData.has_custom_data() checks whether the named
layer exists in the TileSet — it doesn't know whether you actually painted a
value on this tile. And AStarGrid2D.set_point_weight_scale() rejects negative
values but accepts 0.0 without complaint. So one unpainted tile imports as
free to enter, and every step of that is silent.

To be fair, this is documented behavior. has_custom_data() is honest about
what it checks. People still hit it constantly, because the name reads like a
per-tile check. This guard is the usual suspect:

if tile_data.has_custom_data("movement_cost"):
    var cost := float(tile_data.get_custom_data("movement_cost"))
Enter fullscreen mode Exit fullscreen mode

Once the layer exists, that branch succeeds for every tile that has one —
including tiles still holding the typed default 0.0.

The fix is an importer, not a call

What worked for me is treating the TileSet like untrusted input:

  1. Resolve the custom-data layer once, by name, and verify its Variant type.
  2. Read TileData for each used cell. get_cell_tile_data() returns null for scene-collection tiles, so decide what happens to those instead of silently costing them like grass.
  3. Reject non-finite values and anything below your declared minimum. I use 1.0 — it makes an untouched default fail loudly instead of quietly becoming a free tile.
  4. Validate the complete batch first, then write. Half an imported grid is worse than a clean failure.
  5. Apply weights only after AStarGrid2D.update(), because update() clears all point data — solidity and weight scale both. Replay from your normalized data after any later structural update.

If different units read terrain differently (infantry vs vehicles), don't
author a float at all. Store a terrain_id string on the tile and resolve it
through per-profile cost tables at runtime. One authored float can't represent
two movement policies.

Two more one-liners worth stealing:

  • jumping_enabled disables weight scaling entirely. It's in the class reference, and the 4.7.2 source carries a TODO for weighted jumping. If costs must affect route choice, keep jumping off.
  • Weight is charged on entry: the engine multiplies the step cost by the weight scale of the point being entered, so a path's terrain total skips the starting cell.

What's verified

The write-up ships a standalone Godot 4.7.2 project (MIT) with a serialized
TileSet fixture and a 14-check receipt, zero failures, including a clean rerun
after extracting the package. My favorite check opens the .tres as text and
proves the untouched tile has no explicit custom_data_0 property before
loading it and observing the runtime 0.0 — so the fixture can't cheat with
an editor-serialized explicit zero.

It's a correctness proof, not a benchmark. No timing, no throughput, no claims
about large-map import time.

The importer isn't clever, and that's the point. Its job is to make invalid
authoring visible before it becomes a plausible but wrong route.

Full version with the importer source, the fixture, the failure-mode table and
the raw receipt JSON:

TileMapLayer Custom Data for Movement Costs in Godot

Top comments (0)