Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. GDScript
  3. GDScript TileMapLayer Practice
GDScript11 exercises

GDScript TileMapLayer Practice

Learn TileMapLayer in Godot 4: set_cell, erase_cell, get_cell_source_id, map_to_local, local_to_map, multi-layer patterns, and fog-of-war. Build tile-based maps in GDScript.

Common ErrorsQuick ReferencePractice
On this page
  1. 1TileMapLayer vs TileMap (Godot 4.3+ migration)
  2. 2Painting and erasing tiles
  3. 3Reading tile data
  4. Reading custom tile metadata
  5. 4Grid-to-pixel coordinate conversion
  6. Finding map bounds
  7. 5Multi-layer patterns
  8. Fog-of-war pattern
TileMapLayer vs TileMap (Godot 4.3+ migration)Painting and erasing tilesReading tile dataGrid-to-pixel coordinate conversionMulti-layer patterns

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 TileMap to TileMapLayer?

Practice the patterns that trip people up, so they don't trip you up mid-jam.

Related GDScript Topics
GDScript Procedural GenerationGDScript MovementGDScript Foundations

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.


Ready to practice?

Start practicing GDScript TileMapLayer with spaced repetition

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

Prompt

Check Your Understanding: What is the difference between map_to_local() and local_to_map(), and when would you use each?

What a strong answer looks like

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

Paint and erase tiles with set_cell() and erase_cell() on TileMapLayerRead tile data with get_cell_source_id() and get_cell_atlas_coords()Read TileSet custom data with get_cell_tile_data() for gameplay metadataConvert between grid and pixel coordinates with map_to_local() and local_to_map()Snap sprites to grid positions using map_to_local()Implement fog-of-war with a dedicated TileMapLayer and erase_cell()Use get_used_rect() and terrain helpers for bounds and cleaner autotilingMigrate from deprecated TileMap to TileMapLayer nodes in Godot 4.3+

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

Paint a tile at grid position
ground_layer.set_cell(Vector2i(3, 5), 0, Vector2i(1, 0))
Erase a tile
ground_layer.erase_cell(Vector2i(3, 5))
Check if cell is empty
var is_empty := ground_layer.get_cell_source_id(pos) == -1
Read custom tile data
var data := ground_layer.get_cell_tile_data(pos)
if data:
	var power = data.get_custom_data("power")
Snap sprite to grid
player.position = ground_layer.map_to_local(grid_pos)
Mouse click to tile
var tile := ground_layer.local_to_map(get_local_mouse_position())
Terrain-aware path painting
ground_layer.set_cells_terrain_path(path_cells, 0, ROAD_TERRAIN)
Fill-then-carve pattern
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)
Get bounds of painted tiles
var used_rect := ground_layer.get_used_rect()

GDScript TileMapLayer Sample Exercises

Example 1Difficulty: 2/5

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))
Example 2Difficulty: 2/5

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))
Example 3Difficulty: 2/5

Fill in the method that paints a tile onto a TileMapLayer.

set_cell

+ 8 more exercises

Practice in Build a Game

Roguelike: Part 1: Into the CaveBuild a Game

Further Reading

  • GDScript Dictionary map() and map_in_place12 min read
  • Facade Pattern in Godot 4 GDScript: Taming "End Turn" Spaghetti12 min read

Start practicing GDScript TileMapLayer

Free daily exercises with spaced repetition. No credit card required.

← Back to GDScript Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.