Quick answer: A CharacterBody2D snags on tilemap corners when its collider has flat edges that line up with tile seams, when floor_max_angle is too restrictive, or when the snap vector goes to zero on slope transitions. Switch to a capsule collider, raise floor_snap_length, set safe_margin to 0.001, and configure platform_floor_layers correctly.

Here is how to fix Godot CharacterBody2D snagging on tilemap corners. You build a clean platformer level out of a TileMap, drop in a player, and movement feels great until you sprint along a flat run of tiles — then the body suddenly halts mid-stride as if it hit a wall. Sometimes it freezes on a single tile boundary. Sometimes it micro-stutters every few tiles. The collision shapes are exactly the size you expect, the tiles are perfectly aligned in the editor, and yet the player keeps catching on edges that should not exist.

The Symptom

The CharacterBody2D moves smoothly across single, large collision shapes, but on a tilemap the same code stalls on seams. The most common patterns:

Hard stop on flat ground. Walking horizontally across a row of tiles, the body slows or fully halts as it crosses from one tile to the next. Holding the input continues to apply velocity, but move_and_slide reports zero motion until you release and re-press the key.

Snag at slope-to-flat transitions. The body rolls down a 45 degree slope, hits the flat tile at the bottom, and freezes. The slope ended cleanly, but the corner where the diagonal collider meets the rectangle of the next tile traps the body’s bottom edge.

Stair-step jitter. Climbing a one-tile-tall ledge by walking into it works on some attempts and fails on others, with the body bouncing between two positions. is_on_floor() alternates between true and false on consecutive frames.

What Causes This

Convex collision shape edges. A RectangleShape2D on the player and a RectangleShape2D on every tile means every collision is between two perfectly axis-aligned boxes. When the player’s bottom-right corner crosses the seam between two tiles, the physics solver has to resolve contact with two surfaces at once: the floor of the new tile and the vertical edge between the old and new tile. With finite numerical precision, that edge can register as a wall hit and zero out horizontal velocity.

floor_max_angle too restrictive. If floor_max_angle is set below the natural variation in your tile collisions, sub-pixel ridges between tiles look like steep walls. The body classifies them as obstacles instead of walkable floor, and move_and_slide refuses to slide over them.

Snap vector zero on slope transitions. The internal snap behavior depends on floor_snap_length. If it is zero or too small, the body lifts off the surface for a frame on transitions, the next frame it is no longer “on floor,” and the slide direction changes from horizontal to gravity-aligned. The result looks like a snag because the body suddenly moves along a different vector.

Missed move_and_slide iteration. move_and_slide internally performs up to max_slides iterations to resolve compound collisions. With the default of 4, complex corner contacts (floor + wall + adjacent tile seam) can exhaust the iteration budget without resolving cleanly, leaving residual penetration that blocks the next frame’s motion.

The Fix

Step 1: Use a capsule collider for the body. Replace the player’s RectangleShape2D with a CapsuleShape2D sized slightly smaller than the visual sprite. The rounded bottom rolls over tile seams instead of catching the corner. Keep the radius small enough that the body still fits through one-tile-wide gaps.

# Player.gd
extends CharacterBody2D

const SPEED = 220.0
const JUMP_VELOCITY = -380.0
const GRAVITY = 980.0

func _ready():
    # Tunables that prevent tile-seam snags
    floor_snap_length = 4.0
    floor_max_angle = deg_to_rad(46.0)
    safe_margin = 0.001
    max_slides = 6
    platform_floor_layers = 0xFFFFFFFF
    floor_stop_on_slope = false

func _physics_process(delta):
    if not is_on_floor():
        velocity.y += GRAVITY * delta
    var dir = Input.get_axis("move_left", "move_right")
    velocity.x = dir * SPEED
    move_and_slide()

Step 2: Raise floor_snap_length. A value of 4.0 pixels is a good starting point for a tile size of 16. The snap pulls the body back down onto the floor after small bumps, preventing the “left the floor for one frame” problem. If your tiles are larger, scale the value up proportionally.

Step 3: Reduce safe_margin. Godot’s default safe_margin can be too aggressive on tilemaps because every tile contributes its own margin. Setting it to 0.001 tightens the contact resolution and reduces phantom seam hits. Do not set it to zero — the solver needs a tiny epsilon to prevent jitter.

Step 4: Use platform_floor_layers correctly. Set the layer mask to include every layer your tilemap floors live on. If you forget a layer, the body treats those tiles as walls when it stands on them, which produces inconsistent floor detection and snagging.

# Diagnose snags by logging contact data on every frame
func _physics_process(delta):
    # ... movement code ...
    move_and_slide()

    for i in get_slide_collision_count():
        var col = get_slide_collision(i)
        var normal = col.get_normal()
        var angle = rad_to_deg(normal.angle_to(Vector2.UP))
        print("hit ", col.get_collider(),
              " normal ", normal,
              " angle ", angle)

If the angle log shows values just above your floor_max_angle at the snag points, your tile seams are the source. Either raise the angle or simplify the tile colliders so adjacent tiles share clean horizontal edges.

Why This Works

The capsule collider eliminates the geometric source of the snag: a flat horizontal edge meeting a flat vertical edge with no curvature between them. With a capsule, the body’s lowest contact point is always a single tangent point on the rounded section, so the solver only ever resolves one floor contact per frame instead of a floor-plus-wall combination at every seam.

The snap, margin, and angle changes give the physics solver enough slack to absorb minor numerical noise without classifying it as a wall. floor_snap_length ensures the body re-grounds after each move, safe_margin = 0.001 tightens contact accuracy, and floor_max_angle at 46 degrees gives a small buffer over the typical 45 degree slope tile.

Raising max_slides from 4 to 6 helps in the edge case where the body is in contact with three surfaces at once (floor, slope, and adjacent flat tile). Each iteration resolves one contact and re-projects the velocity along the remaining ones; with more iterations available, complex corner cases finish cleanly.

“If your character snags on flat tiles, do not blame the tilemap. Round the bottom of your body and give the solver room to breathe. Capsules are not just for 3D.”

Related Issues

If your character clips through tiles at high speed instead of snagging on them, see Fast-Moving Bodies Tunnelling Through Walls. If the body floats above the floor by a pixel or two, check CharacterBody2D Floats Above Ground.

A capsule collider plus a 4-pixel snap length fixes 90 percent of tilemap snags — tune from there.