libgdx-controllers
Scannednpx machina-cli add skill kyu-n/gdx-claude-skills/libgdx-controllers --openclawlibGDX 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
// Get all currently connected controllers
Array<Controller> controllers = Controllers.getControllers();
// Never assume a controller is available at startup
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();
// Buttons — use these named constants
mapping.buttonA // Xbox A / PS Cross
mapping.buttonB // Xbox B / PS Circle
mapping.buttonX // Xbox X / PS Square
mapping.buttonY // Xbox Y / PS Triangle
mapping.buttonL1 // Left bumper / L1
mapping.buttonR1 // Right bumper / R1
mapping.buttonL2 // Left trigger as button / L2
mapping.buttonR2 // Right trigger as button / R2
mapping.buttonLeftStick // Left stick click / L3
mapping.buttonRightStick // Right stick click / R3
mapping.buttonStart // Start / Options
mapping.buttonBack // Back / Select / Share
mapping.buttonDpadUp // D-pad directions
mapping.buttonDpadDown
mapping.buttonDpadLeft
mapping.buttonDpadRight
// Axes — return float from -1.0 to 1.0
mapping.axisLeftX // Left stick horizontal
mapping.axisLeftY // Left stick vertical
mapping.axisRightX // Right stick horizontal
mapping.axisRightY // Right stick vertical
Polling Input
// In render() — poll current state
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() {
// Check for already-connected controllers
Array<Controller> controllers = Controllers.getControllers();
if (controllers.size > 0) {
activeController = controllers.first();
}
// Listen for connect/disconnect
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;
// Fall back to another controller if available
Array<Controller> remaining = Controllers.getControllers();
if (remaining.size > 0) activeController = remaining.first();
}
}
});
}
public void update(float dt) {
// Always support keyboard as fallback
handleKeyboard(dt);
if (activeController == null) return;
ControllerMapping m = activeController.getMapping();
// Analog stick with dead zone
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;
// Buttons via mapping — works across Xbox, PlayStation, etc.
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
ControllerListenerhaspovMoved,xSliderMoved,ySliderMoved, oraccelerometerMoved, you're using the deprecated 1.x API. The 2.x listener has only 5 methods. Check your Gradle dependency: the group must becom.badlogicgames.gdx-controllers, notcom.badlogic.gdx. - Hardcoding button/axis numbers —
controller.getButton(0)orcontroller.getAxis(1)will break across controllers and platforms. Always usecontroller.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 —
ControllerMappingalready 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.leftXdo NOT exist. The correct API iscontroller.getMapping().buttonA,controller.getMapping().axisLeftX. - Only listening per-controller —
controller.addListener()does not receive connect/disconnect events. UseControllers.addListener()for global events including hotplug.
Source
git clone https://github.com/kyu-n/gdx-claude-skills/blob/master/skills/libgdx-controllers/SKILL.mdView on GitHub Overview
LibGDX gdx-controllers 2.x extension enables controller discovery, event driven input, and cross controller mapping. It normalizes button and axis codes via ControllerMapping, supports multiple controllers, and helps debug controllers not detected or mis-mapped. The 1.x API is deprecated in favor of the streamlined 2.x approach.
How This Skill Works
You query connected controllers with Controllers.getControllers and respond to connect and disconnect events. For each controller, you obtain a ControllerMapping from controller.getMapping which exposes named constants for buttons and axes, avoiding raw indices. Use these mappings to implement input handling that works across different controllers and platforms, including multi controller setups and D pad via mapping buttons.
When to Use It
- Debugging a controller not detected at startup
- Implementing cross device button/axis handling with mapping constants
- Building multiplayer support with multiple controllers
- Responding to runtime connect/disconnect events
- Addressing stick drift by applying dead zones
Quick Start
- Step 1: Add the 2.x gdx-controllers dependencies for core and platform modules (core, desktop, Android, iOS) to your Gradle build
- Step 2: At startup or when needed, fetch connected controllers with Controllers.getControllers() and handle the case where none are available
- Step 3: For a selected controller, obtain its mapping with controller.getMapping() and use mapping.buttonA, mapping.axisLeftX, etc to drive your input logic
Best Practices
- Never hardcode button or axis numbers; rely on ControllerMapping
- Always null-check the controllers list and handle dynamic connect/disconnect
- Listen for connect and disconnect events to keep input state accurate
- Use mapping buttonDpadUp/Down/Left/Right for D pad input
- Apply dead zones to axis input to prevent drift and jitter
Example Use Cases
- Log the names of all currently connected controllers on startup
- Trigger an action using mapping.buttonA to support different controller layouts
- Move a character using mapping.axisLeftX and mapping.axisLeftY instead of raw indices
- Update UI when a controller connects or disconnects at runtime
- Diagnose mapping mismatches by printing mapping values for debugging