Quick answer: Call tilemap_layer.update_internals() after a batch of set_cell calls. Verify the source TileSet’s collision polygon is non-empty for the tile id you placed.

A destructible-wall mechanic clears a tile with set_cell(coords, -1). The visual gap appears immediately; the player’s rigid body still bounces off invisible geometry where the wall used to be. Physics didn’t catch up with the visual.

How TileMapLayer Rebuilds Collision

Each TileMapLayer node maintains an internal PhysicsBody2D per-physics-layer. When you call set_cell, the layer marks itself dirty. On the next frame’s internal update, the body rebuilds based on current tile occupancy and TileSet collision polygons.

Two patterns trip this up:

The Fix: Explicit Update

@onready var walls: TileMapLayer = $WallsLayer

func destroy_wall(coords: Vector2i):
    walls.set_cell(coords, -1)
    walls.update_internals()   # force collision rebuild now

update_internals() runs the dirty-rebuild path synchronously. The physics body is current before the next line of your code executes. Downstream physics queries see the new shape.

Verify TileSet Collision

If update_internals() doesn’t help, the TileSet itself might be missing collision data for the tile you placed. Open the TileSet resource:

  1. TileSet panel → Physics Layers → ensure at least one layer is added.
  2. Click a tile in the atlas. In the right panel, switch to the Physics tab.
  3. Draw a collision polygon for the tile (or use “Add a rectangle” for solid blocks).
  4. Save the TileSet. Tiles using this id now have collision.

Missing polygons silently produce tiles that draw but don’t collide — the inverse of the original bug. Both directions exist.

Batched Updates

For multiple tile changes in one tick:

func clear_room(rect: Rect2i):
    for y in range(rect.position.y, rect.end.y):
        for x in range(rect.position.x, rect.end.x):
            walls.set_cell(Vector2i(x, y), -1)
    walls.update_internals()   # one rebuild for all changes

Calling update_internals once at the end is much faster than per-cell. The TileMapLayer’s internal coalescing handles this if you skip the explicit call, but only on the next frame.

Verifying

Enable Debug → Visible Collision Shapes. Mutate a tile. The collision polygon outline should disappear at the same instant as the visual. If the outline persists for a frame, the rebuild is lagging.

“TileMapLayer collision rebuilds are deferred by default. Call update_internals when you need synchronous correctness.”

Wrap destroy_wall and similar runtime mutations in a helper that always ends with update_internals — saves the lag-by-one-frame trap.