| name | libgdx-controllers |
| description | Use when writing libGDX Java/Kotlin code involving gamepad or controller input — gdx-controllers extension, Controller discovery, ControllerListener events, ControllerMapping for cross-controller button/axis mapping, dead zones, or multi-controller support. Use when debugging controller not detected, wrong button mappings, or stick drift. |
libGDX gdx-controllers Extension (2.x)
Reference for the gdx-controllers 2.x extension — controller discovery, input polling, event-driven input, cross-controller mapping, and common patterns.
This covers gdx-controllers 2.x (the current standalone extension). The old 1.x API bundled with libGDX core is deprecated and has a completely different interface. Do NOT use the old API.
1.x vs 2.x — Critical Differences
| Old 1.x (DEPRECATED) | New 2.x (USE THIS) |
|---|
| Gradle group | com.badlogic.gdx:gdx-controllers | com.badlogicgames.gdx-controllers:gdx-controllers-core |
| ControllerListener | 8 methods: povMoved, xSliderMoved, ySliderMoved, accelerometerMoved, etc. | 5 methods only: connected, disconnected, buttonDown, buttonUp, axisMoved |
| Mapping | None — hardcode button numbers | controller.getMapping() → ControllerMapping |
| D-pad | PovDirection enum | Buttons via mapping.buttonDpadUp/Down/Left/Right |
| Package | com.badlogic.gdx.controllers | com.badlogic.gdx.controllers (same package, different artifact) |
If you see PovDirection, xSliderMoved, ySliderMoved, or accelerometerMoved — that's the old 1.x API. Stop and switch to 2.x.
Gradle Dependencies
// In gradle.properties:
controllersVersion=2.2.5
// core module:
implementation "com.badlogicgames.gdx-controllers:gdx-controllers-core:$controllersVersion"
// desktop (lwjgl3) module:
implementation "com.badlogicgames.gdx-controllers:gdx-controllers-desktop:$controllersVersion"
// android module:
implementation "com.badlogicgames.gdx-controllers:gdx-controllers-android:$controllersVersion"
// ios module:
implementation "com.badlogicgames.gdx-controllers:gdx-controllers-ios:$controllersVersion"
The group ID is com.badlogicgames.gdx-controllers, NOT com.badlogic.gdx. The old com.badlogic.gdx:gdx-controllers is the deprecated 1.x API.
Controller Discovery
Array<Controller> controllers = Controllers.getControllers();
if (controllers.size > 0) {
Controller controller = controllers.first();
Gdx.app.log("Input", "Found: " + controller.getName());
}
Controllers may connect/disconnect at any time. Always null-check before use and listen for connect/disconnect events.
ControllerMapping — Cross-Controller Button/Axis Codes
NEVER hardcode button or axis numbers. Raw codes differ across controllers and platforms. Always use controller.getMapping():
ControllerMapping mapping = controller.getMapping();
mapping.buttonA
mapping.buttonB
mapping.buttonX
mapping.buttonY
mapping.buttonL1
mapping.buttonR1
mapping.buttonL2
mapping.buttonR2
mapping.buttonLeftStick
mapping.buttonRightStick
mapping.buttonStart
mapping.buttonBack
mapping.buttonDpadUp
mapping.buttonDpadDown
mapping.buttonDpadLeft
mapping.buttonDpadRight
mapping.axisLeftX
mapping.axisLeftY
mapping.axisRightX
mapping.axisRightY
Polling Input
ControllerMapping m = controller.getMapping();
boolean jumping = controller.getButton(m.buttonA);
float moveX = controller.getAxis(m.axisLeftX);
float moveY = controller.getAxis(m.axisLeftY);
Event-Driven Input
ControllerListener (2.x — 5 methods only)
public interface ControllerListener {
void connected(Controller controller);
void disconnected(Controller controller);
boolean buttonDown(Controller controller, int buttonCode);
boolean buttonUp(Controller controller, int buttonCode);
boolean axisMoved(Controller controller, int axisCode, float value);
}
Only 5 methods. If you see povMoved, xSliderMoved, ySliderMoved, or accelerometerMoved, you're using the old 1.x API.
ControllerAdapter
ControllerAdapter implements ControllerListener with empty defaults — override only what you need:
Controllers.addListener(new ControllerAdapter() {
@Override
public void connected(Controller controller) {
activeController = controller;
}
@Override
public void disconnected(Controller controller) {
if (activeController == controller) activeController = null;
}
@Override
public boolean buttonDown(Controller controller, int buttonCode) {
ControllerMapping m = controller.getMapping();
if (buttonCode == m.buttonA) {
player.jump();
return true;
}
return false;
}
});
Listener Scope
Controllers.addListener() — Global: receives events from ALL controllers, including connect/disconnect.
controller.addListener() — Per-controller: receives events only from that specific controller. Does NOT receive connect/disconnect.
Dead Zones
Analog sticks have hardware noise — they rarely rest exactly at 0. Apply a dead zone:
private static final float DEAD_ZONE = 0.2f;
float x = controller.getAxis(mapping.axisLeftX);
float y = controller.getAxis(mapping.axisLeftY);
if (Math.abs(x) < DEAD_ZONE) x = 0;
if (Math.abs(y) < DEAD_ZONE) y = 0;
Without dead zones, characters will drift when the stick is untouched. Values between 0.15 and 0.25 are typical thresholds.
Complete Example
import com.badlogic.gdx.controllers.*;
import com.badlogic.gdx.utils.Array;
public class GameScreen {
private Controller activeController;
private static final float DEAD_ZONE = 0.2f;
public void create() {
Array<Controller> controllers = Controllers.getControllers();
if (controllers.size > 0) {
activeController = controllers.first();
}
Controllers.addListener(new ControllerAdapter() {
@Override
public void connected(Controller controller) {
if (activeController == null) {
activeController = controller;
}
}
@Override
public void disconnected(Controller controller) {
if (activeController == controller) {
activeController = null;
Array<Controller> remaining = Controllers.getControllers();
if (remaining.size > 0) activeController = remaining.first();
}
}
});
}
public void update(float dt) {
handleKeyboard(dt);
if (activeController == null) return;
ControllerMapping m = activeController.getMapping();
float moveX = activeController.getAxis(m.axisLeftX);
float moveY = activeController.getAxis(m.axisLeftY);
if (Math.abs(moveX) < DEAD_ZONE) moveX = 0;
if (Math.abs(moveY) < DEAD_ZONE) moveY = 0;
player.x += moveX * SPEED * dt;
player.y += moveY * SPEED * dt;
if (activeController.getButton(m.buttonA)) player.jump();
if (activeController.getButton(m.buttonX)) player.attack();
if (activeController.getButton(m.buttonStart)) togglePause();
}
}
Common Mistakes
- Using the old 1.x controller API — If your
ControllerListener has povMoved, xSliderMoved, ySliderMoved, or accelerometerMoved, you're using the deprecated 1.x API. The 2.x listener has only 5 methods. Check your Gradle dependency: the group must be com.badlogicgames.gdx-controllers, not com.badlogic.gdx.
- Hardcoding button/axis numbers —
controller.getButton(0) or controller.getAxis(1) will break across controllers and platforms. Always use controller.getMapping().buttonA, .axisLeftX, etc.
- Assuming a controller is always connected — Controllers can be disconnected at any time. Null-check before use, listen for disconnect events, and always support keyboard as fallback.
- No dead zone on analog sticks — Without a dead zone (0.15–0.25), characters drift from hardware noise on idle sticks.
- Building a custom mapping system —
ControllerMapping already abstracts Xbox/PlayStation/generic differences. Don't build your own mapping layer with hardcoded per-controller button tables.
- Using invented constants —
ControllerButton.A, ControllerAxis.LeftX, Axes.leftX do NOT exist. The correct API is controller.getMapping().buttonA, controller.getMapping().axisLeftX.
- Only listening per-controller —
controller.addListener() does not receive connect/disconnect events. Use Controllers.addListener() for global events including hotplug.