A lightweight, configurable and reusable touch-control overlay for Android games.
RetroTouch is an engine-agnostic Android touch-controls library for games and source ports. It adds a resolution-independent virtual joystick, configurable action buttons, relative FPS look controls and optional menu navigation above an existing Android game view.
The overlay is delivered as a replaceable Android Archive (.aar) and is suitable for Java, JNI, SDL, OpenGL, SurfaceView, GLSurfaceView and native game engines. The game keeps ownership of its input system: RetroTouch emits stable action IDs and normalized movement/look callbacks, while a small game-specific bridge maps them to keys, axes or engine events.
RetroTouch was created for retro FPS ports that need modern Android multitouch controls without hard-coding a different overlay into every game.
- Android 4.1 / API 16 through Android 16 / API 36
- no AndroidX requirement and no external runtime dependencies
- true pointer-ID-based multitouch
- up to 16 configurable action buttons
- up to two movement sticks, two relative-look zones and two digital D-pads
- normalized layouts for 4:3, 16:9, 20:9, tablets and ultrawide Automotive displays
- independent
OFF,GAMEPLAYand optionalNAVIGATIONmodes - persistent per-game gameplay and navigation layouts
- built-in layout editor for moving, resizing, remapping and changing opacity
- movable, scalable and opacity-adjustable Settings button
- vertically draggable editor toolbar
- automatic one- or two-line button labels with dynamic font fitting
- large, centered one-character labels such as
AorBthat scale with the button - optional game-defined colors per action, per control or for the whole overlay
- optional second analog stick, for example a GameCube C-stick, that reports separately
- FPS-friendly simultaneous movement, turning and held FIRE
- host-controlled latched-button display, suitable for always-run modes
- public input-reset API for respawn, level changes and engine input resets
- controller detection for built-in, USB, Bluetooth, Xbox, PlayStation and generic gamepads
- compatible AAR updates without copying RetroTouch source into each game
- MIT license for use in open-source and commercial projects
RetroTouch is designed for:
- Android game ports and source ports
- retro FPS and action games
- SDL and JNI-based games
- OpenGL and custom-rendered Android games
- Unreal Engine 1 ports, including Unreal and Unreal Tournament 99
- Android handhelds, phones, tablets, ChromeOS and Android Automotive
- projects that need an editable virtual joystick and gamepad-style touch overlay
The current release is RetroTouch 1.0.0-beta.7.
The complete core feature set has been tested with the Android port of Breathless, including multitouch movement, relative look, held FIRE while turning, always-run indication, D-pad menu navigation, controller detection and editable layouts. Beta.5 adds optional game-defined colors; games that set no colors look exactly as before. Beta.6 draws one-character labels such as A or B twice as large, scaled with the button and centered in the circle. Beta.7 adds an optional second analog stick that reports separately from the move stick. The beta phase continues while RetroTouch is tested across additional engines and input stacks.
RetroTouch intentionally separates reusable overlay code from game-specific input logic:
retrotouch.aar— reusable RetroTouch library- game bridge — maps RetroTouch callbacks to the game's input system
- game layout — defines available actions, labels and default positions
With this structure, a compatible RetroTouch update normally requires only replacing app/libs/retrotouch.aar and rebuilding the APK or AAB. The game bridge and native engine code remain unchanged.
The AAR is compiled into the final application. It cannot be exchanged inside an already installed APK without rebuilding and reinstalling the game.
Copy the current AAR into the application module:
app/libs/retrotouch.aar
Add it to the app module's build.gradle:
dependencies {
implementation files('libs/retrotouch.aar')
}Do not also copy RetroTouch Java sources into the app module. Keeping the library behind the AAR boundary makes future updates predictable.
Place RetroTouch above the game's existing view:
FrameLayout.LayoutParams matchParent = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT);
FrameLayout root = new FrameLayout(this);
root.addView(gameView, matchParent);
RetroTouchView retroTouch = new RetroTouchView(this);
root.addView(retroTouch, matchParent);
setContentView(root);Register the actions supported by the game:
retroTouch.registerAction("fire", "Fire");
retroTouch.registerAction("use", "Use");
retroTouch.registerAction("run", "Run");
retroTouch.registerAction("weapon_next", "Next Weapon");
retroTouch.registerAction("menu", "Menu");Define a normalized default gameplay layout:
List<RetroTouchControl> gameplay = new ArrayList<RetroTouchControl>();
gameplay.add(RetroTouchControl.lookZone(
"look", 0.72f, 0.48f, 0.56f, 0.78f));
gameplay.add(RetroTouchControl.moveStick(
"move", 0.16f, 0.76f, 0.25f));
gameplay.add(RetroTouchControl.button(
"fire_button", "fire", "Fire", 0.89f, 0.71f, 0.15f));
retroTouch.setGameplayLayout(
new RetroTouchLayout("my_game", gameplay));Map callbacks to the input path already used by the game:
retroTouch.setListener(new RetroTouchAdapter() {
@Override
public void onAction(String actionId, boolean pressed) {
nativeTouchAction(actionId, pressed);
}
@Override
public void onMove(float x, float y) {
nativeTouchMove(x, y);
}
@Override
public void onLook(float deltaX, float deltaY) {
nativeTouchLook(deltaX, deltaY);
}
});RetroTouch deliberately does not decide what fire, use or menu means. The host game maps each action ID to its own keyboard, mouse, controller, SDL, JNI or native engine input.
RetroTouch provides three universal modes:
retroTouch.setMode(RetroTouchMode.OFF);
retroTouch.setMode(RetroTouchMode.GAMEPLAY);
retroTouch.setMode(RetroTouchMode.NAVIGATION);OFF— draws nothing and lets the game's original touch input handle menus, intros or skip screensGAMEPLAY— shows movement, look and action controlsNAVIGATION— optionally shows a digital D-pad plus freely mapped OK/Back-style buttons
A game is not required to use every mode. For example, a port with native touch menus can keep RetroTouch OFF outside gameplay.
A FIRE button whose center is placed inside a look zone can remain pressed while the same finger continues turning:
retroTouch.setLookWhileHoldingAction("fire", true);The action remains held until the finger is lifted. A second finger can simultaneously operate the movement stick.
Always-run or double-tap logic belongs to the host game because every engine handles running differently. RetroTouch can visually mark the state:
retroTouch.setActionLatched("run", alwaysRunEnabled);A latched action is drawn two opacity levels stronger. The game remains responsible for detecting the double tap and applying the run multiplier while movement is active.
By default all controls use RetroTouch's neutral grey. A game can supply its own colors, for example a green A and a red B button as on a GameCube controller. Like labels, colors come from the game: players cannot change them, and ALPHA still controls opacity only.
// One fill color is enough: pressed color, border and readable text are derived.
RetroTouchColors green = RetroTouchColors.of(Color.rgb(31, 166, 74));
RetroTouchColors red = RetroTouchColors.of(Color.rgb(217, 43, 43));
// On the action: every button mapped to "a" or "b" uses these colors.
retroTouch.registerAction("a", "A", green);
retroTouch.registerAction("b", "B", red);
// On a single control: the optional last parameter of button(), moveStick() or dPad().
gameplay.add(RetroTouchControl.moveStick(
"move", 0.16f, 0.76f, 0.25f, RetroTouchColors.of(Color.rgb(90, 90, 90))));
// For everything else: one base color for all buttons, sticks and D-pads.
retroTouch.setDefaultColors(RetroTouchColors.of(Color.rgb(40, 104, 200)));RetroTouchColors holds a fill, pressed, border and text color. RetroTouchColors.of(fill, pressed, text) and new RetroTouchColors(fill, pressed, border, text) set them explicitly. The most specific setting wins: control, then action, then the default colors, then the built-in grey.
Colors are not stored in saved player layouts. RetroTouch re-applies the game's colors on every load, so existing layouts keep working and pick up color changes from a game update.
The move stick reports through onMove. A game that needs a second, independent stick, such as the C-stick of a GameCube controller, registers a stick action. In the default layout it adds a button with that action, and RetroTouch turns it into a stick automatically:
retroTouch.registerStickAction("c_stick", "C", RetroTouchColors.of(Color.rgb(245, 200, 0)));
gameplay.add(RetroTouchControl.button(
"c_stick", "c_stick", "C", 0.80f, 0.70f, 0.20f));The stick reports through RetroTouchStickListener.onStick(actionId, x, y) with values from -1.0 to 1.0, y pointing down, and 0, 0 on release. The listener implements this interface next to RetroTouchListener, so it must be a named class:
final class TouchBridge extends RetroTouchAdapter implements RetroTouchStickListener {
@Override
public void onMove(float x, float y) {
nativeTouchMove(x, y);
}
@Override
public void onStick(String actionId, float x, float y) {
if ("c_stick".equals(actionId)) nativeTouchCStick(x, y);
}
}
retroTouch.setListener(new TouchBridge());In the editor, ACTION turns a button into a stick and back. A layout holds at most two sticks, including the move stick, and 16 buttons. Stick actions never arrive through onAction.
The Settings button opens the in-game editor. Players can:
- add action buttons
- change a button's registered action
- move and resize controls
- cycle through opacity levels
- delete ordinary controls
- reset the current layout
- save the current gameplay or navigation layout
The Settings button itself can be selected, moved, resized and adjusted with ALPHA, but cannot be deleted. The editor toolbar can be dragged vertically when it covers another control. All settings are stored privately per game/layout ID.
Games that should show RetroTouch only when no physical controller is available can use:
boolean controllerConnected =
RetroTouchControllers.isControllerConnected();
retroTouch.setMode(controllerConnected
? RetroTouchMode.OFF
: wantedMode);Detection uses Android's GAMEPAD and JOYSTICK input sources instead of vendor lists. This covers built-in handheld controls as well as Xbox, PlayStation, USB, Bluetooth and generic Android controllers.
Repeat the check during the game's normal state update to support hot-plugging without restarting.
Engines may discard their internal key or axis state during respawn, checkpoint restart, level loading, focus loss or pause. Reset RetroTouch at the same point:
engineResetInput();
retroTouch.resetInputState();The call releases held actions, sends neutral movement, removes active touch pointers and clears RetroTouch-owned latches without changing the current mode or saved layout. releaseAllInputs() is an equivalent alias.
Recommended development environment:
- Android Studio 2025 or newer
- JDK 17
- Android Gradle Plugin 8.11.1
- Gradle 8.13
- compile SDK 36
- minimum SDK 16
Build the library, sample application and lint checks:
./gradlew :retrotouch:assembleRelease :sample:assembleDebug lintExpected outputs:
retrotouch/build/outputs/aar/retrotouch-release.aar
sample/build/outputs/apk/debug/sample-debug.apk
Starting with the 1.0 beta series, compatible updates preserve existing public classes and methods. New functionality is added without silently removing established APIs. Stable action IDs and game/layout IDs should not be renamed after release because saved user layouts depend on them.
Breaking changes, if ever required, will be documented explicitly and reserved for an appropriate major-version update.
Bug reports and tested integrations are welcome through GitHub Issues.
Please include:
- device and Android version
- game engine or rendering stack
- RetroTouch version
- controller model, if relevant
- reproduction steps
- relevant Logcat output
RetroTouch's interaction model was informed by Emile Belanger's MobileTouchControls project. RetroTouch is an independent clean-room implementation and contains no MobileTouchControls source code.
This separation is important because MobileTouchControls is licensed under GPLv2, while RetroTouch uses the permissive MIT License for straightforward reuse in both open-source and closed-source Android games.
RetroTouch is released under the MIT License.
Copyright © 2026 Andreas 'Andiweli' Stürmer.