gamepad
@profile(t1);
import gamepad from "gamepad";
import io from "io";
import time from "time";
pub fn main() -> i32 {
gamepad.available() or {
io.println("{err}");
return 1;
};
var frames: i64 = 0;
while (frames < 60) {
gamepad.poll(); // once a frame; never blocks
let p = gamepad.pad(0);
if (p.connected_now()) { io.println("hello, {p.name()}"); }
if (p.pressed(gamepad.Button.South)) { // Xbox's A, PlayStation's Cross
io.println("jump");
p.rumble(0.8, 0.2, 150);
}
let move = p.stick(gamepad.Stick.Left); // y > 0 is up, everywhere
if (move.x != 0.0 || move.y != 0.0) { io.println("moving {move.x} {move.y}"); }
time.sleep(time.millis(16));
frames = frames + 1;
}
gamepad.close();
return 0;
}The same layout on Windows (XInput), Linux (evdev) and macOS (GameController). With no pad attached everything says false and zero; that isn’t an error.
Reading pads
| Function | Does |
|---|---|
gamepad.available() -> void or GamepadError | Unsupported where the platform has no gamepads yet (Uusi); no pad attached isn’t an error |
gamepad.poll() | Once a frame: every slot read, pads that came and went noticed, timed rumble stopped. Never blocks. Everything else says what the last poll saw |
gamepad.pad(slot) -> *Pad | Slot 0 to gamepad.MAX_PADS - 1 (8 slots; traps outside). The same Pad each time |
gamepad.count() -> i64 | How many pads are connected |
gamepad.close() | Every motor stopped, every device let go; a later poll finds them again |
A pad keeps its slot as long as it stays connected. On Windows the slot is XInput’s user number when it is free, the light on the pad.
| Method | Is |
|---|---|
p.connected() -> bool | A pad is in this slot |
p.connected_now() -> bool, p.disconnected_now() -> bool | It arrived, or left, in the last poll |
p.name() -> []const u8 | What the platform calls it; empty with no pad here. XInput names nothing: "XInput controller" |
p.down(b) -> bool | Button b is down |
p.pressed(b) -> bool, p.released(b) -> bool | It went down, or came up, since the last poll |
p.stick(which) -> math3d.Vec2 | A stick after its deadzone: x > 0 right, y > 0 up, length up to 1 |
p.axis(a) -> f32 | One axis: sticks -1 to 1, triggers 0 to 1, after the deadzones |
let p = gamepad.pad(0);
if (p.down(gamepad.Button.DPadLeft)) { x = x - speed * dt; }
if (p.released(gamepad.Button.Start)) { paused = !paused; }
let throttle = p.axis(gamepad.Axis.RightTrigger);
let look_x = p.axis(gamepad.Axis.RightX);The layout
pub enum Button : u8 {
South = 0; East = 1; West = 2; North = 3; // by place: South is A or Cross
LeftBumper = 4; RightBumper = 5;
Back = 6; Start = 7; Guide = 8; // View, Menu, the logo
LeftStick = 9; RightStick = 10; // the stick pressed in
DPadUp = 11; DPadDown = 12; DPadLeft = 13; DPadRight = 14;
}
pub enum Axis : u8 { LeftX = 0; LeftY = 1; RightX = 2; RightY = 3; LeftTrigger = 4; RightTrigger = 5; }
pub enum Stick : u8 { Left = 0; Right = 1; }The system may keep Guide for itself.
Deadzones
A worn stick doesn’t rest at zero. Inside its deadzone a stick reads zero, and the
rest of its travel scales to 0..1, keeping the direction (a circle, not a square, so
a stick pushed nearly straight up doesn’t lose its small x). A trigger reads 0 up to
trigger_inner and 1 from trigger_outer.
pub struct Deadzones { stick: f32 = 0.15; trigger_inner: f32 = 0.05; trigger_outer: f32 = 0.95; }let p = gamepad.pad(0);
p.set_deadzones(gamepad.Deadzones { stick: 0.25; }); // an old pad
let z = p.deadzones();They belong to the slot. set_deadzones traps on a stick outside 0..<1, or on
triggers that aren’t 0 <= inner < outer <= 1.
Rumble
let can = p.rumble(1.0, 0.0, 300); // the heavy motor, 300 ms
p.rumble(0.0, 0.0, 0); // stopp.rumble(low, high, ms) -> bool: low the heavy motor, high the light one, each
0.0 to 1.0, for ms milliseconds; a later call replaces it. Returns whether the pad
can: false with no pad here or one without motors, and on macOS for now. The stop comes
from poll, so keep polling, or stop it yourself; it comes when the pad leaves and at
close too. On Windows call close before the program ends: XInput’s motors don’t
stop on their own.
Platforms
| Through | Notes | |
|---|---|---|
| Windows | XInput, xinput1_4.dll or xinput9_1_0.dll; 4 pads | Pads that don’t present themselves as XInput (a DualSense without a driver) aren’t seen yet. An empty slot is asked only now and then, so a pad plugged in appears within 2 s |
| Linux | evdev, /dev/input/event* | A pad needs read permission on its node (rumble needs write): usually the input group or a udev rule. Without it the pad isn’t there. Pads and drivers outside the kernel’s standard codes may have some buttons or axes elsewhere |
| macOS | GameController, its extended profile | Delivers only to the frontmost app. No rumble yet |
| Uusi | available() says Unsupported |
The library is gamepad100 (.dll, .so, .dylib), next to tape and copied beside
a built program; a program that doesn’t import gamepad doesn’t load it.
Reusing the parts
The module’s pure pieces are public, for a test or your own input layer:
let v = gamepad.radial_deadzone(raw_x, raw_y, 0.2); // a math3d.Vec2
let t = gamepad.trigger_deadzone(raw_t, 0.1, 0.9);
let a = gamepad.scale_axis(value, min, max); // -1 to 1
let f = gamepad.scale_trigger(value, min, max); // 0 to 1gamepad.Raw, gamepad.map_buttons, Pad.report and gamepad.Hub are what poll
runs on: what a platform says, in its own units, into the layout. They let a test
drive a Pad with made-up states, and a Hub with made-up polls: h.setup() numbers
its slots, h.plan(count, interval_ms, now_ms) says which devices to ask (as bits),
h.step(&raws[0], seen) takes what those said and moves the slots on, and
h.due_stops(now_ms) says which slots’ timed rumble is due:
var p = gamepad.Pad { .. };
var r = gamepad.Raw { .. };
r.format = gamepad.RAW_XINPUT;
r.id = 1;
r.buttons = 0x1000; // XInput's A
r.latched = 0x1000;
p.report(&r);
@assert(p.pressed(gamepad.Button.South));
var hub = gamepad.Hub { .. };
hub.setup();
var raws: [16]gamepad.Raw;
raws[0] = r;
let ask = hub.plan(4, 500, 500); // bits: device 0 now
hub.step(&raws[0], 1); // device 0 said raws[0]
@assert(hub.pads[0].connected_now());
@assert(hub.free_slot(-1) == 1); // slot 0 is taken
hub.pads[0].rumble_stop_ms = 100;
@assert(hub.due_stops(100) == 1); // slot 0's rumble is dueLast modified: