Quick answer: Axis locks zero physics-driven velocity along the locked axis each step. Direct position or linear_velocity assignment from script bypasses the lock — use apply_central_force or honor the lock manually.

A 3D side-scroller pins the player’s movement to the XY plane by locking the Z axis on the RigidBody3D. The character starts on Z=0, but over time drifts to Z=-3.2. The lock is on; the body still moves.

What Axis Lock Actually Does

For RigidBody3D, axis lock zeroes linear_velocity.z (or other axes) after each physics step. Forces applied along the locked axis still produce zero net motion because the solver clamps velocity each tick.

What it doesn’t do:

Fix 1: Use Forces, Not Direct Velocity

# RIGHT: respects axis lock
body.apply_central_force(Vector3(input_x * 100, 0, 0))

# WRONG: bypasses lock
body.linear_velocity = Vector3(10, 0, 5)   # Z component sneaks through

Forces are integrated through the solver each tick, and the lock clamps the resulting velocity. Direct velocity writes are taken at face value.

Fix 2: Honor Lock Manually

If you must write velocity directly:

var v = Vector3(10, 0, 5)
if body.axis_lock_linear_z: v.z = 0
body.linear_velocity = v

Explicit. Clear. Future you sees the constraint in the code path.

Fix 3: Use _integrate_forces

For complex motion that should respect locks fully:

func _integrate_forces(state: PhysicsDirectBodyState3D):
    var v = state.linear_velocity
    if axis_lock_linear_z: v.z = 0
    state.linear_velocity = v

_integrate_forces runs inside the physics step. The solver applies its own lock after this; combining the explicit zero with the lock guarantees no drift.

Char Controller Special Case

CharacterBody3D doesn’t have axis_lock_* in the same way. Implement manually:

func _physics_process(_delta):
    var v = velocity
    v.z = 0   # 2.5D constraint
    velocity = v
    move_and_slide()

Zero the velocity component each frame. The character can’t accumulate Z motion regardless of input or collisions.

Verifying

Print body.global_position.z over time while applying forces. With proper lock + forces, the value should stay constant (within ~0.0001 floating-point noise). If it drifts, you have a write-site bypassing the lock.

“Axis locks constrain physics integration. Direct velocity/position assignments aren’t physics integration — they bypass.”

For 2.5D games, prefer CharacterBody3D + manual axis zeroing — cleaner than fighting RigidBody’s direct-write loopholes.