Files
horizon-airdef/docs/controllers.md
T
Ludovic Marie 0818190ee4 docs: controller support design note
Vanilla 26.3 has no gamepad code; Horizon's own design follows Bedrock's
default controller layout. Mapping, menus, glyphs, options file, rumble,
Steam Deck and the PR plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:06:50 +02:00

7.9 KiB

Controllers

Horizon plays with PlayStation (DualShock 4, DualSense), Xbox (360, One, Series) and Nintendo Switch Pro controllers, and on the Steam Deck with its built-in controls. Keyboard and mouse keep working exactly as before: with no controller connected, or with controllers turned off, nothing changes.

What vanilla has

Nothing. Minecraft Java 26.3 never calls GLFW's gamepad or joystick API, has no controller options and ships no button glyphs. Its only non-mouse UI input is keyboard focus navigation (InputType.KEYBOARD_ARROW / KEYBOARD_TAB: arrows and Tab move a focus ring between widgets). So controller support is Horizon's own feature. It follows what players already know from Minecraft on consoles (Bedrock Edition's default controller layout) and Horizon's menu design for any new screen.

Input layer

gilrs reads the pads: evdev on Linux (with libudev for hot-plugging), IOKit on macOS, Windows.Gaming.Input on Windows. Its SDL mapping database normalizes every pad to one positional layout (South/East/West/North face buttons, bumpers, triggers, sticks, D-pad, Select/Start). No second windowing or event stack: gilrs is polled once per loop iteration next to winit's events.

  • The pure logic (mapping table, deadzones and curves, button edges, the settings file, glyph choice) lives in crates/horizon-client/src/gamepad.rs and is unit-tested. The game owns a thin gilrs backend and applies actions.
  • Controller actions go through the same path a keyboard key takes after rebinding: an action names a vanilla key mapping (key.jump, key.attack, ...), and the game handles it as that mapping's default key or mouse button. Toggle sneak/sprint/attack/use, screens releasing held keys, and the multiplayer client all behave as they do on the keyboard.
  • Movement is analog: the left stick's direction and tilt feed the player's move vector directly (a half-tilted stick walks slowly, like Bedrock), instead of being rounded to WASD. The forward/back/left/right flags other code reads (sprint start, boats, minecarts, the server's input packet) are set when the stick passes a threshold.
  • The pad last touched is the active one. Several pads are not merged.
  • horizon-ctl pad ... injects a virtual pad (buttons, sticks, triggers) into a running game, for tests without hardware.

Default mapping

Action Xbox PlayStation Mapping
Move Left stick Left stick analog key.forward/back/left/right
Look Right stick Right stick camera
Jump / fly up A ✕ key.jump
Drop item B ○ key.drop
Swap offhand X □ key.swapOffhand
Inventory Y △ key.inventory
Hotbar left / right LB / RB L1 / R1 Horizon pad.hotbarPrevious/Next
Use / place LT L2 key.use
Attack / break RT R2 key.attack
Sprint Left stick click L3 key.sprint
Sneak (toggles) Right stick click R3 key.sneak
Change camera D-pad up D-pad up key.togglePerspective
Pick block D-pad left D-pad left key.pickItem
Chat D-pad right D-pad right key.chat
Advancements D-pad down D-pad down key.advancements
Player list (hold) View Create / Share key.playerlist
Pause / back Menu Options Escape (fixed)

The Guide / PS / Steam button is left to the system. Sneak on a stick click toggles by default (holding a stick in while steering the camera is awkward, which is why Bedrock does the same); the controls options can make it hold-to-sneak.

Menus and inventories

Vanilla's screens are all mouse-driven, and Horizon draws them 1:1, so a pad drives them through a virtual cursor (as Bedrock's classic inventory UI and Java's controller mods do), not focus navigation:

  • Left stick moves the cursor with acceleration; in container screens the cursor is gently pulled toward the nearest slot centre.
  • D-pad steps one slot (18 GUI pixels in containers) or one widget row in menus and snaps to it.
  • A = left click (take / place stack, press a button), X = right click (half stack / place one), Y = quick move (shift-click), B = back / close.
  • LB / RB switch creative tabs and recipe book pages; right stick scrolls (creative grid, lists, chat).
  • Sliders: A grabs, the stick or D-pad left/right moves, A releases.
  • Text fields: the Steam Deck's keyboard (Steam + X) types into them like a real keyboard. Other platforms have no on-screen keyboard yet; text input needs a keyboard.

The cursor is drawn only while the pad is the last-used input; moving the mouse hands control back immediately.

Glyphs

Horizon draws its own button glyphs in the HUD and screens (no Mojang assets, nothing new from the player's jar): a small round or pill shape with the label in the game font, sized to the GUI scale, with two styles:

  • Xbox / Steam Deck / generic: A B X Y in their usual colours, LB RB LT RT, View, Menu, stick icons.
  • PlayStation: ✕ ○ □ △ drawn as shapes in their colours, L1 R1 L2 R2, Create/Share, Options.
  • Nintendo: B A Y X by position.

The style comes from the pad's USB vendor (Sony 054c, Microsoft 045e, Nintendo 057e, Valve 28de) unless the controls options force one. Glyphs replace key names where a prompt names a key (the controls list, tutorial hints, "Press X to dismount") only while the pad is the last-used input.

Options and remapping

Controller settings are Horizon's own and live in horizon-controller.txt next to options.txt, like horizon-graphics.txt: options.txt stays exactly vanilla's, so a vanilla client can share the game folder.

enabled:true
vibration:true
lookSensitivity:0.5
invertX:false
invertY:false
moveDeadzone:0.15
lookDeadzone:0.12
triggerThreshold:0.4
toggleSneak:true
glyphs:auto
bind_key.jump:south
bind_pad.hotbarNext:rb

Only bindings that differ from the defaults are written. The controls options get a Controller page: on/off, vibration, look sensitivity, invert X/Y, deadzones, glyph style, sneak toggle, and a binding list like the key binds screen (pick an action, press a button; conflicts marked as vanilla does; reset).

Deadzones are radial (the stick's length, so diagonals aren't clipped) and the remaining range is rescaled to 0..1. Look uses a squared curve for precise aim near the centre, up to 200°/s yaw and 140°/s pitch at the default sensitivity, slower while scoping with a spyglass.

Rumble

Vibration (on by default, off in the options) through gilrs force feedback: taking damage (stronger with the damage), a block broken, an explosion nearby (scaled by distance), a totem of undying, an elytra boost, a fish biting. Short, low-intensity pulses, as on Bedrock. Linux (evdev force feedback, including Steam Input's virtual pad) and Windows support it; macOS gilrs has no force feedback, so it is silently skipped there.

Steam Deck

  • In Game Mode, or any game launched through Steam, Steam Input presents the Deck's controls as a virtual Xbox pad: Horizon sees it like any Xbox pad and uses Xbox-letter glyphs (the Deck's face buttons are A B X Y). Steam's default "Gamepad" layout works with no setup; the trackpads stay mouse.
  • Started outside Steam on the Deck, the kernel's hid-steam driver exposes the controls as a gamepad too (while Steam isn't holding them), so it also works out of the box.
  • Text entry uses Steam's keyboard (Steam + X).

PR plan

  1. Input layer and gameplay (this note, gilrs, defaults, movement, camera, actions, hotbar, B/Start to leave screens, horizon-ctl pad, horizon-controller.txt with the look/deadzone settings).
  2. Menus and inventories: virtual cursor, slot snapping, clicks, quick move, tabs, scrolling, sliders.
  3. Glyphs and the Controller options page: glyph drawing per pad type, the options page, remapping UI.
  4. Rumble.

Each is shippable alone. Testing is with unit tests, the injected pad and vscreen screenshots; each PR says what still needs a real controller.