gamepad

tape
@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

FunctionDoes
gamepad.available() -> void or GamepadErrorUnsupported 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) -> *PadSlot 0 to gamepad.MAX_PADS - 1 (8 slots; traps outside). The same Pad each time
gamepad.count() -> i64How 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.

MethodIs
p.connected() -> boolA pad is in this slot
p.connected_now() -> bool, p.disconnected_now() -> boolIt arrived, or left, in the last poll
p.name() -> []const u8What the platform calls it; empty with no pad here. XInput names nothing: "XInput controller"
p.down(b) -> boolButton b is down
p.pressed(b) -> bool, p.released(b) -> boolIt went down, or came up, since the last poll
p.stick(which) -> math3d.Vec2A stick after its deadzone: x > 0 right, y > 0 up, length up to 1
p.axis(a) -> f32One axis: sticks -1 to 1, triggers 0 to 1, after the deadzones
tape
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

tape
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.

tape
pub struct Deadzones { stick: f32 = 0.15; trigger_inner: f32 = 0.05; trigger_outer: f32 = 0.95; }
tape
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

tape
let can = p.rumble(1.0, 0.0, 300);       // the heavy motor, 300 ms
p.rumble(0.0, 0.0, 0);                   // stop

p.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

ThroughNotes
WindowsXInput, xinput1_4.dll or xinput9_1_0.dll; 4 padsPads 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
Linuxevdev, /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
macOSGameController, its extended profileDelivers only to the frontmost app. No rumble yet
Uusiavailable() 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:

tape
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 1

gamepad.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:

tape
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 due

Last modified: