Quick answer: Use event.set_grab(True) + mouse.set_visible(False) + mouse.get_rel() for relative movement. Don’t manually set_pos — it injects phantom deltas.

A first-person Pygame demo wraps the mouse to screen center each frame using set_pos. View glitches every frame — jumps in opposing direction. The warp generates phantom motion.

Built-in Relative Mode

pygame.event.set_grab(True)
pygame.mouse.set_visible(False)
# pygame-ce additionally:
pygame.mouse.set_relative_mode(True)

while running:
    dx, dy = pygame.mouse.get_rel()
    yaw += dx * sensitivity
    pitch += dy * sensitivity

get_rel returns the delta since last call. Cursor stays grabbed by the window; no need to manually warp.

Discard First Frame's Delta

The first get_rel call returns the delta since window creation — can be huge. Call once before the loop to consume the initial value:

pygame.mouse.get_rel()   # prime, discard
while running:
    dx, dy = pygame.mouse.get_rel()

Manual Warp Pitfall

The pattern set_pos to screen center, then read offset, was needed before SDL2’s relative mode. Now it’s redundant and produces feedback loops:

# DON'T do this with modern pygame:
mx, my = pygame.mouse.get_pos()
dx = mx - cx
pygame.mouse.set_pos((cx, cy))   # generates a phantom MOUSEMOTION

Sensitivity Scale

Relative mode reports raw OS counts. Apply a sensitivity factor (typically 0.1 – 1.0) before using.

Focus Handling

When the window loses focus (Alt-Tab), relative mode auto-releases. On regain, re-enable:

if event.type == pygame.WINDOWFOCUSGAINED:
    pygame.event.set_grab(True)
    pygame.mouse.set_visible(False)

Verifying

Look around in FPS prototype. Smooth motion, no glitchy jumps. Mouse stays trapped to window. Alt-tab releases cursor; re-focus restores.

“Let SDL2 handle the warp. Manual set_pos is a legacy workaround that hurts more than helps now.”

For local multiplayer with split keyboard control, relative mode complicates UI — consider absolute mode for menus, swap to relative only during play.