Diagnosing Godot 4.x CharacterBody Movement and Collision Failures
A diagnostic guide for Godot 4.x CharacterBody2D/3D movement issues: jitter, tunneling, stuck corners, ignored collisions, and moving-platform sync. Includes symptom-to-cause table, ordered checks, version-specific fixes, and escalation criteria.
03 Jan 2026, 02:42 UTC

When Your Character Doesn't Move Right
You press move_and_slide() but the character jitters against a wall, tunnels through a thin barrier, or gets stuck on a corner. These symptoms share a common root: the physics pipeline is receiving inputs it can't resolve cleanly. This guide isolates the cause by symptom, gives you ordered checks you can run in the editor, and tells you when to escalate to a minimal reproduction or a version-specific workaround.
Symptom-to-Cause Reference Table
| Observable Symptom | Most Likely Cause | Quick Verification |
|---|---|---|
| Jitters against walls or floors | Velocity not reset after collision; floor_snap_length too small for slope | Print velocity each frame; confirm floor_snap_length >= max_step_height |
| Tunnels through thin walls at high speed | Continuous collision detection (CCD) disabled or velocity * delta exceeds wall thickness | Check RigidBody CCD mode; for CharacterBody ensure velocity.length() * delta < wall_thickness or add raycast pre-check |
| Stuck on wall corners | Sharp collision shape corners; floor_max_angle too strict (default 45°) | Switch to CapsuleShape2D/3D or bevel RectangleShape; raise floor_max_angle |
move_and_slide() returns zero velocity but is_on_floor() false | Up direction not set or floor_snap_length = 0 | Call set_up_direction() before move_and_slide(); set floor_snap_length > 0 |
| One-way platforms ignored | Layer/mask mismatch; platform missing one_way_collision = true | Platform collision_layer includes character's collision_mask; one_way_collision_margin set |
| Moving platform doesn't carry character | Platform not AnimatableBody with sync_to_physics = true; character not using get_floor_velocity() | Platform inherits AnimatableBody2D/3D; character applies floor velocity in _physics_process |
| Collision callbacks fire multiple times per contact | Overlapping collision shapes; contact monitoring not configured | Reduce to single convex shape; set contact_monitor = true and contacts_reported > 0 |
| Visual position desyncs from physics | Physics interpolation enabled but node not in SceneTree during physics tick | Project Settings > Physics > Common > Physics Interpolation; ensure CharacterBody in tree before first _physics_process |
Ordered Diagnostic Checks
- Enable debug visualization. Run with Debug > Visible Collision Shapes. Watch the green floor-raycast line during movement. Misaligned shapes or missing raycasts reveal scale or up-direction problems immediately.
- Log runtime state. Add a temporary line in
_physics_process:
Correlate printed values with the symptom frame-by-frame.print(velocity, is_on_floor(), is_on_wall(), get_floor_normal()) - Isolate in a minimal scene. Create a test scene:
CharacterBody2D+CapsuleShape2D+StaticBody2Dfloor + wall. Reproduce the issue without gameplay code. If it disappears, the cause is in your scene composition or script logic. - Verify layer/mask values at runtime. Open Debug > Open Remote Scene Tree while running. Inspect
collision_layerandcollision_maskon every body. Silent mismatches are the most common cause of one-way platform and moving-platform failures. - Check Godot version against known regressions. Project > Project Settings > Application > Config > Version. Godot 4.0–4.2 ignored
floor_snap_lengthwhenup_directionwasn't normalized; fixed in 4.3. If you're exporting from an older template, update or apply the workaround (normalizeup_directionmanually).
Fixes Tied to Findings
Jitter on Slopes
Increase floor_snap_length to at least your maximum step height (default 10 px in 2D, 0.5 m in 3D). Example for a 2D platformer with 16 px steps:
@onready var body = $CharacterBody2D
func _ready():
body.floor_snap_length = 16.0
body.set_up_direction(Vector2.UP)
Run with debug shapes on; the green snap ray should reach the floor on the steepest slope you support.
Tunneling at High Speed
For CharacterBody you cannot enable CCD directly. Instead, clamp velocity or add a predictive raycast:
var max_speed = 300.0
var wall_thickness = 8.0 # thinnest collider in your level
func _physics_process(delta):
velocity = velocity.limit_length(max_speed)
if velocity.length() * delta > wall_thickness:
var ray = PhysicsRayQueryParameters2D(global_position, global_position + velocity * delta)
var hit = get_world_2d().direct_space_state.intersect_ray(ray)
if hit:
velocity = (hit.position - global_position) / delta
move_and_slide()
Adjust wall_thickness to your thinnest StaticBody collider.
Stuck on Corners
Replace RectangleShape2D with CapsuleShape2D (radius = half width, height = body height - radius*2). If you must keep a box, add a 1–2 px bevel via ConvexPolygonShape2D or increase floor_max_angle to 55°:
body.floor_max_angle = deg_to_rad(55)
One-Way Platform Setup
On the platform StaticBody2D:
collision_layer = 2 # platform layer
one_way_collision = true
one_way_collision_margin = 2.0 # pixels
On the character:
collision_mask = 2 # includes platform layer
Verify in Remote Scene Tree that both values match at runtime.
Moving Platform Sync
Platform must be AnimatableBody2D (or 3D) with sync_to_physics = true. Animate via _physics_process or AnimationPlayer with AnimationPlayer.playback_process_mode = ANIMATION_PROCESS_PHYSICS. Character applies floor velocity:
func _physics_process(delta):
velocity = get_floor_velocity()
# add input velocity
velocity.x = input_dir * speed
move_and_slide()
Print get_floor_velocity() and platform global_position each frame to confirm sync.
Escalation Criteria
- Symptom persists in the minimal test scene after applying the relevant fix above.
- Remote Scene Tree shows correct layer/mask but collision still fails — check
TileMapphysics layers:TileSetphysics layer must match charactercollision_mask;TileMap.collision_layermust include that mask. Mismatch is silent. - Non-uniform scale on
CollisionShape3Dbreaks floor detection (raycast originates from scaled shape bottom). Apply scale toMeshInstance3Donly; keep collision shape scale at (1,1,1). - Physics tick rate changed from default 60 Hz (Project Settings > Physics > Common > Physics Ticks Per Second). Velocity tuning must account for new
delta; re-run checks with actualdeltaprinted. - Issue only appears in exported release build — debug symbols stripped. Test with debug build or enable
Visible Collision Shapesvia command line--debug-collisions.
Practical Verification Checklist
- Debug shapes visible; floor raycast hits expected surface every frame.
- Printed
velocity,is_on_floor(),get_floor_normal()match intended state. - Minimal scene reproduces or clears the symptom.
- Remote Scene Tree confirms layer/mask on all bodies including TileMap.
- Godot version >= 4.3 or up_direction normalized manually.
- Physics tick rate known; velocity values scaled accordingly.
If all checks pass and the symptom remains, file a minimal reproduction on the Godot GitHub issue tracker with the test scene, version, and printed logs. Most "phantom" collisions resolve at one of the steps above.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.