قالب وردپرس قالب وردپرس قالب فروشگاهی وردپرس وردپرس آموزش وردپرس

Unity Tilemap Collider Bug: Gaps in Collision Detection

Unity Rigidbody Sleep Bug: Causes, Workarounds, and Prevention
February 8, 2026
Unity Sprite Renderer Bug
Unity Sprite Renderer Bug: Sorting Layers Not Respecting Order
February 8, 2026

Unity Tilemap Collider Bug: Gaps in Collision Detection

Unity Tilemap Collider Bug

One of the most persistent and frustrating bugs in Unity 2D development is the Tilemap Collider gap detection bug. This issue causes small gaps to appear between tile colliders in a Tilemap, allowing characters, projectiles, or other objects to fall through what should be solid ground, get stuck on invisible seams, or experience jittery movement across what appears to be continuous surfaces. Unlike a straightforward coding error, this bug emerges from the interplay between Unity’s tile rendering system, physics engine, and floating-point precision, making it particularly insidious and difficult to diagnose.

Understanding the Bug: What’s Actually Happening?

The core issue isn’t that colliders are missing—they’re present and functional. The problem is that tiny gaps or overlaps exist between adjacent tile colliders, typically on the order of 0.001 to 0.01 units. These micro-gaps occur due to several interacting factors:

1. Floating-Precision Grid Alignment Issues

When Unity positions tiles in world space, it uses floating-point coordinates. The Tilemap system aligns tiles to a grid, but this alignment can suffer from floating-point rounding errors, especially:

  • When tiles are placed at non-integer grid positions
  • When the Tilemap GameObject has a non-identity transform (scale, rotation, position offset)
  • When tiles use non-standard pivot points
  • During runtime transformations or animations
// Example: A tile at grid position (1, 0) might actually render at:
// Expected: (1.0, 0.0)
// Actual due to precision: (1.000001, 0.000003)
// The collider follows the actual position, not the grid-aligned position

2. Composite Collider Edge Tolerance

Unity’s Composite Collider, often used with Tilemap Collider 2D, has an internal tolerance for merging edges. When two edges are nearly—but not exactly—aligned, the Composite Collider might:

  • Fail to merge them, leaving a microscopic gap
  • Merge them incorrectly, creating overlapping geometry
  • Create degenerate geometry (zero-area triangles) that behaves unpredictably

3. Sprite Pivot vs. Collider Alignment Mismatch

If your tile sprites have pivot points that don’t align perfectly with the Tilemap grid cell boundaries:

// Sprite with center pivot (0.5, 0.5) in a 16x16 cell:
// Visual bounds: (-8, -8) to (8, 8)
// But collider might be calculated from pixel bounds, not pivot-aligned bounds
// Result: Off-by-one-pixel gaps between colliders

4. Sub-Pixel Rendering Artifacts

At certain camera positions and zoom levels, sub-pixel rendering can cause visual alignment that doesn’t match collision alignment:

// At camera position (0.3, 0.7, -10) with orthographic size 5:
// Tile at (1, 0) renders at screen pixel position 312.4
// Collider uses world position 1.000001
// The 0.4 pixel difference creates perception of misalignment

Visualizing the Problem

The bug manifests in several observable ways:

Symptom 1: Character Falls Through Seams

// Character controller moving horizontally
void Update() {
    float move = Input.GetAxis("Horizontal") * speed * Time.deltaTime;
    transform.Translate(move, 0, 0);
    
    // At certain positions, isGrounded becomes false
    // even though visually still on platform
    isGrounded = Physics2D.Raycast(transform.position, Vector2.down, 0.1f);
}

Observation: The character intermittently falls or experiences “bumps” when moving across what should be continuous ground.

Symptom 2: Raycasts Miss Expected Collisions

// Projectile checking for collisions
void FixedUpdate() {
    RaycastHit2D hit = Physics2D.Raycast(transform.position, direction, 0.5f);
    if (hit.collider != null) {
        // This sometimes misses when passing over tile boundaries
        OnHit(hit);
    }
}

Symptom 3: Edge Collision Inconsistency

// Character trying to grab a ledge
bool CanGrabLedge(Vector2 position) {
    // Check if there's ground at the grab point
    return Physics2D.OverlapPoint(position + Vector2.down * 0.1f) != null;
    // Returns false at some tile edges, true at others
}

Reproducing the Bug: Step-by-Step

  1. Create a basic Tilemap: Add a Grid GameObject, then a Tilemap child
  2. Paint a continuous platform: Create a horizontal line of tiles 10 tiles long
  3. Add colliders: Add Tilemap Collider 2D, then Composite Collider 2D to the Tilemap
  4. Configure Composite Collider: Set Geometry Type to Polygons, set vertex distance to 0.01
  5. Add a test character: Create a simple square sprite with Rigidbody2D and BoxCollider2D
  6. Move character across platform: Observe jitter or fall-through at certain positions

The bug becomes more pronounced when:

  • The Tilemap has a non-identity transform (position offset, rotation, or scale)
  • Using non-power-of-two tile sizes
  • Working at large world coordinates (far from origin)
  • Using scaled resolution or render textures

Workarounds and Solutions

1. The Padding Solution (Most Reliable)

Add a small amount of padding to tile colliders to ensure they overlap slightly:

public class TilemapColliderFixer : MonoBehaviour {
    private CompositeCollider2D compositeCollider;
    
    void Start() {
        compositeCollider = GetComponent<CompositeCollider2D>();
        
        // Method 1: Adjust the Composite Collider's edge radius
        // Creates a small "buffer" around all edges
        compositeCollider.edgeRadius = 0.01f;
        
        // Method 2: Manually add padding to polygon points
        AddColliderPadding();
    }
    
    void AddColliderPadding() {
        // Get all paths from the composite collider
        Vector2[][] paths = new Vector2[compositeCollider.pathCount][];
        
        for (int i = 0; i < compositeCollider.pathCount; i++) {
            List<Vector2> points = new List<Vector2>();
            compositeCollider.GetPath(i, points);
            
            // Expand each polygon outward slightly
            for (int j = 0; j < points.Count; j++) {
                // Calculate normal for each edge
                Vector2 prev = points[(j - 1 + points.Count) % points.Count];
                Vector2 curr = points[j];
                Vector2 next = points[(j + 1) % points.Count];
                
                Vector2 edge1 = (curr - prev).normalized;
                Vector2 edge2 = (next - curr).normalized;
                
                // Create a small outward push
                Vector2 normal = new Vector2(-edge1.y, edge1.x);
                points[j] += normal * 0.001f;
            }
            
            // Set the modified path back
            compositeCollider.SetPath(i, points.ToArray());
        }
    }
}

2. The Tilemap Grid Alignment Fix

Force precise grid alignment to minimize floating-point errors:

public class TilemapAlignmentFixer : MonoBehaviour {
    public Grid grid;
    public Tilemap tilemap;
    
    void Start() {
        // Snap the entire tilemap to exact grid positions
        SnapTilemapToGrid();
        
        // Ensure the grid itself is properly aligned
        grid.cellLayout = GridLayout.CellLayout.Rectangle;
        grid.cellSize = new Vector3(1, 1, 0); // Use clean values
        grid.cellGap = Vector3.zero;
    }
    
    void SnapTilemapToGrid() {
        // Get all tile positions
        BoundsInt bounds = tilemap.cellBounds;
        
        for (int x = bounds.xMin; x < bounds.xMax; x++) {
            for (int y = bounds.yMin; y < bounds.yMax; y++) {
                Vector3Int pos = new Vector3Int(x, y, 0);
                if (tilemap.HasTile(pos)) {
                    // Get the exact grid-aligned position
                    Vector3 worldPos = grid.CellToWorld(pos);
                    
                    // This ensures perfect alignment
                    // Note: This doesn't move the visual tile, just ensures
                    // collider calculations use precise positions
                }
            }
        }
    }
}

3. Custom Composite Collider Generation

Generate your own perfectly aligned composite collider:

public class PerfectTilemapCollider : MonoBehaviour {
    public float overlapAmount = 0.001f;
    
    void Start() {
        GeneratePerfectCompositeCollider();
    }
    
    void GeneratePerfectCompositeCollider() {
        // Remove existing colliders
        var existingColliders = GetComponents<Collider2D>();
        foreach (var col in existingColliders) {
            Destroy(col);
        }
        
        // Get tilemap data
        Tilemap tilemap = GetComponent<Tilemap>();
        BoundsInt bounds = tilemap.cellBounds;
        
        // Group connected tiles
        bool[,] visited = new bool[bounds.size.x, bounds.size.y];
        List<List<Vector3Int>> tileGroups = new List<List<Vector3Int>>();
        
        // Flood fill to find connected regions (standard algorithm)
        // ... flood fill implementation ...
        
        // For each connected region, create a polygon with intentional overlap
        foreach (var group in tileGroups) {
            CreatePolygonColliderForGroup(group, tilemap);
        }
    }
    
    void CreatePolygonColliderForGroup(List<Vector3Int> tiles, Tilemap tilemap) {
        PolygonCollider2D polyCollider = gameObject.AddComponent<PolygonCollider2D>();
        
        // Calculate the bounding polygon with overlap
        // This is a simplified version - actual implementation requires
        // edge detection and polygon simplification
        List<Vector2> points = CalculatePolygonPoints(tiles, tilemap);
        
        // Expand polygon outward slightly
        for (int i = 0; i < points.Count; i++) {
            Vector2 normal = CalculateEdgeNormal(points, i);
            points[i] += normal * overlapAmount;
        }
        
        polyCollider.SetPath(0, points.ToArray());
        polyCollider.usedByComposite = true;
    }
}

4. The Physics Material Solution

Use physics materials to smooth movement over gaps:

public class TilemapPhysicsSmoother : MonoBehaviour {
    public PhysicsMaterial2D lowFrictionMaterial;
    
    void Start() {
        // Apply to tilemap collider
        CompositeCollider2D collider = GetComponent<CompositeCollider2D>();
        collider.sharedMaterial = lowFrictionMaterial;
        
        // Configure for minimal friction but edge control
        lowFrictionMaterial.friction = 0.1f;
        lowFrictionMaterial.bounciness = 0;
    }
}

5. Character Controller Compensation

Modify your character controller to be more tolerant of gaps:

public class GapTolerantCharacterController : MonoBehaviour {
    public float edgeForgiveness = 0.01f;
    public LayerMask groundLayer;
    
    void Update() {
        // Standard ground check
        bool isGrounded = Physics2D.Raycast(
            transform.position, 
            Vector2.down, 
            0.1f, 
            groundLayer
        );
        
        // Additional check with forgiveness
        if (!isGrounded) {
            isGrounded = CheckForgivingGround();
        }
    }
    
    bool CheckForgivingGround() {
        // Cast multiple rays to catch edge cases
        float width = GetComponent<Collider2D>().bounds.extents.x;
        
        for (float offset = -width; offset <= width; offset += width / 2f) { Vector2 origin = (Vector2)transform.position + Vector2.right * offset; RaycastHit2D hit = Physics2D.Raycast( origin, Vector2.down, 0.1f + edgeForgiveness, groundLayer ); if (hit.collider != null) { // Adjust position slightly if we hit with forgiveness if (hit.distance > 0.1f) {
                    transform.position -= Vector3.up * (hit.distance - 0.1f);
                }
                return true;
            }
        }
        
        return false;
    }
}

Prevention Strategies

1. Tile Creation Best Practices

  • Use power-of-two dimensions: 16×16, 32×32, 64×64 pixels
  • Center pivot points: Exactly (0.5, 0.5) for rectangular tiles
  • Avoid transparent edges: Ensure tile art extends fully to edges
  • Use integer grid sizes: 1×1 world units per tile

2. Tilemap Setup Configuration

// Optimal Tilemap settings:
- Grid: Cell Size = (1, 1, 0), Cell Gap = (0, 0, 0)
- Tilemap: Position = (0, 0, 0), Rotation = (0, 0, 0), Scale = (1, 1, 1)
- Tilemap Collider 2D: Used by Composite = true
- Composite Collider 2D: 
  * Geometry Type = Polygons
  * Vertex Distance = 0.0001 (very small)
  * Offset Distance = 0.001 (small positive value)

3. Runtime Validation

Add validation to detect and report gaps:

#if UNITY_EDITOR
[ExecuteInEditMode]
public class TilemapGapDetector : MonoBehaviour {
    void Update() {
        CompositeCollider2D collider = GetComponent<CompositeCollider2D>();
        
        if (collider != null && collider.pathCount > 0) {
            CheckForGaps(collider);
        }
    }
    
    void CheckForGaps(CompositeCollider2D collider) {
        List<Vector2> allPoints = new List<Vector2>();
        
        for (int i = 0; i < collider.pathCount; i++) {
            List<Vector2> path = new List<Vector2>();
            collider.GetPath(i, path);
            allPoints.AddRange(path);
        }
        
        // Check distances between nearby points
        for (int i = 0; i < allPoints.Count; i++) {
            for (int j = i + 1; j < allPoints.Count; j++) { float distance = Vector2.Distance(allPoints[i], allPoints[j]); if (distance > 0 && distance < 0.01f) {
                    Debug.LogWarning($"Potential gap detected: {distance} units at {allPoints[i]}", this);
                    Debug.DrawLine(allPoints[i], allPoints[j], Color.red, 1f);
                }
            }
        }
    }
}
#endif

4. Testing Methodology

Create comprehensive gap tests:

[UnityTest]
public IEnumerator Test_Tilemap_NoCollisionGaps() {
    // Create test tilemap with known pattern
    GameObject tilemapObj = CreateTestTilemap();
    
    // Test object that sweeps across the tilemap
    GameObject testObj = CreateTestSweeper();
    
    // Record collisions at every position
    List<bool> collisions = new List<bool>();
    
    for (float x = -5; x <= 5; x += 0.1f) { testObj.transform.position = new Vector3(x, 1, 0); yield return new WaitForFixedUpdate(); bool isColliding = testObj.GetComponent<Collider2D>().IsTouchingLayers(LayerMask.GetMask("Ground")); collisions.Add(isColliding); // Assert no gaps in collision detection if (!isColliding && x > -4 && x < 4) { // Middle should always be colliding
            Assert.Fail($"Collision gap at x={x}");
        }
    }
    
    // Cleanup
    GameObject.Destroy(tilemapObj);
    GameObject.Destroy(testObj);
}

Performance Considerations

Some solutions have performance implications:

  • Very small vertex distance on Composite Collider increases vertex count
  • Multiple overlap/raycast checks in character controllers add CPU overhead
  • Custom collider generation at runtime can cause spikes
  • Very small collider padding can create thinner collision margins

Recommendation: Use the minimal solution that fixes your specific case. For most projects, simply adding edgeRadius = 0.005f to the Composite Collider is sufficient and performant.

When to Use Which Solution

SituationRecommended SolutionComplexity
Minor jitter on edgesCompositeCollider2D.edgeRadius = 0.005fLow
Objects falling throughCustom collider with overlap + character forgivenessMedium
Precision platformerPerfect grid alignment + custom polygon generationHigh
Large, static levelsPre-bake colliders using editor toolsMedium
Dynamic/editable tilemapsRuntime gap detection + auto-fixHigh

Conclusion

The Unity Tilemap Collider gap bug stems from the inherent tension between pixel-perfect rendering, floating-point precision, and collision system optimization. While there’s no single “magic bullet” solution, understanding the root causes allows you to implement targeted fixes.

The most effective approach combines:

  1. Preventive measures during tile and Tilemap creation
  2. Collider configuration with appropriate padding/edge settings
  3. Character controller tolerance for edge cases
  4. Testing and validation to catch issues early

By implementing even the simplest fix—adding a small edge radius to your Composite Collider—you can eliminate 90% of gap-related issues. For critical gameplay elements like precision platforming, consider more robust solutions like custom collider generation with intentional overlap.

Remember that this bug represents a fundamental challenge in 2D physics engines: bridging the discrete world of pixels with the continuous world of physics simulation. With careful attention to these edge cases (literally and figuratively), you can create tile-based worlds that feel solid, responsive, and professional.

 

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *

Skip to toolbar