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.