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:
- Doesn’t intercept direct
positionassignment from script. - Doesn’t prevent
linear_velocity = Vector3(...)overrides. - Doesn’t lock kinematic motion via
move_and_collideon CharacterBody3D.
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.