Quick answer: Set continuous_cd to CCD_MODE_CAST_SHAPE on fast rigid bodies, or thicken thin walls to at least speed_per_tick wide. For projectile-style objects, prefer a swept raycast over a physics body.

A bullet leaves the muzzle at 3000 pixels per second. At 60 Hz physics, it moves 50 pixels per tick. Your wall colliders are 4 pixels wide. On most ticks the bullet is in empty space; on the tick where it should hit the wall, it has already crossed to the other side. The collision is never detected and the bullet sails through.

Why Discrete Collision Misses

Godot’s default solver checks for overlap between shapes at the end of each physics step. If shape A and shape B are non-overlapping at tick N and non-overlapping at tick N+1 — even though A’s motion would have intersected B somewhere in between — the solver reports no collision. This is fine for bodies moving slower than their own thickness per step. It fails when speed exceeds the smallest collider dimension along the motion axis.

The math: tunneling occurs when velocity * delta > min(body_thickness, wall_thickness) along the motion vector. Faster bodies, thinner walls, or larger delta — any of those crossing the threshold — produces tunneling.

Fix 1: Continuous Collision Detection

On the moving body, set continuous_cd:

# bullet.gd
extends RigidBody2D

func _ready():
    continuous_cd = CCD_MODE_CAST_SHAPE

Two modes exist:

CCD has no effect on Area2D/Area3D or StaticBody. If your projectile is an Area for damage events, switch it to a RigidBody with a sensor-like collision setup, or replace the area entirely with a raycast (see Fix 3).

Fix 2: Thicken Walls or Bodies

If you can’t enable CCD — for example, on a CharacterBody using move_and_slide with a thin platform — thicken the geometry. Make wall colliders at least as thick as the maximum distance a body can travel in one tick:

# Compute minimum safe wall thickness
var max_body_speed = 2000.0   # pixels per second
var tick_rate = Engine.get_physics_ticks_per_second()
var min_wall_thickness = max_body_speed / tick_rate
print("Walls must be at least %s px thick" % min_wall_thickness)

At 60 Hz and 2000 px/s, walls need to be 34 pixels thick for guaranteed catches. If your art has 4-pixel walls, add an invisible thicker CollisionShape2D for physics, separate from the visual sprite.

Fix 3: Replace Projectiles with Raycasts

For bullets and other linear projectiles, the most reliable solution is to skip physics entirely. Each frame, cast a ray from the previous position to the new position and resolve the first hit:

# bullet.gd (non-physics version)
extends Node2D

var velocity: Vector2

func _physics_process(delta):
    var prev = global_position
    var next = prev + velocity * delta
    var space = get_world_2d().direct_space_state
    var query = PhysicsRayQueryParameters2D.create(prev, next)
    var hit = space.intersect_ray(query)
    if hit:
        _on_hit(hit.collider, hit.position)
        queue_free()
    else:
        global_position = next

This is mathematically equivalent to perfect CCD for point-shaped projectiles. It also runs faster than RigidBody simulation for the same workload because the broadphase, contact manifold, and integration steps are all skipped.

Fix 4: Raise the Physics Tick Rate

In Project Settings → Physics → Common, raise physics_ticks_per_second from 60 to 120 or 240. Halves or quarters the per-tick travel distance respectively. Use this only after profiling — physics work scales linearly with tick rate.

Verifying

Add a temporary trace in _physics_process that draws a line from previous to current position. Tunneling shows up as a line passing cleanly through a wall. After applying CCD or raycasting, the line stops at the wall surface.

“Fast plus thin equals tunneling. Pick one: slow it down, thicken it up, sweep it, or raycast it. Hoping isn’t on the list.”

CCD costs CPU per body — enable it per-projectile, not project-wide.