Quick answer: D-pads vary: some are hats, some are buttons, some are an axis pair. Use the SDL2 Controller API (pygame._sdl2.controller) for a normalized D-pad.
A menu uses the D-pad for navigation. It works on an Xbox pad but not a generic USB controller — that one exposes the D-pad differently.
The Three D-Pad Styles
- Hat:
JOYHATMOTIONevents, value is an (x, y) tuple. - Buttons: four separate buttons for up/down/left/right.
- Axis: some pads map the D-pad to axes 6/7.
Use the Controller API
from pygame._sdl2 import controller
controller.init()
pad = controller.Controller(0)
# normalized button constants regardless of hardware
if pad.get_button(controller.CONTROLLER_BUTTON_DPAD_UP):
menu_up()
SDL2’s GameController layer maps every known pad’s D-pad to the same constants. This is the robust fix.
Raw Joystick Fallback
If you must use the raw Joystick API, handle all three:
if event.type == pygame.JOYHATMOTION:
x, y = event.value
elif event.type == pygame.JOYBUTTONDOWN:
# check known D-pad button indices per controller
pass
Maintain a per-controller mapping table — tedious, which is why the Controller API exists.
Verifying
Test on Xbox, PlayStation, and a generic USB pad. D-pad navigation works on all three with the same code path.
“D-pad hardware varies wildly. The SDL2 Controller API normalizes it — use it.”
Ship an SDL_GameControllerDB file with your game so even obscure controllers get a correct mapping.