Virtual/on-screen joystick support#2803
Conversation
1c6fb91 to
b81c49d
Compare
b81c49d to
ab54490
Compare
This comment was marked as outdated.
This comment was marked as outdated.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive on-screen virtual joystick system for jMonkeyEngine, enabling gamepad-like controls on mobile and desktop platforms. The implementation includes new classes for joystick logic, layout management, and theming, as well as integration with existing camera controllers and application settings. Review feedback identifies critical thread-safety issues in the Android, iOS, and LWJGL3 backends, specifically regarding the cross-thread visibility of the virtual joystick instance and synchronization of shared layout and theme data. A regression was also noted in the iOS implementation where POV Y axis support for physical joysticks was removed.
| private boolean useAndroidSensorJoystick = false; | ||
| private boolean physicalJoystickAvailable = false; | ||
| private boolean keyboardSuppressedAutoJoystick = false; | ||
| private VirtualJoystick virtualJoystick; |
There was a problem hiding this comment.
The virtualJoystick field should be declared as volatile. It is initialized in loadJoysticks (which runs on the JME thread) and accessed in onTouch (which runs on the Android UI thread). Without volatile, the input thread might see a stale null value or a partially initialized object.
| private VirtualJoystick virtualJoystick; | |
| private volatile VirtualJoystick virtualJoystick; |
| @@ -67,12 +70,15 @@ | |||
| public final class IosJoyInput implements JoyInput { | |||
| private static IosJoyInput active; | |||
| private static final int POV_X_AXIS_ID = 0x4000; | |||
There was a problem hiding this comment.
The POV Y axis support for physical joysticks seems to have been removed in this file (previously POV_Y_AXIS_ID = 0x4001). This is a regression for physical gamepads on iOS, as the D-pad vertical axis will no longer be available via getPovYAxis(). Please restore the POV Y axis handling to match the LWJGL3 implementation.
| private String virtualJoystickMode = AppSettings.VIRTUAL_JOYSTICK_AUTO_MINIMIZED; | ||
| private boolean useJoysticks = true; | ||
| private boolean keyboardSuppressedAutoJoystick; | ||
| private VirtualJoystick virtualJoystick; |
There was a problem hiding this comment.
| private float globalJitterThreshold; | ||
| private boolean loadGamepads; | ||
| private boolean loadRaw; | ||
| private VirtualJoystick virtualJoystick; |
There was a problem hiding this comment.
The virtualJoystick field should be declared as volatile to ensure visibility across threads, as it is initialized in loadJoysticks (JME thread) and accessed in onPointerDown/Move/Up (input thread).
| private VirtualJoystick virtualJoystick; | |
| private volatile VirtualJoystick virtualJoystick; |
| public Element getButtonElement(String logicalId) { | ||
| return buttons.get(logicalId); | ||
| } | ||
|
|
||
| public Element getAxisElement(String logicalId) { | ||
| return findAxisElement(logicalId); | ||
| } |
There was a problem hiding this comment.
The getButtonElement and getAxisElement methods access internal maps (buttons, axisElements) that are cleared and repopulated in the read method. Since read is synchronized, these getters should also be synchronized to avoid race conditions or ConcurrentModificationException if the layout is being reloaded while being accessed from another thread.
| 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; | ||
| } | ||
| 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; |
There was a problem hiding this comment.
The sync method updates multiple volatile fields (pixelX, pixelY, pixelWidth, pixelHeight, etc.) sequentially. The input thread calling contains(x, y) might see an inconsistent state where some fields are updated for the current frame and others are still from the previous frame.
It is recommended to calculate all values using local variables and assign them to the volatile fields at the end of the method to minimize the window for race conditions.
| 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(); | ||
| } |
This PR makes joystick support available everywhere by adding an on-screen virtual joystick integrated directly into the input backend.
The advantage of this approach is that a jME application sees the virtual joystick just like any other Xbox-style joystick supported by jME. No special handling is needed: applications can use the usual joystick bindings.
The controls are rendered as spatials attached to the node passed to the joystick update method. When using
SimpleApplication, this is already handled correctly and the controls end up in theguiNode, so normal jME apps do not need any additional setup.(wip needs rebase)
Since it now conflicts with this, the
AndroidSensorJoyInput(that is used to simulate android joysticks using accelerometer), is now disabled by default and gated behind theUseAndroidSensorJoystickapp setting.Other changes to AppSettings:
DisableJoysticksis now off by default, meaning gamepads are always supported when availableVirtualJoystickis set toVIRTUAL_JOYSTICK_AUTO_MINIMIZED, this will display the button to toggle the virtual gamepad on mobile, unless an hardware gamepad is connected, other values are:VIRTUAL_JOYSTICK_DISABLED: disable the virtual gamepad entirelyVIRTUAL_JOYSTICK_ENABLED_MINIMIZEDalways display the "toggle gamepad button" even on desktop and even if an hardware gamepad is detectedVIRTUAL_JOYSTICK_ENABLEDsame asVIRTUAL_JOYSTICK_ENABLED_MINIMIZEDbut will actually show the virtual gamepad, not only the toggle buttonVIRTUAL_JOYSTICK_AUTOwill show the virtual gamepad automatically on mobile if no hardware gamepad is found