Here an example sketch.
// All the needed imports
import android.view.MotionEvent;
import android.os.Vibrator;
import android.content.Context;
// Callback interfaces
interface JoystickListener {
void onJoystickPress(int id);
void onJoystickMove(int id, float x, float y);
void onJoystickRelease(int id);
}
interface ButtonListener {
void onButtonPress(int index, String name);
void onButtonRelease(int index, String name);
}
// Global joystick outputs
HashMap<Integer, Float[]> joystickOutputs = new HashMap<Integer, Float[]>();
// Main sketch
TouchController controller;
Sprite player; // Just for testing
void setup() {
fullScreen();
orientation(LANDSCAPE);
controller = new TouchController("layout.json"); // if the file doesn't exist, a default layout will be loaded.
player = new Sprite(width/2, height/2, 5.0);
// Set joystick listener
controller.setJoystickListener(new JoystickListener() {
@Override
void onJoystickPress(int id) {
println("Joystick " + id + " pressed (listener)");
}
@Override
void onJoystickMove(int id, float x, float y) {
if (id == 1) {
player.update(x, y);
}
}
@Override
void onJoystickRelease(int id) {
println("Joystick " + id + " released (listener)");
}
}
);
// Set button listener
controller.setButtonListener(new ButtonListener() {
@Override
void onButtonPress(int index, String name) {
println("Button " + index + ": " + name + " pressed (listener)");
if (name.equals("fire") || name.equals("toggle")) {
fill(0, 255, 0); // Example: change player color
}
}
@Override
void onButtonRelease(int index, String name) {
println("Button " + index + ": " + name + " released (listener)");
if (name.equals("fire") || name.equals("toggle")) {
fill(255, 0, 0); // Reset color
}
}
}
);
}
void draw() {
background(255);
controller.update();
player.draw();
controller.draw();
// Display joystick values for debugging
textAlign(CENTER);
fill(0);
int yOffset = 50;
for (Integer id : joystickOutputs.keySet()) {
Float[] outputs = joystickOutputs.get(id);
text("Joystick " + id + ": X=" + nf(outputs[0], 1, 2) + ", Y=" + nf(outputs[1], 1, 2), width/2, yOffset);
yOffset += 50;
}
}
void touchStarted(TouchEvent e) {
controller.touchStarted((MotionEvent) e.getNative());
}
void touchMoved(TouchEvent e) {
controller.touchMoved((MotionEvent) e.getNative());
}
void touchEnded(TouchEvent e) {
controller.touchEnded((MotionEvent) e.getNative());
}
// Action functions (fallback)
void action(int buttonIndex, String name) {
println("Button " + buttonIndex + ": " + name + " pressed");
}
void actionUp(int buttonIndex, String name) {
println("Button " + buttonIndex + ": " + name + " released");
}
void actionJoystick(int joystickIndex) {
println("Joystick " + joystickIndex + " pressed");
}
void actionJoystickUp(int joystickIndex) {
println("Joystick " + joystickIndex + " released");
}
// Controller class
class TouchController {
// Timer type constants
public static final String TIMER_TYPE_NONE = "none";
public static final String TIMER_TYPE_BLOCK = "block";
public static final String TIMER_TYPE_LATCHING = "latching";
public static final String TIMER_TYPE_TOGGLE = "toggle";
// Vibrator
private Vibrator vibrator;
private JoystickListener joystickListener;
private ButtonListener buttonListener;
// Joystick class
private class Joystick {
int id;
float x, y;
float radius;
float dotRadius;
float deadzone;
float dotX, dotY;
boolean active;
int touchId;
boolean returnToCenter;
boolean slideOffRelease;
PImage baseImage;
PImage knobImage;
PImage knobPressedImage;
boolean useImageSize;
boolean visible;
float opacity;
float touchRadius;
long vibrateDuration;
boolean vibrateOnMove;
float vibrateOnMoveDist;
float sensitivity;
float lastVibrateX, lastVibrateY;
Joystick(int id, float x, float y, float radius) {
this.id = id;
this.x = x <= 1.0 ? x * width : x;
this.y = y <= 1.0 ? y * height : y;
this.radius = radius;
this.dotRadius = radius * 0.3;
this.deadzone = 0.2;
this.dotX = this.x;
this.dotY = this.y;
this.active = false;
this.touchId = -1;
this.returnToCenter = true;
this.slideOffRelease = false;
this.baseImage = null;
this.knobImage = null;
this.knobPressedImage = null;
this.useImageSize = false;
this.visible = true;
this.opacity = 1.0;
this.touchRadius = radius;
this.vibrateDuration = 0;
this.vibrateOnMove = false;
this.vibrateOnMoveDist = 0.2;
this.sensitivity = 1.0;
this.lastVibrateX = this.x;
this.lastVibrateY = this.y;
joystickOutputs.put(id, new Float[]{0.0, 0.0});
}
}
// Button class
private class Button {
float x, y;
float radius;
boolean active;
int touchId;
int index;
String name;
boolean slideOffRelease;
boolean slideOn;
String timerType;
long timerDuration;
long activationTime;
long blockedUntil;
PImage inactiveImage;
PImage activeImage;
boolean useImageSize;
boolean visible;
float opacity;
float touchRadius;
long vibrateDuration;
Button(String name, float x, float y, float radius) {
this(name, x, y, radius, false, true, TIMER_TYPE_NONE, 0);
}
Button(String name, float x, float y, float radius, boolean slideOffRelease, boolean slideOn,
String timerType, long timerDuration) {
this.name = name;
this.x = x <= 1.0 ? x * width : x;
this.y = y <= 1.0 ? y * height : y;
this.radius = radius;
this.active = false;
this.touchId = -1;
this.index = nextButtonIndex++;
this.slideOffRelease = slideOffRelease;
this.slideOn = slideOn;
this.timerType = timerType;
this.timerDuration = timerDuration;
this.activationTime = 0;
this.blockedUntil = 0;
this.inactiveImage = null;
this.activeImage = null;
this.useImageSize = false;
this.visible = true;
this.opacity = 1.0;
this.touchRadius = radius;
this.vibrateDuration = 0;
}
}
// Storage
private HashMap<Integer, Joystick> joysticks;
private HashMap<String, Button> buttons;
private int nextButtonIndex;
private ArrayList<Integer> touchIds;
// Empty constructor
TouchController() {
joysticks = new HashMap<Integer, Joystick>();
buttons = new HashMap<String, Button>();
nextButtonIndex = 0;
touchIds = new ArrayList<Integer>();
vibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
joystickListener = null;
buttonListener = null;
}
// JSON constructor
TouchController(String filename) {
this();
loadLayout(filename);
}
// Set listeners
void setJoystickListener(JoystickListener listener) {
this.joystickListener = listener;
}
void setButtonListener(ButtonListener listener) {
this.buttonListener = listener;
}
// Vibrate method
void vibrate(long duration) {
if (vibrator != null && duration > 0) {
vibrator.vibrate(duration);
}
}
// Reload layout
void reloadLayout(String filename) {
loadLayout(filename);
}
// Load layout from JSON file
void loadLayout(String filename) {
try {
JSONObject json = loadJSONObject(filename);
joysticks.clear();
joystickOutputs.clear();
buttons.clear();
nextButtonIndex = 0;
// Parse joysticks
JSONArray joysticksJson = json.getJSONArray("joysticks");
for (int i = 0; i < joysticksJson.size(); i++) {
JSONObject joy = joysticksJson.getJSONObject(i);
int id = joy.getInt("id");
float x = joy.getFloat("x");
float y = joy.getFloat("y");
float radius = joy.getFloat("radius");
addJoystick(id, x, y, radius);
Joystick j = joysticks.get(id);
j.dotRadius = joy.isNull("dotRadius") ? j.dotRadius : max(0, joy.getFloat("dotRadius"));
j.deadzone = joy.isNull("deadzone") ? j.deadzone : max(0, min(1, joy.getFloat("deadzone")));
j.slideOffRelease = joy.isNull("slideOffRelease") ? j.slideOffRelease : joy.getBoolean("slideOffRelease");
j.returnToCenter = joy.isNull("returnToCenter") ? j.returnToCenter : joy.getBoolean("returnToCenter");
j.useImageSize = joy.isNull("useImageSize") ? j.useImageSize : joy.getBoolean("useImageSize");
j.visible = joy.isNull("visible") ? j.visible : joy.getBoolean("visible");
j.opacity = joy.isNull("opacity") ? j.opacity : max(0, min(1, joy.getFloat("opacity")));
j.touchRadius = joy.isNull("touchRadius") ? j.touchRadius : max(j.radius, joy.getFloat("touchRadius"));
j.vibrateDuration = joy.isNull("vibrateDuration") ? 0 : (long) max(0, joy.getFloat("vibrateDuration"));
j.vibrateOnMove = joy.isNull("vibrateOnMove") ? j.vibrateOnMove : joy.getBoolean("vibrateOnMove");
j.vibrateOnMoveDist = joy.isNull("vibrateOnMoveDist") ? j.vibrateOnMoveDist : max(0, joy.getFloat("vibrateOnMoveDist"));
j.sensitivity = joy.isNull("sensitivity") ? 1.0 : max(0, min(2, joy.getFloat("sensitivity")));
String baseImage = joy.isNull("baseImage") ? null : joy.getString("baseImage");
String knobImage = joy.isNull("knobImage") ? null : joy.getString("knobImage");
String knobPressedImage = joy.isNull("knobPressedImage") ? null : joy.getString("knobPressedImage");
if (baseImage != null) setJoystickBaseImage(id, baseImage);
if (knobImage != null) setJoystickKnobImage(id, knobImage, knobPressedImage);
}
// Parse buttons
JSONArray buttonsJson = json.getJSONArray("buttons");
for (int i = 0; i < buttonsJson.size(); i++) {
JSONObject btn = buttonsJson.getJSONObject(i);
String name = btn.getString("name");
float x = btn.getFloat("x");
float y = btn.getFloat("y");
float radius = btn.getFloat("radius");
boolean slideOffRelease = btn.isNull("slideOffRelease") ? false : btn.getBoolean("slideOffRelease");
boolean slideOn = btn.isNull("slideOn") ? true : btn.getBoolean("slideOn");
String timerType = btn.isNull("timerType") ? TIMER_TYPE_NONE : btn.getString("timerType");
long timerDuration = btn.isNull("timerDuration") ? 0 : (long) max(0, btn.getFloat("timerDuration"));
boolean useImageSize = btn.isNull("useImageSize") ? false : btn.getBoolean("useImageSize");
boolean visible = btn.isNull("visible") ? true : btn.getBoolean("visible");
float opacity = btn.isNull("opacity") ? 1.0 : max(0, min(1, btn.getFloat("opacity")));
float touchRadius = btn.isNull("touchRadius") ? radius : max(radius, btn.getFloat("touchRadius"));
long vibrateDuration = btn.isNull("vibrateDuration") ? 0 : (long) max(0, btn.getFloat("vibrateDuration"));
if (timerType.equals("none") || timerType.equals("block") || timerType.equals("latching") || timerType.equals("toggle")) {
addButton(name, x, y, radius, slideOffRelease, slideOn, timerType, timerDuration);
Button b = buttons.get(name);
b.useImageSize = useImageSize;
b.visible = visible;
b.opacity = opacity;
b.touchRadius = touchRadius;
b.vibrateDuration = vibrateDuration;
String inactiveImage = btn.isNull("inactiveImage") ? null : btn.getString("inactiveImage");
String activeImage = btn.isNull("activeImage") ? null : btn.getString("activeImage");
if (inactiveImage != null && activeImage != null) {
setButtonImage(name, inactiveImage, activeImage);
}
}
}
}
catch (Exception e) {
// Fallback default layout: one joystick, two buttons
addJoystick(1, 0.2, 0.8, 100);
Joystick j = joysticks.get(1);
j.dotRadius = 30;
j.deadzone = 0.2;
j.slideOffRelease = false;
j.returnToCenter = true;
addButton("fire", 0.8, 0.6, 50, true, true, "none", 0);
addButton("jump", 0.8, 0.4, 50, false, true, "block", 500);
}
}
// Add joystick
void addJoystick(int id, float x, float y, float radius) {
if (!joysticks.containsKey(id)) {
joysticks.put(id, new Joystick(id, x, y, radius));
}
}
// Remove joystick
void removeJoystick(int id) {
if (joysticks.containsKey(id)) {
joysticks.remove(id);
joystickOutputs.remove(id);
}
}
// Clear all joysticks
void clearJoysticks() {
joysticks.clear();
joystickOutputs.clear();
}
// Set joystick properties
void setJoystickBaseImage(int id, String filename) {
if (joysticks.containsKey(id)) {
PImage img = loadImage(filename);
joysticks.get(id).baseImage = img != null ? img : null;
}
}
void setJoystickKnobImage(int id, String filename, String pressedFilename) {
if (joysticks.containsKey(id)) {
Joystick j = joysticks.get(id);
PImage img = loadImage(filename);
j.knobImage = img != null ? img : null;
PImage pressedImg = pressedFilename != null ? loadImage(pressedFilename) : null;
j.knobPressedImage = pressedImg != null ? pressedImg : null;
}
}
void setJoystickDotRadius(int id, float dotRadius) {
if (joysticks.containsKey(id)) {
joysticks.get(id).dotRadius = max(0, dotRadius);
}
}
void setJoystickDeadzone(int id, float deadzone) {
if (joysticks.containsKey(id)) {
joysticks.get(id).deadzone = max(0, min(1, deadzone));
}
}
void setJoystickSlideOffRelease(int id, boolean enabled) {
if (joysticks.containsKey(id)) {
joysticks.get(id).slideOffRelease = enabled;
}
}
void setJoystickReturnToCenter(int id, boolean enabled) {
if (joysticks.containsKey(id)) {
joysticks.get(id).returnToCenter = enabled;
}
}
void setJoystickUseImageSize(int id, boolean enabled) {
if (joysticks.containsKey(id)) {
joysticks.get(id).useImageSize = enabled;
}
}
void setJoystickVisible(int id, boolean visible) {
if (joysticks.containsKey(id)) {
joysticks.get(id).visible = visible;
}
}
void setJoystickOpacity(int id, float opacity) {
if (joysticks.containsKey(id)) {
joysticks.get(id).opacity = max(0, min(1, opacity));
}
}
void setJoystickTouchRadius(int id, float touchRadius) {
if (joysticks.containsKey(id)) {
Joystick j = joysticks.get(id);
j.touchRadius = max(j.radius, touchRadius);
}
}
void setJoystickVibrateDuration(int id, long duration) {
if (joysticks.containsKey(id)) {
joysticks.get(id).vibrateDuration = (long) max(0, duration);
}
}
void setJoystickVibrateOnMove(int id, boolean enabled) {
if (joysticks.containsKey(id)) {
joysticks.get(id).vibrateOnMove = enabled;
}
}
void setJoystickVibrateOnMoveDist(int id, float dist) {
if (joysticks.containsKey(id)) {
joysticks.get(id).vibrateOnMoveDist = max(0, dist);
}
}
void setJoystickSensitivity(int id, float sensitivity) {
if (joysticks.containsKey(id)) {
joysticks.get(id).sensitivity = max(0, min(2, sensitivity));
}
}
// Set button properties
void setButtonImage(String name, String inactiveFilename, String activeFilename) {
if (buttons.containsKey(name)) {
Button btn = buttons.get(name);
btn.inactiveImage = loadImage(inactiveFilename);
btn.activeImage = loadImage(activeFilename);
}
}
void setButtonUseImageSize(String name, boolean enabled) {
if (buttons.containsKey(name)) {
buttons.get(name).useImageSize = enabled;
}
}
void setButtonSlideOffRelease(String name, boolean enabled) {
if (buttons.containsKey(name)) {
buttons.get(name).slideOffRelease = enabled;
}
}
void setButtonSlideOn(String name, boolean enabled) {
if (buttons.containsKey(name)) {
buttons.get(name).slideOn = enabled;
}
}
void setButtonTimerType(String name, String timerType) {
if (buttons.containsKey(name) &&
(timerType.equals("none") ||
timerType.equals("block") ||
timerType.equals("latching") ||
timerType.equals("toggle"))) {
Button btn = buttons.get(name);
btn.timerType = timerType;
if (!timerType.equals(TIMER_TYPE_BLOCK)) {
btn.blockedUntil = 0;
}
if (timerType.equals(TIMER_TYPE_NONE) || timerType.equals(TIMER_TYPE_TOGGLE)) {
btn.timerDuration = 0;
}
}
}
void setButtonTimerDuration(String name, long timerDuration) {
if (buttons.containsKey(name)) {
Button btn = buttons.get(name);
btn.timerDuration = (long) max(0, timerDuration);
if (btn.active && btn.timerType.equals(TIMER_TYPE_BLOCK) && btn.activationTime > 0) {
btn.blockedUntil = btn.activationTime + btn.timerDuration;
}
}
}
void setButtonVisible(String name, boolean visible) {
if (buttons.containsKey(name)) {
buttons.get(name).visible = visible;
}
}
void setButtonOpacity(String name, float opacity) {
if (buttons.containsKey(name)) {
buttons.get(name).opacity = max(0, min(1, opacity));
}
}
void setButtonTouchRadius(String name, float touchRadius) {
if (buttons.containsKey(name)) {
Button btn = buttons.get(name);
btn.touchRadius = max(btn.radius, touchRadius);
}
}
void setButtonVibrateDuration(String name, long duration) {
if (buttons.containsKey(name)) {
buttons.get(name).vibrateDuration = (long) max(0, duration);
}
}
// Add a button (minimal) for quick builds
void addButton(String name, float x, float y, float radius) {
addButton(name, x, y, radius, false, true, TIMER_TYPE_NONE, 0);
}
// Add a button (full, for JSON)
void addButton(String name, float x, float y, float radius, boolean slideOffRelease, boolean slideOn,
String timerType, long timerDuration) {
if (!buttons.containsKey(name) &&
(timerType.equals("none") ||
timerType.equals("block") ||
timerType.equals("latching") ||
timerType.equals("toggle"))) {
buttons.put(name, new Button(name, x, y, radius, slideOffRelease, slideOn, timerType, timerDuration));
}
}
// Remove a button
void removeButton(String name) {
if (buttons.containsKey(name)) {
buttons.remove(name);
}
}
// Clear all buttons
void clearButtons() {
buttons.clear();
nextButtonIndex = 0;
}
void update() {
// Update joysticks
for (Joystick j : joysticks.values()) {
if (!j.active && j.returnToCenter) {
j.dotX = j.x;
j.dotY = j.y;
joystickOutputs.get(j.id)[0] = 0.0;
joystickOutputs.get(j.id)[1] = 0.0;
}
}
// Update button timers
long currentTime = millis();
for (Button btn : buttons.values()) {
if (btn.timerType.equals(TIMER_TYPE_BLOCK) && btn.blockedUntil > 0 && currentTime >= btn.blockedUntil) {
btn.blockedUntil = 0;
}
if (btn.active && (btn.timerType.equals(TIMER_TYPE_BLOCK) || btn.timerType.equals(TIMER_TYPE_LATCHING))) {
if (btn.activationTime > 0 && currentTime >= btn.activationTime + btn.timerDuration) {
btn.active = false;
btn.touchId = -1;
btn.activationTime = 0;
if (buttonListener != null) {
buttonListener.onButtonRelease(btn.index, btn.name);
} else {
actionUp(btn.index, btn.name);
}
}
}
if (!btn.active && !btn.timerType.equals(TIMER_TYPE_LATCHING) && !btn.timerType.equals(TIMER_TYPE_TOGGLE)) {
btn.touchId = -1;
btn.activationTime = 0;
}
}
}
void draw() {
// Draw joysticks
for (Joystick j : joysticks.values()) {
if (!j.visible) continue;
pushStyle();
tint(255, j.opacity * 255);
// Draw base
if (j.baseImage != null && !j.useImageSize) {
image(j.baseImage, j.x - j.radius, j.y - j.radius, j.radius * 2, j.radius * 2);
} else if (j.baseImage != null) {
image(j.baseImage, j.x - j.baseImage.width / 2, j.y - j.baseImage.height / 2);
} else {
fill(100, 100, 100, j.opacity * 150);
ellipse(j.x, j.y, j.radius * 2, j.radius * 2);
}
// Draw deadzone.
fill(150, 150, 150, j.opacity * 100);
ellipse(j.x, j.y, j.radius * j.deadzone * 2, j.radius * j.deadzone * 2);
// Draw knob
if (j.knobImage != null && !j.useImageSize) {
PImage knobImg = (j.active && j.knobPressedImage != null) ? j.knobPressedImage : j.knobImage;
image(knobImg, j.dotX - j.dotRadius, j.dotY - j.dotRadius, j.dotRadius * 2, j.dotRadius * 2);
} else if (j.knobImage != null) {
PImage knobImg = (j.active && j.knobPressedImage != null) ? j.knobPressedImage : j.knobImage;
image(knobImg, j.dotX - knobImg.width / 2, j.dotY - knobImg.height / 2);
} else {
fill(200, 200, 200, j.opacity * 200);
ellipse(j.dotX, j.dotY, j.dotRadius * 2, j.dotRadius * 2);
}
popStyle();
}
// Draw buttons
for (Button btn : buttons.values()) {
if (!btn.visible) continue;
pushStyle();
tint(255, btn.opacity * 255);
if (btn.inactiveImage != null && btn.activeImage != null && !btn.useImageSize) {
PImage img = btn.active ? btn.activeImage : btn.inactiveImage;
image(img, btn.x - btn.radius, btn.y - btn.radius, btn.radius * 2, btn.radius * 2);
} else if (btn.inactiveImage != null && btn.activeImage != null) {
PImage img = btn.active ? btn.activeImage : btn.inactiveImage;
image(img, btn.x - img.width / 2, btn.y - img.height / 2);
} else {
fill(btn.active ? color(255, 100, 100, btn.opacity * 255) : color(100, 100, 255, btn.opacity * 150));
ellipse(btn.x, btn.y, btn.radius * 2, btn.radius * 2);
}
popStyle();
}
}
void touchStarted(MotionEvent me) {
ArrayList<Integer> newIds = getMotionEventTouchIds(me);
if (me.getActionMasked() != MotionEvent.ACTION_DOWN) {
newIds.removeAll(touchIds);
}
for (int id : newIds) {
int ptrIdx = me.findPointerIndex(id);
if (ptrIdx < 0) continue;
float px = me.getX(ptrIdx);
float py = me.getY(ptrIdx);
// Check joysticks
for (Joystick j : joysticks.values()) {
if (!j.visible || j.active || dist(px, py, j.x, j.y) >= j.touchRadius) continue;
j.active = true;
j.touchId = id;
updateJoystick(j, px, py);
if (j.vibrateDuration > 0) vibrate(j.vibrateDuration);
if (joystickListener != null) {
joystickListener.onJoystickPress(j.id);
} else {
actionJoystick(j.id);
}
break;
}
// Check buttons
for (Button btn : buttons.values()) {
if (!btn.visible || dist(px, py, btn.x, btn.y) >= btn.touchRadius) continue;
if (btn.timerType.equals(TIMER_TYPE_BLOCK) && btn.blockedUntil > millis()) continue;
if (btn.timerType.equals(TIMER_TYPE_TOGGLE)) {
// Toggle mode: flip active state
btn.active = !btn.active;
btn.touchId = id;
if (btn.active) {
btn.activationTime = millis();
if (btn.vibrateDuration > 0) vibrate(btn.vibrateDuration);
if (buttonListener != null) {
buttonListener.onButtonPress(btn.index, btn.name);
} else {
action(btn.index, btn.name);
}
} else {
btn.touchId = -1;
btn.activationTime = 0;
if (buttonListener != null) {
buttonListener.onButtonRelease(btn.index, btn.name);
} else {
actionUp(btn.index, btn.name);
}
}
} else if (!btn.active) {
// Non-toggle modes: activate if not already active
btn.active = true;
btn.touchId = id;
btn.activationTime = millis();
if (btn.timerType.equals(TIMER_TYPE_BLOCK)) {
btn.blockedUntil = btn.activationTime + btn.timerDuration;
}
if (btn.vibrateDuration > 0) vibrate(btn.vibrateDuration);
if (buttonListener != null) {
buttonListener.onButtonPress(btn.index, btn.name);
} else {
action(btn.index, btn.name);
}
}
break;
}
}
touchIds = getMotionEventTouchIds(me);
}
void touchMoved(MotionEvent me) {
for (int i = 0; i < me.getPointerCount(); i++) {
int id = me.getPointerId(i);
if (!touchIds.contains(id)) continue;
int ptrIdx = me.findPointerIndex(id);
if (ptrIdx < 0) continue;
float px = me.getX(ptrIdx);
float py = me.getY(ptrIdx);
// Update joysticks
for (Joystick j : joysticks.values()) {
if (!j.active || j.touchId != id) continue;
if (j.slideOffRelease && dist(px, py, j.x, j.y) > j.touchRadius) {
j.active = false;
j.touchId = -1;
if (j.returnToCenter) {
j.dotX = j.x;
j.dotY = j.y;
joystickOutputs.get(j.id)[0] = 0.0;
joystickOutputs.get(j.id)[1] = 0.0;
}
if (joystickListener != null) {
joystickListener.onJoystickRelease(j.id);
} else {
actionJoystickUp(j.id);
}
} else {
float moveDist = dist(px, py, j.lastVibrateX, j.lastVibrateY);
if (j.vibrateOnMove && moveDist > j.radius * j.vibrateOnMoveDist) {
vibrate(j.vibrateDuration);
j.lastVibrateX = px;
j.lastVibrateY = py;
}
updateJoystick(j, px, py);
if (joystickListener != null) {
joystickListener.onJoystickMove(j.id, joystickOutputs.get(j.id)[0], joystickOutputs.get(j.id)[1]);
}
}
}
// Update buttons
for (Button btn : buttons.values()) {
boolean withinRadius = dist(px, py, btn.x, btn.y) < btn.touchRadius;
if (btn.touchId == id) {
if (btn.slideOffRelease && !withinRadius && !btn.timerType.equals(TIMER_TYPE_LATCHING) && !btn.timerType.equals(TIMER_TYPE_TOGGLE)) {
btn.active = false;
btn.touchId = -1;
btn.activationTime = 0;
if (buttonListener != null) {
buttonListener.onButtonRelease(btn.index, btn.name);
} else {
actionUp(btn.index, btn.name);
}
} else if (btn.timerType.equals(TIMER_TYPE_TOGGLE)) {
btn.touchId = -1; // Allow new touch to toggle
}
} else if (btn.slideOn && !btn.active && withinRadius && !btn.timerType.equals(TIMER_TYPE_TOGGLE)) {
if (btn.timerType.equals(TIMER_TYPE_BLOCK) && btn.blockedUntil > millis()) continue;
btn.active = true;
btn.touchId = id;
btn.activationTime = millis();
if (btn.timerType.equals(TIMER_TYPE_BLOCK)) {
btn.blockedUntil = btn.activationTime + btn.timerDuration;
}
if (btn.vibrateDuration > 0) vibrate(btn.vibrateDuration);
if (buttonListener != null) {
buttonListener.onButtonPress(btn.index, btn.name);
} else {
action(btn.index, btn.name);
}
}
}
}
}
void touchEnded(MotionEvent me) {
ArrayList<Integer> oldIds = new ArrayList<Integer>();
touchIds = getMotionEventTouchIds(me);
int action = me.getActionMasked();
if (action == MotionEvent.ACTION_UP) {
oldIds = new ArrayList<Integer>(touchIds);
} else if (action == MotionEvent.ACTION_POINTER_UP) {
int idx = me.getActionIndex();
int id = me.getPointerId(idx);
oldIds.add(id);
} else if (action == MotionEvent.ACTION_MOVE) {
ArrayList<Integer> currentIds = getTouchesTouchIds();
oldIds = new ArrayList<Integer>(currentIds);
oldIds.removeAll(touchIds);
}
// Release joysticks
for (Joystick j : joysticks.values()) {
if (oldIds.contains(j.touchId)) {
j.active = false;
j.touchId = -1;
if (j.returnToCenter) {
j.dotX = j.x;
j.dotY = j.y;
joystickOutputs.get(j.id)[0] = 0.0;
joystickOutputs.get(j.id)[1] = 0.0;
}
if (joystickListener != null) {
joystickListener.onJoystickRelease(j.id);
} else {
actionJoystickUp(j.id);
}
}
}
// Release buttons
for (Button btn : buttons.values()) {
if (oldIds.contains(btn.touchId) && !btn.timerType.equals(TIMER_TYPE_LATCHING) && !btn.timerType.equals(TIMER_TYPE_TOGGLE)) {
btn.active = false;
btn.touchId = -1;
btn.activationTime = 0;
if (buttonListener != null) {
buttonListener.onButtonRelease(btn.index, btn.name);
} else {
actionUp(btn.index, btn.name);
}
} else if (oldIds.contains(btn.touchId)) {
btn.touchId = -1;
}
}
touchIds.removeAll(oldIds);
}
private void updateJoystick(Joystick j, float x, float y) {
float dx = x - j.x;
float dy = y - j.y;
float dist = sqrt(dx * dx + dy * dy);
// Constrain to circle
if (dist > j.radius) {
float angle = atan2(dy, dx);
dx = cos(angle) * j.radius;
dy = sin(angle) * j.radius;
dist = j.radius;
}
j.dotX = j.x + dx;
j.dotY = j.y + dy;
// Apply deadzone and sensitivity
float deadzoneRadius = j.radius * j.deadzone;
if (dist < deadzoneRadius) {
joystickOutputs.get(j.id)[0] = 0.0;
joystickOutputs.get(j.id)[1] = 0.0;
} else {
float range = j.radius - deadzoneRadius;
float outputX = (dx - (dx / dist) * deadzoneRadius) / range;
float outputY = -(dy - (dy / dist) * deadzoneRadius) / range;
joystickOutputs.get(j.id)[0] = constrain(outputX * j.sensitivity, -1.0, 1.0);
joystickOutputs.get(j.id)[1] = constrain(outputY * j.sensitivity, -1.0, 1.0);
}
}
private ArrayList<Integer> getMotionEventTouchIds(MotionEvent me) {
ArrayList<Integer> ids = new ArrayList<Integer>();
for (int i = 0; i < me.getPointerCount(); i++) {
ids.add(me.getPointerId(i));
}
return ids;
}
private ArrayList<Integer> getTouchesTouchIds() {
ArrayList<Integer> ids = new ArrayList<Integer>();
for (int i = 0; i < touches.length; i++) {
ids.add(touches[i].id);
}
return ids;
}
}
// Simple Sprite class for testing
class Sprite {
float x, y;
float speed;
Sprite(float x, float y, float speed) {
this.x = x;
this.y = y;
this.speed = speed;
}
void update(float dx, float dy) {
x += dx * speed;
y += dy * speed;
x = constrain(x, 0, width);
y = constrain(y, 0, height);
}
void draw() {
fill(255, 0, 0);
ellipse(x, y, 50, 50);
}
}