Implement game controller support and add keybindings for gamepad input

This commit is contained in:
John Doe
2026-05-07 17:50:07 +02:00
parent 9421d8d47c
commit 486fea38e7
4 changed files with 367 additions and 45 deletions
+94 -11
View File
@@ -12,6 +12,36 @@ from PIL import Image
from runtime_paths import resolve_bundle_path
CONTROLLER_BUTTON_NAMES = {
sdl2.SDL_CONTROLLER_BUTTON_A: "a",
sdl2.SDL_CONTROLLER_BUTTON_B: "b",
sdl2.SDL_CONTROLLER_BUTTON_X: "x",
sdl2.SDL_CONTROLLER_BUTTON_Y: "y",
sdl2.SDL_CONTROLLER_BUTTON_BACK: "back",
sdl2.SDL_CONTROLLER_BUTTON_GUIDE: "guide",
sdl2.SDL_CONTROLLER_BUTTON_START: "start",
sdl2.SDL_CONTROLLER_BUTTON_LEFTSTICK: "leftstick",
sdl2.SDL_CONTROLLER_BUTTON_RIGHTSTICK: "rightstick",
sdl2.SDL_CONTROLLER_BUTTON_LEFTSHOULDER: "leftshoulder",
sdl2.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: "rightshoulder",
sdl2.SDL_CONTROLLER_BUTTON_DPAD_UP: "dpad_up",
sdl2.SDL_CONTROLLER_BUTTON_DPAD_DOWN: "dpad_down",
sdl2.SDL_CONTROLLER_BUTTON_DPAD_LEFT: "dpad_left",
sdl2.SDL_CONTROLLER_BUTTON_DPAD_RIGHT: "dpad_right",
}
def _decode_sdl_string(value):
if not value:
return None
if isinstance(value, (bytes, bytearray)):
return bytes(value).decode("utf-8", errors="ignore")
try:
return value.decode("utf-8", errors="ignore")
except AttributeError:
return str(value)
class GameWindow:
def __init__(self, width, height, cell_size, title="Default", key_callback=None):
# Display configuration
@@ -76,6 +106,10 @@ class GameWindow:
self.trigger = key_callback
self.button_cursor = [0, 0]
self.buttons = {}
self.joystick = None
self.game_controller = None
self.input_device_kind = "keyboard"
self.input_device_name = None
# Audio system initialization
self._init_audio_system()
@@ -412,9 +446,43 @@ class GameWindow:
# ======================
def load_joystick(self):
"""Initialize joystick support"""
sdl2.SDL_Init(sdl2.SDL_INIT_JOYSTICK)
sdl2.SDL_JoystickOpen(0)
"""Initialize joystick and game controller support."""
sdl2.SDL_Init(sdl2.SDL_INIT_JOYSTICK | sdl2.SDL_INIT_GAMECONTROLLER)
sdl2.SDL_JoystickEventState(sdl2.SDL_ENABLE)
if hasattr(sdl2, "SDL_GameControllerEventState"):
sdl2.SDL_GameControllerEventState(sdl2.SDL_ENABLE)
num_joysticks = sdl2.SDL_NumJoysticks()
if num_joysticks < 1:
print("[input] no joystick detected")
return
for device_index in range(num_joysticks):
if hasattr(sdl2, "SDL_IsGameController") and sdl2.SDL_IsGameController(device_index):
controller = sdl2.SDL_GameControllerOpen(device_index)
if controller:
self.game_controller = controller
self.input_device_kind = "gamecontroller"
self.input_device_name = _decode_sdl_string(
sdl2.SDL_GameControllerName(controller)
)
print(f"[input] game controller detected: {self.input_device_name}")
return
self.joystick = sdl2.SDL_JoystickOpen(0)
if self.joystick:
self.input_device_kind = "joystick"
self.input_device_name = _decode_sdl_string(sdl2.SDL_JoystickName(self.joystick))
print(f"[input] raw joystick detected: {self.input_device_name}")
def get_input_device_summary(self):
return {
"kind": self.input_device_kind,
"name": self.input_device_name,
}
def _controller_button_name(self, button):
return CONTROLLER_BUTTON_NAMES.get(button, str(button))
# ======================
# MAIN GAME LOOP
@@ -456,15 +524,24 @@ class GameWindow:
elif event.type == sdl2.SDL_MOUSEMOTION:
self.trigger(f"mousemove_{event.motion.x}, {event.motion.y}")
elif event.type == sdl2.SDL_JOYBUTTONDOWN:
key = event.jbutton.button
self.trigger(f"joybuttondown_{key}")
if self.game_controller is None:
key = event.jbutton.button
self.trigger(f"joybuttondown_{key}")
elif event.type == sdl2.SDL_JOYBUTTONUP:
key = event.jbutton.button
self.trigger(f"joybuttonup_{key}")
if self.game_controller is None:
key = event.jbutton.button
self.trigger(f"joybuttonup_{key}")
elif event.type == sdl2.SDL_JOYHATMOTION:
hat = event.jhat.hat
value = event.jhat.value
self.trigger(f"joyhatmotion_{hat}_{value}")
if self.game_controller is None:
hat = event.jhat.hat
value = event.jhat.value
self.trigger(f"joyhatmotion_{hat}_{value}")
elif hasattr(sdl2, "SDL_CONTROLLERBUTTONDOWN") and event.type == sdl2.SDL_CONTROLLERBUTTONDOWN:
button_name = self._controller_button_name(event.cbutton.button)
self.trigger(f"controllerbuttondown_{button_name}")
elif hasattr(sdl2, "SDL_CONTROLLERBUTTONUP") and event.type == sdl2.SDL_CONTROLLERBUTTONUP:
button_name = self._controller_button_name(event.cbutton.button)
self.trigger(f"controllerbuttonup_{button_name}")
@@ -537,7 +614,7 @@ class GameWindow:
if event.type == sdl2.SDL_QUIT:
self.running = False
return
elif event.type in (sdl2.SDL_KEYDOWN, sdl2.SDL_JOYBUTTONDOWN):
elif event.type in (sdl2.SDL_KEYDOWN, sdl2.SDL_JOYBUTTONDOWN, sdl2.SDL_CONTROLLERBUTTONDOWN):
skipped = True
if skipped:
@@ -645,6 +722,12 @@ class GameWindow:
def close(self):
"""Close the game window and cleanup"""
if self.game_controller:
sdl2.SDL_GameControllerClose(self.game_controller)
self.game_controller = None
if self.joystick:
sdl2.SDL_JoystickClose(self.joystick)
self.joystick = None
self.running = False
sdl2.ext.quit()