Input: Actions, Gamepads and Touch

The engine reports input as events and as polled state: see Keyboard Support, Mouse Support and Joystick Support. The modules on this page sit on top of that and add what most games write for themselves: named actions with one-step edges, a gamepad read by polling, and on-screen controls for the pointer and the fingers.

Actions with InputMap

acron.InputMap maps action names to keys. The actions are plain data, given when the map is made:

from direct.showbase.ShowBase import ShowBase
from acron import InputMap

base = ShowBase()
inputs = InputMap(
    base,
    held={
        "move_left": ["a", "arrow_left"],
        "move_right": ["d", "arrow_right"],
        "aim": ["mouse3"],
    },
    edges={"jump": ["space"], "pause": ["escape"]},
    edge_from_held=("aim",),
)

There are two kinds of action:

  • A held action is down while any of its keys is down. Read it with is_action_pressed(action), or get_action_strength(action), which is 0.0 or 1.0 for a key.

  • An edge action fires once per press. Read it with is_action_just_pressed(action). One key may belong to several edge actions.

A held action named in edge_from_held gets edges as well: call update_edges() once at the top of your input read, and is_action_just_pressed() and is_action_just_released() report the changes since the last call.

Edges have to be cleared in exactly one place, at one rate. clear_edges() clears them after you have read them; end_step() clears the just-pressed set. acron.GameLoop calls end_step() after each fixed step when it is given the map (GameLoop(..., input_map=inputs)), so a just-pressed action lasts exactly one step. Do not also clear them per render frame, or presses are lost or seen twice.

Mouse look

capture_mouse() (or capture_mouse=True when the map is made) hides the cursor, and mouse_delta() returns how far the pointer moved since the last call, in pixels, and moves it back to the centre of the window. The window stays in M_absolute mode; the map does the centring itself. release_mouse() gives the cursor back. Window y grows downwards, so negate dy if moving the mouse down should look down.

def update(task):
    dx, dy = inputs.mouse_delta()
    heading -= dx * 0.1
    pitch -= dy * 0.1
    return task.cont

With no on-screen window (an offscreen buffer), capturing does nothing and mouse_delta() returns (0.0, 0.0).

Changing the keys

A controls screen changes the bindings while the game runs:

  • bindings() returns every action’s keys, as a copy; keys_for(action) the keys of one.

  • rebind(action, keys) gives an action exactly these keys. A held action’s state is cleared, so a key that was down under the old binding does not stay pressed. Whether an action is held or edged does not change, and an action the map does not know raises KeyError.

acron.input_map.KeyCapture listens for the next button pressed, whatever it is, and hands its name to a callback once – "g", "mouse1", "arrow_up", "lshift". Modifier prefixes are taken off (shift-g is g), bare modifiers and the mouse wheel are ignored, and a cancel key (by default escape) calls the callback with None:

from acron.input_map import KeyCapture

def on_key(name):
    if name is not None:
        inputs.rebind("jump", [name])

capture = KeyCapture(base, on_key)
capture.start()      # stop() ends it early

Other listeners keep hearing their own events while it listens. Where there is no window, a test presses a key with messenger.send(KeyCapture.EVENT, [name]).

Gamepads

acron.gamepad reads a gamepad by polling it, rather than through button events.

GamepadReader(device) wraps one InputDevice:

  • axis(name) is the value of an axis, by its InputDevice.Axis name ("left_x", "right_y", …).

  • pressed(name) is whether a button is down, by its GamepadButton name ("face_a", "start", …).

  • trigger(axis, button) is a trigger’s travel from 0 to 1. It reads the axis when the driver reports one and the button when it does not, and remaps drivers whose triggers rest at -1.

  • latch(edge_pairs, held_pairs=()) updates held and edges for this frame from (button, action) pairs. An action in edge_pairs is held while its button is down and edges once on the press; one in held_pairs is only ever held (a brake that must not fire as a one-shot).

The reader returns the driver’s values unchanged. The engine reverses a gamepad’s y axes so that up is positive; which axes your game flips is up to the game.

dead_zone(v, zone) is 0 inside the zone and rescales the rest so that the stick still reaches 1.

PadSeat gives the game one gamepad: the first one connected takes the seat, later ones are ignored, and when the seated pad is disconnected another connected pad takes its place. Forward the engine’s device events to it and read seat.pad:

from acron.gamepad import PadSeat, dead_zone

seat = PadSeat(base)
base.accept("connect-device", seat.connect)
base.accept("disconnect-device", seat.disconnect)
for device in base.devices.get_devices():
    seat.connect(device)

def read_pad(task):
    pad = seat.pad
    if pad is not None:
        move_x = dead_zone(pad.axis("left_x"), 0.15)
        throttle = pad.trigger("right_trigger", "rtrigger")
        pad.latch([("face_a", "jump")])
        if "jump" in pad.edges:
            jump()
    return task.cont

base.task_mgr.add(read_pad, "read-pad")

PadSeat(base, wrap=...) builds the seated object with your own class (a subclass of GamepadReader, usually); it must keep the device on .device. Connecting a pad attaches it to the data graph with the event prefix gamepad (the prefix argument), so its button events do not mix with the keyboard’s.

On-screen thumbsticks with the pointer

acron.touch.TouchControls draws two thumbsticks and two columns of buttons on the HUD, and is played with the pointer: a press, a drag and a release. The left stick is a position stick; the right one is a look stick, meant to feed a turning rate. A press takes hold of what is under it until it is released, so a thumb that slides off the ring keeps steering.

from acron.touch import TouchControls

touch = TouchControls(
    base,
    left_column=[("map", "MAP")],
    right_column=[("fire", "FIRE"), ("reload", "RELOAD", True)],
)
touch.active = True
touch.build()                      # after the HUD, so it draws on top
touch.set_visible(True)

def update(task):
    touch.place(width, height)     # the window, in authored pixels
    touch.pointer(down, px, py)    # pointer state, from the top-left
    touch.draw()                   # uploads only when something moved
    steer, throttle = touch.lx, -touch.ly
    if "fire" in touch.edges:
        fire()
    touch.end_frame()
    return task.cont

A column entry is (action, caption) or (action, caption, wide). The sticks are lx, ly, rx and ry in -1..1, with y down; held and edges are the buttons’ actions, and edges last until end_frame(). scale (and set_scale() followed by rebuild()) follows a HUD-size setting; the sticks follow it at half strength, so they stay large enough for a thumb. thumb_zone is how much of the bottom of the screen the controls take, which is how far to lift the rest of the HUD while they are shown. The cards are drawn with acron.hud.

The overlay reads one pointer. For a phone or tablet, where every finger counts, use the engine’s touch gamepad below.

The touch gamepad

TouchGamepad is part of the engine: a gamepad drawn on a touch screen and played with the fingers – a stick where a finger lands on the left half of the screen, a look area on the right, and buttons. It is an InputDevice of the gamepad class, and setup_window() registers it with the input device manager, which sends connect-device for it. A game that reads gamepads (with PadSeat above, for instance) therefore plays on a phone with no gamepad attached.

Whether to offer it is GraphicsWindow.has_touch_screen(): true when the window is on a touch screen that is the device’s main input, such as a phone or a tablet.

from acron.core import TouchGamepad

if base.win.has_touch_screen():
    pad = TouchGamepad()
    pad.set_default_layout()   # A B X Y, shoulders, start, back, D-pad
    pad.setup_window(base.win)

By default it is shown while the window’s mouse mode is relative (during play) and hidden otherwise, so menus are tapped and play has the pad. It can be given buttons of its own (add_button()), report the look finger as a mouse-look delta (LM_delta and get_look_delta()), and be themed with images, a font and an opacity. All of that, and how the fingers act as the mouse and in the GUI, is described under The touch gamepad on the Android page.