Skip to content

feat: gamepad input - #12

Merged
LucasAntunesdeAlmeida merged 1 commit into
masterfrom
feat/gamepad
Sep 29, 2026
Merged

LucasAntunesdeAlmeida merged 1 commit into
masterfrom
feat/gamepad

Conversation

@LucasAntunesdeAlmeida

Copy link
Copy Markdown
Owner

What

  • g.PadDown(b PadButton) bool: is a button held on any connected gamepad with a standard layout?
  • g.PadAxis(a PadAxis) float64: a stick axis, raw -1..1, Y down, no dead zone; with several pads the one pushed furthest answers.
  • g.PadConnected() bool: is a standard-layout gamepad connected? For switching on-screen prompts.
  • Constants: PadA, PadB, PadX, PadY, PadLB, PadRB, PadLT, PadRT, PadBack, PadStart, PadLStick, PadRStick, PadUp, PadDown, PadLeft, PadRight; axes PadLeftX, PadLeftY, PadRightX, PadRightY.
  • Agents: Action.Pad []PadButton, Action.StickX, Action.StickY; MCP act gains pad (button names), stickX, stickY.
  • README cheat-sheet rows, agent docs (README + AGENTS.md), horde README section.

Design

  • Aliases, like Key. PadButton = ebiten.StandardGamepadButton and PadAxis = ebiten.StandardGamepadAxis, exactly how Key = ebiten.Key works: no translation table, and the constants read like the rest of the input API (engine.PadA next to engine.Esc).
  • Standard layout only. Only pads with a standard mapping (SDL controller DB: Xbox, PlayStation, Switch Pro, Steam Deck, Steam Input virtual pads, most others) count, for buttons, axes and PadConnected. A pad without one has no known button meaning, so reading it would be guesswork. Buttons map by position: PadA is the bottom face button (Cross on PlayStation, B on Nintendo).
  • Any pad. PadDown ORs every standard pad; PadAxis takes the value with the largest magnitude, so a second idle controller (or a slightly drifting one) never cancels the one in use. Local multiplayer with per-pad reads is left for later; nothing here blocks adding it.
  • Triggers are buttons. PadLT/PadRT report "pulled" by the standard layout's threshold. The standard layout itself models triggers as buttons; analog trigger values can be added later as more axes without breaking anything. Kept out to keep the surface small.
  • Raw axes, no built-in dead zone. Dead zones are game feel (radial vs per-axis, size, analog speed); a built-in per-axis clamp would make a radial dead zone impossible. The doc comment and the cheat sheet say to ignore small tilts, and the example shows a 0.2 radial dead zone with analog speed. Y grows downward like screen coordinates, so hero.Move(x*speed, y*speed) just works.
  • Testability. The four device reads (AppendGamepadIDs, IsStandardGamepadLayoutAvailable, IsStandardGamepadButtonPressed, StandardGamepadAxisValue) are swappable package vars, like PR feat: held pointer and window focus: MouseDown and Focused #6's pointer ones. The ID list reuses one slice, so reading pads allocates nothing per frame.
  • Agents. Action.Pad holds buttons for the frame; StickX/StickY set the left stick, clamped to -1..1 (NaN centers). Agents get only the left stick, mirroring how they get one pointer; aiming stays with keys or the mouse. The agent's pad "connects" on the first action that uses it and stays connected for the run (Headless starts a new run with none): a game that shows pad prompts, or pauses on pad disconnect, must not flicker as the agent lets go of the stick. Headless play never reads real controllers (it uses injected input only), so without pad actions there is no pad. Windowed MCP (mixedInput) ORs agent and real buttons, and the agent's stick answers while pushed, else the real pads.
  • MCP. act takes pad: ["A", "Start", ...] (the constant names without Pad) and stickX/stickY; unknown names error listing the valid ones, like keys. The act argument parsing moved into g.actArgs so it is unit tested.

Proof

  • pad_test.go: fake controllers (a non-standard pad is ignored; a button held on either of two pads counts; axes take the pad pushed furthest and stay raw, 0.04 stays 0.04); mixedInput merges agent and real pads; headless Step with pad actions (no pad before first use, stick clamped, NaN centered, connected for the rest of the run, fresh on Headless, and real hardware held down never leaks into headless); actArgs parses pad/stickX/stickY and errors on unknown or non-string names; the help text lists exactly the accepted names.
  • examples/horde: d-pad or left stick walks (analog speed past a 0.2 radial dead zone), Start pauses and resumes, in the pause overlay A resumes and B gives up, A starts AGAIN on game over, and the labels switch to "A RESUME" / "B GIVE UP" / "A AGAIN" while a pad is connected. The key edge detector now takes any "held" func so keys and pad buttons share it.
  • Driven over MCP headless (COLLIDER_AGENT=mcp): stickX: 1 for 60 frames moves the hero 210 px (full speed); pad: ["Start"] opens the pause overlay with "A RESUME" / "B GIVE UP"; A closes it; Start then B goes to game over showing "A AGAIN"; A restarts; pad: ["Jump"] errors listing the names.
  • Local gate: gofmt, vet, staticcheck, tests, js/wasm vet + build. SceneUpdate benchmark unchanged (0 allocs).

g.PadDown(b PadButton) reports a button held on any connected gamepad
with a standard layout (Xbox, PlayStation, Switch Pro, Steam Deck and
most others, by position). The constants cover the whole layout: PadA,
PadB, PadX, PadY, PadLB, PadRB, PadLT and PadRT (triggers pulled),
PadBack, PadStart, PadLStick, PadRStick, and the d-pad PadUp, PadDown,
PadLeft, PadRight. g.PadAxis(a PadAxis) reads PadLeftX, PadLeftY,
PadRightX or PadRightY, raw -1..1 with Y growing downward and no dead
zone; with several pads, the one pushed furthest answers.
g.PadConnected() lets a game switch its on-screen prompts.

PadButton and PadAxis alias Ebitengine's standard layout types, the
same way Key aliases ebiten.Key, so there is no translation table.
Pads without a standard mapping are ignored. The device reads are
swappable package variables, like the pointer ones, so tests fake
controllers.

Agents hold pad buttons with Action.Pad and push the left stick with
Action.StickX/StickY (clamped to -1..1, NaN centers); over MCP, act
takes "pad" (button names A, B, ..., Start, Up, Down, Left, Right) and
"stickX"/"stickY", and unknown names error with the valid list. An
agent's pad connects with the first action that uses it and stays
connected for the run, so prompts do not flicker. Headless play never
reads real controllers. Windowed MCP merges the agent's pad with the
real ones.

Horde proves it: the d-pad or the left stick walks (analog speed past
a 0.2 dead zone), Start pauses and resumes, A resumes and B gives up
in the pause overlay, A starts AGAIN after a loss, and the menu labels
switch to pad prompts while a pad is connected.
@LucasAntunesdeAlmeida
LucasAntunesdeAlmeida merged commit acae73b into master Sep 29, 2026
1 check passed
@LucasAntunesdeAlmeida
LucasAntunesdeAlmeida deleted the feat/gamepad branch September 29, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant