Skip to content

Gamepad

Wrapper around SDL2 ControllerEventLoop that provides a polling-based interface.

This maintains axis and button state that can be queried at any time, similar to the old HID-based Gamepad API.

Example::

>>> gamepad = Gamepad()
>>> gamepad.axis(0)  # Get left stick X axis
0.0
>>> gamepad.buttons()  # Get list of pressed buttons
['a', 'b']

Initialize the SDL2 gamepad wrapper.

Parameters:

Name Type Description Default
debug bool

If True, print debug information

False
Source code in genesis_forge/gamepads/gamepad.py
def __init__(self, debug: bool = False):
    """
    Initialize the SDL2 gamepad wrapper.

    Args:
        debug: If True, print debug information
    """
    self._debug = debug
    self.is_running = True
    self._lock = threading.Lock()

    # State storage
    self._axis_values: list[float] = [0.0] * 6
    self._button_set: set[str] = set()
    self._dpad: str | None = None

    # Start the SDL2 event loop in a background thread
    self._event_loop = ControllerEventLoop(
        handle_key=self._handle_key,
        alive=threading.Event(),
    )
    self._event_loop.alive.set()
    self._read_thread = threading.Thread(target=self._event_loop.run, daemon=True)
    self._read_thread.start()

    if self._debug:
        print("SDL2 gamepad wrapper initialized")

dpad property

Get the current D-pad direction (e.g., 'up', 'down', 'left', 'right').

is_running = True instance-attribute

axis(index)

Get the value of an axis.

Parameters:

Name Type Description Default
index int

The axis index (0-5)

required

Returns:

Type Description
float

The axis value in range [-1.0, 1.0] for sticks, [0.0, 1.0] for triggers

Source code in genesis_forge/gamepads/gamepad.py
def axis(self, index: int) -> float:
    """
    Get the value of an axis.

    Args:
        index: The axis index (0-5)

    Returns:
        The axis value in range [-1.0, 1.0] for sticks, [0.0, 1.0] for triggers
    """
    with self._lock:
        if index >= len(self._axis_values):
            return 0.0
        return self._axis_values[index]

buttons()

Get the list of currently pressed buttons.

Returns:

Type Description
list[str]

List of button names (lowercase, e.g., 'a', 'b', 'x', 'y')

Source code in genesis_forge/gamepads/gamepad.py
def buttons(self) -> list[str]:
    """
    Get the list of currently pressed buttons.

    Returns:
        List of button names (lowercase, e.g., 'a', 'b', 'x', 'y')
    """
    with self._lock:
        return list(self._button_set)

stop()

Stop reading gamepad input.

Source code in genesis_forge/gamepads/gamepad.py
def stop(self) -> None:
    """Stop reading gamepad input."""
    self.is_running = False
    self._event_loop.stop()

Low-level SDL2 API

For advanced users who want direct access to the SDL2 event loop:

Minimal SDL2 controller event loop for genesis-forge.

This class runs in a background thread and processes SDL2 controller events, converting them to Key objects and passing them to a callback function.

Parameters:

Name Type Description Default
handle_key Callable[[Key], None]

Callback function that receives Key objects for each controller event

required
alive Event | None

Optional threading.Event to control the event loop. When cleared, the loop exits.

None
timeout int

Milliseconds to wait for events before checking the alive flag (default: 2000ms)

2000

Example::

def on_key(key: Key):
    print(f"Key event: {key}")

loop = ControllerEventLoop(handle_key=on_key)
thread = threading.Thread(target=loop.run, daemon=True)
thread.start()

# Later, to stop:
loop.stop()
Source code in genesis_forge/gamepads/sdl2.py
def __init__(
    self,
    handle_key: Callable[[Key], None],
    alive: threading.Event | None = None,
    timeout: int = 2000,
) -> None:
    self.handle_key = handle_key
    self.alive = alive or threading.Event()
    if not self.alive.is_set():
        self.alive.set()
    self.timeout = timeout
    self._controllers: dict[int, sdl2.SDL_GameController] = {}

alive = alive or threading.Event() instance-attribute

handle_key = handle_key instance-attribute

timeout = timeout instance-attribute

run()

Source code in genesis_forge/gamepads/sdl2.py
def run(self) -> None:
    if sdl2.SDL_WasInit(sdl2.SDL_INIT_GAMECONTROLLER) == 0:
        sdl2.SDL_InitSubSystem(sdl2.SDL_INIT_GAMECONTROLLER)

    for idx in range(sdl2.SDL_NumJoysticks()):
        if sdl2.SDL_IsGameController(idx):
            self._open_controller(idx)

    event = sdl2.SDL_Event()
    try:
        while self.alive.is_set():
            if sdl2.SDL_WaitEventTimeout(event, self.timeout):
                self._handle_event(event)
    finally:
        for instance_id in list(self._controllers.keys()):
            self._close_controller(instance_id)
        sdl2.SDL_QuitSubSystem(sdl2.SDL_INIT_GAMECONTROLLER)

stop()

Source code in genesis_forge/gamepads/sdl2.py
def stop(self) -> None:
    self.alive.clear()

Gamepad input abstraction shared across backends.

AXIS = 'Axis' class-attribute instance-attribute

BUTTON = 'Button' class-attribute instance-attribute

HAT = 'Hat' class-attribute instance-attribute

index instance-attribute

keytype instance-attribute

name = None class-attribute instance-attribute

value = None class-attribute instance-attribute

Source code in genesis_forge/gamepads/sdl2.py
def controller_key_from_event(
    event: sdl2.SDL_Event, deadzone: float = 0.05
) -> Key | None:
    etype = event.type

    if etype == sdl2.SDL_CONTROLLERAXISMOTION:
        axis = event.caxis.axis
        raw = event.caxis.value
        limits = (0.0, 1.0) if _is_trigger_axis(axis) else (-1.0, 1.0)
        value = _rescale(raw, -32768, 32767, limits[0], limits[1])
        if not _is_trigger_axis(axis) and abs(value) < deadzone:
            value = 0.0
        return Key(Key.AXIS, axis, AXIS_NAMES.get(axis), value)

    if etype in (sdl2.SDL_CONTROLLERBUTTONDOWN, sdl2.SDL_CONTROLLERBUTTONUP):
        button = event.cbutton.button
        val = 1 if etype == sdl2.SDL_CONTROLLERBUTTONDOWN else 0
        return Key(Key.BUTTON, button, BUTTON_NAMES.get(button), val)

    return None