From b1d277a17e63e0e122f011d245ac02e8d57044bc Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Mon, 25 May 2026 11:47:25 +0200 Subject: [PATCH 1/7] Virtual/on-screen gamepad --- .../input/android/AndroidInputHandler.java | 8 + .../input/android/AndroidInputHandler14.java | 4 + .../jme3/input/android/AndroidJoyInput.java | 118 ++- .../jme3/input/android/AndroidJoyInput14.java | 2 + .../java/com/jme3/app/SimpleApplication.java | 22 + .../main/java/com/jme3/input/ChaseCamera.java | 177 ++++- .../main/java/com/jme3/input/FlyByCamera.java | 86 ++- .../jme3/input/virtual/VirtualJoystick.java | 675 ++++++++++++++++++ .../input/virtual/VirtualJoystickLayout.java | 552 ++++++++++++++ .../input/virtual/VirtualJoystickTheme.java | 162 +++++ .../java/com/jme3/system/AppSettings.java | 88 ++- .../Common/VirtualJoystick/button_circle.png | Bin 0 -> 1549 bytes .../VirtualJoystick/button_circle_wide.png | Bin 0 -> 1619 bytes .../Common/VirtualJoystick/credits.txt | 1 + .../VirtualJoystick/dpad_element_east.png | Bin 0 -> 806 bytes .../VirtualJoystick/dpad_element_north.png | Bin 0 -> 782 bytes .../VirtualJoystick/dpad_element_south.png | Bin 0 -> 726 bytes .../VirtualJoystick/dpad_element_west.png | Bin 0 -> 831 bytes .../Common/VirtualJoystick/icon_back.png | Bin 0 -> 877 bytes .../Common/VirtualJoystick/icon_button_a.png | Bin 0 -> 424 bytes .../Common/VirtualJoystick/icon_button_b.png | Bin 0 -> 339 bytes .../Common/VirtualJoystick/icon_button_x.png | Bin 0 -> 502 bytes .../Common/VirtualJoystick/icon_button_y.png | Bin 0 -> 419 bytes .../Common/VirtualJoystick/icon_dpad.png | Bin 0 -> 1198 bytes .../Common/VirtualJoystick/icon_joystick.png | Bin 0 -> 3097 bytes .../Common/VirtualJoystick/icon_menu.png | Bin 0 -> 198 bytes .../Common/VirtualJoystick/icon_star.png | Bin 0 -> 429 bytes .../VirtualJoystick/joystick_circle_nub_a.png | Bin 0 -> 1776 bytes .../VirtualJoystick/joystick_circle_pad_a.png | Bin 0 -> 3022 bytes jme3-examples/build.gradle | 3 +- .../jme3test/input/TestVirtualJoystick.java | 189 +++++ .../com/jme3/input/ios/IosInputHandler.java | 18 +- .../java/com/jme3/input/ios/IosJoyInput.java | 120 +++- .../jme3/input/lwjgl/SdlJoystickInput.java | 101 ++- .../com/jme3/input/lwjgl/SdlMouseInput.java | 26 + 35 files changed, 2233 insertions(+), 119 deletions(-) create mode 100644 jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java create mode 100644 jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickLayout.java create mode 100644 jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickTheme.java create mode 100644 jme3-core/src/main/resources/Common/VirtualJoystick/button_circle.png create mode 100644 jme3-core/src/main/resources/Common/VirtualJoystick/button_circle_wide.png create mode 100644 jme3-core/src/main/resources/Common/VirtualJoystick/credits.txt create mode 100644 jme3-core/src/main/resources/Common/VirtualJoystick/dpad_element_east.png create mode 100644 jme3-core/src/main/resources/Common/VirtualJoystick/dpad_element_north.png create mode 100644 jme3-core/src/main/resources/Common/VirtualJoystick/dpad_element_south.png create mode 100644 jme3-core/src/main/resources/Common/VirtualJoystick/dpad_element_west.png create mode 100644 jme3-core/src/main/resources/Common/VirtualJoystick/icon_back.png create mode 100644 jme3-core/src/main/resources/Common/VirtualJoystick/icon_button_a.png create mode 100644 jme3-core/src/main/resources/Common/VirtualJoystick/icon_button_b.png create mode 100644 jme3-core/src/main/resources/Common/VirtualJoystick/icon_button_x.png create mode 100644 jme3-core/src/main/resources/Common/VirtualJoystick/icon_button_y.png create mode 100644 jme3-core/src/main/resources/Common/VirtualJoystick/icon_dpad.png create mode 100644 jme3-core/src/main/resources/Common/VirtualJoystick/icon_joystick.png create mode 100644 jme3-core/src/main/resources/Common/VirtualJoystick/icon_menu.png create mode 100644 jme3-core/src/main/resources/Common/VirtualJoystick/icon_star.png create mode 100644 jme3-core/src/main/resources/Common/VirtualJoystick/joystick_circle_nub_a.png create mode 100644 jme3-core/src/main/resources/Common/VirtualJoystick/joystick_circle_pad_a.png create mode 100644 jme3-examples/src/main/java/jme3test/input/TestVirtualJoystick.java diff --git a/jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler.java b/jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler.java index 70c96cbe26..6db3c95c6b 100644 --- a/jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler.java +++ b/jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler.java @@ -208,6 +208,10 @@ public boolean onTouch(View view, MotionEvent event) { // logger.log(Level.INFO, "onTouch source: {0}, isTouch: {1}", // new Object[]{source, isTouch}); + if (isTouch && joyInput != null && joyInput.onTouch(event)) { + return true; + } + if (isTouch && touchInput != null) { // send the event to the touch processor consumed = touchInput.onTouch(event); @@ -234,6 +238,10 @@ public boolean onKey(View view, int keyCode, KeyEvent event) { // logger.log(Level.INFO, "onKey source: {0}, isTouch: {1}", // new Object[]{source, isTouch}); + if ((source & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD && joyInput != null) { + joyInput.onKeyboardInput(); + } + if (touchInput != null) { consumed = touchInput.onKey(event); } diff --git a/jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler14.java b/jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler14.java index bfa07941bb..355d3c55fa 100644 --- a/jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler14.java +++ b/jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler14.java @@ -179,6 +179,10 @@ public boolean onKey(View view, int keyCode, KeyEvent event) { boolean isUnknown = (source & android.view.InputDevice.SOURCE_UNKNOWN) == android.view.InputDevice.SOURCE_UNKNOWN; + if ((source & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD && joyInput != null) { + joyInput.onKeyboardInput(); + } + if (touchInput != null && (isTouch || (isUnknown && this.touchInput.isSimulateKeyboard()))) { // logger.log(Level.INFO, "onKey source: {0}, isTouch: {1}", // new Object[]{source, isTouch}); diff --git a/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java b/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java index 633499eb77..5a482cbce4 100644 --- a/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java +++ b/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java @@ -32,6 +32,7 @@ package com.jme3.input.android; import android.opengl.GLSurfaceView; +import android.view.MotionEvent; import com.jme3.input.InputManager; import com.jme3.input.JoyInput; import com.jme3.input.Joystick; @@ -39,6 +40,7 @@ import com.jme3.input.event.InputEvent; import com.jme3.input.event.JoyAxisEvent; import com.jme3.input.event.JoyButtonEvent; +import com.jme3.input.virtual.VirtualJoystick; import com.jme3.system.AppSettings; import com.jme3.system.JmeSystem; import java.util.ArrayList; @@ -50,7 +52,7 @@ /** * Main class that manages various joystick devices. Joysticks can be many forms * including a simulated joystick to communicate the device orientation as well - * as physical gamepads.
+ * as physical joysticks.
* This class manages all the joysticks and feeds the inputs from each back * to jME's InputManager. * @@ -67,7 +69,7 @@ * * MainActivity needs the following line to enable Joysticks on Android platforms * joystickEventsEnabled = true; - * This is done to allow for battery conservation when sensor data or gamepads + * This is done to allow for battery conservation when sensor data or joysticks * are not required by the application. * * {@code @@ -92,6 +94,11 @@ public class AndroidJoyInput implements JoyInput { private ConcurrentLinkedQueue eventQueue = new ConcurrentLinkedQueue<>(); private AndroidSensorJoyInput sensorJoyInput; private boolean onDeviceJoystickRumble = false; + private String virtualJoystickMode = AppSettings.VIRTUAL_JOYSTICK_AUTO_MINIMIZED; + private boolean useJoysticks = true; + private boolean physicalJoystickAvailable = false; + private boolean keyboardSuppressedAutoJoystick = false; + private VirtualJoystick virtualJoystick; public AndroidJoyInput(AndroidInputHandler inputHandler) { this.inputHandler = inputHandler; @@ -106,6 +113,8 @@ public void setView(GLSurfaceView view) { public void loadSettings(AppSettings settings) { onDeviceJoystickRumble = settings.isOnDeviceJoystickRumble(); + virtualJoystickMode = settings.getVirtualJoystickMode(); + useJoysticks = settings.useJoysticks(); } boolean isOnDeviceJoystickRumble() { @@ -127,6 +136,9 @@ public void pauseJoysticks() { if (onDeviceJoystickRumble) { JmeSystem.stopRumble(); } + if (virtualJoystick != null) { + virtualJoystick.onPointerCancel(0L); + } } @@ -191,12 +203,65 @@ public Joystick[] loadJoysticks(InputManager inputManager) { if (logger.isLoggable(Level.INFO)) { logger.log(Level.INFO, "loading joysticks for {0}", this.getClass().getName()); } + joystickList.clear(); if (!disableSensors) { joystickList.add(sensorJoyInput.loadJoystick(joystickList.size(), inputManager)); } + physicalJoystickAvailable = false; + if (shouldCreateVirtualJoystick()) { + virtualJoystick = new VirtualJoystick(inputManager, this, joystickList.size()); + updateVirtualJoystickAutoVisibility(); + joystickList.add(virtualJoystick); + } else { + virtualJoystick = null; + } return joystickList.toArray( new Joystick[joystickList.size()] ); } + public boolean onTouch(MotionEvent event) { + if (virtualJoystick == null || inputHandler.getView() == null) { + return false; + } + + boolean consumed = false; + int action = event.getAction() & MotionEvent.ACTION_MASK; + int pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) + >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; + long time = event.getEventTime(); + + switch (action) { + case MotionEvent.ACTION_POINTER_DOWN: + case MotionEvent.ACTION_DOWN: + consumed = virtualJoystick.onPointerDown(event.getPointerId(pointerIndex), + toJmeX(event.getX(pointerIndex)), toJmeY(event.getY(pointerIndex)), time); + break; + case MotionEvent.ACTION_POINTER_UP: + case MotionEvent.ACTION_UP: + consumed = virtualJoystick.onPointerUp(event.getPointerId(pointerIndex), + toJmeX(event.getX(pointerIndex)), toJmeY(event.getY(pointerIndex)), time); + break; + case MotionEvent.ACTION_CANCEL: + consumed = virtualJoystick.onPointerCancel(time); + break; + case MotionEvent.ACTION_MOVE: + for (int i = 0; i < event.getPointerCount(); i++) { + consumed = virtualJoystick.onPointerMove(event.getPointerId(i), + toJmeX(event.getX(i)), toJmeY(event.getY(i)), time) || consumed; + } + break; + default: + break; + } + return consumed; + } + + public void onKeyboardInput() { + if (isAutoMode(virtualJoystickMode)) { + keyboardSuppressedAutoJoystick = true; + updateVirtualJoystickAutoVisibility(); + } + } + @Override public void update() { if (sensorJoyInput != null) { @@ -214,7 +279,56 @@ public void update() { } } } + if (virtualJoystick != null) { + virtualJoystick.dispatchEvents(listener); + } + + } + + private float toJmeX(float x) { + return inputHandler.touchInput.getJmeX(x); + } + + private float toJmeY(float y) { + return inputHandler.touchInput.invertY(inputHandler.touchInput.getJmeY(y)); + } + + protected void setPhysicalJoystickAvailable(boolean available) { + physicalJoystickAvailable = available; + updateVirtualJoystickAutoVisibility(); + } + + private boolean shouldCreateVirtualJoystick() { + return useJoysticks && !AppSettings.VIRTUAL_JOYSTICK_DISABLED.equals(virtualJoystickMode); + } + + private void updateVirtualJoystickAutoVisibility() { + if (virtualJoystick == null) { + return; + } + boolean active = isEnabledMode(virtualJoystickMode) + || (isAutoMode(virtualJoystickMode) + && !physicalJoystickAvailable + && !keyboardSuppressedAutoJoystick); + virtualJoystick.setEnabled(active); + if (active && isMinimizedMode(virtualJoystickMode)) { + virtualJoystick.setShown(false); + } + } + + private static boolean isEnabledMode(String mode) { + return AppSettings.VIRTUAL_JOYSTICK_ENABLED.equals(mode) + || AppSettings.VIRTUAL_JOYSTICK_ENABLED_MINIMIZED.equals(mode); + } + + private static boolean isAutoMode(String mode) { + return AppSettings.VIRTUAL_JOYSTICK_AUTO.equals(mode) + || AppSettings.VIRTUAL_JOYSTICK_AUTO_MINIMIZED.equals(mode); + } + private static boolean isMinimizedMode(String mode) { + return AppSettings.VIRTUAL_JOYSTICK_ENABLED_MINIMIZED.equals(mode) + || AppSettings.VIRTUAL_JOYSTICK_AUTO_MINIMIZED.equals(mode); } } diff --git a/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput14.java b/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput14.java index ffdcaa590e..180ed8c17f 100644 --- a/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput14.java +++ b/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput14.java @@ -93,7 +93,9 @@ public Joystick[] loadJoysticks(InputManager inputManager) { // load the simulated joystick for device orientation super.loadJoysticks(inputManager); // load physical gamepads/joysticks + int beforePhysicalJoysticks = joystickList.size(); joystickList.addAll(joystickJoyInput.loadJoysticks(joystickList.size(), inputManager)); + setPhysicalJoystickAvailable(joystickList.size() > beforePhysicalJoysticks); // return the list of joysticks back to InputManager return joystickList.toArray( new Joystick[joystickList.size()] ); } diff --git a/jme3-core/src/main/java/com/jme3/app/SimpleApplication.java b/jme3-core/src/main/java/com/jme3/app/SimpleApplication.java index 7850b4b218..ced517be5f 100644 --- a/jme3-core/src/main/java/com/jme3/app/SimpleApplication.java +++ b/jme3-core/src/main/java/com/jme3/app/SimpleApplication.java @@ -37,10 +37,13 @@ import com.jme3.font.BitmapFont; import com.jme3.font.BitmapText; import com.jme3.input.FlyByCamera; +import com.jme3.input.Joystick; import com.jme3.input.KeyInput; import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; +import com.jme3.input.virtual.VirtualJoystick; import com.jme3.profile.AppStep; +import com.jme3.renderer.Camera; import com.jme3.renderer.RenderManager; import com.jme3.renderer.queue.RenderQueue.Bucket; import com.jme3.scene.Node; @@ -336,6 +339,7 @@ public void update() { if (prof != null) { prof.appStep(AppStep.SpatialUpdate); } + updateVirtualJoystickVisuals(tpf); rootNode.updateLogicalState(tpf); guiNode.updateLogicalState(tpf); @@ -410,4 +414,22 @@ public void simpleUpdate(float tpf) { public void simpleRender(RenderManager rm) { // Default empty implementation; subclasses can override } + + private void updateVirtualJoystickVisuals(float tpf) { + if (inputManager == null || assetManager == null || guiViewPort == null) { + return; + } + Joystick[] joysticks = inputManager.getJoysticks(); + if (joysticks == null) { + return; + } + Camera guiCamera = guiViewPort.getCamera(); + int width = guiCamera != null ? guiCamera.getWidth() : settings.getWidth(); + int height = guiCamera != null ? guiCamera.getHeight() : settings.getHeight(); + for (Joystick joystick : joysticks) { + if (joystick instanceof VirtualJoystick) { + ((VirtualJoystick) joystick).updateVisuals(guiNode, assetManager, width, height, tpf); + } + } + } } diff --git a/jme3-core/src/main/java/com/jme3/input/ChaseCamera.java b/jme3-core/src/main/java/com/jme3/input/ChaseCamera.java index fe494801b3..3ff89e8166 100644 --- a/jme3-core/src/main/java/com/jme3/input/ChaseCamera.java +++ b/jme3-core/src/main/java/com/jme3/input/ChaseCamera.java @@ -50,7 +50,7 @@ * A camera that follows a spatial and can turn around it by dragging the mouse * @author nehon */ -public class ChaseCamera implements ActionListener, AnalogListener, Control, JmeCloneable { +public class ChaseCamera implements ActionListener, AnalogListener, Control, JmeCloneable, JoystickConnectionListener { protected Spatial target = null; protected float minVerticalRotation = 0.00f; @@ -134,8 +134,15 @@ public class ChaseCamera implements ActionListener, AnalogListener, Control, Jme /** * @deprecated use {@link CameraInput#CHASECAM_TOGGLEROTATE} */ - @Deprecated - public static final String ChaseCamToggleRotate = "ChaseCamToggleRotate"; + @Deprecated + public static final String ChaseCamToggleRotate = "ChaseCamToggleRotate"; + + private static final String CHASECAM_JOYDOWN = "ChaseCamJoyDown"; + private static final String CHASECAM_JOYUP = "ChaseCamJoyUp"; + private static final String CHASECAM_JOYMOVELEFT = "ChaseCamJoyMoveLeft"; + private static final String CHASECAM_JOYMOVERIGHT = "ChaseCamJoyMoveRight"; + private static final String CHASECAM_JOYZOOMIN = "ChaseCamJoyZoomIn"; + private static final String CHASECAM_JOYZOOMOUT = "ChaseCamJoyZoomOut"; protected boolean zoomin; protected boolean hideCursorOnRotate = true; @@ -219,14 +226,34 @@ public void onAnalog(String name, float value, float tpf) { distanceLerpFactor = 0; } zoomin = true; - } else if (name.equals(CameraInput.CHASECAM_ZOOMOUT)) { - zoomCamera(+value); - if (zoomin == true) { - distanceLerpFactor = 0; - } - zoomin = false; - } - } + } else if (name.equals(CameraInput.CHASECAM_ZOOMOUT)) { + zoomCamera(+value); + if (zoomin == true) { + distanceLerpFactor = 0; + } + zoomin = false; + } else if (name.equals(CHASECAM_JOYMOVELEFT)) { + rotateCamera(-value, true); + } else if (name.equals(CHASECAM_JOYMOVERIGHT)) { + rotateCamera(value, true); + } else if (name.equals(CHASECAM_JOYUP)) { + vRotateCamera(value, true); + } else if (name.equals(CHASECAM_JOYDOWN)) { + vRotateCamera(-value, true); + } else if (name.equals(CHASECAM_JOYZOOMIN)) { + zoomCamera(-value); + if (zoomin == false) { + distanceLerpFactor = 0; + } + zoomin = true; + } else if (name.equals(CHASECAM_JOYZOOMOUT)) { + zoomCamera(+value); + if (zoomin == true) { + distanceLerpFactor = 0; + } + zoomin = false; + } + } /** * Registers inputs with the input manager @@ -237,10 +264,16 @@ public final void registerWithInput(InputManager inputManager) { String[] inputs = {CameraInput.CHASECAM_TOGGLEROTATE, CameraInput.CHASECAM_DOWN, CameraInput.CHASECAM_UP, - CameraInput.CHASECAM_MOVELEFT, - CameraInput.CHASECAM_MOVERIGHT, - CameraInput.CHASECAM_ZOOMIN, - CameraInput.CHASECAM_ZOOMOUT}; + CameraInput.CHASECAM_MOVELEFT, + CameraInput.CHASECAM_MOVERIGHT, + CameraInput.CHASECAM_ZOOMIN, + CameraInput.CHASECAM_ZOOMOUT, + CHASECAM_JOYDOWN, + CHASECAM_JOYUP, + CHASECAM_JOYMOVELEFT, + CHASECAM_JOYMOVERIGHT, + CHASECAM_JOYZOOMIN, + CHASECAM_JOYZOOMOUT}; this.inputManager = inputManager; if (!invertYaxis) { @@ -271,11 +304,66 @@ public final void registerWithInput(InputManager inputManager) { } inputManager.addMapping(CameraInput.CHASECAM_TOGGLEROTATE, new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); - inputManager.addMapping(CameraInput.CHASECAM_TOGGLEROTATE, - new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)); - - inputManager.addListener(this, inputs); - } + inputManager.addMapping(CameraInput.CHASECAM_TOGGLEROTATE, + new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)); + + Joystick[] joysticks = inputManager.getJoysticks(); + if (joysticks != null) { + for (Joystick joystick : joysticks) { + mapJoystick(joystick); + } + } + + inputManager.addListener(this, inputs); + inputManager.addJoystickConnectionListener(this); + } + + /** + * Configures joystick input mappings for the chase camera. + * + * @param joystick joystick to map + */ + protected void mapJoystick(Joystick joystick) { + JoystickAxis xAxis = joystick.getAxis(JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_X); + JoystickAxis yAxis = joystick.getAxis(JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_Y); + + if (xAxis == null) { + xAxis = joystick.getXAxis(); + } + if (yAxis == null) { + yAxis = joystick.getYAxis(); + } + + if (xAxis != null) { + xAxis.assignAxis(CHASECAM_JOYMOVERIGHT, CHASECAM_JOYMOVELEFT); + } + if (yAxis != null) { + yAxis.assignAxis(CHASECAM_JOYDOWN, CHASECAM_JOYUP); + } + + JoystickAxis leftTrigger = joystick.getAxis(JoystickAxis.AXIS_XBOX_LEFT_TRIGGER); + if (leftTrigger != null) { + inputManager.addMapping(CHASECAM_JOYZOOMOUT, + new JoyAxisTrigger(joystick.getJoyId(), leftTrigger.getAxisId(), false)); + } + + JoystickAxis rightTrigger = joystick.getAxis(JoystickAxis.AXIS_XBOX_RIGHT_TRIGGER); + if (rightTrigger != null) { + inputManager.addMapping(CHASECAM_JOYZOOMIN, + new JoyAxisTrigger(joystick.getJoyId(), rightTrigger.getAxisId(), false)); + } + } + + @Override + public void onConnected(Joystick joystick) { + if (inputManager != null) { + mapJoystick(joystick); + } + } + + @Override + public void onDisconnected(Joystick joystick) { + } /** * Cleans up the input mappings from the input manager. @@ -287,11 +375,18 @@ public void cleanupWithInput(InputManager mgr) { mgr.deleteMapping(CameraInput.CHASECAM_DOWN); mgr.deleteMapping(CameraInput.CHASECAM_UP); mgr.deleteMapping(CameraInput.CHASECAM_MOVELEFT); - mgr.deleteMapping(CameraInput.CHASECAM_MOVERIGHT); - mgr.deleteMapping(CameraInput.CHASECAM_ZOOMIN); - mgr.deleteMapping(CameraInput.CHASECAM_ZOOMOUT); - mgr.removeListener(this); - } + mgr.deleteMapping(CameraInput.CHASECAM_MOVERIGHT); + mgr.deleteMapping(CameraInput.CHASECAM_ZOOMIN); + mgr.deleteMapping(CameraInput.CHASECAM_ZOOMOUT); + mgr.deleteMapping(CHASECAM_JOYDOWN); + mgr.deleteMapping(CHASECAM_JOYUP); + mgr.deleteMapping(CHASECAM_JOYMOVELEFT); + mgr.deleteMapping(CHASECAM_JOYMOVERIGHT); + mgr.deleteMapping(CHASECAM_JOYZOOMIN); + mgr.deleteMapping(CHASECAM_JOYZOOMOUT); + mgr.removeListener(this); + mgr.removeJoystickConnectionListener(this); + } /** * Sets custom triggers for toggling the rotation of the cam @@ -340,12 +435,16 @@ protected void computePosition() { pos.addLocal(target.getWorldTranslation()); } - //rotate the camera around the target on the horizontal plane - protected void rotateCamera(float value) { - if (!canRotate || !enabled) { - return; - } - rotating = true; + //rotate the camera around the target on the horizontal plane + protected void rotateCamera(float value) { + rotateCamera(value, false); + } + + protected void rotateCamera(float value, boolean forceRotate) { + if ((!forceRotate && !canRotate) || !enabled) { + return; + } + rotating = true; targetRotation += value * rotationSpeed; @@ -372,12 +471,16 @@ protected void zoomCamera(float value) { } } - //rotate the camera around the target on the vertical plane - protected void vRotateCamera(float value) { - if (!canRotate || !enabled) { - return; - } - vRotating = true; + //rotate the camera around the target on the vertical plane + protected void vRotateCamera(float value) { + vRotateCamera(value, false); + } + + protected void vRotateCamera(float value, boolean forceRotate) { + if ((!forceRotate && !canRotate) || !enabled) { + return; + } + vRotating = true; float lastGoodRot = targetVRotation; targetVRotation += value * rotationSpeed; if (targetVRotation > maxVerticalRotation) { diff --git a/jme3-core/src/main/java/com/jme3/input/FlyByCamera.java b/jme3-core/src/main/java/com/jme3/input/FlyByCamera.java index 1a39ee9820..d0b54da294 100644 --- a/jme3-core/src/main/java/com/jme3/input/FlyByCamera.java +++ b/jme3-core/src/main/java/com/jme3/input/FlyByCamera.java @@ -54,7 +54,12 @@ * - WASD keys for moving forward/backward and strafing * - QZ keys raise or lower the camera */ -public class FlyByCamera implements AnalogListener, ActionListener { +public class FlyByCamera implements AnalogListener, ActionListener, JoystickConnectionListener { + + private static final String FLYCAM_JOYSTICK_LEFT = "FLYCAM_JoystickLeft"; + private static final String FLYCAM_JOYSTICK_RIGHT = "FLYCAM_JoystickRight"; + private static final String FLYCAM_JOYSTICK_UP = "FLYCAM_JoystickUp"; + private static final String FLYCAM_JOYSTICK_DOWN = "FLYCAM_JoystickDown"; private static final String[] mappings = new String[]{ CameraInput.FLYCAM_LEFT, @@ -74,7 +79,12 @@ public class FlyByCamera implements AnalogListener, ActionListener { CameraInput.FLYCAM_RISE, CameraInput.FLYCAM_LOWER, - CameraInput.FLYCAM_INVERTY + CameraInput.FLYCAM_INVERTY, + + FLYCAM_JOYSTICK_LEFT, + FLYCAM_JOYSTICK_RIGHT, + FLYCAM_JOYSTICK_UP, + FLYCAM_JOYSTICK_DOWN }; /** * camera controlled by this controller (not null) @@ -295,6 +305,7 @@ public void registerWithInput(InputManager inputManager) { mapJoystick(j); } } + inputManager.addJoystickConnectionListener(this); } /** @@ -304,36 +315,40 @@ public void registerWithInput(InputManager inputManager) { * @param joystick The {@link Joystick} to map (not null). */ protected void mapJoystick(Joystick joystick) { - // Map it differently if there are Z axis - if (joystick.getAxis(JoystickAxis.Z_ROTATION) != null - && joystick.getAxis(JoystickAxis.Z_AXIS) != null) { - - // Make the left stick move - joystick.getXAxis().assignAxis(CameraInput.FLYCAM_STRAFERIGHT, CameraInput.FLYCAM_STRAFELEFT); - joystick.getYAxis().assignAxis(CameraInput.FLYCAM_BACKWARD, CameraInput.FLYCAM_FORWARD); - - // And the right stick control the camera - joystick.getAxis(JoystickAxis.Z_ROTATION) - .assignAxis(CameraInput.FLYCAM_DOWN, CameraInput.FLYCAM_UP); - joystick.getAxis(JoystickAxis.Z_AXIS) - .assignAxis(CameraInput.FLYCAM_RIGHT, CameraInput.FLYCAM_LEFT); - - // And let the dpad be up and down - joystick.getPovYAxis().assignAxis(CameraInput.FLYCAM_RISE, CameraInput.FLYCAM_LOWER); - - if (joystick.getButton("Button 8") != null) { - // Let the standard select button be the y invert toggle - joystick.getButton("Button 8").assignButton(CameraInput.FLYCAM_INVERTY); - } + JoystickAxis xAxis = joystick.getXAxis(); + JoystickAxis yAxis = joystick.getYAxis(); + JoystickAxis rightXAxis = joystick.getAxis(JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_X); + JoystickAxis rightYAxis = joystick.getAxis(JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_Y); - } else { - joystick.getPovXAxis().assignAxis(CameraInput.FLYCAM_STRAFERIGHT, CameraInput.FLYCAM_STRAFELEFT); - joystick.getPovYAxis().assignAxis(CameraInput.FLYCAM_FORWARD, CameraInput.FLYCAM_BACKWARD); - joystick.getXAxis().assignAxis(CameraInput.FLYCAM_RIGHT, CameraInput.FLYCAM_LEFT); - joystick.getYAxis().assignAxis(CameraInput.FLYCAM_DOWN, CameraInput.FLYCAM_UP); + if (xAxis != null) { + xAxis.assignAxis(CameraInput.FLYCAM_STRAFERIGHT, CameraInput.FLYCAM_STRAFELEFT); + } + if (yAxis != null) { + yAxis.assignAxis(CameraInput.FLYCAM_BACKWARD, CameraInput.FLYCAM_FORWARD); + } + if (rightXAxis != null && rightYAxis != null && rightXAxis != xAxis && rightYAxis != yAxis) { + rightYAxis.assignAxis(FLYCAM_JOYSTICK_DOWN, FLYCAM_JOYSTICK_UP); + rightXAxis.assignAxis(FLYCAM_JOYSTICK_RIGHT, FLYCAM_JOYSTICK_LEFT); + } + + JoystickButton backButton = joystick.getButton(JoystickButton.BUTTON_XBOX_BACK); + if (backButton != null) { + // Let the standard select button be the y invert toggle + backButton.assignButton(CameraInput.FLYCAM_INVERTY); + } + } + + @Override + public void onConnected(Joystick joystick) { + if (inputManager != null) { + mapJoystick(joystick); } } + @Override + public void onDisconnected(Joystick joystick) { + } + /** * Unregisters this controller from its currently associated {@link InputManager}. */ @@ -349,6 +364,7 @@ public void unregisterInput() { } inputManager.removeListener(this); + inputManager.removeJoystickConnectionListener(this); inputManager.setCursorVisible(!dragToRotate); // Joysticks cannot be "unassigned" in the same way, but mappings are removed with listener. @@ -363,7 +379,11 @@ public void unregisterInput() { * @param axis The axis around which to rotate (a unit vector, unaffected). */ protected void rotateCamera(float value, Vector3f axis) { - if (dragToRotate && !canRotate) { + rotateCamera(value, axis, false); + } + + private void rotateCamera(float value, Vector3f axis, boolean forceRotate) { + if (!forceRotate && dragToRotate && !canRotate) { return; // In drag-to-rotate mode, only rotate if canRotate is true. } @@ -479,6 +499,14 @@ public void onAnalog(String name, float value, float tpf) { rotateCamera(-value * (invertY ? -1 : 1), cam.getLeft(tempLeft)); } else if (name.equals(CameraInput.FLYCAM_DOWN)) { rotateCamera(value * (invertY ? -1 : 1), cam.getLeft(tempLeft)); + } else if (name.equals(FLYCAM_JOYSTICK_LEFT)) { + rotateCamera(value, initialUpVec, true); + } else if (name.equals(FLYCAM_JOYSTICK_RIGHT)) { + rotateCamera(-value, initialUpVec, true); + } else if (name.equals(FLYCAM_JOYSTICK_UP)) { + rotateCamera(-value * (invertY ? -1 : 1), cam.getLeft(tempLeft), true); + } else if (name.equals(FLYCAM_JOYSTICK_DOWN)) { + rotateCamera(value * (invertY ? -1 : 1), cam.getLeft(tempLeft), true); } else if (name.equals(CameraInput.FLYCAM_FORWARD)) { moveCamera(value, false); } else if (name.equals(CameraInput.FLYCAM_BACKWARD)) { diff --git a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java new file mode 100644 index 0000000000..f7f0c9b20d --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java @@ -0,0 +1,675 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.input.virtual; + +import com.jme3.asset.AssetManager; +import com.jme3.font.BitmapFont; +import com.jme3.font.BitmapText; +import com.jme3.input.AbstractJoystick; +import com.jme3.input.DefaultJoystickAxis; +import com.jme3.input.DefaultJoystickButton; +import com.jme3.input.InputManager; +import com.jme3.input.JoyInput; +import com.jme3.input.JoystickAxis; +import com.jme3.input.JoystickButton; +import com.jme3.input.RawInputListener; +import com.jme3.input.event.InputEvent; +import com.jme3.input.event.JoyAxisEvent; +import com.jme3.input.event.JoyButtonEvent; +import com.jme3.input.virtual.VirtualJoystickLayout.Element; +import com.jme3.input.virtual.VirtualJoystickTheme.TextureKey; +import com.jme3.math.FastMath; +import com.jme3.scene.Node; +import com.jme3.system.JmeSystem; +import com.jme3.ui.Picture; +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.Map; + +/** + * A joystick implementation driven by on-screen controls. + */ +public class VirtualJoystick extends AbstractJoystick { + + private static final int AXIS_LEFT_X = 0; + private static final int AXIS_LEFT_Y = 1; + private static final int AXIS_RIGHT_X = 2; + private static final int AXIS_RIGHT_Y = 3; + private static final int AXIS_LEFT_TRIGGER = 4; + private static final int AXIS_RIGHT_TRIGGER = 5; + private static final int AXIS_POV_X_ID = 6; + private static final int AXIS_POV_Y_ID = 7; + + private static final String ROOT_NAME = "Virtual Joystick"; + + private final Map axesByLogicalId = new HashMap<>(); + private final Map buttonsByLogicalId = new HashMap<>(); + private final Map captures = new HashMap<>(); + private final ArrayDeque events = new ArrayDeque<>(); + private final float[] axisValues = new float[8]; + private final boolean[] buttonValues = new boolean[16]; + private final Object inputLock = new Object(); + + private JoystickAxis xAxis; + private JoystickAxis yAxis; + private JoystickAxis povXAxis; + private JoystickAxis povYAxis; + private volatile boolean enabled = true; + private volatile boolean shown = true; + private volatile int buttonStateMask; + private volatile boolean hasEvents; + private volatile VirtualJoystickTheme theme = new VirtualJoystickTheme(); + private volatile VirtualJoystickLayout layout = new VirtualJoystickLayout(); + private volatile int visualWidth; + private volatile int visualHeight; + private Node visualRoot; + private Node visualParent; + private BitmapFont font; + + public VirtualJoystick(InputManager inputManager, JoyInput joyInput, int joyId) { + super(inputManager, joyInput, joyId, "Virtual Joystick"); + addAxes(); + addButtons(inputManager); + releaseAllLocked(0L); + } + + /** + * Returns true if this joystick accepts pointer input. + * + * @return true if enabled + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Enables or disables pointer processing for this joystick. + * + * @param enabled true to accept pointer input + */ + public void setEnabled(boolean enabled) { + synchronized (inputLock) { + this.enabled = enabled; + if (!enabled) { + releaseAllLocked(0L); + } + } + } + + public boolean isShown() { + return shown; + } + + public void setShown(boolean shown) { + synchronized (inputLock) { + this.shown = shown; + if (!shown) { + releaseAllLocked(0L); + } + } + } + + public VirtualJoystickTheme getTheme() { + return theme; + } + + public void setTheme(VirtualJoystickTheme theme) { + this.theme = theme == null ? new VirtualJoystickTheme() : theme; + this.theme.markUpdateNeeded(); + } + + public VirtualJoystickLayout getLayout() { + return layout; + } + + public void setLayout(VirtualJoystickLayout layout) { + synchronized (inputLock) { + releaseAllLocked(0L); + this.layout = layout == null ? new VirtualJoystickLayout() : layout; + this.layout.markUpdateNeeded(); + } + } + + @Override + public void rumble(float amountHigh, float amountLow, float duration) { + if (JmeSystem.isDeviceRumbleSupported()) { + JmeSystem.rumble(amountHigh, amountLow, duration); + } + } + + @Override + public void stopRumble() { + JmeSystem.stopRumble(); + } + + /** + * Restores the default Xbox-like layout. + */ + public void reset() { + synchronized (inputLock) { + layout.resetToDefault(); + releaseAllLocked(0L); + } + } + + /** + * Processes a pointer-down event. + * + * @return true if the pointer was captured by a virtual control + */ + public boolean onPointerDown(int pointerId, float x, float y, long time) { + synchronized (inputLock) { + if (!enabled || captures.containsKey(pointerId)) { + return captures.containsKey(pointerId); + } + + Element toggleElement = layout.getToggleElement(); + if (toggleElement != null && toggleElement.contains(x, y)) { + captures.put(pointerId, new Capture(toggleElement, false, true)); + return true; + } + + if (!shown) { + return false; + } + + for (Element element : layout.getAxisElements()) { + if (element.visible && element.contains(x, y)) { + captures.put(pointerId, new Capture(element, true, false)); + updateAxisCapture(element, x, y, time); + return true; + } + } + + for (Element element : layout.getButtons()) { + if (element.visible && element.contains(x, y)) { + captures.put(pointerId, new Capture(element, false, false)); + if (isToggleButton(element.id)) { + pressButton(element.id, !isButtonPressed(element.id), time); + } else { + pressButton(element.id, true, time); + } + return true; + } + } + + return false; + } + } + + /** + * Processes a pointer-move event. + * + * @return true if the pointer is captured by a virtual control + */ + public boolean onPointerMove(int pointerId, float x, float y, long time) { + synchronized (inputLock) { + Capture capture = captures.get(pointerId); + if (capture == null) { + return false; + } + if (capture.toggle) { + return true; + } + if (capture.axis) { + updateAxisCapture(capture.element, x, y, time); + } + return true; + } + } + + /** + * Processes a pointer-up event. + * + * @return true if the pointer was captured by a virtual control + */ + public boolean onPointerUp(int pointerId, float x, float y, long time) { + synchronized (inputLock) { + Capture capture = captures.remove(pointerId); + if (capture == null) { + return false; + } + if (capture.toggle) { + shown = !shown; + releaseAllLocked(time); + return true; + } + if (capture.axis) { + centerAxisCapture(capture.element, time); + } else if (!isToggleButton(capture.element.id)) { + pressButton(capture.element.id, false, time); + } + return true; + } + } + + /** + * Releases all active pointer captures without activating toggle buttons. + * + * @return true if at least one pointer was captured + */ + public boolean onPointerCancel(long time) { + synchronized (inputLock) { + boolean captured = !captures.isEmpty(); + releaseAllLocked(time); + return captured; + } + } + + /** + * Dispatches pending joystick events to the backend listener. + * + * @param listener listener that receives joystick events + */ + public void dispatchEvents(RawInputListener listener) { + if (!hasEvents) { + return; + } + + ArrayDeque pendingEvents; + synchronized (inputLock) { + if (!hasEvents) { + return; + } + if (listener == null) { + events.clear(); + hasEvents = false; + return; + } + pendingEvents = new ArrayDeque<>(events); + events.clear(); + hasEvents = false; + } + + InputEvent event; + while ((event = pendingEvents.poll()) != null) { + if (event instanceof JoyAxisEvent) { + listener.onJoyAxisEvent((JoyAxisEvent) event); + } else if (event instanceof JoyButtonEvent) { + listener.onJoyButtonEvent((JoyButtonEvent) event); + } + } + } + + /** + * Synchronizes GUI spatials with the current joystick state. + * + * @param parent GUI node that should contain the controls + * @param assetManager asset manager used to load default textures + * @param width GUI width in pixels + * @param height GUI height in pixels + * @param tpf time per frame + */ + public void updateVisuals(Node parent, AssetManager assetManager, int width, int height, float tpf) { + if (parent == null || assetManager == null || width <= 0 || height <= 0) { + return; + } + if (visualRoot == null) { + visualRoot = new Node(ROOT_NAME); + } + if (visualParent != parent) { + visualRoot.removeFromParent(); + parent.attachChild(visualRoot); + visualParent = parent; + } + if (width != visualWidth || height != visualHeight) { + synchronized (inputLock) { + if (width != visualWidth || height != visualHeight) { + visualWidth = width; + visualHeight = height; + releaseAllLocked(0L); + } + } + } + + if (!enabled) { + visualRoot.detachAllChildren(); + return; + } + + VirtualJoystickTheme currentTheme = theme; + VirtualJoystickLayout currentLayout = layout; + boolean themeUpdateNeeded = currentTheme.isUpdateNeeded(); + if (themeUpdateNeeded || currentLayout.isUpdateNeeded()) { + clearVisuals(currentLayout); + if (themeUpdateNeeded) { + font = null; + } + currentTheme.clearUpdateNeeded(); + currentLayout.clearUpdateNeeded(); + } + String fontPath = currentTheme.getFontPath(); + if (font == null && fontPath != null) { + font = assetManager.loadFont(fontPath); + } + + float scale = currentLayout.getScale(); + syncElement(currentLayout.getToggleElement(), assetManager, width, height, scale); + if (!shown) { + for (Element element : currentLayout.getButtons()) { + if (element.node != null) { + element.node.removeFromParent(); + } + } + for (Element element : currentLayout.getAxisElements()) { + if (element.node != null) { + element.node.removeFromParent(); + } + } + return; + } + + for (Element element : currentLayout.getButtons()) { + syncElement(element, assetManager, width, height, scale); + } + for (Element element : currentLayout.getAxisElements()) { + syncElement(element, assetManager, width, height, scale); + } + } + + @Override + public JoystickAxis getXAxis() { + return xAxis; + } + + @Override + public JoystickAxis getYAxis() { + return yAxis; + } + + @Override + public JoystickAxis getPovXAxis() { + return povXAxis; + } + + @Override + public JoystickAxis getPovYAxis() { + return povYAxis; + } + + private void addAxes() { + xAxis = addAxis(AXIS_LEFT_X, "LEFT THUMB STICK (X)", JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_X); + yAxis = addAxis(AXIS_LEFT_Y, "LEFT THUMB STICK (Y)", JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_Y); + addAxis(AXIS_RIGHT_X, "RIGHT THUMB STICK (X)", JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_X); + addAxis(AXIS_RIGHT_Y, "RIGHT THUMB STICK (Y)", JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_Y); + addAxis(AXIS_LEFT_TRIGGER, "LEFT TRIGGER", JoystickAxis.AXIS_XBOX_LEFT_TRIGGER); + addAxis(AXIS_RIGHT_TRIGGER, "RIGHT TRIGGER", JoystickAxis.AXIS_XBOX_RIGHT_TRIGGER); + povXAxis = addAxis(AXIS_POV_X_ID, JoystickAxis.POV_X, JoystickAxis.POV_X); + povYAxis = addAxis(AXIS_POV_Y_ID, JoystickAxis.POV_Y, JoystickAxis.POV_Y); + } + + private JoystickAxis addAxis(int id, String name, String logicalId) { + JoystickAxis axis = new DefaultJoystickAxis(getInputManager(), this, id, name, logicalId, true, false, 0f); + axesByLogicalId.put(logicalId, axis); + super.addAxis(axis); + return axis; + } + + private void addButtons(InputManager inputManager) { + addButton(inputManager, 0, "A", JoystickButton.BUTTON_XBOX_A); + addButton(inputManager, 1, "B", JoystickButton.BUTTON_XBOX_B); + addButton(inputManager, 2, "X", JoystickButton.BUTTON_XBOX_X); + addButton(inputManager, 3, "Y", JoystickButton.BUTTON_XBOX_Y); + addButton(inputManager, 4, "LB", JoystickButton.BUTTON_XBOX_LB); + addButton(inputManager, 5, "RB", JoystickButton.BUTTON_XBOX_RB); + addButton(inputManager, 6, "LT", JoystickButton.BUTTON_XBOX_LT); + addButton(inputManager, 7, "RT", JoystickButton.BUTTON_XBOX_RT); + addButton(inputManager, 8, "BACK", JoystickButton.BUTTON_XBOX_BACK); + addButton(inputManager, 9, "START", JoystickButton.BUTTON_XBOX_START); + addButton(inputManager, 10, "L3", JoystickButton.BUTTON_XBOX_L3); + addButton(inputManager, 11, "R3", JoystickButton.BUTTON_XBOX_R3); + addButton(inputManager, 12, "D-PAD UP", JoystickButton.BUTTON_XBOX_DPAD_UP); + addButton(inputManager, 13, "D-PAD DOWN", JoystickButton.BUTTON_XBOX_DPAD_DOWN); + addButton(inputManager, 14, "D-PAD LEFT", JoystickButton.BUTTON_XBOX_DPAD_LEFT); + addButton(inputManager, 15, "D-PAD RIGHT", JoystickButton.BUTTON_XBOX_DPAD_RIGHT); + } + + private void addButton(InputManager inputManager, int id, String name, String logicalId) { + JoystickButton button = new DefaultJoystickButton(inputManager, this, id, name, logicalId); + buttonsByLogicalId.put(logicalId, button); + super.addButton(button); + } + + private void updateAxisCapture(Element element, float x, float y, long time) { + float radius = element.pixelSize * 0.5f; + if (radius <= 0f) { + return; + } + float dx = x - element.pixelX; + float dy = y - element.pixelY; + float length = FastMath.sqrt(dx * dx + dy * dy); + if (length > radius && length > 0f) { + dx *= radius / length; + dy *= radius / length; + } + + float valueX = FastMath.clamp(dx / radius, -1f, 1f); + float valueY = FastMath.clamp(dy / radius, -1f, 1f); + element.nubX = valueX; + element.nubY = valueY; + + setAxisValue(element.id, valueX, time); + setAxisValue(element.yAxisLogicalId, -valueY, time); + } + + private void centerAxisCapture(Element element, long time) { + element.nubX = 0f; + element.nubY = 0f; + setAxisValue(element.id, 0f, time); + setAxisValue(element.yAxisLogicalId, 0f, time); + } + + private void pressButton(String logicalId, boolean pressed, long time) { + JoystickButton button = buttonsByLogicalId.get(logicalId); + if (button == null) { + return; + } + int buttonId = button.getButtonId(); + if (buttonValues[buttonId] == pressed) { + return; + } + buttonValues[buttonId] = pressed; + if (pressed) { + buttonStateMask |= 1 << buttonId; + } else { + buttonStateMask &= ~(1 << buttonId); + } + JoyButtonEvent event = new JoyButtonEvent(button, pressed); + event.setTime(time); + enqueueEvent(event); + + if (JoystickButton.BUTTON_XBOX_LT.equals(logicalId)) { + setAxisValue(JoystickAxis.AXIS_XBOX_LEFT_TRIGGER, pressed ? 1f : 0f, time); + } else if (JoystickButton.BUTTON_XBOX_RT.equals(logicalId)) { + setAxisValue(JoystickAxis.AXIS_XBOX_RIGHT_TRIGGER, pressed ? 1f : 0f, time); + } else if (isDpad(logicalId)) { + updatePovAxes(time); + } + } + + private boolean isButtonPressed(String logicalId) { + JoystickButton button = buttonsByLogicalId.get(logicalId); + return button != null && buttonValues[button.getButtonId()]; + } + + private boolean isToggleButton(String logicalId) { + return JoystickButton.BUTTON_XBOX_L3.equals(logicalId) + || JoystickButton.BUTTON_XBOX_R3.equals(logicalId); + } + + private boolean isDpad(String logicalId) { + return JoystickButton.BUTTON_XBOX_DPAD_UP.equals(logicalId) + || JoystickButton.BUTTON_XBOX_DPAD_DOWN.equals(logicalId) + || JoystickButton.BUTTON_XBOX_DPAD_LEFT.equals(logicalId) + || JoystickButton.BUTTON_XBOX_DPAD_RIGHT.equals(logicalId); + } + + private void updatePovAxes(long time) { + float x = 0f; + float y = 0f; + if (buttonValues[buttonsByLogicalId.get(JoystickButton.BUTTON_XBOX_DPAD_UP).getButtonId()]) { + y += 1f; + } + if (buttonValues[buttonsByLogicalId.get(JoystickButton.BUTTON_XBOX_DPAD_DOWN).getButtonId()]) { + y -= 1f; + } + if (buttonValues[buttonsByLogicalId.get(JoystickButton.BUTTON_XBOX_DPAD_LEFT).getButtonId()]) { + x -= 1f; + } + if (buttonValues[buttonsByLogicalId.get(JoystickButton.BUTTON_XBOX_DPAD_RIGHT).getButtonId()]) { + x += 1f; + } + setAxisValue(JoystickAxis.POV_X, x, time); + setAxisValue(JoystickAxis.POV_Y, y, time); + } + + private void setAxisValue(String logicalId, float value, long time) { + JoystickAxis axis = axesByLogicalId.get(logicalId); + if (axis == null) { + return; + } + value = FastMath.clamp(value, -1f, 1f); + int axisId = axis.getAxisId(); + if (axisValues[axisId] == value) { + return; + } + axisValues[axisId] = value; + JoyAxisEvent event = new JoyAxisEvent(axis, value, value); + event.setTime(time); + enqueueEvent(event); + } + + private void enqueueEvent(InputEvent event) { + events.add(event); + hasEvents = true; + } + + private void releaseAllLocked(long time) { + captures.clear(); + for (Element element : layout.getAxisElements()) { + centerAxisCapture(element, time); + } + for (String logicalId : buttonsByLogicalId.keySet()) { + pressButton(logicalId, false, time); + } + } + + private void syncElement(Element element, AssetManager assetManager, int width, int height, float scale) { + if (element == null) { + return; + } + if (!element.visible) { + if (element.node != null) { + element.node.removeFromParent(); + } + return; + } + if (element.node == null) { + createVisual(element, assetManager); + } + if (element.node.getParent() != visualRoot) { + visualRoot.attachChild(element.node); + } + JoystickButton button = buttonsByLogicalId.get(element.id); + boolean pressed = button != null && (buttonStateMask & (1 << button.getButtonId())) != 0; + element.sync(width, height, scale, pressed); + } + + private void createVisual(Element element, AssetManager assetManager) { + element.node = new Node("Virtual Joystick " + element.id); + element.base = new Picture(element.id); + TextureKey baseTextureKey; + TextureKey nubTextureKey; + TextureKey iconTextureKey; + String label; + baseTextureKey = element.textureKey; + nubTextureKey = element.nubTextureKey; + iconTextureKey = element.iconTextureKey; + label = element.label; + element.base.setImage(assetManager, texture(baseTextureKey), true); + element.node.attachChild(element.base); + + if (nubTextureKey != null) { + element.nub = new Picture(element.id + " Nub"); + element.nub.setImage(assetManager, texture(nubTextureKey), true); + element.node.attachChild(element.nub); + } + + if (iconTextureKey != null) { + element.icon = new Picture(element.id + " Icon"); + element.icon.setImage(assetManager, texture(iconTextureKey), true); + element.node.attachChild(element.icon); + } + + if (font != null && !label.isEmpty()) { + element.text = new BitmapText(font, false); + element.text.setText(label); + element.node.attachChild(element.text); + } + } + + private String texture(TextureKey textureKey) { + String texturePath = theme.getTexture(textureKey); + if (texturePath == null) { + throw new IllegalStateException("No virtual joystick texture bound for key: " + textureKey); + } + return texturePath; + } + + private void clearVisuals(VirtualJoystickLayout layout) { + for (Element element : layout.getButtons()) { + element.clearVisuals(); + } + for (Element element : layout.getAxisElements()) { + element.clearVisuals(); + } + Element toggle = layout.getToggleElement(); + if (toggle != null) { + toggle.clearVisuals(); + } + if (visualRoot != null) { + visualRoot.detachAllChildren(); + } + } + + private static final class Capture { + final Element element; + final boolean axis; + final boolean toggle; + + Capture(Element element, boolean axis, boolean toggle) { + this.element = element; + this.axis = axis; + this.toggle = toggle; + } + } +} diff --git a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickLayout.java b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickLayout.java new file mode 100644 index 0000000000..89e53a6522 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickLayout.java @@ -0,0 +1,552 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.input.virtual; + +import com.jme3.export.InputCapsule; +import com.jme3.export.JmeExporter; +import com.jme3.export.JmeImporter; +import com.jme3.export.OutputCapsule; +import com.jme3.export.Savable; +import com.jme3.font.BitmapText; +import com.jme3.input.JoystickAxis; +import com.jme3.input.JoystickButton; +import com.jme3.input.virtual.VirtualJoystickTheme.TextureKey; +import com.jme3.math.ColorRGBA; +import com.jme3.math.FastMath; +import com.jme3.math.Vector2f; +import com.jme3.scene.Node; +import com.jme3.ui.Picture; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Virtual joystick controls layout. + */ +public class VirtualJoystickLayout implements Savable { + + private final Map buttons = new LinkedHashMap<>(); + private final Map axisElements = new LinkedHashMap<>(); + private volatile Element toggleElement; + private volatile float scale = 1.15f; + private transient volatile boolean updateNeeded = true; + private transient volatile Element[] buttonSnapshot = new Element[0]; + private transient volatile Element[] axisSnapshot = new Element[0]; + private transient boolean bulkUpdate; + + public VirtualJoystickLayout() { + resetToDefault(); + } + + public synchronized final void resetToDefault() { + buttons.clear(); + axisElements.clear(); + bulkUpdate = true; + + try { + float leftColumn = 0.135f; + float rightColumn = 0.865f; + float upperRow = 0.52f; + float lowerRow = 0.15f; + float faceButtonSize = 0.095f; + float faceButtonOffset = 0.106f; + float shoulderSize = 0.09f; + + addButtonElement(JoystickButton.BUTTON_XBOX_A, "", rightColumn, upperRow, 0f, -faceButtonOffset, faceButtonSize, + TextureKey.BUTTON, TextureKey.BUTTON_A_ICON); + addButtonElement(JoystickButton.BUTTON_XBOX_B, "", rightColumn, upperRow, faceButtonOffset, 0f, faceButtonSize, + TextureKey.BUTTON, TextureKey.BUTTON_B_ICON); + addButtonElement(JoystickButton.BUTTON_XBOX_X, "", rightColumn, upperRow, -faceButtonOffset, 0f, faceButtonSize, + TextureKey.BUTTON, TextureKey.BUTTON_X_ICON); + addButtonElement(JoystickButton.BUTTON_XBOX_Y, "", rightColumn, upperRow, 0f, faceButtonOffset, faceButtonSize, + TextureKey.BUTTON, TextureKey.BUTTON_Y_ICON); + + addButtonElement(JoystickButton.BUTTON_XBOX_LT, "LT", leftColumn, 0.94f, shoulderSize, 2f, + TextureKey.BUTTON_WIDE); + addButtonElement(JoystickButton.BUTTON_XBOX_RT, "RT", rightColumn, 0.94f, shoulderSize, 2f, + TextureKey.BUTTON_WIDE); + addButtonElement(JoystickButton.BUTTON_XBOX_LB, "LB", leftColumn, 0.79f, shoulderSize, 2f, + TextureKey.BUTTON_WIDE); + addButtonElement(JoystickButton.BUTTON_XBOX_RB, "RB", rightColumn, 0.79f, shoulderSize, 2f, + TextureKey.BUTTON_WIDE); + + addButtonElement(JoystickButton.BUTTON_XBOX_BACK, "", 0.44f, 0.06f, 0.078f, 2f, + TextureKey.BUTTON_WIDE, TextureKey.BUTTON_BACK_ICON); + addButtonElement(JoystickButton.BUTTON_XBOX_START, "", 0.56f, 0.06f, 0.078f, 2f, + TextureKey.BUTTON_WIDE, TextureKey.BUTTON_START_ICON); + addButtonElement(JoystickButton.BUTTON_XBOX_L3, "L3", leftColumn, upperRow, -0.145f, 0f, 0.065f, + TextureKey.BUTTON); + addButtonElement(JoystickButton.BUTTON_XBOX_R3, "R3", rightColumn, lowerRow, 0.145f, 0f, 0.065f, + TextureKey.BUTTON); + + addButtonElement(JoystickButton.BUTTON_XBOX_DPAD_UP, "", leftColumn, lowerRow, 0f, 0.064f, 0.085f, + TextureKey.DPAD_UP); + addButtonElement(JoystickButton.BUTTON_XBOX_DPAD_DOWN, "", leftColumn, lowerRow, 0f, -0.064f, 0.085f, + TextureKey.DPAD_DOWN); + addButtonElement(JoystickButton.BUTTON_XBOX_DPAD_LEFT, "", leftColumn, lowerRow, -0.064f, 0f, 0.085f, + TextureKey.DPAD_LEFT); + addButtonElement(JoystickButton.BUTTON_XBOX_DPAD_RIGHT, "", leftColumn, lowerRow, 0.064f, 0f, 0.085f, + TextureKey.DPAD_RIGHT); + + addAxisElement(JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_X, JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_Y, + "", leftColumn, upperRow, 0.235f, TextureKey.STICK_PAD, TextureKey.STICK_NUB); + addAxisElement(JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_X, JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_Y, + "", rightColumn, lowerRow, 0.235f, TextureKey.STICK_PAD, TextureKey.STICK_NUB); + setToggleElement(new Element("toggle", "", 0.5f, 0.94f, 0.055f, TextureKey.BUTTON) + .setIconTextureKey(TextureKey.TOGGLE_ICON)); + } finally { + bulkUpdate = false; + rebuildSnapshots(); + markUpdateNeeded(); + } + } + + public synchronized void addButtonElement(String logicalId, String label, float x, float y, float size, + TextureKey textureKey) { + buttons.put(logicalId, new Element(logicalId, label, x, y, size, textureKey)); + layoutChanged(); + } + + public synchronized void addButtonElement(String logicalId, String label, float x, float y, float shortOffsetX, + float shortOffsetY, float size, TextureKey textureKey) { + buttons.put(logicalId, new Element(logicalId, label, x, y, size, textureKey) + .setShortOffset(shortOffsetX, shortOffsetY)); + layoutChanged(); + } + + public synchronized void addButtonElement(String logicalId, String label, float x, float y, float shortOffsetX, + float shortOffsetY, float size, TextureKey textureKey, TextureKey iconTextureKey) { + buttons.put(logicalId, new Element(logicalId, label, x, y, size, textureKey) + .setShortOffset(shortOffsetX, shortOffsetY) + .setIconTextureKey(iconTextureKey)); + layoutChanged(); + } + + public synchronized void addButtonElement(String logicalId, String label, float x, float y, float size, float aspect, + TextureKey textureKey) { + buttons.put(logicalId, new Element(logicalId, label, x, y, size, textureKey).setAspect(aspect)); + layoutChanged(); + } + + public synchronized void addButtonElement(String logicalId, String label, float x, float y, float size, float aspect, + TextureKey textureKey, TextureKey iconTextureKey) { + buttons.put(logicalId, new Element(logicalId, label, x, y, size, textureKey) + .setAspect(aspect) + .setIconTextureKey(iconTextureKey)); + layoutChanged(); + } + + public synchronized void addAxisElement(String xAxisLogicalId, String yAxisLogicalId, String label, float x, float y, + float size, TextureKey textureKey, TextureKey nubTextureKey) { + axisElements.put(xAxisLogicalId, new Element(xAxisLogicalId, label, x, y, size, textureKey) + .setYAxisLogicalId(yAxisLogicalId) + .setNubTextureKey(nubTextureKey)); + layoutChanged(); + } + + public synchronized void setToggleElement(Element toggleElement) { + this.toggleElement = toggleElement; + layoutChanged(); + } + + public synchronized void setButtonPosition(String logicalId, float x, float y) { + element(buttons, logicalId).setPosition(x, y); + markUpdateNeeded(); + } + + public Vector2f getButtonPosition(String logicalId) { + Element element = element(buttons, logicalId); + return new Vector2f(element.positionX, element.positionY); + } + + public synchronized void setButtonVisible(String logicalId, boolean visible) { + Element element = element(buttons, logicalId); + element.visible = visible; + markUpdateNeeded(); + } + + public boolean isButtonVisible(String logicalId) { + return element(buttons, logicalId).visible; + } + + public synchronized void setAxisPosition(String logicalId, float x, float y) { + axisElement(logicalId).setPosition(x, y); + markUpdateNeeded(); + } + + public Vector2f getAxisPosition(String logicalId) { + Element element = axisElement(logicalId); + return new Vector2f(element.positionX, element.positionY); + } + + public synchronized void setAxisVisible(String logicalId, boolean visible) { + Element element = axisElement(logicalId); + element.visible = visible; + markUpdateNeeded(); + } + + public boolean isAxisVisible(String logicalId) { + return axisElement(logicalId).visible; + } + + public synchronized void setButtonSize(String logicalId, float size) { + Element element = element(buttons, logicalId); + element.size = FastMath.clamp(size, 0.01f, 1f); + markUpdateNeeded(); + } + + public synchronized void setButtonTextureKey(String logicalId, TextureKey textureKey) { + Element element = element(buttons, logicalId); + element.textureKey = textureKey; + markUpdateNeeded(); + } + + public synchronized void setButtonIconTextureKey(String logicalId, TextureKey iconTextureKey) { + Element element = element(buttons, logicalId); + element.iconTextureKey = iconTextureKey; + markUpdateNeeded(); + } + + public synchronized void setAxisSize(String logicalId, float size) { + Element element = axisElement(logicalId); + element.size = FastMath.clamp(size, 0.01f, 1f); + markUpdateNeeded(); + } + + public synchronized void setAxisTextureKey(String logicalId, TextureKey textureKey) { + Element element = axisElement(logicalId); + element.textureKey = textureKey; + markUpdateNeeded(); + } + + public synchronized void setAxisNubTextureKey(String logicalId, TextureKey nubTextureKey) { + Element element = axisElement(logicalId); + element.nubTextureKey = nubTextureKey; + markUpdateNeeded(); + } + + public synchronized void setScale(float scale) { + this.scale = FastMath.clamp(scale, 0.25f, 3f); + markUpdateNeeded(); + } + + public float getScale() { + return scale; + } + + boolean isUpdateNeeded() { + return updateNeeded; + } + + void markUpdateNeeded() { + updateNeeded = true; + } + + void clearUpdateNeeded() { + updateNeeded = false; + } + + Element[] getButtons() { + return buttonSnapshot; + } + + Element[] getAxisElements() { + return axisSnapshot; + } + + public Element getToggleElement() { + return toggleElement; + } + + public Element getButtonElement(String logicalId) { + return buttons.get(logicalId); + } + + public Element getAxisElement(String logicalId) { + return findAxisElement(logicalId); + } + + private Element axisElement(String logicalId) { + Element element = findAxisElement(logicalId); + if (element == null) { + throw new IllegalArgumentException("Unknown virtual joystick axis element: " + logicalId); + } + return element; + } + + private Element findAxisElement(String logicalId) { + Element element = axisElements.get(logicalId); + if (element != null) { + return element; + } + for (Element axisElement : axisElements.values()) { + if (logicalId.equals(axisElement.yAxisLogicalId)) { + return axisElement; + } + } + return null; + } + + private Element element(Map elements, String logicalId) { + Element element = elements.get(logicalId); + if (element == null) { + throw new IllegalArgumentException("Unknown virtual joystick element: " + logicalId); + } + return element; + } + + @Override + public synchronized void write(JmeExporter ex) throws IOException { + OutputCapsule capsule = ex.getCapsule(this); + capsule.write(scale, "scale", 1.15f); + capsule.write(buttons.values().toArray(new Element[0]), "buttons", null); + capsule.write(axisElements.values().toArray(new Element[0]), "axes", null); + capsule.write(toggleElement, "toggle", null); + } + + @Override + public synchronized void read(JmeImporter im) throws IOException { + InputCapsule capsule = im.getCapsule(this); + scale = capsule.readFloat("scale", 1.15f); + buttons.clear(); + axisElements.clear(); + Savable[] readButtons = capsule.readSavableArray("buttons", null); + if (readButtons != null) { + for (Savable savable : readButtons) { + Element element = (Element) savable; + buttons.put(element.id, element); + } + } + Savable[] readAxes = capsule.readSavableArray("axes", null); + if (readAxes != null) { + for (Savable savable : readAxes) { + Element element = (Element) savable; + axisElements.put(element.id, element); + } + } + toggleElement = (Element) capsule.readSavable("toggle", null); + rebuildSnapshots(); + markUpdateNeeded(); + } + + private void layoutChanged() { + if (!bulkUpdate) { + rebuildSnapshots(); + } + markUpdateNeeded(); + } + + private void rebuildSnapshots() { + buttonSnapshot = buttons.values().toArray(new Element[0]); + axisSnapshot = axisElements.values().toArray(new Element[0]); + } + + public static class Element implements Savable { + volatile String id; + volatile String label; + volatile String yAxisLogicalId; + volatile float positionX; + volatile float positionY; + volatile TextureKey textureKey; + volatile TextureKey nubTextureKey; + volatile TextureKey iconTextureKey; + volatile float size; + volatile float aspect = 1f; + volatile float shortOffsetX; + volatile float shortOffsetY; + volatile boolean visible = true; + transient volatile float pixelX; + transient volatile float pixelY; + transient volatile float pixelSize; + transient volatile float pixelWidth; + transient volatile float pixelHeight; + transient volatile float nubX; + transient volatile float nubY; + transient Node node; + transient Picture base; + transient Picture nub; + transient Picture icon; + transient BitmapText text; + + public Element() { + } + + public Element(String id, String label, float x, float y, float size, TextureKey textureKey) { + this.id = id; + this.label = label == null ? "" : label; + this.size = size; + this.textureKey = textureKey; + setPosition(x, y); + } + + public synchronized Element setPosition(float x, float y) { + positionX = FastMath.clamp(x, 0f, 1f); + positionY = FastMath.clamp(y, 0f, 1f); + shortOffsetX = 0f; + shortOffsetY = 0f; + return this; + } + + public synchronized Element setShortOffset(float x, float y) { + shortOffsetX = x; + shortOffsetY = y; + return this; + } + + public synchronized Element setAspect(float aspect) { + this.aspect = aspect; + return this; + } + + public synchronized Element setYAxisLogicalId(String yAxisLogicalId) { + this.yAxisLogicalId = yAxisLogicalId; + return this; + } + + public synchronized Element setNubTextureKey(TextureKey nubTextureKey) { + this.nubTextureKey = nubTextureKey; + return this; + } + + public synchronized Element setIconTextureKey(TextureKey iconTextureKey) { + this.iconTextureKey = iconTextureKey; + return this; + } + + boolean contains(float x, float y) { + return Math.abs(x - pixelX) <= pixelWidth * 0.5f + && Math.abs(y - pixelY) <= pixelHeight * 0.5f; + } + + void sync(int width, int height, float scale, boolean pressed) { + float shortSide = Math.min(width, height); + float scaledShortSide = shortSide * scale; + pixelSize = Math.max(scaledShortSide * size, 1f); + pixelWidth = pixelSize * aspect; + pixelHeight = pixelSize; + pixelX = positionX * width + shortOffsetX * scaledShortSide; + pixelY = positionY * height + shortOffsetY * scaledShortSide; + if (pixelWidth < width) { + pixelX = FastMath.clamp(pixelX, pixelWidth * 0.5f, width - pixelWidth * 0.5f); + } else { + pixelX = width * 0.5f; + } + if (pixelHeight < height) { + pixelY = FastMath.clamp(pixelY, pixelHeight * 0.5f, height - pixelHeight * 0.5f); + } else { + pixelY = height * 0.5f; + } + base.setWidth(pixelWidth); + base.setHeight(pixelHeight); + base.setPosition(-pixelWidth * 0.5f, -pixelHeight * 0.5f); + setColor(base, pressed ? ColorRGBA.White : new ColorRGBA(1f, 1f, 1f, 0.72f)); + + if (nub != null) { + float nubSize = pixelHeight * 0.42f; + nub.setWidth(nubSize); + nub.setHeight(nubSize); + nub.setPosition((nubX * pixelHeight * 0.32f) - nubSize * 0.5f, + (nubY * pixelHeight * 0.32f) - nubSize * 0.5f); + } + + if (icon != null) { + float iconSize = pixelHeight * (aspect > 1f ? 0.38f : 0.46f); + icon.setWidth(iconSize); + icon.setHeight(iconSize); + icon.setPosition(-iconSize * 0.5f, -iconSize * 0.5f); + setColor(icon, ColorRGBA.White); + } + + if (text != null) { + text.setSize(pixelHeight * (label.length() > 2 ? 0.2f : 0.32f)); + text.setColor(ColorRGBA.White); + text.setLocalTranslation(-text.getLineWidth() * 0.5f, + text.getLineHeight() * 0.48f, 1f); + } + + node.setLocalTranslation(pixelX, pixelY, 0f); + } + + synchronized void clearVisuals() { + if (node != null) { + node.removeFromParent(); + } + node = null; + base = null; + nub = null; + icon = null; + text = null; + } + + private void setColor(Picture picture, ColorRGBA color) { + if (picture.getMaterial() != null) { + picture.getMaterial().setColor("Color", color); + } + } + + @Override + public synchronized void write(JmeExporter ex) throws IOException { + OutputCapsule capsule = ex.getCapsule(this); + capsule.write(id, "id", null); + capsule.write(label, "label", ""); + capsule.write(yAxisLogicalId, "yAxisLogicalId", null); + capsule.write(new Vector2f(positionX, positionY), "position", null); + capsule.write(textureKey, "textureKey", null); + capsule.write(nubTextureKey, "nubTextureKey", null); + capsule.write(iconTextureKey, "iconTextureKey", null); + capsule.write(size, "size", 0f); + capsule.write(aspect, "aspect", 1f); + capsule.write(shortOffsetX, "shortOffsetX", 0f); + capsule.write(shortOffsetY, "shortOffsetY", 0f); + capsule.write(visible, "visible", true); + } + + @Override + public synchronized void read(JmeImporter im) throws IOException { + InputCapsule capsule = im.getCapsule(this); + id = capsule.readString("id", null); + label = capsule.readString("label", ""); + yAxisLogicalId = capsule.readString("yAxisLogicalId", null); + Vector2f position = (Vector2f) capsule.readSavable("position", new Vector2f()); + positionX = position.x; + positionY = position.y; + textureKey = capsule.readEnum("textureKey", TextureKey.class, null); + nubTextureKey = capsule.readEnum("nubTextureKey", TextureKey.class, null); + iconTextureKey = capsule.readEnum("iconTextureKey", TextureKey.class, null); + size = capsule.readFloat("size", 0f); + aspect = capsule.readFloat("aspect", 1f); + shortOffsetX = capsule.readFloat("shortOffsetX", 0f); + shortOffsetY = capsule.readFloat("shortOffsetY", 0f); + visible = capsule.readBoolean("visible", true); + } + } +} diff --git a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickTheme.java b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickTheme.java new file mode 100644 index 0000000000..780b74be0e --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickTheme.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.input.virtual; + +import com.jme3.export.InputCapsule; +import com.jme3.export.JmeExporter; +import com.jme3.export.JmeImporter; +import com.jme3.export.OutputCapsule; +import com.jme3.export.Savable; +import java.io.IOException; +import java.util.EnumMap; +import java.util.Map; + +/** + * Virtual joystick look and feel + */ +public class VirtualJoystickTheme implements Savable { + + private static final String DEFAULT_FONT = "Interface/Fonts/Default.fnt"; + + public enum TextureKey { + BUTTON, + BUTTON_WIDE, + BUTTON_A_ICON, + BUTTON_B_ICON, + BUTTON_X_ICON, + BUTTON_Y_ICON, + BUTTON_BACK_ICON, + BUTTON_START_ICON, + STICK_PAD, + STICK_NUB, + TOGGLE_ICON, + DPAD_UP, + DPAD_DOWN, + DPAD_LEFT, + DPAD_RIGHT + } + + private final Map textures = new EnumMap<>(TextureKey.class); + private volatile String fontPath = DEFAULT_FONT; + private transient volatile boolean updateNeeded = true; + + public VirtualJoystickTheme() { + resetToDefault(); + } + + public final void resetToDefault() { + textures.clear(); + fontPath = DEFAULT_FONT; + textures.put(TextureKey.BUTTON, "Common/VirtualJoystick/button_circle.png"); + textures.put(TextureKey.BUTTON_WIDE, "Common/VirtualJoystick/button_circle_wide.png"); + textures.put(TextureKey.BUTTON_A_ICON, "Common/VirtualJoystick/icon_button_a.png"); + textures.put(TextureKey.BUTTON_B_ICON, "Common/VirtualJoystick/icon_button_b.png"); + textures.put(TextureKey.BUTTON_X_ICON, "Common/VirtualJoystick/icon_button_x.png"); + textures.put(TextureKey.BUTTON_Y_ICON, "Common/VirtualJoystick/icon_button_y.png"); + textures.put(TextureKey.BUTTON_BACK_ICON, "Common/VirtualJoystick/icon_menu.png"); + textures.put(TextureKey.BUTTON_START_ICON, "Common/VirtualJoystick/icon_star.png"); + textures.put(TextureKey.STICK_PAD, "Common/VirtualJoystick/joystick_circle_pad_a.png"); + textures.put(TextureKey.STICK_NUB, "Common/VirtualJoystick/joystick_circle_nub_a.png"); + textures.put(TextureKey.TOGGLE_ICON, "Common/VirtualJoystick/icon_dpad.png"); + textures.put(TextureKey.DPAD_UP, "Common/VirtualJoystick/dpad_element_north.png"); + textures.put(TextureKey.DPAD_DOWN, "Common/VirtualJoystick/dpad_element_south.png"); + textures.put(TextureKey.DPAD_LEFT, "Common/VirtualJoystick/dpad_element_west.png"); + textures.put(TextureKey.DPAD_RIGHT, "Common/VirtualJoystick/dpad_element_east.png"); + markUpdateNeeded(); + } + + public String getFontPath() { + return fontPath; + } + + public void setFontPath(String fontPath) { + this.fontPath = fontPath; + markUpdateNeeded(); + } + + public String getTexture(TextureKey key) { + return textures.get(key); + } + + public void setTexture(TextureKey key, String texturePath) { + if (key == null) { + throw new IllegalArgumentException("Texture key cannot be null."); + } + if (texturePath == null) { + textures.remove(key); + } else { + textures.put(key, texturePath); + } + markUpdateNeeded(); + } + + boolean isUpdateNeeded() { + return updateNeeded; + } + + void markUpdateNeeded() { + updateNeeded = true; + } + + void clearUpdateNeeded() { + updateNeeded = false; + } + + @Override + public void write(JmeExporter ex) throws IOException { + OutputCapsule capsule = ex.getCapsule(this); + capsule.write(fontPath, "fontPath", DEFAULT_FONT); + String[] keys = new String[textures.size()]; + int index = 0; + for (TextureKey key : textures.keySet()) { + keys[index++] = key.name(); + } + capsule.write(keys, "keys", null); + capsule.write(textures.values().toArray(new String[0]), "paths", null); + } + + @Override + public void read(JmeImporter im) throws IOException { + InputCapsule capsule = im.getCapsule(this); + fontPath = capsule.readString("fontPath", DEFAULT_FONT); + String[] keys = capsule.readStringArray("keys", null); + String[] paths = capsule.readStringArray("paths", null); + textures.clear(); + if (keys != null && paths != null) { + int count = Math.min(keys.length, paths.length); + for (int i = 0; i < count; i++) { + textures.put(TextureKey.valueOf(keys[i]), paths[i]); + } + } + markUpdateNeeded(); + } +} diff --git a/jme3-core/src/main/java/com/jme3/system/AppSettings.java b/jme3-core/src/main/java/com/jme3/system/AppSettings.java index 053f6b022d..47311c9037 100644 --- a/jme3-core/src/main/java/com/jme3/system/AppSettings.java +++ b/jme3-core/src/main/java/com/jme3/system/AppSettings.java @@ -249,6 +249,12 @@ public final class AppSettings extends HashMap { */ public static final String OPENAL = "OPENAL"; + public static final String VIRTUAL_JOYSTICK_DISABLED = "VirtualJoystickDisabled"; + public static final String VIRTUAL_JOYSTICK_ENABLED_MINIMIZED = "VirtualJoystickEnabledMinimized"; + public static final String VIRTUAL_JOYSTICK_ENABLED = "VirtualJoystickEnabled"; + public static final String VIRTUAL_JOYSTICK_AUTO = "VirtualJoystickAuto"; + public static final String VIRTUAL_JOYSTICK_AUTO_MINIMIZED = "VirtualJoystickAutoMinimized"; + /** * Use the Android MediaPlayer / SoundPool based renderer for Android audio capabilities. @@ -306,27 +312,27 @@ public final class AppSettings extends HashMap { public static final String JOAL = "JOAL"; /** - * Map gamepads to Xbox-like layout. + * Map joysticks to Xbox-like layout. */ public static final String JOYSTICKS_XBOX_MAPPER = "JOYSTICKS_XBOX_MAPPER"; /** - * Map gamepads to an Xbox-like layout, with fallback to raw if the gamepad is not recognized. + * Map joysticks to an Xbox-like layout, with fallback to raw if the joystick is not recognized. */ public static final String JOYSTICKS_XBOX_WITH_FALLBACK_MAPPER = "JOYSTICKS_XBOX_WITH_FALLBACK_MAPPER"; /** - * Map gamepads to an Xbox-like layout using the legacy jME input + * Map joysticks to an Xbox-like layout using the legacy jME input */ public static final String JOYSTICKS_XBOX_LEGACY_MAPPER = "JOYSTICKS_XBOX_LEGACY_MAPPER"; /** - * Map gamepads using the legacy jME mapper and input. + * Map joysticks using the legacy jME mapper and input. */ public static final String JOYSTICKS_LEGACY_MAPPER = "JOYSTICKS_LEGACY_MAPPER"; /** - * Don't map gamepads, use raw events instead (ie. bring your own mapper) + * Don't map joysticks, use raw events instead (ie. bring your own mapper) */ public static final String JOYSTICKS_RAW_MAPPER = "JOYSTICKS_RAW_MAPPER"; @@ -346,7 +352,7 @@ public final class AppSettings extends HashMap { defaults.put("Title", JmeVersion.FULL_NAME); defaults.put("Renderer", ANGLE_GLES3); defaults.put("AudioRenderer", OPENAL); - defaults.put("DisableJoysticks", true); + defaults.put("DisableJoysticks", false); defaults.put("UseInput", true); defaults.put("VSync", true); defaults.put("FrameRate", -1); @@ -365,6 +371,7 @@ public final class AppSettings extends HashMap { defaults.put("JoysticksAxisJitterThreshold", 0.0001f); defaults.put("SDLGameControllerDBResourcePath", ""); defaults.put("OnDeviceJoystickRumble", false); + defaults.put("VirtualJoystick", VIRTUAL_JOYSTICK_AUTO_MINIMIZED); // defaults.put("Icons", null); } @@ -826,6 +833,25 @@ public void setOnDeviceJoystickRumble(boolean enabled) { putBoolean("OnDeviceJoystickRumble", enabled); } + /** + * Sets the on-screen virtual joystick mode. + * + * @param mode one of {@link #VIRTUAL_JOYSTICK_DISABLED}, + * {@link #VIRTUAL_JOYSTICK_ENABLED_MINIMIZED}, + * {@link #VIRTUAL_JOYSTICK_ENABLED}, {@link #VIRTUAL_JOYSTICK_AUTO}, or + * {@link #VIRTUAL_JOYSTICK_AUTO_MINIMIZED} + */ + public void setVirtualJoystick(String mode) { + if (!VIRTUAL_JOYSTICK_DISABLED.equals(mode) + && !VIRTUAL_JOYSTICK_ENABLED_MINIMIZED.equals(mode) + && !VIRTUAL_JOYSTICK_ENABLED.equals(mode) + && !VIRTUAL_JOYSTICK_AUTO.equals(mode) + && !VIRTUAL_JOYSTICK_AUTO_MINIMIZED.equals(mode)) { + throw new IllegalArgumentException("Unsupported virtual joystick mode: " + mode); + } + putString("VirtualJoystick", mode); + } + /** * Set the graphics renderer to use, one of:
*
    @@ -1306,6 +1332,56 @@ public boolean isOnDeviceJoystickRumble() { return getBoolean("OnDeviceJoystickRumble"); } + /** + * Get whether supporting backends should expose an on-screen virtual joystick. + * + * @return true to expose the on-screen virtual joystick + * @see #setVirtualJoystick(String) + */ + public boolean isVirtualJoystick() { + return !VIRTUAL_JOYSTICK_DISABLED.equals(getVirtualJoystickMode()); + } + + /** + * Get whether supporting backends should expose an on-screen virtual joystick. + * + * @return true to expose the on-screen virtual joystick + * @see #setVirtualJoystick(String) + */ + public boolean isVirtualJoystickEnabled() { + return isVirtualJoystick(); + } + + /** + * Gets the on-screen virtual joystick mode. + * + * @return one of {@link #VIRTUAL_JOYSTICK_DISABLED}, + * {@link #VIRTUAL_JOYSTICK_ENABLED_MINIMIZED}, + * {@link #VIRTUAL_JOYSTICK_ENABLED}, {@link #VIRTUAL_JOYSTICK_AUTO}, or + * {@link #VIRTUAL_JOYSTICK_AUTO_MINIMIZED} + */ + public String getVirtualJoystickMode() { + Object value = get("VirtualJoystick"); + if (value instanceof Boolean) { + return (Boolean) value ? VIRTUAL_JOYSTICK_ENABLED : VIRTUAL_JOYSTICK_DISABLED; + } + if (value instanceof String) { + String mode = (String) value; + if (VIRTUAL_JOYSTICK_DISABLED.equalsIgnoreCase(mode)) { + return VIRTUAL_JOYSTICK_DISABLED; + } else if (VIRTUAL_JOYSTICK_ENABLED_MINIMIZED.equalsIgnoreCase(mode)) { + return VIRTUAL_JOYSTICK_ENABLED_MINIMIZED; + } else if (VIRTUAL_JOYSTICK_ENABLED.equalsIgnoreCase(mode)) { + return VIRTUAL_JOYSTICK_ENABLED; + } else if (VIRTUAL_JOYSTICK_AUTO.equalsIgnoreCase(mode)) { + return VIRTUAL_JOYSTICK_AUTO; + } else if (VIRTUAL_JOYSTICK_AUTO_MINIMIZED.equalsIgnoreCase(mode)) { + return VIRTUAL_JOYSTICK_AUTO_MINIMIZED; + } + } + return VIRTUAL_JOYSTICK_AUTO_MINIMIZED; + } + /** * Get the audio renderer * diff --git a/jme3-core/src/main/resources/Common/VirtualJoystick/button_circle.png b/jme3-core/src/main/resources/Common/VirtualJoystick/button_circle.png new file mode 100644 index 0000000000000000000000000000000000000000..75d8fefcce652b14bc0a7b087bf5b947a7eb2a46 GIT binary patch literal 1549 zcmW+#2~-nT6petEMT}?_kdSqd@e6iTzsVS4RM*w4_$h6Yla{-XT@-_N5X;zJlgZrNSnlo|g79ZDd7pd|0 zACCL8*&-ZQLVXPLB?u**jsP!$2qY3;sni!>zzYDNCJ0}^OC*cgY@tNr198X_i+upY zU~pM19+aiixpX=QA~>!9*?>n;1)D8^Eu6xZ$wYA64~lqrC_wT+A~`4osn9NHK6r%N0Sh3KY4zh>xM^)pY*ZXYk5Q>qhLMUHm3ocR zLh2ZLota_8GD)41G?E%UX~Uhpb>%OD z`c*Ew%?a7$mGhg)D-(nHq<>JCYW~jO!%xhsi;s>O_}FrBLb@lA8LFou2su{#kX!Dz zW4&O+?M6aSr|S?J9Fg;UT-Q{iJbAZa^@;&S#i8db`5%8-H+J<&G&9tloQ>?O-@V?Y{ zx-U(bcFyR`fZRltb=lSX$DZ`OK;~7`ct2d3Gb!(wk+yv9NxIcw2u>VNG3_ZDx4Be( zZTINe$)Bzl_Y_$#F*UJ0W@hcO>PKbO{KD+-92#>)xwrDV3O8Ps8MOOs$NGks4qYjV ziSMPnq3$unkm6l+)W#~CX6H>ME>$V*j>*m$9d1EM)VWrD>q8fu2{$ECajU*ls>!&r zJtH$}tVDLvA2><&hgVbT!`g-(*bG&FC{Ve`DkDwe`udIi>#latAFnc{xo4Ovv%I`x zT`C>xQ|BFSYwRlS+g_1`;>w5$O7i@S*=vK}b|ly|JDm`um26erE=2TD!)R52hZ-U?%OJWjni(X?MMW zhjnbz9uowG4#h-!-8rwlXyaqFP2ep(ClBxmZbj$tU06GNEM?~C*Aur;t!LZlonGHV zc&F%me_H=la#iCtkL-%goq0E8gX-VpIW*1Um_+Y@V-s~`WJ=Y-s@3wiZcC&(qV107 zjxCCan=iSMN5>dlb8D*~ts9_q%2&o$WVC9g_RgDADr|S|wB6NMdZq_08%sHI{IsTg z`Ltu?f+OL><13Cosz}dIz06zmYV1n9ao=t-Q0lE erw^saO;TNUJfl*%Z+hT|ri6z?1vdpIZT}xc3P^$g literal 0 HcmV?d00001 diff --git a/jme3-core/src/main/resources/Common/VirtualJoystick/button_circle_wide.png b/jme3-core/src/main/resources/Common/VirtualJoystick/button_circle_wide.png new file mode 100644 index 0000000000000000000000000000000000000000..92dedaa90531dc89430d183a0d61fdfa693d0344 GIT binary patch literal 1619 zcmW+$2~ZPf6y7LN1Tg~f;K;oRIZ2ic0Rpm#B1aVjK`aUl-qaTHK(tjFLM>v+5D@Bx zx=O`@GZOF`w4(tFWjur8QEW9R#aq{^h@gZ%*!hq5zyE*lec$(Hc1L1DjE9@A8-gGn zaj{YJVK0EGa~%t7ud5{3$>oat z{ZWZTi{p4;pcca-a9qb9`EVZ}UQmz@!)A)bK?+4Emn(#0P!NG(T0UQ@Qbqdt@pwFm zM51Q11u|KvNTd=7WD-fRREo1$;KA5zJ{YhHg@FM9Bey8302{*~j}9_;dh!DU!`W=1 zN;QMSL18GB;aqM23?5GefeJ+!)P*WarBT`Dm0FUP3C! z$>Yf&6;$MKgn@x!Jf4`(hpTW7M*t51|0o5hC5Au@(}OWuOUz>NrBV&RLs7X*rjg4v zfL1CEk;#H_JPPPYrNIJ$970qo9cU#KD*XMq02M_?c!6`2njXr50m@)lBw)ZW0@#6i zfFH*p7C>sHQnf%Jl}PYW)*=8P7GrXG2%v?uP+h5v02nAL1N}uJCGY|mBGHJL&=e4X z#~aZqAOHhYAT02e%R@mEXo^CimB~V&DWEwh0DP|;(hNY$b93h>BqJojVHlM*27^O# z9Apk<<&Xv?$7UeyWDaRnI&8)qn}M)UMhC+{S}41Ho<&JnNW0apG*VV0OuLPwtdx?p zIUEj)!%9&`(qJbD!e+5rtOm+9k7AG{r8F2wB8PJ1Sm(umnsX9C#umm!=~J>EwC(L* z+^L)1&R~jX(YwUr(6!Y0{s&vA^DzyrylK04S5~I{un+yk$?Yw1q#hc6=&X1cGX>PkEC^iCwZ3- z%<=A+wlF_4_V(3v@00tB&0S$fZ7VmGh0Xc-r1A5Qdvlm=W%4U6mz=Nl#-w@;>1~X1c7EH_+nYpv=blKCcu#6er_c4tGk+K&p9^Dmi5htR-gUqB ze2agE@<*OkcJ+<+Qxh*e3e&>QG@v zkLTUP$9?E))5jfYXr|5LpE>^rP4nhGBOrEn3zOBrGRQdZ}n}y*|3ry6^qQH_?APAzC}TT)*WmW9^K> z#*_=RYOgHLAgvqq@(%U(ZN0a}_1Zesxt8#P5ALlSI$lPar}VP8QqyM5>QvRv<^$Z~ zs-zi_p+UpQdY7sfH`Lbx9=&07Ph9#=y={0k!F1A?tXpWo!FI%e4NO2*FV9>$;5vEi zM(5a8tbD6Wae;4e-$yjwNxO<_LJkjnoNRB8VzxgXs2y?_F_Ify9_6VSr9s10^T&jbX&jI!zWiK%3f}w)5Srx?L`>#t^zg ze%)Jnm$~`OW_$0+?)08TMMdLorZog<{pq=-3l2SRO^cbjJ?0ZOp|rDcFR~&ygJnoH zMXuRc+h=<7ZN@QuV!YR${l3g46(6tHgrg?!e|MLkMP08Pxu;p7uDjE5ueh(F|My$Q z%#4*)FP9(F?lG97i@(aA7Oio!=6R0uSy&y$oqXe-O)-L zsLN+o;XNDny)F(2cRt^ElkVAf_rc*w;cr)Le(BQ{a@=iU-lWdHPD5B)#eMw5XZ;`^ z{VF0g{PBgZGYNmn4chD1m%Mt`KQV1VsOfy?#a$Nj<_=ZM{hkwN&Qj|WUX+{|?$Rm0 nX}WVRa^dT_s8X5*bq@tpwpCisIrj;^NZU+NPSCKm&m;)B?IqNl8;*-$qeU1L!1qc@;f9D@jQ?QBf%| zF)3+j1)#Ir{$Buvk5WmHUogY@_viKB>+|aa>G$XNzvsW-|6cz6`~3U$_v`EL-#=gf zeo5#=O<)-9@N{tui8#FU`pu|A1_Esl122k*n>pT@KJCOKC&z+#2ga}e@;zp7wVF*{ za=8AH=RR}0ry2~iI*ZhT)u*4F!kQ7TbCp+HbnEx`702FoJgqu!9kJ>Ax07{GFXb)U z_gi-*uc^Zgd)pmXRCpu4N*!3YoN?kl-fI`XtvllUXCBjgR*>{Y@I&={mV|fA z2fp9VTF@!|Z--lg0PBVp=Rd)5_6acgB=SAr{LJ7T!Jx3Us<(QxxC+|=aV3W$MJ^SwB!?hH zK8LLJ-J69~=CSTuz;xi|+Xp!;o$8JHD;NvRq<8QyR`@9Mft726n8$&z1&rQ~%nh4k zkA5v}I--0)e2ard&k6I(9^7YG4*R`qUp{$l#2ble!kaAbKE8Q!_OCsQ>-Rn9(tXSI c`1gN?+Pqt8FVdQ&MBb@0Mg+nrvLx| literal 0 HcmV?d00001 diff --git a/jme3-core/src/main/resources/Common/VirtualJoystick/dpad_element_north.png b/jme3-core/src/main/resources/Common/VirtualJoystick/dpad_element_north.png new file mode 100644 index 0000000000000000000000000000000000000000..885aa2aa553455207c00577f7c894146b62e044f GIT binary patch literal 782 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!SkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?sm)3-AeX1=6~@mO47-#>Ng>TE^PirdnDi+S+DnYKA&GX2!-YN=iUMLtR~S zZEX`xO(P8rBSS+6BO?bOX=vyKB!L=%;yOAOdU_Uea>_s{QBg@FBYOh_Td=W?K-2W} ztPBn9fF=X&($}}q(lXW8w+3pFkdTp;Rnpb9Ff_E+)3XNJuBmA(C8eOKs9|7WXJBBX zrlzm1Zm6ecsi9%4re>h7Zlt87DJ`w2rltpUiM+guii(byn6#9XytK4}l9Gmkf|`bg zp|Y};n3$A;f_la^re#1MN|glp1v9*VzyJOF_xk$p`T6zVzwf_)zy7`a`TY0w_c`v& zjs%9y3Qrfukch)?ubm7!rAbe8^abQ*(~s2H#BFrd-cj# zyz$q*U$68EdN(pm`PdlT!@A%PcLRu1(g7s?H!!?25Nnte6`^n;lbu<^xnbox|HllI zr>(m!Y+2C9%FtH)*0hOXO3;J5do6b7EO^TFZ8gu1gEfuj@87-q!K{(m@Ro5KkHx_V zdzC5s*=EFQes7-1eMN4L?-z4B?iJY9x1oAv z!p!d$Ten_v7fz`aalF^gome|V;2wA4=EkozET`W*-0kbBFTCjML8mKr9v%0DbD!sJ zeAkpPGqz^hZM8=nS+;>%7mwLEeyN!6Ay~-rwJ!3(7pEH;NsCXPUoIGztMQeaY4d-< yS!W>P#?Njie|6H>QQLS^IQ5g?{PQQy)iCnZmA2i~+NA)DSq4v6KbLh*2~7arlOt0A literal 0 HcmV?d00001 diff --git a/jme3-core/src/main/resources/Common/VirtualJoystick/dpad_element_south.png b/jme3-core/src/main/resources/Common/VirtualJoystick/dpad_element_south.png new file mode 100644 index 0000000000000000000000000000000000000000..5ff17b32e0133a117ba9efc4691219da3911c260 GIT binary patch literal 726 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!SkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?skk4)6(a1=31N#wsdW>gon6D%z^5dP+)KN=izqsyeEw`f6$hf`UpwDMdv? zAX`>eNls2#PEJW$S`kPBk&==oP(4skR8&$kUsY8XD5#>MqpYkYBcrIGpr)v( zAuq2AG~CF@K~Yg%Ss7%Dnwp-txU{^y3ea!`1$7AtStTV6piv46suB`1+S+Ey%0NSO z4Giq`^=&jX4E6M^6csgfbuBbCjm5;Iq@?8K<<+F56vV}4lm}MP|p>Z-@Nv|F3w!@%!Jax3d&meod6Q$vt7;l9h444mH>C*9HEz5~{m-Ym#Z{dtR$F$m9EUDVr@v=1Mt$Pj)R~p3bYA1TI2OW6D&%w4g%;1$N zlZyJ5ncKn-*~sl zeNkMf8#Sj+f6EcxcXv01H?=Q)@i;EGI{wEIBL=HWb#wo-U%Q-;9iFh5Ic$eV!aoUy z!x;?T4<9a?@7Vuq`i%63rD7A#ueyKs=Yfd3E$Ukj@Q8nx+J5<^**DXqN`?uu=aw*+ z*sCy2vk+@oYHe_ABZE#qZoKNYeFT@0h!}W8%o%B0p*{XACP3cy6*C-MQU@y wgj<(+-`C*?%>VG)E%vXcR>+RpqyPWuUsrVxzhzo(4U9YnPgg&ebxsLQ0KKFJ0ssI2 literal 0 HcmV?d00001 diff --git a/jme3-core/src/main/resources/Common/VirtualJoystick/dpad_element_west.png b/jme3-core/src/main/resources/Common/VirtualJoystick/dpad_element_west.png new file mode 100644 index 0000000000000000000000000000000000000000..920a0ddb33b03b74a6702abf6c98b9c45f13419a GIT binary patch literal 831 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!SkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?slv2=EDU1=8BuW~!?CnwrLHYT9aQ25M@CN=n9xirQLQrYb7BT3RN`$~xNG z<~lkS%F5bGAfTDq^)hCqM`*Z+;f18o611E?KnsgaQb&=5&UIiLwL zGK$*TrfO;+Th!GJH8p|a^78VkN=jM^3TjG9n)32$ii#SVnkM@CHuCZ+K!*XnprK)` zr)MQ0AuB2>Wnf?@CMG2(2b9v0l2UNWbqECdS*aw*FPP!{`}6wm_4)OI^!xMs-}Cq1 z&wu~^z5M<6_v?Z1e0}{IW)@9gsO|T3aSVw#y!3i{@F53*V;{H5ZSx3z(<!lyhGjcB4zFU0W_Ba2{CDrno7*;*1w7ymu=gPPpt zaj?+guSMRg(>)3-Z=5JxF1|v^2OA|_%M=9XmF~!4 zoF}Iw^n%knO`%7L?FHwBV@n&tiHrFf$* z@Sx?nFG{wydv5>MKXdFDW0b}6@SSzS@`+z>KWF^)OU9x8J>Qb5bcMTz<@|W=9K9~V z)9<9GaCCDsQ&J{E+IevO4(-WpOwr?IWZQFlz z_emdp=8ia)fW0gV)r=0`FB}j&a5b9Uf7w}E?t;Ja7q-?Jm(G;CCN=41b;MnvoNM=- v3(GdhEeLq_tEll`jsCR06H~iF{xZK7y)Cu-(V}=@#4~uh`njxgN@xNAmI)w< literal 0 HcmV?d00001 diff --git a/jme3-core/src/main/resources/Common/VirtualJoystick/icon_back.png b/jme3-core/src/main/resources/Common/VirtualJoystick/icon_back.png new file mode 100644 index 0000000000000000000000000000000000000000..706eb8000fd8aa4d6826bb42aa8d8a1c0b07616d GIT binary patch literal 877 zcmb7C`%jZs82&&S0VPyZjM|CX@J8Nmx7iYU$-2ySuV3Z;eiyPfZHzVB<%u4Xz47>|fJvYOlVc)Yd7 zgX4I;6*n3+WmaRa#&fa!)e_|BOT8}LoZU54TYSo;mUpIa+^8BpS@SXTVC&G$rk3&} zzYmAPo@+6hR(_EUEf|;;BaV zu_U7_%F|0f~;?3aPyx@gniUr96fhrpvT{TylF#ji=smI?S~0-JEOml z+NbhWD+^j|qX){*ox7U0Yg^_gvHES0YwV89GhDHM^)snd_BVUyy08K#^pVL&w{=@K z={okUr7Me^>Y+zDOG*#Ecx@DiQ`N5=*yipWw>u?jvOU+E<=#KzTD@KOJk$KokA;gy zn1JKF)}5>ah1)y4d;VD4uC|WOi>#}wzD~cZa)+mDFUk9lwkni=n;;Cd!lvI6;x#X;^) z4C~IxyaaNL1AIbUf%Jb8!S<<-nt<8_N`m}?8TR|%=hr_U|9-xF|9boU`VLcuy+FC| zo-U3d9>)-ZoeYWK+eVgGq?adz(nWvXmDgIHke57r-lk5H0 z@WZ8yDkuA#7PqFYkJr|{a#e?+jB#_oFQ=JG)i-*@Z}Hqb$R^S#x#{G&6)NW^@NQF) z&T;CycvvlkfqnLbY>w`;d9q*fHfjE}dY-oF=epTP^>PkoZg?47mv=D1ys<@b_9Xv8 zk3?oZ=6MI@_$u9$ZyMidR=mz$ZSbTaEul^4hNSFC#U0r`emUqhFB(ZTW{88awgTe~DWM4fg^Zdb literal 0 HcmV?d00001 diff --git a/jme3-core/src/main/resources/Common/VirtualJoystick/icon_button_x.png b/jme3-core/src/main/resources/Common/VirtualJoystick/icon_button_x.png new file mode 100644 index 0000000000000000000000000000000000000000..ee5c55193bf85850cdd610d34cd491d564d8e245 GIT binary patch literal 502 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaNL1AIbUf%Jb8!S<<-nt<8_N`m}?8Sd|2e|~=adwzZY{C@d*`#p1~lFz@i@LW?Dn)~1s#|7nO`}D~n)x-0j?egS!Bm)gujz`;*Pko76D;qPzj}N1Q@rzo zx*zQ2*E21CUhiQ#^tX2A)9p-hD>|N>kY4G(dPV-d&g#i*ankGs_ZfD)*rxMpWznT& zuE%;s|JKAq_-<#rY-b?6P-kj;y{g;=2 zPUth&*VSi!h9CN{^_5oE>VPZpjJ?O+hppT2|3)3diob3hlHL>l0Rx-C)78&qol`;+ E0EjgFO8@`> literal 0 HcmV?d00001 diff --git a/jme3-core/src/main/resources/Common/VirtualJoystick/icon_button_y.png b/jme3-core/src/main/resources/Common/VirtualJoystick/icon_button_y.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d3ab4609270e882c63aaa352c3d1e20eb0cc05 GIT binary patch literal 419 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDB3?!H8JlO)ISkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?nV(;1l8sr2mrwtbJKPw(yq(`2{n)=RdDMKYsoF{C@lW_3|N4+aiHd-#lF$ zLo7}wCn#_ieCyUeaKoq3p6S7xU;qE}CdfSd|9}7C5KWMc+CEBhCk&eme!gjZ^PgKm zMo-}PXJ&)jf=B-S=T(}>|E#0o=JSJZ{!2ULXthqa{0U--$x^EE*2i0Ynzd;b~2BwjNe@>lp!Y&V7Rv+nPDlK=*8?dp(^=RXKs zb(osb#BSYfv+0LByGx6Ol6Qx~Q4#4)E=)x-&y#+3 QFa#JpUHx3vIVCg!0BuOR#sB~S literal 0 HcmV?d00001 diff --git a/jme3-core/src/main/resources/Common/VirtualJoystick/icon_dpad.png b/jme3-core/src/main/resources/Common/VirtualJoystick/icon_dpad.png new file mode 100644 index 0000000000000000000000000000000000000000..a95cdb7eec655accedf98774e573d89e1bd5f028 GIT binary patch literal 1198 zcmXX_4NOy46uys2!9ppWv_OIW|G(FNC>4P|SP_}F!;YyCH4wI{6W6NDG-~V%ILA*A z7ZlX#Qgw=M2FHpK4YSp`xJ^4`iycGR1Lpimbd;%)32S#TCg+~-oO{o?`R+OAHkaE< z=`;=v06;HWVzEOc=2kof>Str0{tIE=x*}^40KM%Avo*2MN4DBai$TxNnUA2IP`<39 z1a4xO5sE^g6AFa}gN4ZyVp#5+X~0p`h@!dB6^T%>7!?RmE>{R8BSWCq<2oIll?7c9 zG$axg)M7Cv5@9-BG0c4xDPpnGVLA+kM=%Cns8AU6dNY?R777(2ky5SB^gB7s1r(-rD;W*$$hROYBuCdfx3QA?$2$PL3x zD4HXa>D1~)a=G4YwraJ7e7=Owmua>6LZMut(6iYby}lqj+iWxzYqfb8mZMbaO{S9c zbS{U(PfN=@b@mqvEC;XNZhHqzdUy;5!_V*!l1{Q+GwIQI2HVL=lJrdSoTTPtSrh;Y zyUbEl;U0N1@?gsdp0a2By!E5W(ZyAXo0H`%yLh7hUdnZwKZ?$OiFfd?xVAZlHfPU7 z3Qs%*4K?iD-cGZki`Sv-di$T^9oKi*k9}J^I`j07{miD(!7H5=bxRFnILqZtca>f| z1bVQy{0_vu_bg44;G=re!--7fU=9cp0OIXL_wFWelY@rGwH7gzOdU?P33tkxSRUD2yYtTxn3 z-Yt$!X-0N}9lhfFn`}gZrqhiqJ$~v9rgzJa0%BXl0W)?*61;0Fogo!6P=tZH`*G7v zu|#l|n3)4=fN@&H2F8rQd-$E-F5GaUl)SzBUjlpH(K2FCP7S*v#{aH%k>{M3_W^t8 z&j-zHW9y6cDYw!xJyw7C!dm~iY@$9x2aN5M>F<2qk5VJdnC%u~f5e1mw1G^$x0~u( zj11=|Y5OU1BI7_8e^=@{B&;3EV~IfmrtY(sGnb^cQ_kbJ`OGb;~mKl zDgDi#L{nXi-kmA6J|s-3dd=6}yb288Wc1^&9&ivTka@lA9k;>&qW=*VEg{i5I}}E2y|+ z+VyN21j8jujx-L=3#Ul_*t1E|VUAsO&>C01`6x|O{Wxw21H9z9fgz;A80|(-Nm)JD z?%TW`^W#MMh9k>?E#VmSZZ-#wQU heJh8LO_ImV5i>h!?XsiYL-2D9#*(O%<*I~?CE3bYvJ__Q zdj?45Tn_Y8Ua5-#^su`92ghHWFcn4Qo_Dxubg!d8u zUked1q>kj@qG2%-K19OD3-BQVs=44k3Mog7kTUPTyjK(HEae6gHw^5BA${o6Mq-Z^ zwC;hP|MEgKl=A~c1*mFJE(irEq&c4h$^_t%5ESvkQ(35$hG7G!7KL0Cw8$euLwb0#~ z1GW|?6mUTS8VWd}Ul%GxphFE=#k^1@3IncbNWB=+e-ivo0NA^=UY&@2bd zvOt!CYB8u0g&IkykpQwJ;se5I2qXFkcBK$hio$b6Xj6s~eq=r5ho`#{4EA&n1(8SW zd24qd4PBbhBo8fn;F&x;+XIbq(4vT_v+d2}K&D_1&L&|qv*Fpyh1`hNfFAT}BaSu5 zzymJm)Ie0awGaumw~r;DN(?clMhcqc5iRz~6(Z2Cf&etiLZ1#4^8twm$^@Z)7u4-U zFiHiWXD`B8E{qUTRFNq<)Dg(}P^cCM@(yT{gC~-RCv(9t;|EjTFdGcBXJ8BmJytMe z4|C_B)ev60!5d$g@`M+MU^WO|n8UOWymE$dcj(uL>YdP|12wWRYz!15m;F7&ruVsxV{)RQ9C&(4-A7kHXL&@WL7f_QA9#JXM7%Nffn8N>`Kol2wA$QcY0k>R9AH&ScULZSH7?QvMwOK-pCNBc^+ z$@c4=oA7!on6Fzj*|f*jN~0k}ETE-1Nl6&*JQK$t1w|~(VVd0xYBHn7Z9Q+~7(SHJ zNOA8-OiKE&_1ov$1jg}gdh0#n*7kVZ=0+>?3L}~^ffH34&wYPSurM;y`Zy24Fh0G* z)?YW@G7>9#tAKdF$-Rd;GH`0_R=r_tU$6~h{K8_As7d`t(-L52n={{>edsB?;B-Q;z=TXW29I2x=BQo*_L8X$h9(sw% zW3a`6LyvYfEludFr?fKT+6SMOaNje+riTdia{V(?-?iU`%(7#7x}$sUD>F}DknQ-a z^WHNva@F5q^MTpsc(0fj@A4H!5O4X_L4xz{*{{(K)?aVbYOg4+8GQacnYLt}PRr(2 z4DDylDP&Ui-iZArHH`l*xBF>y{7Ssnm4>41krh5g&#}#O6Yk{#!;EFV(I+AEVLE07 zCY~pGB1(!%ddxID2d^D$`*N*0?x$_hi8~oPR*E&aFludwtto@r724mEOYVfQ_`SIj zrXTm33b96#LPJ>7*luh*UEQ9D4(d3JL2U;nP&#GT?A&Jsd_|43IEwBl-WK$-yyX_w zSx#~fcV|V-bnSV`soSA>6u()J`FOU~+t7?Dwv=Oa^G~f;+Lg|i(ekysf_HwL9R2G) zE&G&7q*aP?lOaB^yo%zwb@TWGUf&ZLhnDqlpWkV68MJC)$v>7cu@z@34)R`0$Z+;F z@j;yvYZ4DGp(^}aT%S?h_%3m%-Q$Jt_Hq3+lgVaZ@y2~t#+}W$jr^oF*Y*65@+%wN z^tAGVq+PD~vU7Z^7#_Et{NgkYQg+o(2b3e$1bv%%XXen!gYtXgIK;?zF@5b;RqyM# z7jNOaGZ!9i;fF%F>=py8lsXQb9tyi;6fl)+JFkX$tp3r>MRF_6^~oy(mDTZwdD!+~ z{le^9GhX<}?n5QQ0nhdmuuopE%Zjg@sV}Tpt{s`p@p^HAJ4nb~;)m1``qAqI4qPEm z8j31@Z&cNWYW+3IEV257uXF^XYw>4}N57uHU|o=0#327@9DgHEHcKXxD>yBVSMAd; z@qYak%r(IfDZ@|oVh)^V@B1oe8KuNgtW4I^yCaRmRMzqDIiI_({fk}i(o!Pq@dBRn za^!}zb=Uga1Iw7?mHN9(wW844y93E*(CKYL`a?VgOvNE74B88~jrS#TaDC%km09J| zH09C%4m^B}%+vlToBp|kX!|;YgyH}+ZZ6*0bbji!vs4`)P6|hHHcQvNQ>R~)J&-!c zv-8q!-%O$0jX$c4cVsNMDAB||j+gQO(Ic{Ns0F%Z7f8 zqUx1Jh}~9>(0eGTTDSll+}-sfi!^*$yiJzek?ZF71FqrM)hXn1HP-bC8xOVRnCzR& zha5PiljWCQ6dwCDYhkH&^xABX#mOPzVu#)IjPvi(V#;!wuUJ>jGwr6Z+&1#6(j#SF zXv=-ZShScEQ!4$k)Q?*?PiW{TeA%t!GwvK^xODU>loy|f)li&61s@(4vnaPJOi-2# znBqvO5Z8r9@d(Z|_uNwL16Pl{^|mlg688Fv%l~}hyxgkc`CX|V2?oBq>T={e9wi76 zwvTUiS8{Kksjv}#+>&wxIPG5DB?+vaDB>5rT1r>DlVJbS*3KBqkCvN#_D#+G{nSM< z?fJl58}#IoLoAOQV`BYM9;zm`#Nuy>3e$Wy9rV|(ERJYYvX^wrHjn$sszK>zc7YN( zhCRht84vY#r(cJa594^ZQaRL?xPlGuzLI|KwryA}nY7U1J%I90-uL{Q=EQ4VA#E$l zmtfPaMsJh+&ka@qmin8l*HP1>v3Vhh&kNJG)z_RFF3wJv|7lP;`=p9{AiB9Bk@r+< zaQ@vOscps%q z_2v!dS#Fb05yIg0exDkX$zx43&AD$a&d0aYUw)L+dA%cc-*nP1^Mb1yA0n0Ju6SQ& z#Xp^YliJ>uT52!-$;l#0Eq==$Ef(zacgN30xF?z9n@ANo;P;pT2WgzLgrMM7JfL_O|wm0Oi_|$-b!@T zqs7Uh&cTAbkP=fzBFp?iu3V@}gWU3N!SeUfBAafds@SvN^D(8)_k8`_3hDd9C`I#T z`J(M1x3@9-C)9@>}=kSlGBX)t>&)@31hp`d!{-BOzK?O_G}s$sd(Lws+CP0eAV m&4L)^GgtPrRQcq;fNB$3@QX;CXaD1)Q1-URa5dJxN&f>zzEM8_ literal 0 HcmV?d00001 diff --git a/jme3-core/src/main/resources/Common/VirtualJoystick/icon_menu.png b/jme3-core/src/main/resources/Common/VirtualJoystick/icon_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..56c4193c1101f68b1834670103987c129d3cb438 GIT binary patch literal 198 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDB3?!H8JlO)ISkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?sl51o(uw0_p$QfO6g3c%U46NswPK!~XjF@8kO;uiu>q6teVmaSX9Iot&T` zxq)k1GGh}X&#R{qj)KkBWt~E8MKTEqs%JKKi$DIRAYInU!dBIA@rOijzo4)@L-Uc2 Vw&huzhk=GLc)I$ztaD0e0svJvNyPvF literal 0 HcmV?d00001 diff --git a/jme3-core/src/main/resources/Common/VirtualJoystick/icon_star.png b/jme3-core/src/main/resources/Common/VirtualJoystick/icon_star.png new file mode 100644 index 0000000000000000000000000000000000000000..ab2f6ad86aae902fda685f1ad4fcf4581336d340 GIT binary patch literal 429 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaNL1AIbUf%Jb8!S<<-nt<8_N`m}?8T#MH+t07x&wu{DyuW__`pP#`egWnF zc)B=-cpP6l?Y2;}0f&oklGD0V|Lb$3mtW1`nXv7*R{EPmO>>w1&tNQwi+jg7|5DMR zhid9;^?!s3F@0ck$dD*cotRw95XcmxUegDnu$iKo~wUr#NWDREyJ}zXSQZ2U+MVx_Gj@y+5SX_iw^JZ|ES)1zwx1E z$K+qS4|e7lpJYgGIOoQjvDI~dgfGK!f3CFiz41x*weLM|d$WDImAyKgVZ)sot4psN efB&mL$GmsF|FfdYmD<2?VDNPHb6Mw<&;$S=Thh<~ literal 0 HcmV?d00001 diff --git a/jme3-core/src/main/resources/Common/VirtualJoystick/joystick_circle_nub_a.png b/jme3-core/src/main/resources/Common/VirtualJoystick/joystick_circle_nub_a.png new file mode 100644 index 0000000000000000000000000000000000000000..e2cfc1b87492515a3d58ce909f15943a08d6667e GIT binary patch literal 1776 zcmX9;2~?9;77oiOh!_Pdd;aX(pY10rAt3}p5iE&CbimdFE>pp(fKU;a1_nGx2PRb+ zwIKY)fm-zp0R;;R1gTOGZKN%V8&pv`Fxs${*n_}a={fh^`~Lgxeee6e_s(CkP9NjB zEMOUdK=6!Rqf3Ao@ou=efUo+t;1vYd9ol#;fzV*`c)R5<@a>eH5EDtL`yptQKyVhU z`yeqIk|7}x7^YgaiW(S5p;ERYZ=#3DGv@{yZ36CLj?ALIEd= zssw^}c`B6+8^S6i5rod?D|kEwAV&}eiNt_~pvL0$Y2aL-xu{z>um}x(AMh`VpoUwJ%cEqnNTo7HBGF1D5i(hnT&{zg z4GoP}D0Cu`QmI@6Qi#PGxg0XlJf0NZKuCyMD%FOC#R>!pcp(yr=05|~_u<84=OpOY z6L29>NF-W`h*e`UY3wPu9YIot47lB>8M0?saf>O#CbS|McH_{{kPWw5EVwa>YT-&LJ*7%SiR>$1`6UcYK{=pBRcl{t*3@s7cR*+IpG8(W;bj&~fZ{f6J<&5A$q z&zggCxoUj^*EQ{l!}svp!6_BunQfnS{<8DtBhRQ)QvXNw`<$2G95YK<$|P~)wG_wA z=c`#>$qmi8qhY+l@{wzEZ03cWrFm%eZ)F3;#IrnXv;9R z_uPj>gD~avPQ+UBu6~P9AZ%D<4qs?N;GHBedML-8uy$&>efPXGmp&_ z6-}T!n*y>fsb{6wkZ~4n-u5z27C)NH{iEHm-EW(0xoG&auinhJIhqbT6?+zLA6BM)?b^Y^ zN|(+P6={CTxG!%^nHNko-Zg<;bNq$u>AZ;`zSC%F`g~>1*-%oKO_;Vs6*qTIMyAH^ zuX&b|{@5?frLbYQWxG?xru&@F4y69P{^akr>-2G7 z-I3KxPdvzTnLhLhIzVT)-!8GZRpyhEHcr`J8O|FOKYi!jmvoYRkFaY)z#$~Ob!fvi zufC+yOPU2&uXwiq?Lo{{bX8f|X5V|KHs(K!iO_i~wY1_JdpfS{`75L{{V$1`{p!?W_I>f0pQE#u4)n~^THNr0s?^+u zqnQOBrC;27N+|EGtNt}Iw-^60aP)BU3Lk-U^8D!~t&Co`sW;g1mxi>C)AWD2KhH0( z%}w5x{C(Y<0{6-u<$L3s{@uGgZs6`lX~poR#y{GWO{YkulRl* zuewoPlYVKFOXvCuCY^c46B(Q;`CR)`b(_Ros&+TO4$PY0`XHvWAR}u3H@mwxIh#iq z|Gm>lFcgkg&tR{$Zs$i|^!FUOx3IQV|20Jw5bR^$r8j=Kb9zNp*@lO@yxi|=l>18c zXWIvT!>iKp63^akLfQFWs*AjTpRCC<<~1p6%Xg~Tob~-6N=|eQ?*`GR^IP@vlWEBH|8)_qTM}zGMwMSIyZtzD_DQwlx7oM7ekW#U sR$R844(&_bYwW#lob2$WaXJ?WIjqgA_lLFA!oP+P8?D#XMW+7!|0aO5SO5S3 literal 0 HcmV?d00001 diff --git a/jme3-core/src/main/resources/Common/VirtualJoystick/joystick_circle_pad_a.png b/jme3-core/src/main/resources/Common/VirtualJoystick/joystick_circle_pad_a.png new file mode 100644 index 0000000000000000000000000000000000000000..74e2c93ca80d1a4b6a83d5ab2f0d6ae8e5929695 GIT binary patch literal 3022 zcmX9=dpy(o8~@IjlDU-IY_r+LHoMPlC`0adx)B*2{hZ`-{F3S<8>Lc&3Q^37a-@td zsAS5eB$g17wj<$}YU<>~N&G&iv)AkU`R?=VdEU?a{eGT5mK_w}iPSRHf*=U#?d28% z-r|3rRd8^AQ5GzMmwK#=zY7F4oAV6zmb%ACVlPoZ$|c(Rq1 z8_1zjt*}@kkLS$gI+I9D3cxpL@rmrWZKi|>p?O|V6hySOglWDLZ<^_ z8^Et1LL^cF0uD!Ju^a&_0Eowfhznr#qm~Z}fdmqXVP;19kwfC~oB#-D1YqfOTQ1iX zTtN;NiwDGLG#d*G4v=GE!2(}^8$iep!aySsMPx8+&CRJm7pNi<>7X5vNCWDKL!(HFb5HE2r_p#I2LOVT!LXyztiVa9 z112nx2Cg)kHJQu??pa#efHQwa*NI33Lt%nOz$i9*JrD(40<;0tzm3ezDaOVauoyrEmC7|S!J3$0 zz`~%>cra5S)6^6PJVc{$fB=w8p>V;ffVBa_fv$h~15u`?crY3;ZVZNCXov!XHZn2= zl7WX{OkiQKSTJ-;;0Z7g_-SeiI9mYUNF)|m1{4a5Lg82}dkTez!%@J>;qkz5wviDU zH~@@IsMiyNg}#^)60iwkvn9H^a$Q{+P9(yql2l1JxlFE`B$G+y0s%XTt-z@iq9ln( zkS7!cszef%N~U6~R8sILR0@Sap%O|JLWx8XC=m!1LRp?lkSCYQg}6X@AgEIbMM)xw zP#_e^;=Nkdjjyi{sgP$9 zf_5_WPn1=8=ym%T7MB`sLa&RZXgsl8LN2^M%Y5n0c-g>`YwnTB9yO81%cz#dz0J#| zJ=3b%fu$blopgk|n^oOdNN3J(SDC|MvlIC@p46JacWenuJz?|3f7g~A?=6K1PriwS zVcjv8pA;_o9l!K>K!BnZvuVTB}9du zN8iZw{=tZa>azT$uhH3+H;pJ+XKQ|WxZB=fS!c`GN?*iBUg2X|6XjA&IQFE@(&iz7 zkGL-k_EzF{5q|2w6Zd}YcGZN z>1oGB-aj7g@DBN|ij#l&NX?@q?a?<;xt~XJ6VsR;u??p;@F%!euD+Q(yuRW}e)RaB zj;7aJ+f7k%54m4U;>%UDwsqgk;w##B&s2=Xj=E&BP3>9|va9n_-*kmp&qnoat1w!Z zc9$4k(cbaFCH_Idbj*|@S01OEoKoJIHR{jj_wZW#9)$h$XK})a&W+4GjB*BDlp6*M zco?g`Yjkb53udLNL2JUE6;$jRPP)8iT_37UL~@V_;>&zi~_KK~P=f_;rUuB^2 zu{lMneH-kvZa#;qqnjK_6O3WAI^fu0osy%lm$3S?^WODS;Z=eI zr6(2yP<3GDrszOj&n{>f-n_|XnJC2H(J0MVw}aRLrU$ee55OA-h_nIc0gjCOj z?yG6HXw0hlZ&q^_BH&3Ib@FY+e?p%Wi?Ht6e4B~`Fh_QxQY&anTvF3iy`QMq9z z@$|E!hOqau$#B)B!mZ`EV6BG9iX8OFG3&N7wrx7l0iKO+(P?Nm9HuFr4py~?{NaUG z8bC6S-mG2cEb6G@=@5By0bvs91}zb6u*}rCN#vZP-a!Z3XY2J5r41S1+QjEyti0_J zJR2^$%glt?X~;Ixdd0(?oJG4f4lBuLBj+e`DKlmm_51PVZFFr;PWZ6&=_t(q@Nt7d zZR?4TfqS$${C!-eno#^Y=j);iDl( zeg9-O0S9>4=_8ig5p`~-q?CzNZ6&>?HrigF1}ST6darvi7Cm`JZ5L|dLXUL|&K-kA z_y^Zx2rHpZ>QQL-!$*rIXU!F-nv*(&eohPe*-N#kC}A#oLGOiEvFmA#_w;1=kYoPN zBQZ$rE+{Lr~Q-x8~XUU?AKtwqf#se8SAboMxFZFkHjugX7mrvFO+ ztDL-j;R`AJkzAB#j#I&mN7|SDJts4k94%g1cllYO`_!c|!ABl@M?#pbycaPyeCu7L zHkHlkus_i()FFp){d>--^sAYYrZ+SDca;Vms5LftDuWAf9SxEme&zGV;C)e3T_b1C zKYzKCu;N}Q>n}POQ2ikyEwZ|;#7@bbT2sdVc*nLf?DpBeR@P2hrv^#R4~_o(bpOQf z-vW8%q1Ws=@w(fGK5lFOTY?mPR2roHReUYF_La`KeBR>&yB4v2*O@!rZyS9b8(ck* znd^3C4*&l5@xfn;N1p~ro3QUYPI@Dx*Alb`{xfH4**O@cd0}c6GveM~p*d2x*i0SW zt9*pLcs+7za^X|j#Dl%r{Mgveygg-BC97Od+ff3%mJd(geJ{9azcM1(yqFacX~GYl zqYyBM(pflD|L~YV>eJXmr>00w7hBT$Z((G&EpP8JA~~I!HYh2!ZnQf^WV}<24SvPt@HuJnR`R@C>OD+%cx3siIQnnjawwRdMkekJi zJ>5%QNMx;iT}gxy?tUol=4;$SZeB-1y+dce{&JI8@V2AVa~iHYA3c axisValues = new LinkedHashMap<>(); + private BitmapText axisDebug; + + public static void main(String[] args) { + TestVirtualJoystick app = new TestVirtualJoystick(); + AppSettings settings = new AppSettings(true); + configureSettings(settings); + app.setSettings(settings); + app.start(); + } + + public static void configureSettings(AppSettings settings) { + settings.setUseJoysticks(true); + settings.setJoysticksMapper(AppSettings.JOYSTICKS_XBOX_MAPPER); + settings.setVirtualJoystick(defaultVirtualJoystickMode()); + } + + private static String defaultVirtualJoystickMode() { + Platform.Os os = JmeSystem.getPlatform().getOs(); + if (os == Platform.Os.Android || os == Platform.Os.iOS) { + return AppSettings.VIRTUAL_JOYSTICK_AUTO; + } + return AppSettings.VIRTUAL_JOYSTICK_ENABLED; + } + + @Override + public void simpleInitApp() { + cam.setLocation(new Vector3f(0f, 5f, 22f)); + cam.lookAt(new Vector3f(0f, 5f, 0f), Vector3f.UNIT_Y); + cam.setFrustumPerspective(60f, cam.getAspect(), 0.1f, 500f); + + flyCam.setMoveSpeed(18f); + flyCam.setRotationSpeed(2f); + flyCam.setDragToRotate(true); + inputManager.setCursorVisible(true); + setupAxisDebug(); + + AmbientLight ambient = new AmbientLight(); + ambient.setColor(new ColorRGBA(0.25f, 0.25f, 0.25f, 1f)); + rootNode.addLight(ambient); + + DirectionalLight sun = new DirectionalLight(); + sun.setDirection(new Vector3f(-0.35f, -0.8f, -0.45f).normalizeLocal()); + sun.setColor(ColorRGBA.White.mult(0.8f)); + rootNode.addLight(sun); + + buildScene(); + } + + @Override + public void simpleUpdate(float tpf) { + updateAxisDebug(); + } + + private void setupAxisDebug() { + axisDebug = new BitmapText(guiFont, false); + axisDebug.setSize(16f); + axisDebug.setColor(ColorRGBA.White); + axisDebug.setLocalTranslation(12f, cam.getHeight() - 12f, 10f); + guiNode.attachChild(axisDebug); + + inputManager.addRawInputListener(new RawInputListenerAdapter() { + @Override + public void onJoyAxisEvent(JoyAxisEvent evt) { + JoystickAxis axis = evt.getAxis(); + String key = axis.getJoystick().getJoyId() + " " + + axis.getJoystick().getName() + " " + + axis.getLogicalId() + "[" + axis.getAxisId() + "]"; + axisValues.put(key, evt.getValue()); + } + }); + } + + private void updateAxisDebug() { + if (axisDebug == null) { + return; + } + + StringBuilder text = new StringBuilder("Joystick axes\n"); + for (Map.Entry entry : axisValues.entrySet()) { + text.append(entry.getKey()) + .append(" = ") + .append(String.format("%.3f", entry.getValue())) + .append('\n'); + } + axisDebug.setText(text.toString()); + } + + private void buildScene() { + Material floorMaterial = assetManager.loadMaterial("Textures/Terrain/Pond/Pond.j3m"); + Material wallMaterial = assetManager.loadMaterial("Textures/Terrain/BrickWall/BrickWall.j3m"); + Material columnMaterial = assetManager.loadMaterial("Textures/Terrain/Rock/Rock.j3m"); + + Geometry floor = new Geometry("Textured Floor", new Box(24f, 0.1f, 24f)); + floor.getMesh().scaleTextureCoordinates(new Vector2f(12f, 12f)); + floor.setLocalTranslation(0f, -0.1f, 0f); + floor.setMaterial(floorMaterial); + rootNode.attachChild(floor); + + addWall("Back Wall", wallMaterial, 0f, 3f, -24f, 24f, 3f, 0.15f); + addWall("Left Wall", wallMaterial, -24f, 3f, 0f, 0.15f, 3f, 24f); + addWall("Right Wall", wallMaterial, 24f, 3f, 0f, 0.15f, 3f, 24f); + + addColumn(columnMaterial, -10f, 0f); + addColumn(columnMaterial, 10f, 0f); + addColumn(columnMaterial, -10f, -12f); + addColumn(columnMaterial, 10f, -12f); + + Spatial tank = assetManager.loadModel("Models/Tank/tank.j3o"); + tank.setLocalTranslation(0f, 0.15f, -8f); + tank.setLocalScale(2f); + rootNode.attachChild(tank); + } + + private void addWall(String name, Material material, float x, float y, float z, float xExtent, float yExtent, + float zExtent) { + Geometry wall = new Geometry(name, new Box(xExtent, yExtent, zExtent)); + wall.getMesh().scaleTextureCoordinates(new Vector2f(8f, 2f)); + wall.setLocalTranslation(x, y, z); + wall.setMaterial(material); + rootNode.attachChild(wall); + } + + private void addColumn(Material material, float x, float z) { + Geometry column = new Geometry("Column", new Box(1.25f, 3f, 1.25f)); + column.getMesh().scaleTextureCoordinates(new Vector2f(2f, 2f)); + column.setLocalTranslation(x, 3f, z); + column.setMaterial(material); + rootNode.attachChild(column); + } +} diff --git a/jme3-ios/src/main/java/com/jme3/input/ios/IosInputHandler.java b/jme3-ios/src/main/java/com/jme3/input/ios/IosInputHandler.java index 74d31906a6..a5d07ec7a9 100644 --- a/jme3-ios/src/main/java/com/jme3/input/ios/IosInputHandler.java +++ b/jme3-ios/src/main/java/com/jme3/input/ios/IosInputHandler.java @@ -241,13 +241,19 @@ void dispatchBridgeEvent(int[] intData, float[] floatData, long time) { int type = intData[0]; switch (type) { case LibJGLIOSInputBridge.EVENT_TOUCH_DOWN: - injectTouchDown(intData[1], time, nativeX(floatData[0]), nativeY(floatData[1])); + if (!IosJoyInput.dispatchPointerDown(intData[1], nativeX(floatData[0]), touchY(floatData[1]), time)) { + injectTouchDown(intData[1], time, nativeX(floatData[0]), nativeY(floatData[1])); + } break; case LibJGLIOSInputBridge.EVENT_TOUCH_UP: - injectTouchUp(intData[1], time, nativeX(floatData[0]), nativeY(floatData[1])); + if (!IosJoyInput.dispatchPointerUp(intData[1], nativeX(floatData[0]), touchY(floatData[1]), time)) { + injectTouchUp(intData[1], time, nativeX(floatData[0]), nativeY(floatData[1])); + } break; case LibJGLIOSInputBridge.EVENT_TOUCH_MOVE: - injectTouchMove(intData[1], time, nativeX(floatData[0]), nativeY(floatData[1])); + if (!IosJoyInput.dispatchPointerMove(intData[1], nativeX(floatData[0]), touchY(floatData[1]), time)) { + injectTouchMove(intData[1], time, nativeX(floatData[0]), nativeY(floatData[1])); + } break; case LibJGLIOSInputBridge.EVENT_MOUSE_BUTTON: MouseButtonEvent button = new MouseButtonEvent( @@ -270,6 +276,7 @@ void dispatchBridgeEvent(int[] intData, float[] floatData, long time) { addEvent(motion); break; case LibJGLIOSInputBridge.EVENT_KEY: + IosJoyInput.dispatchKeyboardInput(); KeyInputEvent key = new KeyInputEvent( IosSdlKeyMap.toJmeKeyCode(intData[1]), '\0', @@ -282,6 +289,7 @@ void dispatchBridgeEvent(int[] intData, float[] floatData, long time) { if (!isSingleCharTextInput(intData[1])) { break; } + IosJoyInput.dispatchKeyboardInput(); KeyInputEvent text = new KeyInputEvent( KeyInput.KEY_UNKNOWN, (char) intData[1], @@ -327,6 +335,10 @@ private float mouseY(float value) { return mouseEventsInvertY ? invertY(y) : y; } + private float touchY(float value) { + return invertY(nativeY(value)); + } + private float mouseDeltaX(float value) { float deltaX = nativeDeltaX(value); return mouseEventsInvertX ? -deltaX : deltaX; diff --git a/jme3-ios/src/main/java/com/jme3/input/ios/IosJoyInput.java b/jme3-ios/src/main/java/com/jme3/input/ios/IosJoyInput.java index 227ddc0fd1..e858a2946a 100644 --- a/jme3-ios/src/main/java/com/jme3/input/ios/IosJoyInput.java +++ b/jme3-ios/src/main/java/com/jme3/input/ios/IosJoyInput.java @@ -11,10 +11,13 @@ import com.jme3.input.RawInputListener; import com.jme3.input.event.JoyAxisEvent; import com.jme3.input.event.JoyButtonEvent; +import com.jme3.input.virtual.VirtualJoystick; import com.jme3.math.FastMath; import com.jme3.system.AppSettings; import com.jme3.system.JmeSystem; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.ngengine.libjglios.core.LibJGLIOSInputBridge; import static org.ngengine.libjglios.sdl3.SDL3.SDL_GAMEPAD_AXIS_COUNT; @@ -67,12 +70,15 @@ public final class IosJoyInput implements JoyInput { private static IosJoyInput active; private static final int POV_X_AXIS_ID = 0x4000; - private static final int POV_Y_AXIS_ID = 0x4001; private final Map joysticks = new HashMap<>(); private RawInputListener listener; private InputManager inputManager; private boolean initialized; private boolean onDeviceJoystickRumble; + private String virtualJoystickMode = AppSettings.VIRTUAL_JOYSTICK_AUTO_MINIMIZED; + private boolean useJoysticks = true; + private boolean keyboardSuppressedAutoJoystick; + private VirtualJoystick virtualJoystick; public static void dispatchNativeEvent(int[] intData, float[] floatData) { IosJoyInput joyInput = active; @@ -81,6 +87,28 @@ public static void dispatchNativeEvent(int[] intData, float[] floatData) { } } + public static boolean dispatchPointerDown(int pointerId, float x, float y, long time) { + IosJoyInput joyInput = active; + return joyInput != null && joyInput.onPointerDown(pointerId, x, y, time); + } + + public static boolean dispatchPointerMove(int pointerId, float x, float y, long time) { + IosJoyInput joyInput = active; + return joyInput != null && joyInput.onPointerMove(pointerId, x, y, time); + } + + public static boolean dispatchPointerUp(int pointerId, float x, float y, long time) { + IosJoyInput joyInput = active; + return joyInput != null && joyInput.onPointerUp(pointerId, x, y, time); + } + + public static void dispatchKeyboardInput() { + IosJoyInput joyInput = active; + if (joyInput != null) { + joyInput.onKeyboardInput(); + } + } + @Override public void initialize() { initialized = true; @@ -91,6 +119,9 @@ public void initialize() { @Override public void update() { + if (virtualJoystick != null) { + virtualJoystick.dispatchEvents(listener); + } } @Override @@ -145,16 +176,43 @@ public void stopJoyRumble(int joyId) { public void loadSettings(AppSettings settings) { onDeviceJoystickRumble = settings.isOnDeviceJoystickRumble(); + virtualJoystickMode = settings.getVirtualJoystickMode(); + useJoysticks = settings.useJoysticks(); } @Override public Joystick[] loadJoysticks(InputManager inputManager) { this.inputManager = inputManager; refreshJoysticks(false); + if (shouldCreateVirtualJoystick()) { + virtualJoystick = new VirtualJoystick(inputManager, this, nextVirtualJoyId()); + updateVirtualJoystickAutoVisibility(); + } else { + virtualJoystick = null; + } drainPendingEvents(); return currentJoysticks(); } + private boolean onPointerDown(int pointerId, float x, float y, long time) { + return virtualJoystick != null && virtualJoystick.onPointerDown(pointerId, x, y, time); + } + + private boolean onPointerMove(int pointerId, float x, float y, long time) { + return virtualJoystick != null && virtualJoystick.onPointerMove(pointerId, x, y, time); + } + + private boolean onPointerUp(int pointerId, float x, float y, long time) { + return virtualJoystick != null && virtualJoystick.onPointerUp(pointerId, x, y, time); + } + + private void onKeyboardInput() { + if (isAutoMode(virtualJoystickMode)) { + keyboardSuppressedAutoJoystick = true; + updateVirtualJoystickAutoVisibility(); + } + } + private void drainPendingEvents() { int[] intData = new int[4]; float[] floatData = new float[4]; @@ -265,6 +323,7 @@ private IosJoystick connect(IosJoystick joystick, boolean fireConnectionEvent) { joysticks.put(joystick.getJoyId(), joystick); try { if (inputManager != null) { + updateVirtualJoystickAutoVisibility(); inputManager.setJoysticks(currentJoysticks()); if (fireConnectionEvent) { inputManager.fireJoystickConnectedEvent(joystick); @@ -282,14 +341,61 @@ private void disconnect(int id) { if (joystick != null) { joystick.close(); if (inputManager != null) { + updateVirtualJoystickAutoVisibility(); inputManager.setJoysticks(currentJoysticks()); inputManager.fireJoystickDisconnectedEvent(joystick); } } } + private boolean shouldCreateVirtualJoystick() { + return useJoysticks + && !AppSettings.VIRTUAL_JOYSTICK_DISABLED.equals(virtualJoystickMode); + } + + private void updateVirtualJoystickAutoVisibility() { + if (virtualJoystick == null) { + return; + } + boolean active = isEnabledMode(virtualJoystickMode) + || (isAutoMode(virtualJoystickMode) + && joysticks.isEmpty() + && !keyboardSuppressedAutoJoystick); + virtualJoystick.setEnabled(active); + if (active && isMinimizedMode(virtualJoystickMode)) { + virtualJoystick.setShown(false); + } + } + + private static boolean isEnabledMode(String mode) { + return AppSettings.VIRTUAL_JOYSTICK_ENABLED.equals(mode) + || AppSettings.VIRTUAL_JOYSTICK_ENABLED_MINIMIZED.equals(mode); + } + + private static boolean isAutoMode(String mode) { + return AppSettings.VIRTUAL_JOYSTICK_AUTO.equals(mode) + || AppSettings.VIRTUAL_JOYSTICK_AUTO_MINIMIZED.equals(mode); + } + + private static boolean isMinimizedMode(String mode) { + return AppSettings.VIRTUAL_JOYSTICK_ENABLED_MINIMIZED.equals(mode) + || AppSettings.VIRTUAL_JOYSTICK_AUTO_MINIMIZED.equals(mode); + } + private Joystick[] currentJoysticks() { - return joysticks.values().toArray(new Joystick[0]); + List current = new ArrayList<>(joysticks.values()); + if (virtualJoystick != null) { + current.add(virtualJoystick); + } + return current.toArray(new Joystick[0]); + } + + private int nextVirtualJoyId() { + int id = 0; + for (Integer joystickId : joysticks.keySet()) { + id = Math.max(id, joystickId + 1); + } + return id; } private static String displayName(String sdlName, String fallback, int id) { @@ -321,7 +427,6 @@ private static final class IosJoystick extends AbstractJoystick { private JoystickAxis xAxis; private JoystickAxis yAxis; private JoystickAxis povXAxis; - private JoystickAxis povYAxis; IosJoystick(InputManager inputManager, JoyInput joyInput, int id, boolean gamepad, long gamepadHandle, long joystickHandle, String name, int axisCount, int buttonCount) { @@ -405,10 +510,6 @@ private void addPovAxes(InputManager inputManager) { JoystickAxis povX = new DefaultJoystickAxis(inputManager, this, POV_X_AXIS_ID, JoystickAxis.POV_X, JoystickAxis.POV_X, true, false, 0f); addAxis(POV_X_AXIS_ID, povX); - - JoystickAxis povY = new DefaultJoystickAxis(inputManager, this, POV_Y_AXIS_ID, JoystickAxis.POV_Y, - JoystickAxis.POV_Y, true, false, 0f); - addAxis(POV_Y_AXIS_ID, povY); } JoystickAxis getAxisById(int id) { @@ -436,7 +537,7 @@ public JoystickAxis getPovXAxis() { @Override public JoystickAxis getPovYAxis() { - return povYAxis; + return null; } private void addAxis(int index, JoystickAxis axis) { @@ -452,9 +553,6 @@ private void addAxis(int index, JoystickAxis axis) { case POV_X_AXIS_ID: povXAxis = axis; break; - case POV_Y_AXIS_ID: - povYAxis = axis; - break; default: break; } diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlJoystickInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlJoystickInput.java index f09acc452a..1aaf1335ee 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlJoystickInput.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlJoystickInput.java @@ -3,11 +3,14 @@ import com.jme3.input.*; import com.jme3.input.event.JoyAxisEvent; import com.jme3.input.event.JoyButtonEvent; +import com.jme3.input.virtual.VirtualJoystick; import com.jme3.math.FastMath; import com.jme3.system.AppSettings; import java.nio.ByteBuffer; import java.nio.IntBuffer; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; @@ -28,9 +31,6 @@ public class SdlJoystickInput implements JoyInput { private static final Logger LOGGER = Logger.getLogger(SdlJoystickInput.class.getName()); - private static final int POV_X_AXIS_ID = 7; - private static final int POV_Y_AXIS_ID = 8; - private final AppSettings settings; private final Map joysticks = new HashMap<>(); private final Map joyButtonPressed = new HashMap<>(); @@ -42,6 +42,7 @@ public class SdlJoystickInput implements JoyInput { private float globalJitterThreshold; private boolean loadGamepads; private boolean loadRaw; + private VirtualJoystick virtualJoystick; private RawInputListener listener; @@ -170,13 +171,17 @@ private void onDeviceConnected(InputManager inputManager, int deviceIndex, boole } // Virtual POV axes for D-pad. - JoystickAxis povX = new DefaultJoystickAxis(inputManager, joystick, POV_X_AXIS_ID, JoystickAxis.POV_X, + int povXAxisId = joystick.getAxisCount(); + JoystickAxis povX = new DefaultJoystickAxis(inputManager, joystick, povXAxisId, JoystickAxis.POV_X, JoystickAxis.POV_X, true, false, 0.0f); - joystick.addAxis(POV_X_AXIS_ID, povX); + joystick.addAxis(povXAxisId, povX); + joystick.povAxisX = povX; - JoystickAxis povY = new DefaultJoystickAxis(inputManager, joystick, POV_Y_AXIS_ID, JoystickAxis.POV_Y, + int povYAxisId = joystick.getAxisCount(); + JoystickAxis povY = new DefaultJoystickAxis(inputManager, joystick, povYAxisId, JoystickAxis.POV_Y, JoystickAxis.POV_Y, true, false, 0.0f); - joystick.addAxis(POV_Y_AXIS_ID, povY); + joystick.addAxis(povYAxisId, povY); + joystick.povAxisY = povY; inputManager.fireJoystickConnectedEvent(joystick); @@ -241,7 +246,7 @@ public Joystick[] loadJoysticks(InputManager inputManager) { } if (loadRaw) { - // load raw gamepads + // load raw joysticks IntBuffer joys = SDL_GetJoysticks(); if (joys != null) { try { @@ -256,12 +261,34 @@ public Joystick[] loadJoysticks(InputManager inputManager) { } } - return joysticks.values().toArray(new Joystick[0]); + if (settings.useJoysticks() && isEnabledMode(settings.getVirtualJoystickMode())) { + virtualJoystick = new VirtualJoystick(inputManager, this, nextVirtualJoyId()); + virtualJoystick.setShown(!isMinimizedMode(settings.getVirtualJoystickMode())); + } else { + virtualJoystick = null; + } + + return currentJoysticks(); } @Override public void update() { handleInputEvents(); + if (virtualJoystick != null) { + virtualJoystick.dispatchEvents(listener); + } + } + + public boolean onPointerDown(int pointerId, float x, float y, long time) { + return virtualJoystick != null && virtualJoystick.onPointerDown(pointerId, x, y, time); + } + + public boolean onPointerMove(int pointerId, float x, float y, long time) { + return virtualJoystick != null && virtualJoystick.onPointerMove(pointerId, x, y, time); + } + + public boolean onPointerUp(int pointerId, float x, float y, long time) { + return virtualJoystick != null && virtualJoystick.onPointerUp(pointerId, x, y, time); } public void onSDLEvent(SDL_Event evt) { @@ -301,11 +328,11 @@ private void handleInputEvents() { long gp = js.gamepad; for (JoystickAxis axis : js.getAxes()) { - int axisIndex = axis.getAxisId(); - if (isVirtualPovAxis(axisIndex)) { + if (axis == js.getPovXAxis() || axis == js.getPovYAxis()) { continue; } + int axisIndex = axis.getAxisId(); String jmeAxisId = axis.getLogicalId(); short v = SDL_GetGamepadAxis(gp, axisIndex); @@ -336,14 +363,14 @@ private void handleInputEvents() { boolean pressed = SDL_GetGamepadButton(gp, buttonId); updateButton(button, pressed); - if (JoystickButton.BUTTON_XBOX_DPAD_UP.equals(jmeButtonId)) { - povYValue += pressed ? 1f : 0f; - } else if (JoystickButton.BUTTON_XBOX_DPAD_DOWN.equals(jmeButtonId)) { - povYValue += pressed ? -1f : 0f; - } else if (JoystickButton.BUTTON_XBOX_DPAD_LEFT.equals(jmeButtonId)) { + if (JoystickButton.BUTTON_XBOX_DPAD_LEFT.equals(jmeButtonId)) { povXValue += pressed ? -1f : 0f; } else if (JoystickButton.BUTTON_XBOX_DPAD_RIGHT.equals(jmeButtonId)) { povXValue += pressed ? 1f : 0f; + } else if (JoystickButton.BUTTON_XBOX_DPAD_UP.equals(jmeButtonId)) { + povYValue += pressed ? 1f : 0f; + } else if (JoystickButton.BUTTON_XBOX_DPAD_DOWN.equals(jmeButtonId)) { + povYValue += pressed ? -1f : 0f; } } @@ -351,16 +378,16 @@ private void handleInputEvents() { if (povXAxis != null) { updateAxis(povXAxis, povXValue, povXValue); } - JoystickAxis povYAxis = js.getPovYAxis(); if (povYAxis != null) { updateAxis(povYAxis, povYValue, povYValue); } + } else { long joy = js.joystick; for (JoystickAxis axis : js.getAxes()) { - if (isVirtualPovAxis(axis.getAxisId())) { + if (axis == js.getPovXAxis() || axis == js.getPovYAxis()) { continue; } @@ -378,8 +405,30 @@ private void handleInputEvents() { } } - private boolean isVirtualPovAxis(int axisId) { - return axisId == POV_X_AXIS_ID || axisId == POV_Y_AXIS_ID; + private Joystick[] currentJoysticks() { + List current = new ArrayList<>(joysticks.values()); + if (virtualJoystick != null) { + current.add(virtualJoystick); + } + return current.toArray(new Joystick[0]); + } + + private int nextVirtualJoyId() { + int id = 0; + for (Integer joystickId : joysticks.keySet()) { + id = Math.max(id, joystickId + 1); + } + return id; + } + + private static boolean isEnabledMode(String mode) { + return AppSettings.VIRTUAL_JOYSTICK_ENABLED.equals(mode) + || AppSettings.VIRTUAL_JOYSTICK_ENABLED_MINIMIZED.equals(mode); + } + + private static boolean isMinimizedMode(String mode) { + return AppSettings.VIRTUAL_JOYSTICK_ENABLED_MINIMIZED.equals(mode) + || AppSettings.VIRTUAL_JOYSTICK_AUTO_MINIMIZED.equals(mode); } @Override @@ -471,7 +520,7 @@ private String getButtonLabel(SdlJoystick gamepad, int sdlButtonIndex) { } } - private String getAxisLabel(SdlJoystick gamepad, int sdlAxisIndex) { + private String getAxisLabel(SdlJoystick joystick, int sdlAxisIndex) { switch (sdlAxisIndex) { case SDL_GAMEPAD_AXIS_LEFTX: return "LEFT THUMB STICK (X)"; @@ -621,8 +670,8 @@ private static class SdlJoystick extends AbstractJoystick { private JoystickAxis povAxisX; private JoystickAxis povAxisY; - long gamepad; long joystick; + long gamepad; SdlJoystick(InputManager inputManager, JoyInput joyInput, int joyId, String name, long gamepad, long joystick) { @@ -647,14 +696,6 @@ void addAxis(int index, JoystickAxis axis) { yAxis = axis; break; } - case POV_X_AXIS_ID: { - povAxisX = axis; - break; - } - case POV_Y_AXIS_ID: { - povAxisY = axis; - break; - } } } diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlMouseInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlMouseInput.java index 2370974e1d..545a4c495b 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlMouseInput.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlMouseInput.java @@ -32,6 +32,7 @@ package com.jme3.input.lwjgl; import com.jme3.cursors.plugins.JmeCursor; +import com.jme3.input.JoyInput; import com.jme3.input.MouseInput; import com.jme3.input.RawInputListener; import com.jme3.input.event.MouseButtonEvent; @@ -174,6 +175,9 @@ public void onSDLEvent(SDL_Event event) { } if (xDelta != 0 || yDelta != 0) { + if (onPointerMove(0, x, y, event.motion().timestamp())) { + return; + } MouseMotionEvent mouseMotionEvent = new MouseMotionEvent(x, y, xDelta, yDelta, mouseWheel, 0); mouseMotionEvent.setTime(event.motion().timestamp()); @@ -205,6 +209,9 @@ public void onSDLEvent(SDL_Event event) { refreshWindowMetrics(); mouseX = toInputX(event.button().x()); mouseY = toInputY(event.button().y()); + if (onPointerButton(0, event.button().down(), mouseX, mouseY, event.button().timestamp())) { + return; + } int button = Byte.toUnsignedInt(event.button().button()); MouseButtonEvent mouseButtonEvent = @@ -214,6 +221,25 @@ public void onSDLEvent(SDL_Event event) { } } + private boolean onPointerButton(int pointerId, boolean pressed, float x, float y, long time) { + JoyInput joyInput = context.getJoyInput(); + if (joyInput instanceof SdlJoystickInput) { + if (pressed) { + return ((SdlJoystickInput) joyInput).onPointerDown(pointerId, x, y, time); + } + return ((SdlJoystickInput) joyInput).onPointerUp(pointerId, x, y, time); + } + return false; + } + + private boolean onPointerMove(int pointerId, float x, float y, long time) { + JoyInput joyInput = context.getJoyInput(); + if (joyInput instanceof SdlJoystickInput) { + return ((SdlJoystickInput) joyInput).onPointerMove(pointerId, x, y, time); + } + return false; + } + private void refreshWindowMetrics() { AppSettings settings = context.getSettings(); currentWidth = Math.max(settings.getWidth(), 1); From 8725dadf4fa0d05a7fd199616e283a9d605f4668 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Mon, 25 May 2026 14:00:54 +0200 Subject: [PATCH 2/7] documentation --- .../java/com/jme3/system/AppSettings.java | 81 ++++++++++++++++++- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/system/AppSettings.java b/jme3-core/src/main/java/com/jme3/system/AppSettings.java index 47311c9037..f1feec40c8 100644 --- a/jme3-core/src/main/java/com/jme3/system/AppSettings.java +++ b/jme3-core/src/main/java/com/jme3/system/AppSettings.java @@ -249,10 +249,43 @@ public final class AppSettings extends HashMap { */ public static final String OPENAL = "OPENAL"; + /** + * Disable the on-screen virtual gamepad entirely. + * + * @see #setVirtualJoystick(String) + */ public static final String VIRTUAL_JOYSTICK_DISABLED = "VirtualJoystickDisabled"; + + /** + * Always display the virtual gamepad toggle button, even on desktop and + * even when a hardware gamepad is detected. + * + * @see #setVirtualJoystick(String) + */ public static final String VIRTUAL_JOYSTICK_ENABLED_MINIMIZED = "VirtualJoystickEnabledMinimized"; + + /** + * Always display the full virtual gamepad, even on desktop and even when a + * hardware gamepad is detected. + * + * @see #setVirtualJoystick(String) + */ public static final String VIRTUAL_JOYSTICK_ENABLED = "VirtualJoystickEnabled"; + + /** + * Display the full virtual gamepad automatically on mobile when no + * hardware gamepad is detected. + * + * @see #setVirtualJoystick(String) + */ public static final String VIRTUAL_JOYSTICK_AUTO = "VirtualJoystickAuto"; + + /** + * Display the virtual gamepad toggle button automatically on mobile when no + * hardware gamepad is detected. + * + * @see #setVirtualJoystick(String) + */ public static final String VIRTUAL_JOYSTICK_AUTO_MINIMIZED = "VirtualJoystickAutoMinimized"; @@ -371,6 +404,7 @@ public final class AppSettings extends HashMap { defaults.put("JoysticksAxisJitterThreshold", 0.0001f); defaults.put("SDLGameControllerDBResourcePath", ""); defaults.put("OnDeviceJoystickRumble", false); + defaults.put("UseAndroidSensorJoystick", false); defaults.put("VirtualJoystick", VIRTUAL_JOYSTICK_AUTO_MINIMIZED); // defaults.put("Icons", null); } @@ -817,8 +851,9 @@ public void setUseInput(boolean use) { /** * @param use If true, the application will initialize and use joystick - * input. Set to false if no joystick input is desired. - * (Default: false) + * input, including hardware gamepads when available. Set to false if no + * joystick input is desired. + * (Default: true) */ public void setUseJoysticks(boolean use) { putBoolean("DisableJoysticks", !use); @@ -834,7 +869,37 @@ public void setOnDeviceJoystickRumble(boolean enabled) { } /** - * Sets the on-screen virtual joystick mode. + * @param use If true, Android exposes device orientation sensors as a + * joystick. This is disabled by default because the sensor joystick reports + * movement from device rotation and can conflict with gamepad or virtual + * joystick mappings. + * (Default: false) + */ + public void setUseAndroidSensorJoystick(boolean use) { + putBoolean("UseAndroidSensorJoystick", use); + } + + /** + * Sets the on-screen virtual gamepad mode. + *

    + * The default mode is {@link #VIRTUAL_JOYSTICK_AUTO_MINIMIZED}, which + * displays the button to show the virtual gamepad on mobile unless a + * hardware gamepad is connected. + *

      + *
    • {@link #VIRTUAL_JOYSTICK_DISABLED}: disable the virtual gamepad + * entirely.
    • + *
    • {@link #VIRTUAL_JOYSTICK_ENABLED_MINIMIZED}: always display the + * virtual gamepad toggle button, even on desktop and even when a hardware + * gamepad is detected.
    • + *
    • {@link #VIRTUAL_JOYSTICK_ENABLED}: same as + * {@link #VIRTUAL_JOYSTICK_ENABLED_MINIMIZED}, but display the full virtual + * gamepad instead of only the toggle button.
    • + *
    • {@link #VIRTUAL_JOYSTICK_AUTO}: display the full virtual gamepad + * automatically on mobile when no hardware gamepad is detected.
    • + *
    • {@link #VIRTUAL_JOYSTICK_AUTO_MINIMIZED}: display the virtual gamepad + * toggle button automatically on mobile when no hardware gamepad is + * detected.
    • + *
    * * @param mode one of {@link #VIRTUAL_JOYSTICK_DISABLED}, * {@link #VIRTUAL_JOYSTICK_ENABLED_MINIMIZED}, @@ -1332,6 +1397,16 @@ public boolean isOnDeviceJoystickRumble() { return getBoolean("OnDeviceJoystickRumble"); } + /** + * Get the Android sensor joystick state. + * + * @return true to expose Android device orientation sensors as a joystick + * @see #setUseAndroidSensorJoystick(boolean) + */ + public boolean useAndroidSensorJoystick() { + return getBoolean("UseAndroidSensorJoystick"); + } + /** * Get whether supporting backends should expose an on-screen virtual joystick. * From ab544908918a7ee924c77fbd09ec1dfc2da242ab Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Mon, 25 May 2026 14:01:16 +0200 Subject: [PATCH 3/7] disable AndroidSensorJoyInput --- .../jme3test/android/TestAndroidSensors.java | 9 +++++++- .../com/jme3/app/AndroidHarnessFragment.java | 10 ++++---- .../jme3/input/android/AndroidJoyInput.java | 23 +++++++++++-------- .../jme3/input/android/AndroidJoyInput14.java | 2 +- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/jme3-android-examples/src/main/java/jme3test/android/TestAndroidSensors.java b/jme3-android-examples/src/main/java/jme3test/android/TestAndroidSensors.java index b38bba76c5..c6e3bcc8c4 100644 --- a/jme3-android-examples/src/main/java/jme3test/android/TestAndroidSensors.java +++ b/jme3-android-examples/src/main/java/jme3test/android/TestAndroidSensors.java @@ -17,6 +17,7 @@ import com.jme3.scene.Mesh; import com.jme3.scene.shape.Box; import com.jme3.scene.shape.Line; +import com.jme3.system.AppSettings; import com.jme3.texture.Texture; import com.jme3.util.IntMap; @@ -79,6 +80,12 @@ public class TestAndroidSensors extends SimpleApplication implements ActionListe // Make sure to set joystickEventsEnabled = true in MainActivity for Android + public static void configureSettings(AppSettings settings) { + settings.setUseJoysticks(true); + settings.setUseAndroidSensorJoystick(true); + settings.setVirtualJoystick(AppSettings.VIRTUAL_JOYSTICK_DISABLED); + } + private float toDegrees(float rad) { return rad * FastMath.RAD_TO_DEG; } @@ -311,4 +318,4 @@ public void onAnalog(String string, float value, float tpf) { } } -} \ No newline at end of file +} diff --git a/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java b/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java index 9a7a53cd7a..371819034a 100644 --- a/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java +++ b/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java @@ -42,7 +42,7 @@ import androidx.fragment.app.Fragment; import com.jme3.audio.AudioRenderer; import com.jme3.input.JoyInput; -import com.jme3.input.android.AndroidSensorJoyInput; +import com.jme3.input.android.AndroidJoyInput; import com.jme3.system.AppSettings; import com.jme3.system.SystemListener; import com.jme3.system.android.JmeAndroidSystem; @@ -269,8 +269,8 @@ public void gainFocus() { } JoyInput joyInput = app.getContext() != null ? app.getContext().getJoyInput() : null; - if (joyInput instanceof AndroidSensorJoyInput) { - ((AndroidSensorJoyInput) joyInput).resumeSensors(); + if (joyInput instanceof AndroidJoyInput) { + ((AndroidJoyInput) joyInput).resumeJoysticks(); } app.gainFocus(); @@ -295,8 +295,8 @@ public void loseFocus() { } JoyInput joyInput = app.getContext() != null ? app.getContext().getJoyInput() : null; - if (joyInput instanceof AndroidSensorJoyInput) { - ((AndroidSensorJoyInput) joyInput).pauseSensors(); + if (joyInput instanceof AndroidJoyInput) { + ((AndroidJoyInput) joyInput).pauseJoysticks(); } } } diff --git a/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java b/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java index 5a482cbce4..c7b5abf6f7 100644 --- a/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java +++ b/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java @@ -50,9 +50,8 @@ import java.util.logging.Logger; /** - * Main class that manages various joystick devices. Joysticks can be many forms - * including a simulated joystick to communicate the device orientation as well - * as physical joysticks.
    + * Main class that manages joystick devices. Joysticks can be physical gamepads, + * the on-screen virtual joystick, or an explicitly-enabled sensor joystick.
    * This class manages all the joysticks and feeds the inputs from each back * to jME's InputManager. * @@ -81,7 +80,6 @@ */ public class AndroidJoyInput implements JoyInput { private static final Logger logger = Logger.getLogger(AndroidJoyInput.class.getName()); - public static boolean disableSensors = false; protected AndroidInputHandler inputHandler; protected List joystickList = new ArrayList<>(); @@ -96,16 +94,18 @@ public class AndroidJoyInput implements JoyInput { private boolean onDeviceJoystickRumble = false; private String virtualJoystickMode = AppSettings.VIRTUAL_JOYSTICK_AUTO_MINIMIZED; private boolean useJoysticks = true; + private boolean useAndroidSensorJoystick = false; private boolean physicalJoystickAvailable = false; private boolean keyboardSuppressedAutoJoystick = false; private VirtualJoystick virtualJoystick; + private GLSurfaceView view; public AndroidJoyInput(AndroidInputHandler inputHandler) { this.inputHandler = inputHandler; - sensorJoyInput = new AndroidSensorJoyInput(this); } public void setView(GLSurfaceView view) { + this.view = view; if (sensorJoyInput != null) { sensorJoyInput.setView(view); } @@ -115,6 +115,7 @@ public void loadSettings(AppSettings settings) { onDeviceJoystickRumble = settings.isOnDeviceJoystickRumble(); virtualJoystickMode = settings.getVirtualJoystickMode(); useJoysticks = settings.useJoysticks(); + useAndroidSensorJoystick = settings.useAndroidSensorJoystick(); } boolean isOnDeviceJoystickRumble() { @@ -150,7 +151,6 @@ public void resumeJoysticks() { if (sensorJoyInput != null) { sensorJoyInput.resumeSensors(); } - } @Override @@ -166,12 +166,11 @@ public boolean isInitialized() { @Override public void destroy() { initialized = false; - if (sensorJoyInput != null) { sensorJoyInput.destroy(); + sensorJoyInput = null; } - - setView(null); + view = null; } @Override @@ -204,7 +203,11 @@ public Joystick[] loadJoysticks(InputManager inputManager) { logger.log(Level.INFO, "loading joysticks for {0}", this.getClass().getName()); } joystickList.clear(); - if (!disableSensors) { + if (useJoysticks && useAndroidSensorJoystick) { + if (sensorJoyInput == null) { + sensorJoyInput = new AndroidSensorJoyInput(this); + sensorJoyInput.setView(view); + } joystickList.add(sensorJoyInput.loadJoystick(joystickList.size(), inputManager)); } physicalJoystickAvailable = false; diff --git a/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput14.java b/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput14.java index 180ed8c17f..f4a4625ed1 100644 --- a/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput14.java +++ b/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput14.java @@ -90,7 +90,7 @@ public void destroy() { @Override public Joystick[] loadJoysticks(InputManager inputManager) { - // load the simulated joystick for device orientation + // load virtual joystick if enabled super.loadJoysticks(inputManager); // load physical gamepads/joysticks int beforePhysicalJoysticks = joystickList.size(); From 0d5efb5895ef3e20bf8d25ac0b07de4f328f758a Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Mon, 25 May 2026 14:14:43 +0200 Subject: [PATCH 4/7] reduce objects allocations --- .../jme3/input/virtual/VirtualJoystick.java | 21 ++-- .../input/virtual/VirtualJoystickLayout.java | 114 +++++++++++++++--- 2 files changed, 110 insertions(+), 25 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java index f7f0c9b20d..553958b1b5 100644 --- a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java +++ b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java @@ -75,6 +75,7 @@ public class VirtualJoystick extends AbstractJoystick { private final Map buttonsByLogicalId = new HashMap<>(); private final Map captures = new HashMap<>(); private final ArrayDeque events = new ArrayDeque<>(); + private final ArrayDeque auxiliaryEvents = new ArrayDeque<>(); private final float[] axisValues = new float[8]; private final boolean[] buttonValues = new boolean[16]; private final Object inputLock = new Object(); @@ -295,7 +296,6 @@ public void dispatchEvents(RawInputListener listener) { return; } - ArrayDeque pendingEvents; synchronized (inputLock) { if (!hasEvents) { return; @@ -305,13 +305,16 @@ public void dispatchEvents(RawInputListener listener) { hasEvents = false; return; } - pendingEvents = new ArrayDeque<>(events); - events.clear(); + auxiliaryEvents.clear(); + InputEvent event; + while ((event = events.poll()) != null) { + auxiliaryEvents.add(event); + } hasEvents = false; } InputEvent event; - while ((event = pendingEvents.poll()) != null) { + while ((event = auxiliaryEvents.poll()) != null) { if (event instanceof JoyAxisEvent) { listener.onJoyAxisEvent((JoyAxisEvent) event); } else if (event instanceof JoyButtonEvent) { @@ -352,7 +355,9 @@ public void updateVisuals(Node parent, AssetManager assetManager, int width, int } if (!enabled) { - visualRoot.detachAllChildren(); + if (visualRoot.getQuantity() > 0) { + visualRoot.detachAllChildren(); + } return; } @@ -376,12 +381,12 @@ public void updateVisuals(Node parent, AssetManager assetManager, int width, int syncElement(currentLayout.getToggleElement(), assetManager, width, height, scale); if (!shown) { for (Element element : currentLayout.getButtons()) { - if (element.node != null) { + if (element.node != null && element.node.getParent() != null) { element.node.removeFromParent(); } } for (Element element : currentLayout.getAxisElements()) { - if (element.node != null) { + if (element.node != null && element.node.getParent() != null) { element.node.removeFromParent(); } } @@ -588,7 +593,7 @@ private void syncElement(Element element, AssetManager assetManager, int width, return; } if (!element.visible) { - if (element.node != null) { + if (element.node != null && element.node.getParent() != null) { element.node.removeFromParent(); } return; diff --git a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickLayout.java b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickLayout.java index 89e53a6522..41561ab023 100644 --- a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickLayout.java +++ b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickLayout.java @@ -371,6 +371,8 @@ private void rebuildSnapshots() { } public static class Element implements Savable { + private static final ColorRGBA DEFAULT_COLOR = new ColorRGBA(1f, 1f, 1f, 0.72f); + volatile String id; volatile String label; volatile String yAxisLogicalId; @@ -396,6 +398,26 @@ public static class Element implements Savable { transient Picture nub; transient Picture icon; transient BitmapText text; + private transient boolean baseGeometrySynced; + private transient boolean baseColorSynced; + private transient boolean nubGeometrySynced; + private transient boolean iconGeometrySynced; + private transient boolean iconColorSynced; + private transient boolean textGeometrySynced; + private transient boolean textColorSynced; + private transient boolean nodePositionSynced; + private transient boolean lastPressed; + private transient float lastBaseWidth; + private transient float lastBaseHeight; + private transient float lastBaseX; + private transient float lastBaseY; + private transient float lastNubSize; + private transient float lastNubX; + private transient float lastNubY; + private transient float lastIconSize; + private transient float lastTextSize; + private transient float lastNodeX; + private transient float lastNodeY; public Element() { } @@ -465,35 +487,81 @@ void sync(int width, int height, float scale, boolean pressed) { } else { pixelY = height * 0.5f; } - base.setWidth(pixelWidth); - base.setHeight(pixelHeight); - base.setPosition(-pixelWidth * 0.5f, -pixelHeight * 0.5f); - setColor(base, pressed ? ColorRGBA.White : new ColorRGBA(1f, 1f, 1f, 0.72f)); + float baseX = -pixelWidth * 0.5f; + float baseY = -pixelHeight * 0.5f; + if (!baseGeometrySynced || lastBaseWidth != pixelWidth) { + base.setWidth(pixelWidth); + lastBaseWidth = pixelWidth; + } + if (!baseGeometrySynced || lastBaseHeight != pixelHeight) { + base.setHeight(pixelHeight); + lastBaseHeight = pixelHeight; + } + if (!baseGeometrySynced || lastBaseX != baseX || lastBaseY != baseY) { + base.setPosition(baseX, baseY); + lastBaseX = baseX; + lastBaseY = baseY; + } + baseGeometrySynced = true; + if (!baseColorSynced || lastPressed != pressed) { + setColor(base, pressed ? ColorRGBA.White : DEFAULT_COLOR); + lastPressed = pressed; + baseColorSynced = true; + } if (nub != null) { float nubSize = pixelHeight * 0.42f; - nub.setWidth(nubSize); - nub.setHeight(nubSize); - nub.setPosition((nubX * pixelHeight * 0.32f) - nubSize * 0.5f, - (nubY * pixelHeight * 0.32f) - nubSize * 0.5f); + float nubPixelX = (nubX * pixelHeight * 0.32f) - nubSize * 0.5f; + float nubPixelY = (nubY * pixelHeight * 0.32f) - nubSize * 0.5f; + if (!nubGeometrySynced || lastNubSize != nubSize) { + nub.setWidth(nubSize); + nub.setHeight(nubSize); + lastNubSize = nubSize; + } + if (!nubGeometrySynced || lastNubX != nubPixelX || lastNubY != nubPixelY) { + nub.setPosition(nubPixelX, nubPixelY); + lastNubX = nubPixelX; + lastNubY = nubPixelY; + } + nubGeometrySynced = true; } if (icon != null) { float iconSize = pixelHeight * (aspect > 1f ? 0.38f : 0.46f); - icon.setWidth(iconSize); - icon.setHeight(iconSize); - icon.setPosition(-iconSize * 0.5f, -iconSize * 0.5f); - setColor(icon, ColorRGBA.White); + if (!iconGeometrySynced || lastIconSize != iconSize) { + icon.setWidth(iconSize); + icon.setHeight(iconSize); + icon.setPosition(-iconSize * 0.5f, -iconSize * 0.5f); + lastIconSize = iconSize; + iconGeometrySynced = true; + } + if (!iconColorSynced) { + setColor(icon, ColorRGBA.White); + iconColorSynced = true; + } } if (text != null) { - text.setSize(pixelHeight * (label.length() > 2 ? 0.2f : 0.32f)); - text.setColor(ColorRGBA.White); - text.setLocalTranslation(-text.getLineWidth() * 0.5f, - text.getLineHeight() * 0.48f, 1f); + float textSize = pixelHeight * (label.length() > 2 ? 0.2f : 0.32f); + if (!textGeometrySynced || lastTextSize != textSize) { + text.setSize(textSize); + lastTextSize = textSize; + text.setLocalTranslation(-text.getLineWidth() * 0.5f, + text.getLineHeight() * 0.48f, 1f); + textGeometrySynced = true; + } + if (!textColorSynced) { + text.setColor(ColorRGBA.White); + textColorSynced = true; + } } - node.setLocalTranslation(pixelX, pixelY, 0f); + if (!nodePositionSynced || lastNodeX != pixelX || lastNodeY != pixelY) { + node.setLocalTranslation(pixelX, pixelY, 0f); + lastNodeX = pixelX; + lastNodeY = pixelY; + nodePositionSynced = true; + } } synchronized void clearVisuals() { @@ -505,6 +573,7 @@ synchronized void clearVisuals() { nub = null; icon = null; text = null; + clearSyncState(); } private void setColor(Picture picture, ColorRGBA color) { @@ -513,6 +582,17 @@ private void setColor(Picture picture, ColorRGBA color) { } } + private void clearSyncState() { + baseGeometrySynced = false; + baseColorSynced = false; + nubGeometrySynced = false; + iconGeometrySynced = false; + iconColorSynced = false; + textGeometrySynced = false; + textColorSynced = false; + nodePositionSynced = false; + } + @Override public synchronized void write(JmeExporter ex) throws IOException { OutputCapsule capsule = ex.getCapsule(this); From 1b4aa0cba2bf4c6344efbd9fb57b3ec2a8c92dee Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Mon, 25 May 2026 15:20:27 +0200 Subject: [PATCH 5/7] fixes --- .../jme3/input/android/AndroidJoyInput.java | 13 +-- .../jme3/input/virtual/VirtualJoystick.java | 27 ++++--- .../input/virtual/VirtualJoystickLayout.java | 80 ++++++++++++++----- .../input/virtual/VirtualJoystickTheme.java | 10 +-- .../java/com/jme3/input/ios/IosJoyInput.java | 76 +++++++++++++++--- .../jme3/input/lwjgl/SdlJoystickInput.java | 21 +++-- 6 files changed, 166 insertions(+), 61 deletions(-) diff --git a/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java b/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java index c7b5abf6f7..a0e1122032 100644 --- a/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java +++ b/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java @@ -97,7 +97,7 @@ public class AndroidJoyInput implements JoyInput { private boolean useAndroidSensorJoystick = false; private boolean physicalJoystickAvailable = false; private boolean keyboardSuppressedAutoJoystick = false; - private VirtualJoystick virtualJoystick; + private volatile VirtualJoystick virtualJoystick; private GLSurfaceView view; public AndroidJoyInput(AndroidInputHandler inputHandler) { @@ -222,7 +222,8 @@ public Joystick[] loadJoysticks(InputManager inputManager) { } public boolean onTouch(MotionEvent event) { - if (virtualJoystick == null || inputHandler.getView() == null) { + VirtualJoystick joystick = virtualJoystick; + if (joystick == null || inputHandler.getView() == null) { return false; } @@ -235,20 +236,20 @@ public boolean onTouch(MotionEvent event) { switch (action) { case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_DOWN: - consumed = virtualJoystick.onPointerDown(event.getPointerId(pointerIndex), + consumed = joystick.onPointerDown(event.getPointerId(pointerIndex), toJmeX(event.getX(pointerIndex)), toJmeY(event.getY(pointerIndex)), time); break; case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_UP: - consumed = virtualJoystick.onPointerUp(event.getPointerId(pointerIndex), + consumed = joystick.onPointerUp(event.getPointerId(pointerIndex), toJmeX(event.getX(pointerIndex)), toJmeY(event.getY(pointerIndex)), time); break; case MotionEvent.ACTION_CANCEL: - consumed = virtualJoystick.onPointerCancel(time); + consumed = joystick.onPointerCancel(time); break; case MotionEvent.ACTION_MOVE: for (int i = 0; i < event.getPointerCount(); i++) { - consumed = virtualJoystick.onPointerMove(event.getPointerId(i), + consumed = joystick.onPointerMove(event.getPointerId(i), toJmeX(event.getX(i)), toJmeY(event.getY(i)), time) || consumed; } break; diff --git a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java index 553958b1b5..48df1ba168 100644 --- a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java +++ b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java @@ -74,11 +74,12 @@ public class VirtualJoystick extends AbstractJoystick { private final Map axesByLogicalId = new HashMap<>(); private final Map buttonsByLogicalId = new HashMap<>(); private final Map captures = new HashMap<>(); - private final ArrayDeque events = new ArrayDeque<>(); - private final ArrayDeque auxiliaryEvents = new ArrayDeque<>(); + private ArrayDeque events = new ArrayDeque<>(); + private ArrayDeque readyEvents = new ArrayDeque<>(); private final float[] axisValues = new float[8]; private final boolean[] buttonValues = new boolean[16]; private final Object inputLock = new Object(); + private final Element.BoundsSnapshot inputBounds = new Element.BoundsSnapshot(); private JoystickAxis xAxis; private JoystickAxis yAxis; @@ -189,8 +190,9 @@ public void reset() { */ public boolean onPointerDown(int pointerId, float x, float y, long time) { synchronized (inputLock) { - if (!enabled || captures.containsKey(pointerId)) { - return captures.containsKey(pointerId); + Capture existingCapture = captures.get(pointerId); + if (!enabled || existingCapture != null) { + return existingCapture != null; } Element toggleElement = layout.getToggleElement(); @@ -305,16 +307,14 @@ public void dispatchEvents(RawInputListener listener) { hasEvents = false; return; } - auxiliaryEvents.clear(); - InputEvent event; - while ((event = events.poll()) != null) { - auxiliaryEvents.add(event); - } + ArrayDeque pendingEvents = events; + events = readyEvents; + readyEvents = pendingEvents; hasEvents = false; } InputEvent event; - while ((event = auxiliaryEvents.poll()) != null) { + while ((event = readyEvents.poll()) != null) { if (event instanceof JoyAxisEvent) { listener.onJoyAxisEvent((JoyAxisEvent) event); } else if (event instanceof JoyButtonEvent) { @@ -465,12 +465,13 @@ private void addButton(InputManager inputManager, int id, String name, String lo } private void updateAxisCapture(Element element, float x, float y, long time) { - float radius = element.pixelSize * 0.5f; + element.copyBoundsTo(inputBounds); + float radius = inputBounds.size * 0.5f; if (radius <= 0f) { return; } - float dx = x - element.pixelX; - float dy = y - element.pixelY; + float dx = x - inputBounds.x; + float dy = y - inputBounds.y; float length = FastMath.sqrt(dx * dx + dy * dy); if (length > radius && length > 0f) { dx *= radius / length; diff --git a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickLayout.java b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickLayout.java index 41561ab023..84f91708aa 100644 --- a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickLayout.java +++ b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickLayout.java @@ -183,7 +183,7 @@ public synchronized void setButtonPosition(String logicalId, float x, float y) { markUpdateNeeded(); } - public Vector2f getButtonPosition(String logicalId) { + public synchronized Vector2f getButtonPosition(String logicalId) { Element element = element(buttons, logicalId); return new Vector2f(element.positionX, element.positionY); } @@ -194,7 +194,7 @@ public synchronized void setButtonVisible(String logicalId, boolean visible) { markUpdateNeeded(); } - public boolean isButtonVisible(String logicalId) { + public synchronized boolean isButtonVisible(String logicalId) { return element(buttons, logicalId).visible; } @@ -203,7 +203,7 @@ public synchronized void setAxisPosition(String logicalId, float x, float y) { markUpdateNeeded(); } - public Vector2f getAxisPosition(String logicalId) { + public synchronized Vector2f getAxisPosition(String logicalId) { Element element = axisElement(logicalId); return new Vector2f(element.positionX, element.positionY); } @@ -214,7 +214,7 @@ public synchronized void setAxisVisible(String logicalId, boolean visible) { markUpdateNeeded(); } - public boolean isAxisVisible(String logicalId) { + public synchronized boolean isAxisVisible(String logicalId) { return axisElement(logicalId).visible; } @@ -287,11 +287,11 @@ public Element getToggleElement() { return toggleElement; } - public Element getButtonElement(String logicalId) { + public synchronized Element getButtonElement(String logicalId) { return buttons.get(logicalId); } - public Element getAxisElement(String logicalId) { + public synchronized Element getAxisElement(String logicalId) { return findAxisElement(logicalId); } @@ -386,11 +386,7 @@ public static class Element implements Savable { volatile float shortOffsetX; volatile float shortOffsetY; volatile boolean visible = true; - transient volatile float pixelX; - transient volatile float pixelY; - transient volatile float pixelSize; - transient volatile float pixelWidth; - transient volatile float pixelHeight; + private transient volatile Bounds bounds = Bounds.EMPTY; transient volatile float nubX; transient volatile float nubY; transient Node node; @@ -465,18 +461,28 @@ public synchronized Element setIconTextureKey(TextureKey iconTextureKey) { } boolean contains(float x, float y) { - return Math.abs(x - pixelX) <= pixelWidth * 0.5f - && Math.abs(y - pixelY) <= pixelHeight * 0.5f; + Bounds current = bounds; + return Math.abs(x - current.x) <= current.width * 0.5f + && Math.abs(y - current.y) <= current.height * 0.5f; + } + + void copyBoundsTo(BoundsSnapshot target) { + Bounds current = bounds; + target.x = current.x; + target.y = current.y; + target.size = current.size; + target.width = current.width; + target.height = current.height; } void sync(int width, int height, float scale, boolean pressed) { float shortSide = Math.min(width, height); float scaledShortSide = shortSide * scale; - pixelSize = Math.max(scaledShortSide * size, 1f); - pixelWidth = pixelSize * aspect; - pixelHeight = pixelSize; - pixelX = positionX * width + shortOffsetX * scaledShortSide; - pixelY = positionY * height + shortOffsetY * scaledShortSide; + float pixelSize = Math.max(scaledShortSide * size, 1f); + float pixelWidth = pixelSize * aspect; + float pixelHeight = pixelSize; + float pixelX = positionX * width + shortOffsetX * scaledShortSide; + float pixelY = positionY * height + shortOffsetY * scaledShortSide; if (pixelWidth < width) { pixelX = FastMath.clamp(pixelX, pixelWidth * 0.5f, width - pixelWidth * 0.5f); } else { @@ -562,6 +568,11 @@ void sync(int width, int height, float scale, boolean pressed) { lastNodeY = pixelY; nodePositionSynced = true; } + Bounds current = bounds; + if (current.x != pixelX || current.y != pixelY || current.size != pixelSize + || current.width != pixelWidth || current.height != pixelHeight) { + publishBounds(pixelX, pixelY, pixelSize, pixelWidth, pixelHeight); + } } synchronized void clearVisuals() { @@ -573,6 +584,7 @@ synchronized void clearVisuals() { nub = null; icon = null; text = null; + publishBounds(0f, 0f, 0f, 0f, 0f); clearSyncState(); } @@ -593,6 +605,38 @@ private void clearSyncState() { nodePositionSynced = false; } + private void publishBounds(float x, float y, float size, float width, float height) { + bounds = x == 0f && y == 0f && size == 0f && width == 0f && height == 0f + ? Bounds.EMPTY + : new Bounds(x, y, size, width, height); + } + + private static final class Bounds { + static final Bounds EMPTY = new Bounds(0f, 0f, 0f, 0f, 0f); + + final float x; + final float y; + final float size; + final float width; + final float height; + + Bounds(float x, float y, float size, float width, float height) { + this.x = x; + this.y = y; + this.size = size; + this.width = width; + this.height = height; + } + } + + static final class BoundsSnapshot { + float x; + float y; + float size; + float width; + float height; + } + @Override public synchronized void write(JmeExporter ex) throws IOException { OutputCapsule capsule = ex.getCapsule(this); diff --git a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickTheme.java b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickTheme.java index 780b74be0e..5d83a14351 100644 --- a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickTheme.java +++ b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickTheme.java @@ -73,7 +73,7 @@ public VirtualJoystickTheme() { resetToDefault(); } - public final void resetToDefault() { + public synchronized final void resetToDefault() { textures.clear(); fontPath = DEFAULT_FONT; textures.put(TextureKey.BUTTON, "Common/VirtualJoystick/button_circle.png"); @@ -103,11 +103,11 @@ public void setFontPath(String fontPath) { markUpdateNeeded(); } - public String getTexture(TextureKey key) { + public synchronized String getTexture(TextureKey key) { return textures.get(key); } - public void setTexture(TextureKey key, String texturePath) { + public synchronized void setTexture(TextureKey key, String texturePath) { if (key == null) { throw new IllegalArgumentException("Texture key cannot be null."); } @@ -132,7 +132,7 @@ void clearUpdateNeeded() { } @Override - public void write(JmeExporter ex) throws IOException { + public synchronized void write(JmeExporter ex) throws IOException { OutputCapsule capsule = ex.getCapsule(this); capsule.write(fontPath, "fontPath", DEFAULT_FONT); String[] keys = new String[textures.size()]; @@ -145,7 +145,7 @@ public void write(JmeExporter ex) throws IOException { } @Override - public void read(JmeImporter im) throws IOException { + public synchronized void read(JmeImporter im) throws IOException { InputCapsule capsule = im.getCapsule(this); fontPath = capsule.readString("fontPath", DEFAULT_FONT); String[] keys = capsule.readStringArray("keys", null); diff --git a/jme3-ios/src/main/java/com/jme3/input/ios/IosJoyInput.java b/jme3-ios/src/main/java/com/jme3/input/ios/IosJoyInput.java index e858a2946a..b390bca48e 100644 --- a/jme3-ios/src/main/java/com/jme3/input/ios/IosJoyInput.java +++ b/jme3-ios/src/main/java/com/jme3/input/ios/IosJoyInput.java @@ -68,8 +68,9 @@ import static org.ngengine.libjglios.sdl3.SDL3.SDL_SetJoystickEventsEnabled; public final class IosJoyInput implements JoyInput { - private static IosJoyInput active; + private static volatile IosJoyInput active; private static final int POV_X_AXIS_ID = 0x4000; + private static final int POV_Y_AXIS_ID = 0x4001; private final Map joysticks = new HashMap<>(); private RawInputListener listener; private InputManager inputManager; @@ -78,7 +79,7 @@ public final class IosJoyInput implements JoyInput { private String virtualJoystickMode = AppSettings.VIRTUAL_JOYSTICK_AUTO_MINIMIZED; private boolean useJoysticks = true; private boolean keyboardSuppressedAutoJoystick; - private VirtualJoystick virtualJoystick; + private volatile VirtualJoystick virtualJoystick; public static void dispatchNativeEvent(int[] intData, float[] floatData) { IosJoyInput joyInput = active; @@ -119,8 +120,9 @@ public void initialize() { @Override public void update() { - if (virtualJoystick != null) { - virtualJoystick.dispatchEvents(listener); + VirtualJoystick joystick = virtualJoystick; + if (joystick != null) { + joystick.dispatchEvents(listener); } } @@ -195,15 +197,18 @@ public Joystick[] loadJoysticks(InputManager inputManager) { } private boolean onPointerDown(int pointerId, float x, float y, long time) { - return virtualJoystick != null && virtualJoystick.onPointerDown(pointerId, x, y, time); + VirtualJoystick joystick = virtualJoystick; + return joystick != null && joystick.onPointerDown(pointerId, x, y, time); } private boolean onPointerMove(int pointerId, float x, float y, long time) { - return virtualJoystick != null && virtualJoystick.onPointerMove(pointerId, x, y, time); + VirtualJoystick joystick = virtualJoystick; + return joystick != null && joystick.onPointerMove(pointerId, x, y, time); } private boolean onPointerUp(int pointerId, float x, float y, long time) { - return virtualJoystick != null && virtualJoystick.onPointerUp(pointerId, x, y, time); + VirtualJoystick joystick = virtualJoystick; + return joystick != null && joystick.onPointerUp(pointerId, x, y, time); } private void onKeyboardInput() { @@ -255,9 +260,11 @@ private void handleNativeEvent(int[] intData, float[] floatData) { return; } JoystickButton button = joystick.getButtonById(intData[2]); + boolean pressed = intData[3] != 0; if (button != null && listener != null) { - listener.onJoyButtonEvent(new JoyButtonEvent(button, intData[3] != 0)); + listener.onJoyButtonEvent(new JoyButtonEvent(button, pressed)); } + joystick.updateDpadPov(intData[2], pressed, listener); } } @@ -384,8 +391,9 @@ private static boolean isMinimizedMode(String mode) { private Joystick[] currentJoysticks() { List current = new ArrayList<>(joysticks.values()); - if (virtualJoystick != null) { - current.add(virtualJoystick); + VirtualJoystick joystick = virtualJoystick; + if (joystick != null) { + current.add(joystick); } return current.toArray(new Joystick[0]); } @@ -427,6 +435,13 @@ private static final class IosJoystick extends AbstractJoystick { private JoystickAxis xAxis; private JoystickAxis yAxis; private JoystickAxis povXAxis; + private JoystickAxis povYAxis; + private boolean dpadUp; + private boolean dpadDown; + private boolean dpadLeft; + private boolean dpadRight; + private float povXValue; + private float povYValue; IosJoystick(InputManager inputManager, JoyInput joyInput, int id, boolean gamepad, long gamepadHandle, long joystickHandle, String name, int axisCount, int buttonCount) { @@ -510,6 +525,9 @@ private void addPovAxes(InputManager inputManager) { JoystickAxis povX = new DefaultJoystickAxis(inputManager, this, POV_X_AXIS_ID, JoystickAxis.POV_X, JoystickAxis.POV_X, true, false, 0f); addAxis(POV_X_AXIS_ID, povX); + JoystickAxis povY = new DefaultJoystickAxis(inputManager, this, POV_Y_AXIS_ID, JoystickAxis.POV_Y, + JoystickAxis.POV_Y, true, false, 0f); + addAxis(POV_Y_AXIS_ID, povY); } JoystickAxis getAxisById(int id) { @@ -537,7 +555,7 @@ public JoystickAxis getPovXAxis() { @Override public JoystickAxis getPovYAxis() { - return null; + return povYAxis; } private void addAxis(int index, JoystickAxis axis) { @@ -553,11 +571,47 @@ private void addAxis(int index, JoystickAxis axis) { case POV_X_AXIS_ID: povXAxis = axis; break; + case POV_Y_AXIS_ID: + povYAxis = axis; + break; default: break; } } + private void updateDpadPov(int buttonId, boolean pressed, RawInputListener listener) { + if (!gamepad) { + return; + } + switch (buttonId) { + case SDL_GAMEPAD_BUTTON_DPAD_UP: + dpadUp = pressed; + break; + case SDL_GAMEPAD_BUTTON_DPAD_DOWN: + dpadDown = pressed; + break; + case SDL_GAMEPAD_BUTTON_DPAD_LEFT: + dpadLeft = pressed; + break; + case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: + dpadRight = pressed; + break; + default: + return; + } + + float nextPovX = (dpadRight ? 1f : 0f) + (dpadLeft ? -1f : 0f); + float nextPovY = (dpadUp ? 1f : 0f) + (dpadDown ? -1f : 0f); + if (listener != null && povXAxis != null && povXValue != nextPovX) { + listener.onJoyAxisEvent(new JoyAxisEvent(povXAxis, nextPovX, nextPovX)); + } + if (listener != null && povYAxis != null && povYValue != nextPovY) { + listener.onJoyAxisEvent(new JoyAxisEvent(povYAxis, nextPovY, nextPovY)); + } + povXValue = nextPovX; + povYValue = nextPovY; + } + protected void addButton(JoystickButton button) { buttons.put(button.getButtonId(), button); super.addButton(button); diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlJoystickInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlJoystickInput.java index 1aaf1335ee..7f7ef84d1f 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlJoystickInput.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlJoystickInput.java @@ -42,7 +42,7 @@ public class SdlJoystickInput implements JoyInput { private float globalJitterThreshold; private boolean loadGamepads; private boolean loadRaw; - private VirtualJoystick virtualJoystick; + private volatile VirtualJoystick virtualJoystick; private RawInputListener listener; @@ -274,21 +274,25 @@ public Joystick[] loadJoysticks(InputManager inputManager) { @Override public void update() { handleInputEvents(); - if (virtualJoystick != null) { - virtualJoystick.dispatchEvents(listener); + VirtualJoystick joystick = virtualJoystick; + if (joystick != null) { + joystick.dispatchEvents(listener); } } public boolean onPointerDown(int pointerId, float x, float y, long time) { - return virtualJoystick != null && virtualJoystick.onPointerDown(pointerId, x, y, time); + VirtualJoystick joystick = virtualJoystick; + return joystick != null && joystick.onPointerDown(pointerId, x, y, time); } public boolean onPointerMove(int pointerId, float x, float y, long time) { - return virtualJoystick != null && virtualJoystick.onPointerMove(pointerId, x, y, time); + VirtualJoystick joystick = virtualJoystick; + return joystick != null && joystick.onPointerMove(pointerId, x, y, time); } public boolean onPointerUp(int pointerId, float x, float y, long time) { - return virtualJoystick != null && virtualJoystick.onPointerUp(pointerId, x, y, time); + VirtualJoystick joystick = virtualJoystick; + return joystick != null && joystick.onPointerUp(pointerId, x, y, time); } public void onSDLEvent(SDL_Event evt) { @@ -407,8 +411,9 @@ private void handleInputEvents() { private Joystick[] currentJoysticks() { List current = new ArrayList<>(joysticks.values()); - if (virtualJoystick != null) { - current.add(virtualJoystick); + VirtualJoystick joystick = virtualJoystick; + if (joystick != null) { + current.add(joystick); } return current.toArray(new Joystick[0]); } From 91e4d2d01526bf2333cea5131ed63aa90f799e28 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Mon, 25 May 2026 15:20:43 +0200 Subject: [PATCH 6/7] fixes autoshow --- .../com/jme3/input/android/AndroidJoyInput.java | 4 ++-- .../java/jme3test/ios/IosTestChooserLauncher.java | 14 ++++++++++++++ .../main/java/com/jme3/input/ios/IosJoyInput.java | 4 ++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java b/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java index a0e1122032..0bfa413f7b 100644 --- a/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java +++ b/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java @@ -315,8 +315,8 @@ private void updateVirtualJoystickAutoVisibility() { && !physicalJoystickAvailable && !keyboardSuppressedAutoJoystick); virtualJoystick.setEnabled(active); - if (active && isMinimizedMode(virtualJoystickMode)) { - virtualJoystick.setShown(false); + if (active) { + virtualJoystick.setShown(!isMinimizedMode(virtualJoystickMode)); } } diff --git a/jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooserLauncher.java b/jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooserLauncher.java index e85aa77450..241b42e2b2 100644 --- a/jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooserLauncher.java +++ b/jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooserLauncher.java @@ -218,9 +218,23 @@ private static void configureIosSettings(Application application) { AppSettings settings = new AppSettings(true); settings.setUseJoysticks(true); settings.setOnDeviceJoystickRumble(true); + invokeConfigureSettings(application, settings); application.setSettings(settings); } + private static void invokeConfigureSettings(Application application, AppSettings settings) { + try { + java.lang.reflect.Method method = application.getClass().getMethod("configureSettings", AppSettings.class); + Object target = java.lang.reflect.Modifier.isStatic(method.getModifiers()) ? null : application; + method.invoke(target, settings); + } catch (NoSuchMethodException ignored) { + // Most examples rely on default settings. + } catch (IllegalAccessException | InvocationTargetException exception) { + throw new IllegalStateException("Could not configure iOS settings for " + + application.getClass().getName(), exception); + } + } + private static Object invokeIfPresent(Object target, String name, Class[] parameterTypes, Object... args) { if (target == null) { return MissingMethod.INSTANCE; diff --git a/jme3-ios/src/main/java/com/jme3/input/ios/IosJoyInput.java b/jme3-ios/src/main/java/com/jme3/input/ios/IosJoyInput.java index b390bca48e..3c21528343 100644 --- a/jme3-ios/src/main/java/com/jme3/input/ios/IosJoyInput.java +++ b/jme3-ios/src/main/java/com/jme3/input/ios/IosJoyInput.java @@ -369,8 +369,8 @@ private void updateVirtualJoystickAutoVisibility() { && joysticks.isEmpty() && !keyboardSuppressedAutoJoystick); virtualJoystick.setEnabled(active); - if (active && isMinimizedMode(virtualJoystickMode)) { - virtualJoystick.setShown(false); + if (active) { + virtualJoystick.setShown(!isMinimizedMode(virtualJoystickMode)); } } From d6dbe256f48ee5642056515b1fe2d54e1db6b3a2 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Mon, 25 May 2026 22:15:12 +0200 Subject: [PATCH 7/7] improved layouts --- .../jme3/input/android/AndroidJoyInput.java | 37 ++-- .../main/java/com/jme3/input/FlyByCamera.java | 139 ++++++++------ .../java/com/jme3/input/InputManager.java | 14 ++ .../jme3/input/virtual/VirtualJoystick.java | 181 +++++++++--------- .../virtual/VirtualJoystickDynamicLayout.java | 114 +++++++++++ .../input/virtual/VirtualJoystickLayout.java | 139 ++++---------- .../input/virtual/VirtualJoystickTheme.java | 2 - .../virtual/VirtualJoystickXboxLayout.java | 96 ++++++++++ .../java/com/jme3/system/AppSettings.java | 98 ++++++---- .../java/jme3test/input/TestJoystick.java | 19 +- .../jme3test/input/TestVirtualJoystick.java | 139 +++++--------- .../java/jme3test/ios/IosTestChooser.java | 13 +- .../jme3test/ios/IosTestChooserLauncher.java | 6 +- .../java/com/jme3/input/ios/IosJoyInput.java | 53 ++--- .../jme3/input/lwjgl/SdlJoystickInput.java | 35 +--- 15 files changed, 600 insertions(+), 485 deletions(-) create mode 100644 jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickDynamicLayout.java create mode 100644 jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickXboxLayout.java diff --git a/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java b/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java index 0bfa413f7b..ad6c1388ba 100644 --- a/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java +++ b/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java @@ -92,7 +92,8 @@ public class AndroidJoyInput implements JoyInput { private ConcurrentLinkedQueue eventQueue = new ConcurrentLinkedQueue<>(); private AndroidSensorJoyInput sensorJoyInput; private boolean onDeviceJoystickRumble = false; - private String virtualJoystickMode = AppSettings.VIRTUAL_JOYSTICK_AUTO_MINIMIZED; + private String virtualJoystickMode = AppSettings.VIRTUAL_JOYSTICK_AUTO; + private String virtualJoystickDefaultLayout = AppSettings.VIRTUAL_JOYSTICK_LAYOUT_DYNAMIC; private boolean useJoysticks = true; private boolean useAndroidSensorJoystick = false; private boolean physicalJoystickAvailable = false; @@ -114,6 +115,7 @@ public void setView(GLSurfaceView view) { public void loadSettings(AppSettings settings) { onDeviceJoystickRumble = settings.isOnDeviceJoystickRumble(); virtualJoystickMode = settings.getVirtualJoystickMode(); + virtualJoystickDefaultLayout = settings.getVirtualJoystickDefaultLayout(); useJoysticks = settings.useJoysticks(); useAndroidSensorJoystick = settings.useAndroidSensorJoystick(); } @@ -213,6 +215,8 @@ public Joystick[] loadJoysticks(InputManager inputManager) { physicalJoystickAvailable = false; if (shouldCreateVirtualJoystick()) { virtualJoystick = new VirtualJoystick(inputManager, this, joystickList.size()); + virtualJoystick.setLayout(VirtualJoystick.createLayout(virtualJoystickDefaultLayout)); + virtualJoystick.setEnabled(false); updateVirtualJoystickAutoVisibility(); joystickList.add(virtualJoystick); } else { @@ -260,7 +264,7 @@ public boolean onTouch(MotionEvent event) { } public void onKeyboardInput() { - if (isAutoMode(virtualJoystickMode)) { + if (AppSettings.VIRTUAL_JOYSTICK_AUTO.equals(virtualJoystickMode)) { keyboardSuppressedAutoJoystick = true; updateVirtualJoystickAutoVisibility(); } @@ -284,6 +288,7 @@ public void update() { } } if (virtualJoystick != null) { + updateVirtualJoystickAutoVisibility(); virtualJoystick.dispatchEvents(listener); } @@ -310,29 +315,15 @@ private void updateVirtualJoystickAutoVisibility() { if (virtualJoystick == null) { return; } - boolean active = isEnabledMode(virtualJoystickMode) - || (isAutoMode(virtualJoystickMode) + boolean wasEnabled = virtualJoystick.isEnabled(); + boolean active = AppSettings.VIRTUAL_JOYSTICK_ENABLED.equals(virtualJoystickMode) + || (AppSettings.VIRTUAL_JOYSTICK_AUTO.equals(virtualJoystickMode) && !physicalJoystickAvailable - && !keyboardSuppressedAutoJoystick); - virtualJoystick.setEnabled(active); - if (active) { - virtualJoystick.setShown(!isMinimizedMode(virtualJoystickMode)); + && !keyboardSuppressedAutoJoystick + && virtualJoystick.hasInputBindings()); + if (wasEnabled != active) { + virtualJoystick.setEnabled(active); } } - private static boolean isEnabledMode(String mode) { - return AppSettings.VIRTUAL_JOYSTICK_ENABLED.equals(mode) - || AppSettings.VIRTUAL_JOYSTICK_ENABLED_MINIMIZED.equals(mode); - } - - private static boolean isAutoMode(String mode) { - return AppSettings.VIRTUAL_JOYSTICK_AUTO.equals(mode) - || AppSettings.VIRTUAL_JOYSTICK_AUTO_MINIMIZED.equals(mode); - } - - private static boolean isMinimizedMode(String mode) { - return AppSettings.VIRTUAL_JOYSTICK_ENABLED_MINIMIZED.equals(mode) - || AppSettings.VIRTUAL_JOYSTICK_AUTO_MINIMIZED.equals(mode); - } - } diff --git a/jme3-core/src/main/java/com/jme3/input/FlyByCamera.java b/jme3-core/src/main/java/com/jme3/input/FlyByCamera.java index d0b54da294..f5d1076664 100644 --- a/jme3-core/src/main/java/com/jme3/input/FlyByCamera.java +++ b/jme3-core/src/main/java/com/jme3/input/FlyByCamera.java @@ -115,9 +115,10 @@ public class FlyByCamera implements AnalogListener, ActionListener, JoystickConn * drag-to-rotate mode flag */ protected boolean dragToRotate = false; - protected boolean canRotate = false; - protected boolean invertY = false; - protected InputManager inputManager; + protected boolean canRotate = false; + protected boolean invertY = false; + protected InputManager inputManager; + private boolean inputMappingsRegistered; // Reusable temporary objects to reduce allocations during updates private final Matrix3f tempMat = new Matrix3f(); @@ -213,14 +214,21 @@ public float getZoomSpeed() { * * @param enable true to enable, false to disable */ - public void setEnabled(boolean enable) { - if (enabled && !enable) { - if (inputManager != null && (!dragToRotate || (dragToRotate && canRotate))) { - inputManager.setCursorVisible(true); - } - } - enabled = enable; - } + public void setEnabled(boolean enable) { + if (enabled == enable) { + return; + } + if (enabled && !enable) { + if (inputManager != null && (!dragToRotate || (dragToRotate && canRotate))) { + inputManager.setCursorVisible(true); + } + unregisterInputMappings(); + } + enabled = enable; + if (enabled) { + registerInputMappings(); + } + } /** * Checks whether this camera controller is currently enabled. @@ -253,12 +261,12 @@ public boolean isDragToRotate() { * * @param dragToRotate true to enable, false to disable */ - public void setDragToRotate(boolean dragToRotate) { - this.dragToRotate = dragToRotate; - if (inputManager != null) { - inputManager.setCursorVisible(dragToRotate); - } - } + public void setDragToRotate(boolean dragToRotate) { + this.dragToRotate = dragToRotate; + if (inputManager != null) { + inputManager.setCursorVisible(dragToRotate || !isEnabled()); + } + } /** * Registers this controller to receive input events from the specified @@ -267,12 +275,23 @@ public void setDragToRotate(boolean dragToRotate) { * * @param inputManager The InputManager instance to register with (must not be null). */ - public void registerWithInput(InputManager inputManager) { - this.inputManager = inputManager; - - // Mouse and Keyboard Mappings for Rotation - inputManager.addMapping(CameraInput.FLYCAM_LEFT, new MouseAxisTrigger(MouseInput.AXIS_X, true), - new KeyTrigger(KeyInput.KEY_LEFT)); + public void registerWithInput(InputManager inputManager) { + this.inputManager = inputManager; + if (enabled) { + registerInputMappings(); + } else { + inputManager.setCursorVisible(true); + } + inputManager.addJoystickConnectionListener(this); + } + + private void registerInputMappings() { + if (inputManager == null || inputMappingsRegistered) { + return; + } + // Mouse and Keyboard Mappings for Rotation + inputManager.addMapping(CameraInput.FLYCAM_LEFT, new MouseAxisTrigger(MouseInput.AXIS_X, true), + new KeyTrigger(KeyInput.KEY_LEFT)); inputManager.addMapping(CameraInput.FLYCAM_RIGHT, new MouseAxisTrigger(MouseInput.AXIS_X, false), new KeyTrigger(KeyInput.KEY_RIGHT)); @@ -301,12 +320,12 @@ public void registerWithInput(InputManager inputManager) { Joystick[] joysticks = inputManager.getJoysticks(); if (joysticks != null && joysticks.length > 0) { - for (Joystick j : joysticks) { - mapJoystick(j); - } - } - inputManager.addJoystickConnectionListener(this); - } + for (Joystick j : joysticks) { + mapJoystick(j); + } + } + inputMappingsRegistered = true; + } /** * Configures joystick input mappings for the camera controller. This method @@ -331,19 +350,14 @@ protected void mapJoystick(Joystick joystick) { rightXAxis.assignAxis(FLYCAM_JOYSTICK_RIGHT, FLYCAM_JOYSTICK_LEFT); } - JoystickButton backButton = joystick.getButton(JoystickButton.BUTTON_XBOX_BACK); - if (backButton != null) { - // Let the standard select button be the y invert toggle - backButton.assignButton(CameraInput.FLYCAM_INVERTY); - } - } + } - @Override - public void onConnected(Joystick joystick) { - if (inputManager != null) { - mapJoystick(joystick); - } - } + @Override + public void onConnected(Joystick joystick) { + if (inputManager != null && inputMappingsRegistered) { + mapJoystick(joystick); + } + } @Override public void onDisconnected(Joystick joystick) { @@ -352,25 +366,30 @@ public void onDisconnected(Joystick joystick) { /** * Unregisters this controller from its currently associated {@link InputManager}. */ - public void unregisterInput() { - if (inputManager == null) { - return; - } - - for (String s : mappings) { - if (inputManager.hasMapping(s)) { - inputManager.deleteMapping(s); - } - } - - inputManager.removeListener(this); - inputManager.removeJoystickConnectionListener(this); - inputManager.setCursorVisible(!dragToRotate); - - // Joysticks cannot be "unassigned" in the same way, but mappings are removed with listener. - // Joystick-specific mapping might persist but won't trigger this listener. - inputManager = null; // Clear reference - } + public void unregisterInput() { + if (inputManager == null) { + return; + } + + unregisterInputMappings(); + inputManager.removeJoystickConnectionListener(this); + inputManager.setCursorVisible(!dragToRotate); + inputManager = null; // Clear reference + } + + private void unregisterInputMappings() { + if (inputManager == null || !inputMappingsRegistered) { + return; + } + for (String s : mappings) { + if (inputManager.hasMapping(s)) { + inputManager.deleteMapping(s); + } + } + + inputManager.removeListener(this); + inputMappingsRegistered = false; + } /** * Rotates the camera by the specified amount around the given axis. diff --git a/jme3-core/src/main/java/com/jme3/input/InputManager.java b/jme3-core/src/main/java/com/jme3/input/InputManager.java index b45433d1d9..7928f33ffc 100644 --- a/jme3-core/src/main/java/com/jme3/input/InputManager.java +++ b/jme3-core/src/main/java/com/jme3/input/InputManager.java @@ -606,6 +606,20 @@ public boolean hasMapping(String mappingName) { return mappings.containsKey(mappingName); } + + + /** + * Returns true if at least one mapping is registered for the specified + * trigger hash. + * + * @param triggerHash hash returned by {@link Trigger#triggerHashCode()} + * @return true if the trigger hash is registered to at least one mapping + */ + public boolean hasTriggerMapping(int triggerHash) { + ArrayList maps = bindings.get(triggerHash); + return maps != null && !maps.isEmpty(); + } + /** * Deletes a mapping from receiving trigger events. * diff --git a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java index 48df1ba168..e364ef9bd7 100644 --- a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java +++ b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java @@ -41,7 +41,13 @@ import com.jme3.input.JoyInput; import com.jme3.input.JoystickAxis; import com.jme3.input.JoystickButton; +import com.jme3.input.MouseInput; import com.jme3.input.RawInputListener; +import com.jme3.input.TouchInput; +import com.jme3.input.controls.JoyAxisTrigger; +import com.jme3.input.controls.JoyButtonTrigger; +import com.jme3.input.controls.MouseAxisTrigger; +import com.jme3.input.controls.TouchTrigger; import com.jme3.input.event.InputEvent; import com.jme3.input.event.JoyAxisEvent; import com.jme3.input.event.JoyButtonEvent; @@ -49,6 +55,7 @@ import com.jme3.input.virtual.VirtualJoystickTheme.TextureKey; import com.jme3.math.FastMath; import com.jme3.scene.Node; +import com.jme3.system.AppSettings; import com.jme3.system.JmeSystem; import com.jme3.ui.Picture; import java.util.ArrayDeque; @@ -67,7 +74,6 @@ public class VirtualJoystick extends AbstractJoystick { private static final int AXIS_LEFT_TRIGGER = 4; private static final int AXIS_RIGHT_TRIGGER = 5; private static final int AXIS_POV_X_ID = 6; - private static final int AXIS_POV_Y_ID = 7; private static final String ROOT_NAME = "Virtual Joystick"; @@ -76,7 +82,7 @@ public class VirtualJoystick extends AbstractJoystick { private final Map captures = new HashMap<>(); private ArrayDeque events = new ArrayDeque<>(); private ArrayDeque readyEvents = new ArrayDeque<>(); - private final float[] axisValues = new float[8]; + private final float[] axisValues = new float[7]; private final boolean[] buttonValues = new boolean[16]; private final Object inputLock = new Object(); private final Element.BoundsSnapshot inputBounds = new Element.BoundsSnapshot(); @@ -84,13 +90,11 @@ public class VirtualJoystick extends AbstractJoystick { private JoystickAxis xAxis; private JoystickAxis yAxis; private JoystickAxis povXAxis; - private JoystickAxis povYAxis; private volatile boolean enabled = true; - private volatile boolean shown = true; private volatile int buttonStateMask; private volatile boolean hasEvents; private volatile VirtualJoystickTheme theme = new VirtualJoystickTheme(); - private volatile VirtualJoystickLayout layout = new VirtualJoystickLayout(); + private volatile VirtualJoystickLayout layout = new VirtualJoystickDynamicLayout(true); private volatile int visualWidth; private volatile int visualHeight; private Node visualRoot; @@ -127,19 +131,6 @@ public void setEnabled(boolean enabled) { } } - public boolean isShown() { - return shown; - } - - public void setShown(boolean shown) { - synchronized (inputLock) { - this.shown = shown; - if (!shown) { - releaseAllLocked(0L); - } - } - } - public VirtualJoystickTheme getTheme() { return theme; } @@ -156,11 +147,18 @@ public VirtualJoystickLayout getLayout() { public void setLayout(VirtualJoystickLayout layout) { synchronized (inputLock) { releaseAllLocked(0L); - this.layout = layout == null ? new VirtualJoystickLayout() : layout; + this.layout = layout == null ? new VirtualJoystickDynamicLayout(true) : layout; this.layout.markUpdateNeeded(); } } + public static VirtualJoystickLayout createLayout(String layout) { + if (AppSettings.VIRTUAL_JOYSTICK_LAYOUT_XBOX.equalsIgnoreCase(layout)) { + return new VirtualJoystickXboxLayout(); + } + return new VirtualJoystickDynamicLayout(true); + } + @Override public void rumble(float amountHigh, float amountLow, float duration) { if (JmeSystem.isDeviceRumbleSupported()) { @@ -173,16 +171,6 @@ public void stopRumble() { JmeSystem.stopRumble(); } - /** - * Restores the default Xbox-like layout. - */ - public void reset() { - synchronized (inputLock) { - layout.resetToDefault(); - releaseAllLocked(0L); - } - } - /** * Processes a pointer-down event. * @@ -195,19 +183,9 @@ public boolean onPointerDown(int pointerId, float x, float y, long time) { return existingCapture != null; } - Element toggleElement = layout.getToggleElement(); - if (toggleElement != null && toggleElement.contains(x, y)) { - captures.put(pointerId, new Capture(toggleElement, false, true)); - return true; - } - - if (!shown) { - return false; - } - for (Element element : layout.getAxisElements()) { if (element.visible && element.contains(x, y)) { - captures.put(pointerId, new Capture(element, true, false)); + captures.put(pointerId, new Capture(element, true)); updateAxisCapture(element, x, y, time); return true; } @@ -215,7 +193,7 @@ public boolean onPointerDown(int pointerId, float x, float y, long time) { for (Element element : layout.getButtons()) { if (element.visible && element.contains(x, y)) { - captures.put(pointerId, new Capture(element, false, false)); + captures.put(pointerId, new Capture(element, false)); if (isToggleButton(element.id)) { pressButton(element.id, !isButtonPressed(element.id), time); } else { @@ -240,9 +218,6 @@ public boolean onPointerMove(int pointerId, float x, float y, long time) { if (capture == null) { return false; } - if (capture.toggle) { - return true; - } if (capture.axis) { updateAxisCapture(capture.element, x, y, time); } @@ -261,11 +236,6 @@ public boolean onPointerUp(int pointerId, float x, float y, long time) { if (capture == null) { return false; } - if (capture.toggle) { - shown = !shown; - releaseAllLocked(time); - return true; - } if (capture.axis) { centerAxisCapture(capture.element, time); } else if (!isToggleButton(capture.element.id)) { @@ -276,7 +246,7 @@ public boolean onPointerUp(int pointerId, float x, float y, long time) { } /** - * Releases all active pointer captures without activating toggle buttons. + * Releases all active pointer captures. * * @return true if at least one pointer was captured */ @@ -339,11 +309,7 @@ public void updateVisuals(Node parent, AssetManager assetManager, int width, int if (visualRoot == null) { visualRoot = new Node(ROOT_NAME); } - if (visualParent != parent) { - visualRoot.removeFromParent(); - parent.attachChild(visualRoot); - visualParent = parent; - } + attachVisualRootOnTop(parent); if (width != visualWidth || height != visualHeight) { synchronized (inputLock) { if (width != visualWidth || height != visualHeight) { @@ -363,6 +329,7 @@ public void updateVisuals(Node parent, AssetManager assetManager, int width, int VirtualJoystickTheme currentTheme = theme; VirtualJoystickLayout currentLayout = layout; + currentLayout.update(this); boolean themeUpdateNeeded = currentTheme.isUpdateNeeded(); if (themeUpdateNeeded || currentLayout.isUpdateNeeded()) { clearVisuals(currentLayout); @@ -378,20 +345,6 @@ public void updateVisuals(Node parent, AssetManager assetManager, int width, int } float scale = currentLayout.getScale(); - syncElement(currentLayout.getToggleElement(), assetManager, width, height, scale); - if (!shown) { - for (Element element : currentLayout.getButtons()) { - if (element.node != null && element.node.getParent() != null) { - element.node.removeFromParent(); - } - } - for (Element element : currentLayout.getAxisElements()) { - if (element.node != null && element.node.getParent() != null) { - element.node.removeFromParent(); - } - } - return; - } for (Element element : currentLayout.getButtons()) { syncElement(element, assetManager, width, height, scale); @@ -401,6 +354,22 @@ public void updateVisuals(Node parent, AssetManager assetManager, int width, int } } + private void attachVisualRootOnTop(Node parent) { + if (visualParent != parent || visualRoot.getParent() != parent) { + visualRoot.removeFromParent(); + parent.attachChild(visualRoot); + visualParent = parent; + return; + } + + int childIndex = parent.getChildIndex(visualRoot); + int topIndex = parent.getQuantity() - 1; + if (childIndex >= 0 && childIndex < topIndex) { + visualRoot.removeFromParent(); + parent.attachChild(visualRoot); + } + } + @Override public JoystickAxis getXAxis() { return xAxis; @@ -418,7 +387,58 @@ public JoystickAxis getPovXAxis() { @Override public JoystickAxis getPovYAxis() { - return povYAxis; + return null; + } + + public boolean hasInputBindings() { + for (JoystickAxis axis : getAxes()) { + if (isAxisBound(axis.getLogicalId())) { + return true; + } + } + for (JoystickButton button : getButtons()) { + if (isButtonBound(button.getLogicalId())) { + return true; + } + } + return false; + } + + public boolean isAxisBound(String logicalId) { + JoystickAxis axis = axesByLogicalId.get(logicalId); + if (axis == null) { + return false; + } + InputManager manager = getInputManager(); + return manager != null + && (manager.hasTriggerMapping(JoyAxisTrigger.joyAxisHash(getJoyId(), axis.getAxisId(), false)) + || manager.hasTriggerMapping(JoyAxisTrigger.joyAxisHash(getJoyId(), axis.getAxisId(), true))); + } + + public boolean isButtonBound(String logicalId) { + JoystickButton button = buttonsByLogicalId.get(logicalId); + if (button == null) { + return false; + } + InputManager manager = getInputManager(); + return manager != null + && manager.hasTriggerMapping(JoyButtonTrigger.joyButtonHash(getJoyId(), button.getButtonId())); + } + + public boolean hasPointerLookBindings() { + InputManager manager = getInputManager(); + if (manager == null) { + return false; + } + boolean mouseLook = hasMouseAxisBinding(manager, MouseInput.AXIS_X) + && hasMouseAxisBinding(manager, MouseInput.AXIS_Y); + boolean touchLook = manager.hasTriggerMapping(TouchTrigger.touchHash(TouchInput.ALL)); + return touchLook || (mouseLook && manager.isSimulateMouse()); + } + + private boolean hasMouseAxisBinding(InputManager manager, int axis) { + return manager.hasTriggerMapping(MouseAxisTrigger.mouseAxisHash(axis, false)) + || manager.hasTriggerMapping(MouseAxisTrigger.mouseAxisHash(axis, true)); } private void addAxes() { @@ -429,7 +449,6 @@ private void addAxes() { addAxis(AXIS_LEFT_TRIGGER, "LEFT TRIGGER", JoystickAxis.AXIS_XBOX_LEFT_TRIGGER); addAxis(AXIS_RIGHT_TRIGGER, "RIGHT TRIGGER", JoystickAxis.AXIS_XBOX_RIGHT_TRIGGER); povXAxis = addAxis(AXIS_POV_X_ID, JoystickAxis.POV_X, JoystickAxis.POV_X); - povYAxis = addAxis(AXIS_POV_Y_ID, JoystickAxis.POV_Y, JoystickAxis.POV_Y); } private JoystickAxis addAxis(int id, String name, String logicalId) { @@ -518,7 +537,7 @@ private void pressButton(String logicalId, boolean pressed, long time) { } else if (JoystickButton.BUTTON_XBOX_RT.equals(logicalId)) { setAxisValue(JoystickAxis.AXIS_XBOX_RIGHT_TRIGGER, pressed ? 1f : 0f, time); } else if (isDpad(logicalId)) { - updatePovAxes(time); + updatePovXAxis(time); } } @@ -539,15 +558,8 @@ private boolean isDpad(String logicalId) { || JoystickButton.BUTTON_XBOX_DPAD_RIGHT.equals(logicalId); } - private void updatePovAxes(long time) { + private void updatePovXAxis(long time) { float x = 0f; - float y = 0f; - if (buttonValues[buttonsByLogicalId.get(JoystickButton.BUTTON_XBOX_DPAD_UP).getButtonId()]) { - y += 1f; - } - if (buttonValues[buttonsByLogicalId.get(JoystickButton.BUTTON_XBOX_DPAD_DOWN).getButtonId()]) { - y -= 1f; - } if (buttonValues[buttonsByLogicalId.get(JoystickButton.BUTTON_XBOX_DPAD_LEFT).getButtonId()]) { x -= 1f; } @@ -555,7 +567,6 @@ private void updatePovAxes(long time) { x += 1f; } setAxisValue(JoystickAxis.POV_X, x, time); - setAxisValue(JoystickAxis.POV_Y, y, time); } private void setAxisValue(String logicalId, float value, long time) { @@ -658,10 +669,6 @@ private void clearVisuals(VirtualJoystickLayout layout) { for (Element element : layout.getAxisElements()) { element.clearVisuals(); } - Element toggle = layout.getToggleElement(); - if (toggle != null) { - toggle.clearVisuals(); - } if (visualRoot != null) { visualRoot.detachAllChildren(); } @@ -670,12 +677,10 @@ private void clearVisuals(VirtualJoystickLayout layout) { private static final class Capture { final Element element; final boolean axis; - final boolean toggle; - Capture(Element element, boolean axis, boolean toggle) { + Capture(Element element, boolean axis) { this.element = element; this.axis = axis; - this.toggle = toggle; } } } diff --git a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickDynamicLayout.java b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickDynamicLayout.java new file mode 100644 index 0000000000..a8f972d0b5 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickDynamicLayout.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.input.virtual; + +import com.jme3.input.JoystickAxis; +import com.jme3.input.JoystickButton; + +/** + * Xbox-like layout that only shows controls currently bound in the input manager. + */ +public class VirtualJoystickDynamicLayout extends VirtualJoystickXboxLayout { + + private final boolean useTouchForRightAnalog; + private long visibilityMask = -1L; + + public VirtualJoystickDynamicLayout(boolean useTouchForRightAnalog) { + this.useTouchForRightAnalog = useTouchForRightAnalog; + } + + @Override + public void update(VirtualJoystick joystick) { + if (joystick == null) { + return; + } + + boolean leftTrigger = joystick.isButtonBound(JoystickButton.BUTTON_XBOX_LT) + || joystick.isAxisBound(JoystickAxis.AXIS_XBOX_LEFT_TRIGGER); + boolean rightTrigger = joystick.isButtonBound(JoystickButton.BUTTON_XBOX_RT) + || joystick.isAxisBound(JoystickAxis.AXIS_XBOX_RIGHT_TRIGGER); + + boolean leftStick = joystick.isAxisBound(JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_X) + || joystick.isAxisBound(JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_Y); + boolean rightStick = joystick.isAxisBound(JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_X) + || joystick.isAxisBound(JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_Y); + if (useTouchForRightAnalog && joystick.hasPointerLookBindings()) { + rightStick = false; + } + + long nextVisibilityMask = 0L; + nextVisibilityMask |= joystick.isButtonBound(JoystickButton.BUTTON_XBOX_A) ? 1L : 0L; + nextVisibilityMask |= joystick.isButtonBound(JoystickButton.BUTTON_XBOX_B) ? 1L << 1 : 0L; + nextVisibilityMask |= joystick.isButtonBound(JoystickButton.BUTTON_XBOX_X) ? 1L << 2 : 0L; + nextVisibilityMask |= joystick.isButtonBound(JoystickButton.BUTTON_XBOX_Y) ? 1L << 3 : 0L; + nextVisibilityMask |= leftTrigger ? 1L << 4 : 0L; + nextVisibilityMask |= rightTrigger ? 1L << 5 : 0L; + nextVisibilityMask |= joystick.isButtonBound(JoystickButton.BUTTON_XBOX_LB) ? 1L << 6 : 0L; + nextVisibilityMask |= joystick.isButtonBound(JoystickButton.BUTTON_XBOX_RB) ? 1L << 7 : 0L; + nextVisibilityMask |= joystick.isButtonBound(JoystickButton.BUTTON_XBOX_BACK) ? 1L << 8 : 0L; + nextVisibilityMask |= joystick.isButtonBound(JoystickButton.BUTTON_XBOX_START) ? 1L << 9 : 0L; + nextVisibilityMask |= joystick.isButtonBound(JoystickButton.BUTTON_XBOX_L3) ? 1L << 10 : 0L; + nextVisibilityMask |= joystick.isButtonBound(JoystickButton.BUTTON_XBOX_R3) ? 1L << 11 : 0L; + nextVisibilityMask |= joystick.isButtonBound(JoystickButton.BUTTON_XBOX_DPAD_UP) ? 1L << 12 : 0L; + nextVisibilityMask |= joystick.isButtonBound(JoystickButton.BUTTON_XBOX_DPAD_DOWN) ? 1L << 13 : 0L; + nextVisibilityMask |= joystick.isButtonBound(JoystickButton.BUTTON_XBOX_DPAD_LEFT) + || joystick.isAxisBound(JoystickAxis.POV_X) ? 1L << 14 : 0L; + nextVisibilityMask |= joystick.isButtonBound(JoystickButton.BUTTON_XBOX_DPAD_RIGHT) + || joystick.isAxisBound(JoystickAxis.POV_X) ? 1L << 15 : 0L; + nextVisibilityMask |= leftStick ? 1L << 16 : 0L; + nextVisibilityMask |= rightStick ? 1L << 17 : 0L; + + if (visibilityMask == nextVisibilityMask) { + return; + } + visibilityMask = nextVisibilityMask; + + setButtonVisible(JoystickButton.BUTTON_XBOX_A, (nextVisibilityMask & 1L) != 0L); + setButtonVisible(JoystickButton.BUTTON_XBOX_B, (nextVisibilityMask & (1L << 1)) != 0L); + setButtonVisible(JoystickButton.BUTTON_XBOX_X, (nextVisibilityMask & (1L << 2)) != 0L); + setButtonVisible(JoystickButton.BUTTON_XBOX_Y, (nextVisibilityMask & (1L << 3)) != 0L); + setButtonVisible(JoystickButton.BUTTON_XBOX_LT, (nextVisibilityMask & (1L << 4)) != 0L); + setButtonVisible(JoystickButton.BUTTON_XBOX_RT, (nextVisibilityMask & (1L << 5)) != 0L); + setButtonVisible(JoystickButton.BUTTON_XBOX_LB, (nextVisibilityMask & (1L << 6)) != 0L); + setButtonVisible(JoystickButton.BUTTON_XBOX_RB, (nextVisibilityMask & (1L << 7)) != 0L); + setButtonVisible(JoystickButton.BUTTON_XBOX_BACK, (nextVisibilityMask & (1L << 8)) != 0L); + setButtonVisible(JoystickButton.BUTTON_XBOX_START, (nextVisibilityMask & (1L << 9)) != 0L); + setButtonVisible(JoystickButton.BUTTON_XBOX_L3, (nextVisibilityMask & (1L << 10)) != 0L); + setButtonVisible(JoystickButton.BUTTON_XBOX_R3, (nextVisibilityMask & (1L << 11)) != 0L); + setButtonVisible(JoystickButton.BUTTON_XBOX_DPAD_UP, (nextVisibilityMask & (1L << 12)) != 0L); + setButtonVisible(JoystickButton.BUTTON_XBOX_DPAD_DOWN, (nextVisibilityMask & (1L << 13)) != 0L); + setButtonVisible(JoystickButton.BUTTON_XBOX_DPAD_LEFT, (nextVisibilityMask & (1L << 14)) != 0L); + setButtonVisible(JoystickButton.BUTTON_XBOX_DPAD_RIGHT, (nextVisibilityMask & (1L << 15)) != 0L); + setAxisVisible(JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_X, leftStick); + setAxisVisible(JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_X, rightStick); + } +} diff --git a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickLayout.java b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickLayout.java index 84f91708aa..9b84f62133 100644 --- a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickLayout.java +++ b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickLayout.java @@ -37,8 +37,6 @@ import com.jme3.export.OutputCapsule; import com.jme3.export.Savable; import com.jme3.font.BitmapText; -import com.jme3.input.JoystickAxis; -import com.jme3.input.JoystickButton; import com.jme3.input.virtual.VirtualJoystickTheme.TextureKey; import com.jme3.math.ColorRGBA; import com.jme3.math.FastMath; @@ -52,98 +50,31 @@ /** * Virtual joystick controls layout. */ -public class VirtualJoystickLayout implements Savable { +public abstract class VirtualJoystickLayout implements Savable { private final Map buttons = new LinkedHashMap<>(); private final Map axisElements = new LinkedHashMap<>(); - private volatile Element toggleElement; private volatile float scale = 1.15f; private transient volatile boolean updateNeeded = true; private transient volatile Element[] buttonSnapshot = new Element[0]; private transient volatile Element[] axisSnapshot = new Element[0]; - private transient boolean bulkUpdate; - - public VirtualJoystickLayout() { - resetToDefault(); + protected VirtualJoystickLayout() { } - public synchronized final void resetToDefault() { - buttons.clear(); - axisElements.clear(); - bulkUpdate = true; - - try { - float leftColumn = 0.135f; - float rightColumn = 0.865f; - float upperRow = 0.52f; - float lowerRow = 0.15f; - float faceButtonSize = 0.095f; - float faceButtonOffset = 0.106f; - float shoulderSize = 0.09f; - - addButtonElement(JoystickButton.BUTTON_XBOX_A, "", rightColumn, upperRow, 0f, -faceButtonOffset, faceButtonSize, - TextureKey.BUTTON, TextureKey.BUTTON_A_ICON); - addButtonElement(JoystickButton.BUTTON_XBOX_B, "", rightColumn, upperRow, faceButtonOffset, 0f, faceButtonSize, - TextureKey.BUTTON, TextureKey.BUTTON_B_ICON); - addButtonElement(JoystickButton.BUTTON_XBOX_X, "", rightColumn, upperRow, -faceButtonOffset, 0f, faceButtonSize, - TextureKey.BUTTON, TextureKey.BUTTON_X_ICON); - addButtonElement(JoystickButton.BUTTON_XBOX_Y, "", rightColumn, upperRow, 0f, faceButtonOffset, faceButtonSize, - TextureKey.BUTTON, TextureKey.BUTTON_Y_ICON); - - addButtonElement(JoystickButton.BUTTON_XBOX_LT, "LT", leftColumn, 0.94f, shoulderSize, 2f, - TextureKey.BUTTON_WIDE); - addButtonElement(JoystickButton.BUTTON_XBOX_RT, "RT", rightColumn, 0.94f, shoulderSize, 2f, - TextureKey.BUTTON_WIDE); - addButtonElement(JoystickButton.BUTTON_XBOX_LB, "LB", leftColumn, 0.79f, shoulderSize, 2f, - TextureKey.BUTTON_WIDE); - addButtonElement(JoystickButton.BUTTON_XBOX_RB, "RB", rightColumn, 0.79f, shoulderSize, 2f, - TextureKey.BUTTON_WIDE); - - addButtonElement(JoystickButton.BUTTON_XBOX_BACK, "", 0.44f, 0.06f, 0.078f, 2f, - TextureKey.BUTTON_WIDE, TextureKey.BUTTON_BACK_ICON); - addButtonElement(JoystickButton.BUTTON_XBOX_START, "", 0.56f, 0.06f, 0.078f, 2f, - TextureKey.BUTTON_WIDE, TextureKey.BUTTON_START_ICON); - addButtonElement(JoystickButton.BUTTON_XBOX_L3, "L3", leftColumn, upperRow, -0.145f, 0f, 0.065f, - TextureKey.BUTTON); - addButtonElement(JoystickButton.BUTTON_XBOX_R3, "R3", rightColumn, lowerRow, 0.145f, 0f, 0.065f, - TextureKey.BUTTON); - - addButtonElement(JoystickButton.BUTTON_XBOX_DPAD_UP, "", leftColumn, lowerRow, 0f, 0.064f, 0.085f, - TextureKey.DPAD_UP); - addButtonElement(JoystickButton.BUTTON_XBOX_DPAD_DOWN, "", leftColumn, lowerRow, 0f, -0.064f, 0.085f, - TextureKey.DPAD_DOWN); - addButtonElement(JoystickButton.BUTTON_XBOX_DPAD_LEFT, "", leftColumn, lowerRow, -0.064f, 0f, 0.085f, - TextureKey.DPAD_LEFT); - addButtonElement(JoystickButton.BUTTON_XBOX_DPAD_RIGHT, "", leftColumn, lowerRow, 0.064f, 0f, 0.085f, - TextureKey.DPAD_RIGHT); - - addAxisElement(JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_X, JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_Y, - "", leftColumn, upperRow, 0.235f, TextureKey.STICK_PAD, TextureKey.STICK_NUB); - addAxisElement(JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_X, JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_Y, - "", rightColumn, lowerRow, 0.235f, TextureKey.STICK_PAD, TextureKey.STICK_NUB); - setToggleElement(new Element("toggle", "", 0.5f, 0.94f, 0.055f, TextureKey.BUTTON) - .setIconTextureKey(TextureKey.TOGGLE_ICON)); - } finally { - bulkUpdate = false; - rebuildSnapshots(); - markUpdateNeeded(); - } - } - - public synchronized void addButtonElement(String logicalId, String label, float x, float y, float size, + protected final synchronized void addButtonElement(String logicalId, String label, float x, float y, float size, TextureKey textureKey) { buttons.put(logicalId, new Element(logicalId, label, x, y, size, textureKey)); layoutChanged(); } - public synchronized void addButtonElement(String logicalId, String label, float x, float y, float shortOffsetX, + protected final synchronized void addButtonElement(String logicalId, String label, float x, float y, float shortOffsetX, float shortOffsetY, float size, TextureKey textureKey) { buttons.put(logicalId, new Element(logicalId, label, x, y, size, textureKey) .setShortOffset(shortOffsetX, shortOffsetY)); layoutChanged(); } - public synchronized void addButtonElement(String logicalId, String label, float x, float y, float shortOffsetX, + protected final synchronized void addButtonElement(String logicalId, String label, float x, float y, float shortOffsetX, float shortOffsetY, float size, TextureKey textureKey, TextureKey iconTextureKey) { buttons.put(logicalId, new Element(logicalId, label, x, y, size, textureKey) .setShortOffset(shortOffsetX, shortOffsetY) @@ -151,13 +82,13 @@ public synchronized void addButtonElement(String logicalId, String label, float layoutChanged(); } - public synchronized void addButtonElement(String logicalId, String label, float x, float y, float size, float aspect, + protected final synchronized void addButtonElement(String logicalId, String label, float x, float y, float size, float aspect, TextureKey textureKey) { buttons.put(logicalId, new Element(logicalId, label, x, y, size, textureKey).setAspect(aspect)); layoutChanged(); } - public synchronized void addButtonElement(String logicalId, String label, float x, float y, float size, float aspect, + protected final synchronized void addButtonElement(String logicalId, String label, float x, float y, float size, float aspect, TextureKey textureKey, TextureKey iconTextureKey) { buttons.put(logicalId, new Element(logicalId, label, x, y, size, textureKey) .setAspect(aspect) @@ -165,7 +96,7 @@ public synchronized void addButtonElement(String logicalId, String label, float layoutChanged(); } - public synchronized void addAxisElement(String xAxisLogicalId, String yAxisLogicalId, String label, float x, float y, + protected final synchronized void addAxisElement(String xAxisLogicalId, String yAxisLogicalId, String label, float x, float y, float size, TextureKey textureKey, TextureKey nubTextureKey) { axisElements.put(xAxisLogicalId, new Element(xAxisLogicalId, label, x, y, size, textureKey) .setYAxisLogicalId(yAxisLogicalId) @@ -173,12 +104,7 @@ public synchronized void addAxisElement(String xAxisLogicalId, String yAxisLogic layoutChanged(); } - public synchronized void setToggleElement(Element toggleElement) { - this.toggleElement = toggleElement; - layoutChanged(); - } - - public synchronized void setButtonPosition(String logicalId, float x, float y) { + protected final synchronized void setButtonPosition(String logicalId, float x, float y) { element(buttons, logicalId).setPosition(x, y); markUpdateNeeded(); } @@ -188,8 +114,11 @@ public synchronized Vector2f getButtonPosition(String logicalId) { return new Vector2f(element.positionX, element.positionY); } - public synchronized void setButtonVisible(String logicalId, boolean visible) { + protected final synchronized void setButtonVisible(String logicalId, boolean visible) { Element element = element(buttons, logicalId); + if (element.visible == visible) { + return; + } element.visible = visible; markUpdateNeeded(); } @@ -198,7 +127,7 @@ public synchronized boolean isButtonVisible(String logicalId) { return element(buttons, logicalId).visible; } - public synchronized void setAxisPosition(String logicalId, float x, float y) { + protected final synchronized void setAxisPosition(String logicalId, float x, float y) { axisElement(logicalId).setPosition(x, y); markUpdateNeeded(); } @@ -208,8 +137,11 @@ public synchronized Vector2f getAxisPosition(String logicalId) { return new Vector2f(element.positionX, element.positionY); } - public synchronized void setAxisVisible(String logicalId, boolean visible) { + protected final synchronized void setAxisVisible(String logicalId, boolean visible) { Element element = axisElement(logicalId); + if (element.visible == visible) { + return; + } element.visible = visible; markUpdateNeeded(); } @@ -218,43 +150,43 @@ public synchronized boolean isAxisVisible(String logicalId) { return axisElement(logicalId).visible; } - public synchronized void setButtonSize(String logicalId, float size) { + protected final synchronized void setButtonSize(String logicalId, float size) { Element element = element(buttons, logicalId); element.size = FastMath.clamp(size, 0.01f, 1f); markUpdateNeeded(); } - public synchronized void setButtonTextureKey(String logicalId, TextureKey textureKey) { + protected final synchronized void setButtonTextureKey(String logicalId, TextureKey textureKey) { Element element = element(buttons, logicalId); element.textureKey = textureKey; markUpdateNeeded(); } - public synchronized void setButtonIconTextureKey(String logicalId, TextureKey iconTextureKey) { + protected final synchronized void setButtonIconTextureKey(String logicalId, TextureKey iconTextureKey) { Element element = element(buttons, logicalId); element.iconTextureKey = iconTextureKey; markUpdateNeeded(); } - public synchronized void setAxisSize(String logicalId, float size) { + protected final synchronized void setAxisSize(String logicalId, float size) { Element element = axisElement(logicalId); element.size = FastMath.clamp(size, 0.01f, 1f); markUpdateNeeded(); } - public synchronized void setAxisTextureKey(String logicalId, TextureKey textureKey) { + protected final synchronized void setAxisTextureKey(String logicalId, TextureKey textureKey) { Element element = axisElement(logicalId); element.textureKey = textureKey; markUpdateNeeded(); } - public synchronized void setAxisNubTextureKey(String logicalId, TextureKey nubTextureKey) { + protected final synchronized void setAxisNubTextureKey(String logicalId, TextureKey nubTextureKey) { Element element = axisElement(logicalId); element.nubTextureKey = nubTextureKey; markUpdateNeeded(); } - public synchronized void setScale(float scale) { + protected final synchronized void setScale(float scale) { this.scale = FastMath.clamp(scale, 0.25f, 3f); markUpdateNeeded(); } @@ -283,8 +215,7 @@ Element[] getAxisElements() { return axisSnapshot; } - public Element getToggleElement() { - return toggleElement; + public void update(VirtualJoystick joystick) { } public synchronized Element getButtonElement(String logicalId) { @@ -330,7 +261,6 @@ public synchronized void write(JmeExporter ex) throws IOException { capsule.write(scale, "scale", 1.15f); capsule.write(buttons.values().toArray(new Element[0]), "buttons", null); capsule.write(axisElements.values().toArray(new Element[0]), "axes", null); - capsule.write(toggleElement, "toggle", null); } @Override @@ -353,15 +283,12 @@ public synchronized void read(JmeImporter im) throws IOException { axisElements.put(element.id, element); } } - toggleElement = (Element) capsule.readSavable("toggle", null); rebuildSnapshots(); markUpdateNeeded(); } private void layoutChanged() { - if (!bulkUpdate) { - rebuildSnapshots(); - } + rebuildSnapshots(); markUpdateNeeded(); } @@ -426,7 +353,7 @@ public Element(String id, String label, float x, float y, float size, TextureKey setPosition(x, y); } - public synchronized Element setPosition(float x, float y) { + synchronized Element setPosition(float x, float y) { positionX = FastMath.clamp(x, 0f, 1f); positionY = FastMath.clamp(y, 0f, 1f); shortOffsetX = 0f; @@ -434,28 +361,28 @@ public synchronized Element setPosition(float x, float y) { return this; } - public synchronized Element setShortOffset(float x, float y) { + synchronized Element setShortOffset(float x, float y) { shortOffsetX = x; shortOffsetY = y; return this; } - public synchronized Element setAspect(float aspect) { + synchronized Element setAspect(float aspect) { this.aspect = aspect; return this; } - public synchronized Element setYAxisLogicalId(String yAxisLogicalId) { + synchronized Element setYAxisLogicalId(String yAxisLogicalId) { this.yAxisLogicalId = yAxisLogicalId; return this; } - public synchronized Element setNubTextureKey(TextureKey nubTextureKey) { + synchronized Element setNubTextureKey(TextureKey nubTextureKey) { this.nubTextureKey = nubTextureKey; return this; } - public synchronized Element setIconTextureKey(TextureKey iconTextureKey) { + synchronized Element setIconTextureKey(TextureKey iconTextureKey) { this.iconTextureKey = iconTextureKey; return this; } diff --git a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickTheme.java b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickTheme.java index 5d83a14351..4c16af3009 100644 --- a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickTheme.java +++ b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickTheme.java @@ -58,7 +58,6 @@ public enum TextureKey { BUTTON_START_ICON, STICK_PAD, STICK_NUB, - TOGGLE_ICON, DPAD_UP, DPAD_DOWN, DPAD_LEFT, @@ -86,7 +85,6 @@ public synchronized final void resetToDefault() { textures.put(TextureKey.BUTTON_START_ICON, "Common/VirtualJoystick/icon_star.png"); textures.put(TextureKey.STICK_PAD, "Common/VirtualJoystick/joystick_circle_pad_a.png"); textures.put(TextureKey.STICK_NUB, "Common/VirtualJoystick/joystick_circle_nub_a.png"); - textures.put(TextureKey.TOGGLE_ICON, "Common/VirtualJoystick/icon_dpad.png"); textures.put(TextureKey.DPAD_UP, "Common/VirtualJoystick/dpad_element_north.png"); textures.put(TextureKey.DPAD_DOWN, "Common/VirtualJoystick/dpad_element_south.png"); textures.put(TextureKey.DPAD_LEFT, "Common/VirtualJoystick/dpad_element_west.png"); diff --git a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickXboxLayout.java b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickXboxLayout.java new file mode 100644 index 0000000000..4b90d40ab4 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystickXboxLayout.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.input.virtual; + +import com.jme3.input.JoystickAxis; +import com.jme3.input.JoystickButton; +import com.jme3.input.virtual.VirtualJoystickTheme.TextureKey; + +/** + * Default Xbox-like virtual joystick layout. + */ +public class VirtualJoystickXboxLayout extends VirtualJoystickLayout { + + public VirtualJoystickXboxLayout() { + float leftStickColumn = 0.145f; + float dpadColumn = 0.255f; + float rightStickColumn = 0.745f; + float faceColumn = 0.855f; + float leftStickRow = 0.565f; + float faceRow = 0.535f; + float lowerRow = 0.285f; + float faceButtonSize = 0.086f; + float faceButtonOffset = 0.09f; + float shoulderSize = 0.09f; + + addButtonElement(JoystickButton.BUTTON_XBOX_A, "", faceColumn, faceRow, 0f, -faceButtonOffset, + faceButtonSize, TextureKey.BUTTON, TextureKey.BUTTON_A_ICON); + addButtonElement(JoystickButton.BUTTON_XBOX_B, "", faceColumn, faceRow, faceButtonOffset, 0f, + faceButtonSize, TextureKey.BUTTON, TextureKey.BUTTON_B_ICON); + addButtonElement(JoystickButton.BUTTON_XBOX_X, "", faceColumn, faceRow, -faceButtonOffset, 0f, + faceButtonSize, TextureKey.BUTTON, TextureKey.BUTTON_X_ICON); + addButtonElement(JoystickButton.BUTTON_XBOX_Y, "", faceColumn, faceRow, 0f, faceButtonOffset, + faceButtonSize, TextureKey.BUTTON, TextureKey.BUTTON_Y_ICON); + + addButtonElement(JoystickButton.BUTTON_XBOX_LT, "LT", leftStickColumn, 0.94f, shoulderSize, 2f, + TextureKey.BUTTON_WIDE); + addButtonElement(JoystickButton.BUTTON_XBOX_RT, "RT", faceColumn, 0.94f, shoulderSize, 2f, + TextureKey.BUTTON_WIDE); + addButtonElement(JoystickButton.BUTTON_XBOX_LB, "LB", leftStickColumn, 0.79f, shoulderSize, 2f, + TextureKey.BUTTON_WIDE); + addButtonElement(JoystickButton.BUTTON_XBOX_RB, "RB", faceColumn, 0.79f, shoulderSize, 2f, + TextureKey.BUTTON_WIDE); + + addButtonElement(JoystickButton.BUTTON_XBOX_BACK, "", 0.44f, 0.06f, 0.078f, 2f, + TextureKey.BUTTON_WIDE, TextureKey.BUTTON_BACK_ICON); + addButtonElement(JoystickButton.BUTTON_XBOX_START, "", 0.56f, 0.06f, 0.078f, 2f, + TextureKey.BUTTON_WIDE, TextureKey.BUTTON_START_ICON); + addButtonElement(JoystickButton.BUTTON_XBOX_L3, "L3", leftStickColumn, leftStickRow, -0.145f, -0.07f, 0.065f, + TextureKey.BUTTON); + addButtonElement(JoystickButton.BUTTON_XBOX_R3, "R3", rightStickColumn, lowerRow, 0.145f, -0.07f, 0.065f, + TextureKey.BUTTON); + + addButtonElement(JoystickButton.BUTTON_XBOX_DPAD_UP, "", dpadColumn, lowerRow, 0f, 0.064f, 0.085f, + TextureKey.DPAD_UP); + addButtonElement(JoystickButton.BUTTON_XBOX_DPAD_DOWN, "", dpadColumn, lowerRow, 0f, -0.064f, 0.085f, + TextureKey.DPAD_DOWN); + addButtonElement(JoystickButton.BUTTON_XBOX_DPAD_LEFT, "", dpadColumn, lowerRow, -0.064f, 0f, 0.085f, + TextureKey.DPAD_LEFT); + addButtonElement(JoystickButton.BUTTON_XBOX_DPAD_RIGHT, "", dpadColumn, lowerRow, 0.064f, 0f, 0.085f, + TextureKey.DPAD_RIGHT); + + addAxisElement(JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_X, JoystickAxis.AXIS_XBOX_LEFT_THUMB_STICK_Y, + "", leftStickColumn, leftStickRow, 0.235f, TextureKey.STICK_PAD, TextureKey.STICK_NUB); + addAxisElement(JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_X, JoystickAxis.AXIS_XBOX_RIGHT_THUMB_STICK_Y, + "", rightStickColumn, lowerRow, 0.235f, TextureKey.STICK_PAD, TextureKey.STICK_NUB); + } +} diff --git a/jme3-core/src/main/java/com/jme3/system/AppSettings.java b/jme3-core/src/main/java/com/jme3/system/AppSettings.java index f1feec40c8..2625be0321 100644 --- a/jme3-core/src/main/java/com/jme3/system/AppSettings.java +++ b/jme3-core/src/main/java/com/jme3/system/AppSettings.java @@ -256,14 +256,6 @@ public final class AppSettings extends HashMap { */ public static final String VIRTUAL_JOYSTICK_DISABLED = "VirtualJoystickDisabled"; - /** - * Always display the virtual gamepad toggle button, even on desktop and - * even when a hardware gamepad is detected. - * - * @see #setVirtualJoystick(String) - */ - public static final String VIRTUAL_JOYSTICK_ENABLED_MINIMIZED = "VirtualJoystickEnabledMinimized"; - /** * Always display the full virtual gamepad, even on desktop and even when a * hardware gamepad is detected. @@ -281,12 +273,18 @@ public final class AppSettings extends HashMap { public static final String VIRTUAL_JOYSTICK_AUTO = "VirtualJoystickAuto"; /** - * Display the virtual gamepad toggle button automatically on mobile when no - * hardware gamepad is detected. + * Use the fixed Xbox-like virtual joystick layout. * - * @see #setVirtualJoystick(String) + * @see #setVirtualJoystickDefaultLayout(String) */ - public static final String VIRTUAL_JOYSTICK_AUTO_MINIMIZED = "VirtualJoystickAutoMinimized"; + public static final String VIRTUAL_JOYSTICK_LAYOUT_XBOX = "Xbox"; + + /** + * Use a virtual joystick layout that only displays bound controls. + * + * @see #setVirtualJoystickDefaultLayout(String) + */ + public static final String VIRTUAL_JOYSTICK_LAYOUT_DYNAMIC = "Dynamic"; /** @@ -405,7 +403,8 @@ public final class AppSettings extends HashMap { defaults.put("SDLGameControllerDBResourcePath", ""); defaults.put("OnDeviceJoystickRumble", false); defaults.put("UseAndroidSensorJoystick", false); - defaults.put("VirtualJoystick", VIRTUAL_JOYSTICK_AUTO_MINIMIZED); + defaults.put("VirtualJoystick", VIRTUAL_JOYSTICK_AUTO); + defaults.put("VirtualJoystickDefaultLayout", VIRTUAL_JOYSTICK_LAYOUT_DYNAMIC); // defaults.put("Icons", null); } @@ -880,43 +879,49 @@ public void setUseAndroidSensorJoystick(boolean use) { } /** - * Sets the on-screen virtual gamepad mode. + * Sets the on-screen virtual joystick mode. *

    - * The default mode is {@link #VIRTUAL_JOYSTICK_AUTO_MINIMIZED}, which - * displays the button to show the virtual gamepad on mobile unless a + * The default mode is {@link #VIRTUAL_JOYSTICK_AUTO}, which displays the + * virtual joystick on mobile only when joystick mappings exist and no * hardware gamepad is connected. *

      *
    • {@link #VIRTUAL_JOYSTICK_DISABLED}: disable the virtual gamepad * entirely.
    • - *
    • {@link #VIRTUAL_JOYSTICK_ENABLED_MINIMIZED}: always display the - * virtual gamepad toggle button, even on desktop and even when a hardware - * gamepad is detected.
    • - *
    • {@link #VIRTUAL_JOYSTICK_ENABLED}: same as - * {@link #VIRTUAL_JOYSTICK_ENABLED_MINIMIZED}, but display the full virtual - * gamepad instead of only the toggle button.
    • + *
    • {@link #VIRTUAL_JOYSTICK_ENABLED}: always display the virtual + * joystick, even on desktop and even when a hardware gamepad is detected.
    • *
    • {@link #VIRTUAL_JOYSTICK_AUTO}: display the full virtual gamepad - * automatically on mobile when no hardware gamepad is detected.
    • - *
    • {@link #VIRTUAL_JOYSTICK_AUTO_MINIMIZED}: display the virtual gamepad - * toggle button automatically on mobile when no hardware gamepad is - * detected.
    • + * automatically on mobile when joystick mappings exist and no hardware + * gamepad is detected. *
    * * @param mode one of {@link #VIRTUAL_JOYSTICK_DISABLED}, - * {@link #VIRTUAL_JOYSTICK_ENABLED_MINIMIZED}, - * {@link #VIRTUAL_JOYSTICK_ENABLED}, {@link #VIRTUAL_JOYSTICK_AUTO}, or - * {@link #VIRTUAL_JOYSTICK_AUTO_MINIMIZED} + * {@link #VIRTUAL_JOYSTICK_ENABLED}, or {@link #VIRTUAL_JOYSTICK_AUTO} */ public void setVirtualJoystick(String mode) { if (!VIRTUAL_JOYSTICK_DISABLED.equals(mode) - && !VIRTUAL_JOYSTICK_ENABLED_MINIMIZED.equals(mode) && !VIRTUAL_JOYSTICK_ENABLED.equals(mode) - && !VIRTUAL_JOYSTICK_AUTO.equals(mode) - && !VIRTUAL_JOYSTICK_AUTO_MINIMIZED.equals(mode)) { + && !VIRTUAL_JOYSTICK_AUTO.equals(mode)) { throw new IllegalArgumentException("Unsupported virtual joystick mode: " + mode); } putString("VirtualJoystick", mode); } + /** + * Sets the default virtual joystick layout used by supporting backends. + * + * @param layout one of {@link #VIRTUAL_JOYSTICK_LAYOUT_XBOX} or + * {@link #VIRTUAL_JOYSTICK_LAYOUT_DYNAMIC} + */ + public void setVirtualJoystickDefaultLayout(String layout) { + if (VIRTUAL_JOYSTICK_LAYOUT_XBOX.equalsIgnoreCase(layout)) { + putString("VirtualJoystickDefaultLayout", VIRTUAL_JOYSTICK_LAYOUT_XBOX); + } else if (VIRTUAL_JOYSTICK_LAYOUT_DYNAMIC.equalsIgnoreCase(layout)) { + putString("VirtualJoystickDefaultLayout", VIRTUAL_JOYSTICK_LAYOUT_DYNAMIC); + } else { + throw new IllegalArgumentException("Unsupported virtual joystick layout: " + layout); + } + } + /** * Set the graphics renderer to use, one of:
    *
      @@ -1431,9 +1436,7 @@ public boolean isVirtualJoystickEnabled() { * Gets the on-screen virtual joystick mode. * * @return one of {@link #VIRTUAL_JOYSTICK_DISABLED}, - * {@link #VIRTUAL_JOYSTICK_ENABLED_MINIMIZED}, - * {@link #VIRTUAL_JOYSTICK_ENABLED}, {@link #VIRTUAL_JOYSTICK_AUTO}, or - * {@link #VIRTUAL_JOYSTICK_AUTO_MINIMIZED} + * {@link #VIRTUAL_JOYSTICK_ENABLED}, or {@link #VIRTUAL_JOYSTICK_AUTO} */ public String getVirtualJoystickMode() { Object value = get("VirtualJoystick"); @@ -1444,17 +1447,32 @@ public String getVirtualJoystickMode() { String mode = (String) value; if (VIRTUAL_JOYSTICK_DISABLED.equalsIgnoreCase(mode)) { return VIRTUAL_JOYSTICK_DISABLED; - } else if (VIRTUAL_JOYSTICK_ENABLED_MINIMIZED.equalsIgnoreCase(mode)) { - return VIRTUAL_JOYSTICK_ENABLED_MINIMIZED; } else if (VIRTUAL_JOYSTICK_ENABLED.equalsIgnoreCase(mode)) { return VIRTUAL_JOYSTICK_ENABLED; } else if (VIRTUAL_JOYSTICK_AUTO.equalsIgnoreCase(mode)) { return VIRTUAL_JOYSTICK_AUTO; - } else if (VIRTUAL_JOYSTICK_AUTO_MINIMIZED.equalsIgnoreCase(mode)) { - return VIRTUAL_JOYSTICK_AUTO_MINIMIZED; + } + } + return VIRTUAL_JOYSTICK_AUTO; + } + + /** + * Gets the default virtual joystick layout. + * + * @return one of {@link #VIRTUAL_JOYSTICK_LAYOUT_XBOX} or + * {@link #VIRTUAL_JOYSTICK_LAYOUT_DYNAMIC} + */ + public String getVirtualJoystickDefaultLayout() { + Object value = get("VirtualJoystickDefaultLayout"); + if (value instanceof String) { + String layout = (String) value; + if (VIRTUAL_JOYSTICK_LAYOUT_XBOX.equalsIgnoreCase(layout)) { + return VIRTUAL_JOYSTICK_LAYOUT_XBOX; + } else if (VIRTUAL_JOYSTICK_LAYOUT_DYNAMIC.equalsIgnoreCase(layout)) { + return VIRTUAL_JOYSTICK_LAYOUT_DYNAMIC; } } - return VIRTUAL_JOYSTICK_AUTO_MINIMIZED; + return VIRTUAL_JOYSTICK_LAYOUT_DYNAMIC; } /** diff --git a/jme3-examples/src/main/java/jme3test/input/TestJoystick.java b/jme3-examples/src/main/java/jme3test/input/TestJoystick.java index 4a517ea240..811387fd99 100644 --- a/jme3-examples/src/main/java/jme3test/input/TestJoystick.java +++ b/jme3-examples/src/main/java/jme3test/input/TestJoystick.java @@ -30,6 +30,7 @@ import com.jme3.scene.shape.Quad; import com.jme3.system.AppSettings; import com.jme3.system.JmeSystem; +import com.jme3.system.Platform; import java.io.File; import java.io.FileWriter; import java.io.IOException; @@ -49,11 +50,25 @@ public class TestJoystick extends SimpleApplication { public static void main(String[] args){ TestJoystick app = new TestJoystick(); AppSettings settings = new AppSettings(true); + configureSettings(settings); + app.setSettings(settings); + app.start(); + } + + public static void configureSettings(AppSettings settings) { settings.setJoysticksMapper(AppSettings.JOYSTICKS_XBOX_MAPPER); settings.setUseJoysticks(true); + settings.setVirtualJoystick(defaultVirtualJoystickMode()); + settings.setVirtualJoystickDefaultLayout(AppSettings.VIRTUAL_JOYSTICK_LAYOUT_XBOX); settings.setX11PlatformPreferred(true); - app.setSettings(settings); - app.start(); + } + + private static String defaultVirtualJoystickMode() { + Platform.Os os = JmeSystem.getPlatform().getOs(); + if (os == Platform.Os.Android || os == Platform.Os.iOS) { + return AppSettings.VIRTUAL_JOYSTICK_ENABLED; + } + return AppSettings.VIRTUAL_JOYSTICK_AUTO; } @Override diff --git a/jme3-examples/src/main/java/jme3test/input/TestVirtualJoystick.java b/jme3-examples/src/main/java/jme3test/input/TestVirtualJoystick.java index 68b23c6b65..b8a00613c2 100644 --- a/jme3-examples/src/main/java/jme3test/input/TestVirtualJoystick.java +++ b/jme3-examples/src/main/java/jme3test/input/TestVirtualJoystick.java @@ -32,32 +32,32 @@ package jme3test.input; import com.jme3.app.SimpleApplication; -import com.jme3.font.BitmapText; -import com.jme3.input.JoystickAxis; -import com.jme3.input.RawInputListenerAdapter; -import com.jme3.input.event.JoyAxisEvent; +import com.jme3.input.Joystick; +import com.jme3.input.JoystickButton; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.input.virtual.VirtualJoystick; +import com.jme3.input.virtual.VirtualJoystickDynamicLayout; +import com.jme3.input.virtual.VirtualJoystickXboxLayout; import com.jme3.light.AmbientLight; import com.jme3.light.DirectionalLight; -import com.jme3.material.Material; import com.jme3.math.ColorRGBA; -import com.jme3.math.Vector2f; import com.jme3.math.Vector3f; -import com.jme3.scene.Geometry; import com.jme3.scene.Spatial; -import com.jme3.scene.shape.Box; import com.jme3.system.AppSettings; import com.jme3.system.JmeSystem; import com.jme3.system.Platform; -import java.util.LinkedHashMap; -import java.util.Map; +import com.jme3.util.SkyFactory; /** * Manual test for the on-screen virtual joystick. */ public class TestVirtualJoystick extends SimpleApplication { - private final Map axisValues = new LinkedHashMap<>(); - private BitmapText axisDebug; + private static final String TOGGLE_LAYOUT = "ToggleVirtualJoystickLayout"; + + private boolean dynamicLayout; public static void main(String[] args) { TestVirtualJoystick app = new TestVirtualJoystick(); @@ -83,107 +83,68 @@ private static String defaultVirtualJoystickMode() { @Override public void simpleInitApp() { - cam.setLocation(new Vector3f(0f, 5f, 22f)); - cam.lookAt(new Vector3f(0f, 5f, 0f), Vector3f.UNIT_Y); + setDisplayStatView(false); + setDisplayFps(false); + + cam.setLocation(new Vector3f(42f, 18f, 18f)); + cam.lookAt(new Vector3f(-18f, 4f, -10f), Vector3f.UNIT_Y); cam.setFrustumPerspective(60f, cam.getAspect(), 0.1f, 500f); flyCam.setMoveSpeed(18f); flyCam.setRotationSpeed(2f); flyCam.setDragToRotate(true); inputManager.setCursorVisible(true); - setupAxisDebug(); + setupLayoutToggle(); AmbientLight ambient = new AmbientLight(); - ambient.setColor(new ColorRGBA(0.25f, 0.25f, 0.25f, 1f)); + ambient.setColor(new ColorRGBA(0.35f, 0.35f, 0.35f, 1f)); rootNode.addLight(ambient); DirectionalLight sun = new DirectionalLight(); - sun.setDirection(new Vector3f(-0.35f, -0.8f, -0.45f).normalizeLocal()); - sun.setColor(ColorRGBA.White.mult(0.8f)); + sun.setDirection(new Vector3f(-0.4f, -0.75f, -0.3f).normalizeLocal()); + sun.setColor(ColorRGBA.White.mult(1.1f)); rootNode.addLight(sun); buildScene(); } - @Override - public void simpleUpdate(float tpf) { - updateAxisDebug(); - } - - private void setupAxisDebug() { - axisDebug = new BitmapText(guiFont, false); - axisDebug.setSize(16f); - axisDebug.setColor(ColorRGBA.White); - axisDebug.setLocalTranslation(12f, cam.getHeight() - 12f, 10f); - guiNode.attachChild(axisDebug); - - inputManager.addRawInputListener(new RawInputListenerAdapter() { - @Override - public void onJoyAxisEvent(JoyAxisEvent evt) { - JoystickAxis axis = evt.getAxis(); - String key = axis.getJoystick().getJoyId() + " " - + axis.getJoystick().getName() + " " - + axis.getLogicalId() + "[" + axis.getAxisId() + "]"; - axisValues.put(key, evt.getValue()); + private void setupLayoutToggle() { + inputManager.addMapping(TOGGLE_LAYOUT, new KeyTrigger(KeyInput.KEY_L)); + Joystick[] joysticks = inputManager.getJoysticks(); + if (joysticks != null) { + for (Joystick joystick : joysticks) { + JoystickButton start = joystick.getButton(JoystickButton.BUTTON_XBOX_START); + if (start != null) { + start.assignButton(TOGGLE_LAYOUT); + } } - }); + } + inputManager.addListener(layoutListener, TOGGLE_LAYOUT); } - private void updateAxisDebug() { - if (axisDebug == null) { + private final ActionListener layoutListener = (name, isPressed, tpf) -> { + if (!TOGGLE_LAYOUT.equals(name) || !isPressed) { return; } - - StringBuilder text = new StringBuilder("Joystick axes\n"); - for (Map.Entry entry : axisValues.entrySet()) { - text.append(entry.getKey()) - .append(" = ") - .append(String.format("%.3f", entry.getValue())) - .append('\n'); + dynamicLayout = !dynamicLayout; + Joystick[] joysticks = inputManager.getJoysticks(); + if (joysticks == null) { + return; } - axisDebug.setText(text.toString()); - } + for (Joystick joystick : joysticks) { + if (joystick instanceof VirtualJoystick) { + ((VirtualJoystick) joystick).setLayout(dynamicLayout + ? new VirtualJoystickDynamicLayout(true) + : new VirtualJoystickXboxLayout()); + } + } + }; private void buildScene() { - Material floorMaterial = assetManager.loadMaterial("Textures/Terrain/Pond/Pond.j3m"); - Material wallMaterial = assetManager.loadMaterial("Textures/Terrain/BrickWall/BrickWall.j3m"); - Material columnMaterial = assetManager.loadMaterial("Textures/Terrain/Rock/Rock.j3m"); - - Geometry floor = new Geometry("Textured Floor", new Box(24f, 0.1f, 24f)); - floor.getMesh().scaleTextureCoordinates(new Vector2f(12f, 12f)); - floor.setLocalTranslation(0f, -0.1f, 0f); - floor.setMaterial(floorMaterial); - rootNode.attachChild(floor); - - addWall("Back Wall", wallMaterial, 0f, 3f, -24f, 24f, 3f, 0.15f); - addWall("Left Wall", wallMaterial, -24f, 3f, 0f, 0.15f, 3f, 24f); - addWall("Right Wall", wallMaterial, 24f, 3f, 0f, 0.15f, 3f, 24f); - - addColumn(columnMaterial, -10f, 0f); - addColumn(columnMaterial, 10f, 0f); - addColumn(columnMaterial, -10f, -12f); - addColumn(columnMaterial, 10f, -12f); - - Spatial tank = assetManager.loadModel("Models/Tank/tank.j3o"); - tank.setLocalTranslation(0f, 0.15f, -8f); - tank.setLocalScale(2f); - rootNode.attachChild(tank); - } - - private void addWall(String name, Material material, float x, float y, float z, float xExtent, float yExtent, - float zExtent) { - Geometry wall = new Geometry(name, new Box(xExtent, yExtent, zExtent)); - wall.getMesh().scaleTextureCoordinates(new Vector2f(8f, 2f)); - wall.setLocalTranslation(x, y, z); - wall.setMaterial(material); - rootNode.attachChild(wall); - } + Spatial scene = assetManager.loadModel("BlenderParity/scene.glb"); + rootNode.attachChild(scene); - private void addColumn(Material material, float x, float z) { - Geometry column = new Geometry("Column", new Box(1.25f, 3f, 1.25f)); - column.getMesh().scaleTextureCoordinates(new Vector2f(2f, 2f)); - column.setLocalTranslation(x, 3f, z); - column.setMaterial(material); - rootNode.attachChild(column); + Spatial sky = SkyFactory.createSky(assetManager, "Textures/Sky/Path.hdr", SkyFactory.EnvMapType.EquirectMap); + rootNode.attachChild(sky); } } diff --git a/jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooser.java b/jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooser.java index 2e6acd5ad5..8d41ea518b 100644 --- a/jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooser.java +++ b/jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooser.java @@ -1,6 +1,10 @@ package jme3test.ios; +import com.jme3.app.DebugKeysAppState; import com.jme3.app.SimpleApplication; +import com.jme3.app.StatsAppState; +import com.jme3.app.state.ConstantVerifierState; +import com.jme3.audio.AudioListenerState; import com.jme3.font.BitmapText; import com.jme3.input.KeyInput; import com.jme3.input.MouseInput; @@ -45,12 +49,19 @@ public final class IosTestChooser extends SimpleApplication implements ActionLis private RawInputListenerAdapter keyboardListener; private boolean searchInputActive; + public IosTestChooser() { + super(new StatsAppState(), + new AudioListenerState(), + new DebugKeysAppState(), + new ConstantVerifierState()); + } + @Override public void simpleInitApp() { setDisplayStatView(false); setDisplayFps(false); if (flyCam != null) { - flyCam.setEnabled(false); + flyCam.unregisterInput(); } viewPort.setBackgroundColor(BACKGROUND_COLOR); buttonMaterial = material(EXAMPLE_BUTTON_COLOR); diff --git a/jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooserLauncher.java b/jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooserLauncher.java index 241b42e2b2..568d52dcac 100644 --- a/jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooserLauncher.java +++ b/jme3-ios-examples/src/main/java/jme3test/ios/IosTestChooserLauncher.java @@ -218,11 +218,6 @@ private static void configureIosSettings(Application application) { AppSettings settings = new AppSettings(true); settings.setUseJoysticks(true); settings.setOnDeviceJoystickRumble(true); - invokeConfigureSettings(application, settings); - application.setSettings(settings); - } - - private static void invokeConfigureSettings(Application application, AppSettings settings) { try { java.lang.reflect.Method method = application.getClass().getMethod("configureSettings", AppSettings.class); Object target = java.lang.reflect.Modifier.isStatic(method.getModifiers()) ? null : application; @@ -233,6 +228,7 @@ private static void invokeConfigureSettings(Application application, AppSettings throw new IllegalStateException("Could not configure iOS settings for " + application.getClass().getName(), exception); } + application.setSettings(settings); } private static Object invokeIfPresent(Object target, String name, Class[] parameterTypes, Object... args) { diff --git a/jme3-ios/src/main/java/com/jme3/input/ios/IosJoyInput.java b/jme3-ios/src/main/java/com/jme3/input/ios/IosJoyInput.java index 3c21528343..0f5677d7d2 100644 --- a/jme3-ios/src/main/java/com/jme3/input/ios/IosJoyInput.java +++ b/jme3-ios/src/main/java/com/jme3/input/ios/IosJoyInput.java @@ -70,13 +70,13 @@ public final class IosJoyInput implements JoyInput { private static volatile IosJoyInput active; private static final int POV_X_AXIS_ID = 0x4000; - private static final int POV_Y_AXIS_ID = 0x4001; private final Map joysticks = new HashMap<>(); private RawInputListener listener; private InputManager inputManager; private boolean initialized; private boolean onDeviceJoystickRumble; - private String virtualJoystickMode = AppSettings.VIRTUAL_JOYSTICK_AUTO_MINIMIZED; + private String virtualJoystickMode = AppSettings.VIRTUAL_JOYSTICK_AUTO; + private String virtualJoystickDefaultLayout = AppSettings.VIRTUAL_JOYSTICK_LAYOUT_DYNAMIC; private boolean useJoysticks = true; private boolean keyboardSuppressedAutoJoystick; private volatile VirtualJoystick virtualJoystick; @@ -122,6 +122,7 @@ public void initialize() { public void update() { VirtualJoystick joystick = virtualJoystick; if (joystick != null) { + updateVirtualJoystickAutoVisibility(); joystick.dispatchEvents(listener); } } @@ -179,6 +180,7 @@ public void stopJoyRumble(int joyId) { public void loadSettings(AppSettings settings) { onDeviceJoystickRumble = settings.isOnDeviceJoystickRumble(); virtualJoystickMode = settings.getVirtualJoystickMode(); + virtualJoystickDefaultLayout = settings.getVirtualJoystickDefaultLayout(); useJoysticks = settings.useJoysticks(); } @@ -188,6 +190,8 @@ public Joystick[] loadJoysticks(InputManager inputManager) { refreshJoysticks(false); if (shouldCreateVirtualJoystick()) { virtualJoystick = new VirtualJoystick(inputManager, this, nextVirtualJoyId()); + virtualJoystick.setLayout(VirtualJoystick.createLayout(virtualJoystickDefaultLayout)); + virtualJoystick.setEnabled(false); updateVirtualJoystickAutoVisibility(); } else { virtualJoystick = null; @@ -212,7 +216,7 @@ private boolean onPointerUp(int pointerId, float x, float y, long time) { } private void onKeyboardInput() { - if (isAutoMode(virtualJoystickMode)) { + if (AppSettings.VIRTUAL_JOYSTICK_AUTO.equals(virtualJoystickMode)) { keyboardSuppressedAutoJoystick = true; updateVirtualJoystickAutoVisibility(); } @@ -364,31 +368,17 @@ private void updateVirtualJoystickAutoVisibility() { if (virtualJoystick == null) { return; } - boolean active = isEnabledMode(virtualJoystickMode) - || (isAutoMode(virtualJoystickMode) + boolean wasEnabled = virtualJoystick.isEnabled(); + boolean active = AppSettings.VIRTUAL_JOYSTICK_ENABLED.equals(virtualJoystickMode) + || (AppSettings.VIRTUAL_JOYSTICK_AUTO.equals(virtualJoystickMode) && joysticks.isEmpty() - && !keyboardSuppressedAutoJoystick); - virtualJoystick.setEnabled(active); - if (active) { - virtualJoystick.setShown(!isMinimizedMode(virtualJoystickMode)); + && !keyboardSuppressedAutoJoystick + && virtualJoystick.hasInputBindings()); + if (wasEnabled != active) { + virtualJoystick.setEnabled(active); } } - private static boolean isEnabledMode(String mode) { - return AppSettings.VIRTUAL_JOYSTICK_ENABLED.equals(mode) - || AppSettings.VIRTUAL_JOYSTICK_ENABLED_MINIMIZED.equals(mode); - } - - private static boolean isAutoMode(String mode) { - return AppSettings.VIRTUAL_JOYSTICK_AUTO.equals(mode) - || AppSettings.VIRTUAL_JOYSTICK_AUTO_MINIMIZED.equals(mode); - } - - private static boolean isMinimizedMode(String mode) { - return AppSettings.VIRTUAL_JOYSTICK_ENABLED_MINIMIZED.equals(mode) - || AppSettings.VIRTUAL_JOYSTICK_AUTO_MINIMIZED.equals(mode); - } - private Joystick[] currentJoysticks() { List current = new ArrayList<>(joysticks.values()); VirtualJoystick joystick = virtualJoystick; @@ -435,13 +425,11 @@ private static final class IosJoystick extends AbstractJoystick { private JoystickAxis xAxis; private JoystickAxis yAxis; private JoystickAxis povXAxis; - private JoystickAxis povYAxis; private boolean dpadUp; private boolean dpadDown; private boolean dpadLeft; private boolean dpadRight; private float povXValue; - private float povYValue; IosJoystick(InputManager inputManager, JoyInput joyInput, int id, boolean gamepad, long gamepadHandle, long joystickHandle, String name, int axisCount, int buttonCount) { @@ -525,9 +513,6 @@ private void addPovAxes(InputManager inputManager) { JoystickAxis povX = new DefaultJoystickAxis(inputManager, this, POV_X_AXIS_ID, JoystickAxis.POV_X, JoystickAxis.POV_X, true, false, 0f); addAxis(POV_X_AXIS_ID, povX); - JoystickAxis povY = new DefaultJoystickAxis(inputManager, this, POV_Y_AXIS_ID, JoystickAxis.POV_Y, - JoystickAxis.POV_Y, true, false, 0f); - addAxis(POV_Y_AXIS_ID, povY); } JoystickAxis getAxisById(int id) { @@ -555,7 +540,7 @@ public JoystickAxis getPovXAxis() { @Override public JoystickAxis getPovYAxis() { - return povYAxis; + return null; } private void addAxis(int index, JoystickAxis axis) { @@ -571,9 +556,6 @@ private void addAxis(int index, JoystickAxis axis) { case POV_X_AXIS_ID: povXAxis = axis; break; - case POV_Y_AXIS_ID: - povYAxis = axis; - break; default: break; } @@ -601,15 +583,10 @@ private void updateDpadPov(int buttonId, boolean pressed, RawInputListener liste } float nextPovX = (dpadRight ? 1f : 0f) + (dpadLeft ? -1f : 0f); - float nextPovY = (dpadUp ? 1f : 0f) + (dpadDown ? -1f : 0f); if (listener != null && povXAxis != null && povXValue != nextPovX) { listener.onJoyAxisEvent(new JoyAxisEvent(povXAxis, nextPovX, nextPovX)); } - if (listener != null && povYAxis != null && povYValue != nextPovY) { - listener.onJoyAxisEvent(new JoyAxisEvent(povYAxis, nextPovY, nextPovY)); - } povXValue = nextPovX; - povYValue = nextPovY; } protected void addButton(JoystickButton button) { diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlJoystickInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlJoystickInput.java index 7f7ef84d1f..dac51d3159 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlJoystickInput.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlJoystickInput.java @@ -177,12 +177,6 @@ private void onDeviceConnected(InputManager inputManager, int deviceIndex, boole joystick.addAxis(povXAxisId, povX); joystick.povAxisX = povX; - int povYAxisId = joystick.getAxisCount(); - JoystickAxis povY = new DefaultJoystickAxis(inputManager, joystick, povYAxisId, JoystickAxis.POV_Y, - JoystickAxis.POV_Y, true, false, 0.0f); - joystick.addAxis(povYAxisId, povY); - joystick.povAxisY = povY; - inputManager.fireJoystickConnectedEvent(joystick); } @@ -261,9 +255,10 @@ public Joystick[] loadJoysticks(InputManager inputManager) { } } - if (settings.useJoysticks() && isEnabledMode(settings.getVirtualJoystickMode())) { + if (settings.useJoysticks() + && AppSettings.VIRTUAL_JOYSTICK_ENABLED.equals(settings.getVirtualJoystickMode())) { virtualJoystick = new VirtualJoystick(inputManager, this, nextVirtualJoyId()); - virtualJoystick.setShown(!isMinimizedMode(settings.getVirtualJoystickMode())); + virtualJoystick.setLayout(VirtualJoystick.createLayout(settings.getVirtualJoystickDefaultLayout())); } else { virtualJoystick = null; } @@ -358,8 +353,6 @@ private void handleInputEvents() { } float povXValue = 0f; - float povYValue = 0f; - for (JoystickButton button : js.getButtons()) { int buttonId = button.getButtonId(); String jmeButtonId = button.getLogicalId(); @@ -371,10 +364,6 @@ private void handleInputEvents() { povXValue += pressed ? -1f : 0f; } else if (JoystickButton.BUTTON_XBOX_DPAD_RIGHT.equals(jmeButtonId)) { povXValue += pressed ? 1f : 0f; - } else if (JoystickButton.BUTTON_XBOX_DPAD_UP.equals(jmeButtonId)) { - povYValue += pressed ? 1f : 0f; - } else if (JoystickButton.BUTTON_XBOX_DPAD_DOWN.equals(jmeButtonId)) { - povYValue += pressed ? -1f : 0f; } } @@ -382,11 +371,6 @@ private void handleInputEvents() { if (povXAxis != null) { updateAxis(povXAxis, povXValue, povXValue); } - JoystickAxis povYAxis = js.getPovYAxis(); - if (povYAxis != null) { - updateAxis(povYAxis, povYValue, povYValue); - } - } else { long joy = js.joystick; @@ -426,16 +410,6 @@ private int nextVirtualJoyId() { return id; } - private static boolean isEnabledMode(String mode) { - return AppSettings.VIRTUAL_JOYSTICK_ENABLED.equals(mode) - || AppSettings.VIRTUAL_JOYSTICK_ENABLED_MINIMIZED.equals(mode); - } - - private static boolean isMinimizedMode(String mode) { - return AppSettings.VIRTUAL_JOYSTICK_ENABLED_MINIMIZED.equals(mode) - || AppSettings.VIRTUAL_JOYSTICK_AUTO_MINIMIZED.equals(mode); - } - @Override public void setJoyRumble(int joyId, float amountHigh, float amountLow, float duration) { SdlJoystick js = joysticks.get(joyId); @@ -673,7 +647,6 @@ private static class SdlJoystick extends AbstractJoystick { private JoystickAxis xAxis; private JoystickAxis yAxis; private JoystickAxis povAxisX; - private JoystickAxis povAxisY; long joystick; long gamepad; @@ -722,7 +695,7 @@ public JoystickAxis getPovXAxis() { @Override public JoystickAxis getPovYAxis() { - return povAxisY; + return null; } @Override