Skip to content

Articles ·

Snap to a TileMap in Godot: 2D grid placement

How to snap to a TileMap in Godot 4: place 2D furniture, props, and buildings on a TileMapLayer with anchored scenes, live cell previews, and commit results.

This recipe snaps 2D furniture, props, and buildings to a TileMapLayer: pick an entry, hover the map, watch a live preview lock to cells, and commit. That is the whole loop a 2D grid-based building system runs on. Grid Placement owns targeting, preview, validation, and commit; your game supplies the entry scenes and reads back where things landed.

The same three steps work on an isometric or hexagonal map: the plugin snaps to whatever cells your TileSet defines, so cell shape and size stay a TileSet configuration question (see Isometric 2D Placement). If you are placing in 3D, the 3D version of this recipe swaps the TileMapLayer for a GridMap and adds mounts and rotation.

1. Author the entry scene around the cell

The scene root is the placement origin. Grid Placement seats the root on the targeted cell and replaces the root's saved position, so keep the root at the scene origin and put deliberate offsets on children: Sprite2D children around the intended origin, collision children matching the gameplay footprint.

Then set ScenePlacementEntry.cell_anchor_mode per axis using the even/odd rule: an odd cell span on an axis seats at the cell center, an even span seats at the minimum cell edge. On a 32px grid a 32×32 sprite (1×1) takes CELL_CENTER; a 64×64 sprite (2×2) takes CELL_TOP_LEFT. After changing the mode, re-select the entry so the preview rebuilds. Anchor modes never infer rotation or facing — that is presentation your game owns, covered in the 2D Object Scene Setup guide.

2. Read the cursor cell for feedback

Convert the active 2D positioner's world position through the target TileMapLayer. This is cursor and UI feedback only — never proof that placement succeeded:

@export var session: PlacementSession

func get_cursor_cell() -> Vector2i:
    var targeting: GridTargetingState = session.get_states().targeting
    var map: TileMapLayer = targeting.target_map
    var positioner: Node2D = targeting.positioner
    if map == null or positioner == null:
        return Vector2i(-1, -1)
    return PlacementPositioning2DUtils.get_tile_from_global_position(positioner.global_position, map)

3. Consume the commit result, not the cursor

When placement commits, read the result. In 6.x the success flag and the placed node arrive on data.report — do not depend on removed legacy fields of PlacementActionData:

func _ready() -> void:
    session.get_building_state().action_performed.connect(_on_action_performed)

func _on_action_performed(data: PlacementActionData) -> void:
    if data == null or data.report == null or not data.report.is_successful():
        return
    var placed: Node = data.report.placed
    var map: TileMapLayer = session.get_states().targeting.target_map
    if placed == null or map == null:
        return
    var cell := PlacementPositioning2DUtils.get_tile_from_global_position(placed.global_position, map)
    _record_occupancy(cell, placed)

That cell is the grid-aligned fact your game keeps: occupancy for pathfinding, save metadata, or a link between the placed object and the TileMapLayer cells it covers. The full coordinate rules — including terrain paint callbacks, which already expose their target cell — are in Capturing Tile Positions.