| name | libgdx-input-handling |
| description | Use when writing libGDX Java/Kotlin code involving input handling — InputProcessor, InputAdapter, InputMultiplexer, GestureDetector, polling (isKeyPressed/isTouched), coordinate unprojection, or Android back/menu key catching. Use when debugging missed input events, wrong click positions, or UI not receiving touches. |
libGDX Input Handling
Reference for polling vs event-driven input, InputProcessor, InputMultiplexer, GestureDetector, coordinate unprojection, and platform input quirks.
Polling vs Event-Driven
Polling — check input state each frame in render():
if (Gdx.input.isKeyPressed(Input.Keys.W)) player.y += speed * delta;
if (Gdx.input.isKeyPressed(Input.Keys.SPACE)) fireBullet();
if (Gdx.input.isKeyJustPressed(Input.Keys.ESCAPE)) togglePause();
if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE)) jump();
if (Gdx.input.isTouched()) {
int screenX = Gdx.input.getX();
int screenY = Gdx.input.getY();
}
if (Gdx.input.isTouched(1)) { }
if (Gdx.input.justTouched()) { }
isKeyPressed vs isKeyJustPressed: Using isKeyJustPressed for movement causes stuttering (only moves one frame per key press). Using isKeyPressed for a one-shot action like jumping causes repeated firing every frame the key is held.
Event-driven — implement InputProcessor and register it:
Gdx.input.setInputProcessor(myProcessor);
Use polling for continuous actions (movement, held buttons). Use event-driven for discrete actions (clicks, key presses, gestures).
InputProcessor Interface
All 9 methods — every one returns boolean:
public interface InputProcessor {
boolean keyDown(int keycode);
boolean keyUp(int keycode);
boolean keyTyped(char character);
boolean touchDown(int screenX, int screenY, int pointer, int button);
boolean touchUp(int screenX, int screenY, int pointer, int button);
boolean touchCancelled(int screenX, int screenY, int pointer, int button);
boolean touchDragged(int screenX, int screenY, int pointer);
boolean mouseMoved(int screenX, int screenY);
boolean scrolled(float amountX, float amountY);
}
Return value: true = event consumed (stops propagation in InputMultiplexer). false = event not handled (passes to next processor).
keyDown vs keyTyped: keyDown(int keycode) receives Input.Keys constants — use for game controls (movement, actions). keyTyped(char character) receives the Unicode character produced — use for text input. Never compare keyTyped's char against Input.Keys constants (e.g., character == Input.Keys.A compares a char against int 29, which is silently wrong).
touchDragged vs mouseMoved: touchDragged only fires while a finger/button is held down. mouseMoved fires on mouse movement with no buttons pressed (desktop only). Do not use touchDragged for hover/mouseover detection — it won't fire without a touch.
scrolled direction: amountY is positive for scroll-down, negative for scroll-up (on most platforms). Values are typically -1 or +1 per scroll notch, not pixel deltas. Common mistake: inverting the sign when implementing zoom (scroll-up should zoom in, and amountY is negative when scrolling up).
There is NO onClick(), onTap(), or onKeyPress() method. Clicks are touchDown + touchUp. For tap detection, use GestureDetector.
InputAdapter
Convenience class with all 9 methods returning false. Extend it and override only what you need — same pattern as ApplicationAdapter:
Gdx.input.setInputProcessor(new InputAdapter() {
@Override
public boolean keyDown(int keycode) {
if (keycode == Input.Keys.ESCAPE) { togglePause(); return true; }
return false;
}
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
Vector3 world = new Vector3(screenX, screenY, 0);
camera.unproject(world);
spawnEntity(world.x, world.y);
return true;
}
});
InputMultiplexer
Chains multiple InputProcessors. Dispatches events in order; stops when one returns true.
InputMultiplexer multiplexer = new InputMultiplexer();
multiplexer.addProcessor(stage);
multiplexer.addProcessor(gameInput);
Gdx.input.setInputProcessor(multiplexer);
Stage MUST be first. If game input is first, it may return true and the Stage never receives the event — UI buttons stop working. Stage returns true only when a UI actor handles the event, so game input still receives clicks on empty areas.
Key Constants
com.badlogic.gdx.Input.Keys:
| Constant | Value | Notes |
|---|
Input.Keys.A .. Z | 29..54 | Letters |
Input.Keys.NUM_0 .. NUM_9 | 7..16 | Number row |
Input.Keys.SPACE | 62 | |
Input.Keys.ENTER | 66 | |
Input.Keys.ESCAPE | 131 | Desktop; no equivalent on mobile |
Input.Keys.BACK | 4 | Android back button |
Input.Keys.MENU | 82 | Android menu button |
Input.Keys.SHIFT_LEFT/RIGHT | 59/60 | |
Input.Keys.CONTROL_LEFT/RIGHT | 129/130 | |
Input.Keys.ALT_LEFT/RIGHT | 57/58 | |
Input.Keys.UP/DOWN/LEFT/RIGHT | 19/20/21/22 | Arrow keys |
Touch / Mouse
Parameters in touchDown/touchUp/touchDragged:
screenX, screenY — screen coordinates (Y-down, origin at top-left). NOT world coordinates.
pointer — finger index for multi-touch (0 = first finger, 1 = second, etc.). Always 0 for mouse.
button — which mouse button. On touch devices, always LEFT.
Button constants (Input.Buttons):
| Constant | Value |
|---|
Input.Buttons.LEFT | 0 |
Input.Buttons.RIGHT | 1 |
Input.Buttons.MIDDLE | 2 |
Input.Buttons.BACK | 3 |
Input.Buttons.FORWARD | 4 |
Coordinate Conversion (CRITICAL)
touchDown coordinates are screen pixels (Y-down, top-left origin). To interact with game world objects, you MUST unproject through the camera:
private final Vector3 touchPos = new Vector3();
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
touchPos.set(screenX, screenY, 0);
camera.unproject(touchPos);
placeTower(touchPos.x, touchPos.y);
return true;
}
Using raw screenX/screenY for game logic is always a bug. The Y axis is flipped (screen Y-down vs world Y-up), and camera position/zoom are ignored.
With a Viewport (preferred when using FitViewport, ExtendViewport, etc. with letterboxing/pillarboxing — accounts for viewport offset automatically):
private final Vector2 touchPos2 = new Vector2();
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
touchPos2.set(screenX, screenY);
viewport.unproject(touchPos2);
placeTower(touchPos2.x, touchPos2.y);
return true;
}
Note: viewport.unproject() takes a Vector2. camera.unproject() takes a Vector3. Don't mix them up.
GestureDetector
Wraps an InputProcessor to detect complex gestures. GestureDetector implements InputProcessor — so it plugs directly into setInputProcessor() or InputMultiplexer.
GestureDetector detector = new GestureDetector(new GestureDetector.GestureAdapter() {
@Override
public boolean tap(float x, float y, int count, int button) {
return true;
}
@Override
public boolean fling(float velocityX, float velocityY, int button) {
return true;
}
@Override
public boolean longPress(float x, float y) {
return true;
}
@Override
public boolean pan(float x, float y, float deltaX, float deltaY) {
return true;
}
@Override
public boolean pinch(Vector2 initialPointer1, Vector2 initialPointer2,
Vector2 pointer1, Vector2 pointer2) {
return true;
}
@Override
public boolean zoom(float initialDistance, float distance) {
return true;
}
});
multiplexer.addProcessor(stage);
multiplexer.addProcessor(detector);
multiplexer.addProcessor(gameInput);
Gdx.input.setInputProcessor(multiplexer);
GestureListener methods: touchDown, tap, longPress, fling, pan, panStop, zoom, pinch, pinchStop. Use GestureAdapter for empty defaults.
Swipe detection: Use fling(velocityX, velocityY, button). Compare velocity components to determine direction (e.g., Math.abs(velocityX) > Math.abs(velocityY) → horizontal swipe). There is no onSwipe(), SwipeListener, or SwipeDetector class — fling() is the swipe handler.
GestureDetector coordinates are also screen coordinates. Unproject them through the camera for world-space use.
Mobile Text Input
Gdx.input.setOnscreenKeyboardVisible(true) shows the soft keyboard on mobile (no-op on desktop). Gdx.input.getTextInput() shows a native text input dialog:
Gdx.input.getTextInput(new Input.TextInputListener() {
@Override public void input(String text) { playerName = text; }
@Override public void canceled() { }
}, "Enter Name", "Player1", "Your name here");
On desktop this shows a Swing dialog. Non-blocking — callbacks fire asynchronously. For in-game text fields, use Scene2D's TextField widget instead (covered by Scene2D skill).
Accelerometer
Mobile only. Returns device tilt in m/s² (gravity ~ 9.8).
if (Gdx.input.isPeripheralAvailable(Input.Peripheral.Accelerometer)) {
float ax = Gdx.input.getAccelerometerX();
float ay = Gdx.input.getAccelerometerY();
float az = Gdx.input.getAccelerometerZ();
}
Returns 0 on desktop. Always check isPeripheralAvailable() or provide keyboard fallback.
Android Back / Menu Keys
Use setCatchKey() (the modern API). setCatchBackKey() and setCatchMenuKey() are deprecated.
Gdx.input.setCatchKey(Input.Keys.BACK, true);
Gdx.input.setCatchKey(Input.Keys.MENU, true);
@Override
public boolean keyDown(int keycode) {
if (keycode == Input.Keys.BACK) {
showPauseMenu();
return true;
}
return false;
}
Do NOT use Gdx.input.setCatchBackKey(true) — it is deprecated. Use setCatchKey(Input.Keys.BACK, true).
Common Mistakes
- Using screen coordinates for game logic without unprojecting —
touchDown gives screen pixels (Y-down). Always camera.unproject() before using with game world.
- Putting game input before Stage in InputMultiplexer — Stage must be first so UI buttons receive events. Game input goes after Stage.
- Forgetting to return true from InputProcessor methods — If your processor doesn't return
true, InputMultiplexer passes the event to the next processor. Events get handled twice or by the wrong processor.
- Inventing methods that don't exist on InputProcessor — There is no
onClick(), onTap(), onKeyPress(), onTouch(), onSwipe(), onSwipeLeft(), onSwipeRight(), onSwipeUp(), or onSwipeDown(). Use touchDown/touchUp for clicks, GestureDetector for taps/flings. Swipe detection = GestureDetector.fling() — there is no dedicated swipe method or SwipeListener class.
- Not knowing GestureDetector implements InputProcessor — GestureDetector IS an InputProcessor. Add it directly to InputMultiplexer. No adapter or wrapper needed.
- Using deprecated
setCatchBackKey()/setCatchMenuKey() — Use setCatchKey(Input.Keys.BACK, true) and setCatchKey(Input.Keys.MENU, true) instead.
- Allocating
new Vector3() in every touchDown call — Reuse a field-level Vector3. Allocating per-event creates garbage and triggers GC pauses on Android.
- Not unprojecting GestureDetector coordinates — GestureDetector's tap/pan/fling/longPress coordinates are also screen coordinates. Unproject them just like touchDown coords.
- Using
isKeyJustPressed for continuous movement — Only true for one frame per press, causing stuttering. Use isKeyPressed for held actions (movement, firing).
- Using
touchDragged for hover/mouseover on desktop — touchDragged only fires while a finger/button is held. Use mouseMoved for mouse hover without clicking.
- Comparing
keyTyped char against Input.Keys constants — keyTyped receives a Unicode character, not a key constant. character == Input.Keys.A compares char to int 29, which is silently wrong. Use keyDown for key identity checks.
- Inverting scroll direction —
scrolled() amountY is positive for scroll-down, negative for scroll-up. When implementing zoom, scroll-up (negative amountY) should zoom in (decrease camera.zoom).