Skip to content

Keyboard, mouse, and gamepads

Arepy exposes input through the Input resource. Put gameplay controls in the INPUT pipeline so intent is collected before movement and other updates.

The gamepad example visualizing sticks, triggers, buttons, and device state

The example remains useful without a connected controller because it shows connection state and the expected controls.

Pressed or down?

This distinction prevents many beginner input bugs:

Check True when Good for
is_key_pressed() The key changed from up to down this frame. Jump, fire once, toggle a menu.
is_key_down() The key remains held. Movement, aiming, charging.
is_key_released() The key was released this frame. Finish a charge or drag.
is_key_up() The key is not held. State checks.

Keyboard movement

from arepy import Input, Key
from arepy.bundle.components import RigidBody2D
from arepy.ecs import Component, Entity, Query, With


class Player(Component):
    __slots__ = ("speed",)

    def __init__(self, speed: float) -> None:
        self.speed = speed


def player_input(
    query: Query[Entity, With[Player, RigidBody2D]],
    input_device: Input,
) -> None:
    for player, body in query.iter_components(Player, RigidBody2D):
        move_x = (
            input_device.is_key_down(Key.D)
            - input_device.is_key_down(Key.A)
        )
        move_y = (
            input_device.is_key_down(Key.S)
            - input_device.is_key_down(Key.W)
        )

        body.velocity.x = move_x * player.speed
        body.velocity.y = move_y * player.speed

This mutates the existing velocity vector. It does not allocate a replacement Vec2 on every frame. Attach Player(speed=...) and one RigidBody2D during entity setup, then register player_input in SystemPipeline.INPUT.

Mouse input

from arepy import Input, MouseButton
from arepy.bundle.components import Transform
from arepy.ecs import Entity, Query, With


def mouse_input(
    query: Query[Entity, With[Transform]],
    input_device: Input,
) -> None:
    if not input_device.is_mouse_button_pressed(MouseButton.LEFT):
        return

    mouse_x, mouse_y = input_device.get_mouse_position()
    for transform, in query.iter_components(Transform):
        transform.position.x = mouse_x
        transform.position.y = mouse_y

Useful mouse methods include get_mouse_delta(), get_mouse_wheel_delta(), and the pressed/down/released/up variants for each button.

Gamepads

Do not assume controller 0 exists. Discover connected slots first:

from arepy import GamepadAxis, GamepadButton, Input


class PlayerIntent:
    __slots__ = ("jump_requested", "move_x")

    def __init__(self) -> None:
        self.jump_requested = False
        self.move_x = 0.0


# Register one instance after creating the world:
# world.add_resource(PlayerIntent())
def gamepad_input(input_device: Input, intent: PlayerIntent) -> None:
    intent.jump_requested = False
    intent.move_x = 0.0

    connected = input_device.get_available_gamepads()
    if not connected:
        return

    gamepad_id = connected[0]
    move_x = input_device.get_gamepad_axis_movement(
        GamepadAxis.LEFT_X,
        gamepad_id,
    )
    intent.move_x = move_x if abs(move_x) >= 0.15 else 0.0
    intent.jump_requested = input_device.is_gamepad_button_pressed(
        GamepadButton.FACE_DOWN,
        gamepad_id,
    )

Stick values close to zero can drift on physical controllers. Apply a deadzone before turning them into movement. The complete examples/gamepad_demo.py also shows device names, generic button labels, triggers, multiple pads, and optional vibration.

Create the state once with world.add_resource(PlayerIntent()), then register gamepad_input in SystemPipeline.INPUT. The same object carries intent into later update systems without creating a replacement each frame.

Input and ImGui together

When a text field or widget is active, ImGui may want the keyboard or mouse. After installing the imgui extra, check its IO flags before also sending that input to the game:

from arepy import Input, Key, MouseButton, imgui


class SceneInput:
    __slots__ = ("place_requested", "reset_requested")

    def __init__(self) -> None:
        self.place_requested = False
        self.reset_requested = False


# Register one SceneInput instance as a world resource during setup.
def input_with_imgui(input_device: Input, intent: SceneInput) -> None:
    io = imgui.get_io()
    intent.reset_requested = (
        not io.want_capture_keyboard
        and input_device.is_key_pressed(Key.SPACE)
    )
    intent.place_requested = (
        not io.want_capture_mouse
        and input_device.is_mouse_button_pressed(MouseButton.LEFT)
    )

This prevents the player from moving while typing into a debug panel. Register one SceneInput resource and add input_with_imgui to SystemPipeline.INPUT during world setup.

Web support

The web backend exposes keyboard state, mouse buttons, and pointer position. Gamepads and non-zero mouse delta or wheel delta are desktop features.

Next: draw the controlled entity or add an ImGui panel.