Play Minecraft Java with any gamepad.
Lightweight, zero-allocation Fabric mod with analog look, full GUI navigation, creative double-tap fly and a virtual keyboard.
Minecraft has no native gamepad support on Java. Existing solutions ship 7k+ lines of code and load 4 controller layouts even if you only own one stick. BetterController does one job — make a gamepad feel native — and tries to stay small, predictable and allocation-free while doing it.
- Plug-and-play. Auto-detects Xbox, PlayStation, Switch Pro and generic XInput controllers, swaps glyphs accordingly, survives hot-disconnect.
- Analog everything. Per-axis deadzone, anti-deadzone, response curve, adaptive smoothing on the right stick.
- Full GUI navigation. Inventories, chests, creative search, options, world list, virtual keyboard for chat. Right stick scrolls scrollable screens (creative inventory, world list).
- Creative double-tap fly. The original toggle works — no synthetic jump pulses fighting Minecraft's native detection.
- 5-slider settings screen. Movement deadzone, look X / Y sensitivity, look speed multiplier, trigger threshold. Everything else in JSON.
- Debug overlay (
F8) showing raw axes, triggers, processed vectors, pressed buttons. - No vibration noise, no radial menu bloat. Removed on purpose — see Why no rumble? below.
- Fabric Loader for Minecraft 1.21.11.
- Fabric API in
mods/. bettercontroller-<version>.jarfrom the latest release inmods/.- Launch. Plug a controller. Done.
| Action | Button |
|---|---|
| Movement | Left stick |
| Look | Right stick |
| Menu scroll | Right stick Y |
| Jump | A / Cross |
| Sprint | L3 |
| Sneak | R3 |
| Attack / mine | RT |
| Use / place | LT |
| Inventory | Y / Triangle |
| Drop item | B / Circle |
| Swap hands | X / Square |
| Pick block / Hotbar next | RB |
| Hotbar previous | LB |
| Open chat | D-pad ↑ |
| Toggle perspective | D-pad ↓ |
| Pause | Start |
| Player list | Back / Select |
Menus: D-pad / left stick navigate, A confirms, B goes back, RB/LB switch tabs, RT/LT page, right stick scrolls.
The runtime config is at config/bettercontroller.json and reloads live
within ~500 ms.
The in-game settings screen
(Options → Controls → Controller Settings, or the button on the pause
menu) has the five sliders most users want:
- Movement deadzone
- Look sensitivity X
- Look sensitivity Y
- Look speed multiplier
- Trigger threshold
Everything else — response curve, smoothing, bindings, axes — lives in the
JSON. See
src/main/resources/bettercontroller.default.json
for the full schema with comments.
- Aliases supported:
A/CROSS/SWITCH_A/SOUTH,LB/L1,RT/R2, ... - Prefix an axis token with
-to invert it (e.g."-RIGHT_Y"). - An action can have multiple bindings:
"hotbar_next": ["RB", "RT"].
| Goal | Try |
|---|---|
| Snappier camera | lookSpeedMultiplier 2.5–3.0 |
| Less stick drift | movementDeadzone 0.14–0.18 |
| Softer center, fast outer | lookResponseCurve exponential_light |
| Faster menu navigation | menuInitialRepeatDelayMs 90, menuRepeatIntervalMs 30 |
| Slower triggers feel laggy | Lower triggerThreshold to 0.35 |
ControllerPoller
│ ControllerSnapshot
▼
InputTranslator ───► GameplayInputFrame (mutable, reused per tick)
│
├──► MinecraftInputApplier keybindings + analog vector
│
└──► ControllerGuiNavigationHooks
│
▼
GuiNavigationController
├── SlotNavigator (handled screens)
├── WidgetNavigator (clickable widgets)
├── CreativeTabNavigator (creative inventory tabs)
└── CursorCaptureManager (OS cursor)
- All functions stay under 60 lines.
- Zero allocation in the steady-state tick loop.
GameplayInputFrameis a single mutable instance,input.movementVectoris mutated in place, andinput.playerInputis left toKeyboardInput.tickso Minecraft's native rising-edge detection (used by creative double-tap fly) keeps working. - No reflection.
setSelectedTabon the creative inventory is reached through a Mixin Accessor / Invoker. - All loops bounded. Fixed-size snapshots, finite widget / slot lists.
- Validate at boundaries, not in hot paths.
# Windows
.\gradlew.bat build
.\gradlew.bat runClient
# Linux / macOS
./gradlew build
./gradlew runClientArtifacts land in build/libs/bettercontroller-<version>.jar.
Requires JDK 21 (Temurin, JBR, GraalVM — anything Java 21).
The repo tracks several Minecraft versions in parallel:
| Branch | Tracks | Status |
|---|---|---|
main |
Latest supported Minecraft version | stable |
develop |
Active development for the latest version | active |
fabric-1.21.11 |
Frozen archive — Fabric 0.18.4, MC 1.21.11 | maintenance |
When Minecraft cuts a new major (e.g. 26.x), main migrates to it and the
previous line gets a fabric-<version> branch so old releases stay
buildable and patchable. To build a specific version, check out the
matching branch:
git checkout fabric-1.21.11
./gradlew buildTags (v0.1.0, etc.) always point to the commit that produced a published
release, regardless of which version branch it lives on.
GLFW (the input backend Minecraft ships) doesn't expose a cross-platform rumble API. The old codebase carried a full haptics architecture that silently no-op'd. We deleted it. If LWJGL adds rumble support in a future release, it'll come back as a clean addition rather than dead architecture.
It was a custom feature competing with the vanilla hotbar and inventory. Both are already perfectly usable with a controller via this mod, so the radial menu added complexity without enough value. Removed.
The previous version duplicated bindings four times (Xbox / PlayStation / Switch / generic) to display friendly button names. Now the bindings are unified (generic token names) and the glyphs still adapt to your controller. Same UX, ~400 fewer lines of config.
Open an issue
with the GLFW name and GUID — F8 debug overlay shows both. Detection is
heuristic and easy to extend.
See CONTRIBUTING.md. The TL;DR:
- Branch from
develop, targetdevelopin your PR. - One concern per PR.
- Test with a real controller and describe what you tested.
Read CODE_OF_CONDUCT.md and SECURITY.md for the rest.
MIT — do what you want, just keep the copyright notice.
{ "bindings": { "jump": ["A"], "attack": ["RT"], "menu_confirm": ["A"] }, "axes": { "move_x": "LEFT_X", "move_y": "LEFT_Y", "look_x": "RIGHT_X", "look_y": "RIGHT_Y" } }