Input: Actions, Gamepads and Touch¶
The acron framework modules on this page (acron.input_map,
acron.gamepad and acron.touch) are Python; they ship in the
acron wheel. The touch gamepad at
the end of the page is part of the engine and works the same from C++.
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), orget_action_strength(action), which is0.0or1.0for 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 raisesKeyError.
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 itsInputDevice.Axisname ("left_x","right_y", …).pressed(name)is whether a button is down, by itsGamepadButtonname ("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=())updatesheldandedgesfor this frame from (button, action) pairs. An action inedge_pairsis held while its button is down and edges once on the press; one inheld_pairsis 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.
#include "touchGamepad.h"
GraphicsWindow *win = window->get_graphics_window();
if (win != nullptr && win->has_touch_screen()) {
PT(TouchGamepad) pad = new TouchGamepad;
pad->set_default_layout(); // A B X Y, shoulders, start, back, D-pad
pad->setup_window(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.