Every tile-based game in Godot runs on TileMapLayer. Roguelikes, platformers, strategy games, puzzle games. If it has a grid, it has a TileMapLayer.
The API is small (set_cell, erase_cell, map_to_local, local_to_map) but the gotchas are real. Source ID -1 means empty (not 0). map_to_local returns the tile center (not the corner). And if you're coming from Godot 3, the old TileMap node is deprecated.
Quick answers:
- How do I use
set_cell()? - What does
map_to_local()return? - How do I read tile metadata?
- How do I migrate from
TileMaptoTileMapLayer?
Practice the patterns that trip people up, so they don't trip you up mid-jam.
Godot 4.3 deprecated the multi-layer TileMap node in favor of individual TileMapLayer nodes. Each layer is now its own node in the scene tree.
# OLD (Godot 4.2 and earlier) - deprecated
$TileMap.set_cell(0, Vector2i(3, 5), 0, Vector2i(0, 0)) # layer 0
# NEW (Godot 4.3+) - each layer is a separate node
$GroundLayer.set_cell(Vector2i(3, 5), 0, Vector2i(0, 0))
$WallLayer.set_cell(Vector2i(3, 5), 0, Vector2i(1, 0))
Migration: The Godot editor has a built-in migration tool that converts old TileMap nodes to TileMapLayer nodes automatically. Each layer becomes a child node you reference with @onready.
Runtime gotcha: TileMapLayer batches updates until the end of the frame. If you paint cells and immediately need fresh collision, navigation, or scene tile state, call update_internals() once after the batch.
set_cell() takes three required arguments: grid coordinates, source ID, and atlas coordinates. The source ID identifies which TileSetSource to use (usually 0 for a single tileset). Atlas coordinates pick the specific tile from the atlas texture.
@onready var ground_layer: TileMapLayer = $GroundLayer
const FLOOR_ATLAS := Vector2i(1, 0)
const WALL_ATLAS := Vector2i(0, 0)
# Paint a floor tile at grid position (3, 5)
ground_layer.set_cell(Vector2i(3, 5), 0, FLOOR_ATLAS)
# Erase a tile (removes it completely)
ground_layer.erase_cell(Vector2i(3, 5))
Fill-then-carve pattern for procedural generation: fill the entire grid with walls, then selectively replace wall tiles with floor tiles where the algorithm carves passages.
func fill_with_walls() -> void:
for x in range(width):
for y in range(height):
ground_layer.set_cell(Vector2i(x, y), 0, WALL_ATLAS)
func carve_floor(pos: Vector2i) -> void:
ground_layer.set_cell(pos, 0, FLOOR_ATLAS)
get_cell_source_id() returns -1 for empty cells, not 0. This is the most common source of bugs when checking tile types.
func is_wall(pos: Vector2i) -> bool:
var source_id := ground_layer.get_cell_source_id(pos)
if source_id == -1:
return true # Empty = treat as wall
var atlas := ground_layer.get_cell_atlas_coords(pos)
return atlas == WALL_ATLAS
func is_floor(pos: Vector2i) -> bool:
var source_id := ground_layer.get_cell_source_id(pos)
if source_id == -1:
return false # Empty = not walkable
return ground_layer.get_cell_atlas_coords(pos) == FLOOR_ATLAS
Roguelike collision without physics: Instead of using CollisionShape2D for every wall tile, check is_wall() at the target position before allowing movement. Faster and simpler for grid-based games.
Reading custom tile metadata
If your TileSet stores custom data layers like movement cost, hazard damage, or biome type, use get_cell_tile_data():
func get_tile_power(pos: Vector2i) -> int:
var data := ground_layer.get_cell_tile_data(pos)
if data == null:
return 0
return data.get_custom_data("power")
This scales better than comparing atlas coordinates everywhere once tiles start carrying gameplay meaning.
map_to_local() converts grid coordinates to pixel coordinates. It returns the center of the tile, not the top-left corner.
# Grid to pixel (center of tile)
var pixel_pos: Vector2 = ground_layer.map_to_local(Vector2i(3, 5))
# Pixel to grid (which tile is the mouse over?)
var grid_pos: Vector2i = ground_layer.local_to_map(get_local_mouse_position())
Sprite snapping: To snap a character to a grid tile, set its position to map_to_local(grid_pos). The sprite centers on the tile automatically.
var grid_pos := Vector2i(5, 3)
player.position = ground_layer.map_to_local(grid_pos)
Mouse click to tile: Convert the mouse position to local coordinates first, then use local_to_map(). Both functions work in the TileMapLayer's local coordinate space. If the layer has been moved or scaled, use to_local() to convert global positions first.
Finding map bounds
When you need dungeon bounds, camera limits, or a quick "how much of this layer is painted?" check, get_used_rect() gives you the rectangle containing all non-empty cells:
var used_rect := ground_layer.get_used_rect()
Use separate TileMapLayer nodes for different purposes: ground, walls, decorations, and fog-of-war. Each layer references the same TileSet but is painted independently.
@onready var ground_layer: TileMapLayer = $GroundLayer
@onready var decor_layer: TileMapLayer = $DecorLayer
@onready var fog_layer: TileMapLayer = $FogLayer
Fog-of-war pattern
Fill the fog layer completely, then erase tiles around the player to reveal the map:
func reveal_around(center: Vector2i, radius: int) -> void:
for dx in range(-radius, radius + 1):
for dy in range(-radius, radius + 1):
if dx * dx + dy * dy <= radius * radius:
fog_layer.erase_cell(center + Vector2i(dx, dy))
The fog layer renders on top of everything. Erasing tiles punches holes that reveal the layers below.
When to Use GDScript TileMapLayer
- Building any tile-based game: roguelikes, platformers, strategy games, or puzzle games
- Procedural generation that paints floors and walls onto a grid at runtime
- Grid-based movement where characters snap to tile positions and collision checks use tile data instead of physics bodies
Check Your Understanding: GDScript TileMapLayer
Check Your Understanding: What is the difference between map_to_local() and local_to_map(), and when would you use each?
map_to_local(Vector2i) converts grid coordinates to pixel coordinates (returns the center of the tile as a Vector2). Use it to position sprites on the grid. local_to_map(Vector2) converts pixel coordinates to grid coordinates (returns a Vector2i). Use it to find which tile the mouse is hovering over or which tile a character is standing on. map_to_local returns the tile CENTER, not the top-left corner.
What You'll Practice: GDScript TileMapLayer
Common GDScript TileMapLayer Pitfalls
- Using the deprecated TileMap node instead of TileMapLayer in Godot 4.3+; it still works but is no longer maintained and will be removed in a future version
- Checking get_cell_source_id() == 0 for empty cells. Empty cells return -1, not 0; source ID 0 is a valid tileset source
- Calling map_to_local() before the node enters the scene tree. The tile size and transform are not available until _ready()
- Assuming map_to_local() returns the top-left corner. It returns the tile CENTER; subtract half the tile size if you need the corner
- Painting terrain transitions tile-by-tile with set_cell() when your TileSet uses terrains. Prefer set_cells_terrain_connect() or set_cells_terrain_path() so neighbors join cleanly
GDScript TileMapLayer FAQ
What is TileMapLayer in Godot 4?
TileMapLayer is the Godot 4.3+ replacement for the deprecated TileMap node. Each layer (ground, walls, decorations) is now a separate TileMapLayer node in the scene tree, so each layer is easier to manage on its own.
Why does get_cell_source_id return -1?
A return value of -1 means the cell is empty (no tile placed). Source ID 0 is a valid tileset source. Always check for -1, not 0, when testing if a cell is empty. This is the #1 source of tilemap bugs.
What are the parameters for set_cell?
set_cell(coords: Vector2i, source_id: int, atlas_coords: Vector2i). coords is the grid position, source_id identifies the TileSetSource (usually 0), and atlas_coords picks the specific tile from the atlas. An optional fourth parameter (alternative_tile) is rarely needed.
How do I use set_cell in Godot 4?
Call it on a TileMapLayer node, not on the deprecated multi-layer TileMap API: ground_layer.set_cell(Vector2i(x, y), source_id, atlas_coords). The coordinates are grid cells, not pixel positions. If you have a mouse click or sprite position, convert it with local_to_map() first.
Does map_to_local return the tile center or corner?
The CENTER of the tile, as a Vector2. Sprites placed at this position center on the tile automatically. If you need the top-left corner, subtract half the tile size.
How do I convert a mouse click to a tile position?
Use local_to_map(get_local_mouse_position()) to convert the mouse position to grid coordinates. If the TileMapLayer is offset or scaled, convert the global mouse position to local coordinates first with to_local(get_global_mouse_position()).
Why is map_to_local or local_to_map giving me the wrong coordinates?
The usual cause is mixing coordinate spaces. TileMapLayer methods use the layer's local space, not global space. Convert global positions with to_local() first, and remember that map_to_local() returns the tile center, not its top-left corner.
How do I read custom data from a tile?
Use get_cell_tile_data(coords) to get the TileData for that cell, then call data.get_custom_data("name"). This is the clean way to attach movement cost, damage, biome, or loot metadata to tiles in the TileSet editor.
How do I migrate from TileMap to TileMapLayer?
Godot 4.3+ has a built-in migration tool in the editor. The main change: instead of $TileMap.set_cell(layer, coords, ...) you use $LayerNode.set_cell(coords, ...). The layer index parameter is gone because each layer is its own node.
How do I do collision detection with tiles?
For grid-based games (roguelikes, turn-based), skip physics entirely. Check get_cell_source_id() or get_cell_atlas_coords() at the target position before allowing movement. For real-time games, use TileSet collision shapes which integrate with Godot's physics system.
Why do my tile edits not seem fully updated until the next frame?
TileMapLayer batches updates for performance. If you need fresh collision, navigation, or scene tile state immediately after a big batch of edits, call update_internals() once after the batch.
GDScript TileMapLayer Syntax Quick Reference
ground_layer.set_cell(Vector2i(3, 5), 0, Vector2i(1, 0))ground_layer.erase_cell(Vector2i(3, 5))var is_empty := ground_layer.get_cell_source_id(pos) == -1var data := ground_layer.get_cell_tile_data(pos)
if data:
var power = data.get_custom_data("power")player.position = ground_layer.map_to_local(grid_pos)var tile := ground_layer.local_to_map(get_local_mouse_position())ground_layer.set_cells_terrain_path(path_cells, 0, ROAD_TERRAIN)for x in range(width):
for y in range(height):
ground_layer.set_cell(Vector2i(x, y), 0, WALL_ATLAS)
# Then carve floors where algorithm dictates
ground_layer.set_cell(floor_pos, 0, FLOOR_ATLAS)var used_rect := ground_layer.get_used_rect()GDScript TileMapLayer Sample Exercises
Place a floor tile at grid position (3, 5) on ground_layer using source ID 0 and atlas coords (0, 0).
ground_layer.set_cell(Vector2i(3, 5), 0, Vector2i(0, 0))Place a wall tile at grid position (0, 0) on ground_layer using source ID 0 and atlas coords (1, 0).
ground_layer.set_cell(Vector2i(0, 0), 0, Vector2i(1, 0))Fill in the method that paints a tile onto a TileMapLayer.
set_cell+ 8 more exercises