Skip to content

Commit 1a32b66

Browse files
authored
Virtual/on-screen joystick support (#2803)
* Virtual/on-screen gamepad * documentation * disable AndroidSensorJoyInput * reduce objects allocations * fixes * fixes autoshow * improved layouts
1 parent 64ffd13 commit 1a32b66

43 files changed

Lines changed: 2716 additions & 198 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

jme3-android-examples/src/main/java/jme3test/android/TestAndroidSensors.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.jme3.scene.Mesh;
1818
import com.jme3.scene.shape.Box;
1919
import com.jme3.scene.shape.Line;
20+
import com.jme3.system.AppSettings;
2021
import com.jme3.texture.Texture;
2122
import com.jme3.util.IntMap;
2223

@@ -79,6 +80,12 @@ public class TestAndroidSensors extends SimpleApplication implements ActionListe
7980

8081
// Make sure to set joystickEventsEnabled = true in MainActivity for Android
8182

83+
public static void configureSettings(AppSettings settings) {
84+
settings.setUseJoysticks(true);
85+
settings.setUseAndroidSensorJoystick(true);
86+
settings.setVirtualJoystick(AppSettings.VIRTUAL_JOYSTICK_DISABLED);
87+
}
88+
8289
private float toDegrees(float rad) {
8390
return rad * FastMath.RAD_TO_DEG;
8491
}
@@ -311,4 +318,4 @@ public void onAnalog(String string, float value, float tpf) {
311318
}
312319

313320
}
314-
}
321+
}

jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
import androidx.fragment.app.Fragment;
4343
import com.jme3.audio.AudioRenderer;
4444
import com.jme3.input.JoyInput;
45-
import com.jme3.input.android.AndroidSensorJoyInput;
45+
import com.jme3.input.android.AndroidJoyInput;
4646
import com.jme3.system.AppSettings;
4747
import com.jme3.system.SystemListener;
4848
import com.jme3.system.android.JmeAndroidSystem;
@@ -269,8 +269,8 @@ public void gainFocus() {
269269
}
270270

271271
JoyInput joyInput = app.getContext() != null ? app.getContext().getJoyInput() : null;
272-
if (joyInput instanceof AndroidSensorJoyInput) {
273-
((AndroidSensorJoyInput) joyInput).resumeSensors();
272+
if (joyInput instanceof AndroidJoyInput) {
273+
((AndroidJoyInput) joyInput).resumeJoysticks();
274274
}
275275

276276
app.gainFocus();
@@ -295,8 +295,8 @@ public void loseFocus() {
295295
}
296296

297297
JoyInput joyInput = app.getContext() != null ? app.getContext().getJoyInput() : null;
298-
if (joyInput instanceof AndroidSensorJoyInput) {
299-
((AndroidSensorJoyInput) joyInput).pauseSensors();
298+
if (joyInput instanceof AndroidJoyInput) {
299+
((AndroidJoyInput) joyInput).pauseJoysticks();
300300
}
301301
}
302302
}

jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ public boolean onTouch(View view, MotionEvent event) {
208208
// logger.log(Level.INFO, "onTouch source: {0}, isTouch: {1}",
209209
// new Object[]{source, isTouch});
210210

211+
if (isTouch && joyInput != null && joyInput.onTouch(event)) {
212+
return true;
213+
}
214+
211215
if (isTouch && touchInput != null) {
212216
// send the event to the touch processor
213217
consumed = touchInput.onTouch(event);
@@ -234,6 +238,10 @@ public boolean onKey(View view, int keyCode, KeyEvent event) {
234238
// logger.log(Level.INFO, "onKey source: {0}, isTouch: {1}",
235239
// new Object[]{source, isTouch});
236240

241+
if ((source & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD && joyInput != null) {
242+
joyInput.onKeyboardInput();
243+
}
244+
237245
if (touchInput != null) {
238246
consumed = touchInput.onKey(event);
239247
}

jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler14.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,10 @@ public boolean onKey(View view, int keyCode, KeyEvent event) {
179179
boolean isUnknown =
180180
(source & android.view.InputDevice.SOURCE_UNKNOWN) == android.view.InputDevice.SOURCE_UNKNOWN;
181181

182+
if ((source & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD && joyInput != null) {
183+
joyInput.onKeyboardInput();
184+
}
185+
182186
if (touchInput != null && (isTouch || (isUnknown && this.touchInput.isSimulateKeyboard()))) {
183187
// logger.log(Level.INFO, "onKey source: {0}, isTouch: {1}",
184188
// new Object[]{source, isTouch});

jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java

Lines changed: 120 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@
3232
package com.jme3.input.android;
3333

3434
import android.opengl.GLSurfaceView;
35+
import android.view.MotionEvent;
3536
import com.jme3.input.InputManager;
3637
import com.jme3.input.JoyInput;
3738
import com.jme3.input.Joystick;
3839
import com.jme3.input.RawInputListener;
3940
import com.jme3.input.event.InputEvent;
4041
import com.jme3.input.event.JoyAxisEvent;
4142
import com.jme3.input.event.JoyButtonEvent;
43+
import com.jme3.input.virtual.VirtualJoystick;
4244
import com.jme3.system.AppSettings;
4345
import com.jme3.system.JmeSystem;
4446
import java.util.ArrayList;
@@ -48,9 +50,8 @@
4850
import java.util.logging.Logger;
4951

5052
/**
51-
* Main class that manages various joystick devices. Joysticks can be many forms
52-
* including a simulated joystick to communicate the device orientation as well
53-
* as physical gamepads. <br>
53+
* Main class that manages joystick devices. Joysticks can be physical gamepads,
54+
* the on-screen virtual joystick, or an explicitly-enabled sensor joystick. <br>
5455
* This class manages all the joysticks and feeds the inputs from each back
5556
* to jME's InputManager.
5657
*
@@ -67,7 +68,7 @@
6768
*
6869
* MainActivity needs the following line to enable Joysticks on Android platforms
6970
* joystickEventsEnabled = true;
70-
* This is done to allow for battery conservation when sensor data or gamepads
71+
* This is done to allow for battery conservation when sensor data or joysticks
7172
* are not required by the application.
7273
*
7374
* {@code
@@ -79,7 +80,6 @@
7980
*/
8081
public class AndroidJoyInput implements JoyInput {
8182
private static final Logger logger = Logger.getLogger(AndroidJoyInput.class.getName());
82-
public static boolean disableSensors = false;
8383

8484
protected AndroidInputHandler inputHandler;
8585
protected List<Joystick> joystickList = new ArrayList<>();
@@ -92,20 +92,32 @@ public class AndroidJoyInput implements JoyInput {
9292
private ConcurrentLinkedQueue<InputEvent> eventQueue = new ConcurrentLinkedQueue<>();
9393
private AndroidSensorJoyInput sensorJoyInput;
9494
private boolean onDeviceJoystickRumble = false;
95+
private String virtualJoystickMode = AppSettings.VIRTUAL_JOYSTICK_AUTO;
96+
private String virtualJoystickDefaultLayout = AppSettings.VIRTUAL_JOYSTICK_LAYOUT_DYNAMIC;
97+
private boolean useJoysticks = true;
98+
private boolean useAndroidSensorJoystick = false;
99+
private boolean physicalJoystickAvailable = false;
100+
private boolean keyboardSuppressedAutoJoystick = false;
101+
private volatile VirtualJoystick virtualJoystick;
102+
private GLSurfaceView view;
95103

96104
public AndroidJoyInput(AndroidInputHandler inputHandler) {
97105
this.inputHandler = inputHandler;
98-
sensorJoyInput = new AndroidSensorJoyInput(this);
99106
}
100107

101108
public void setView(GLSurfaceView view) {
109+
this.view = view;
102110
if (sensorJoyInput != null) {
103111
sensorJoyInput.setView(view);
104112
}
105113
}
106114

107115
public void loadSettings(AppSettings settings) {
108116
onDeviceJoystickRumble = settings.isOnDeviceJoystickRumble();
117+
virtualJoystickMode = settings.getVirtualJoystickMode();
118+
virtualJoystickDefaultLayout = settings.getVirtualJoystickDefaultLayout();
119+
useJoysticks = settings.useJoysticks();
120+
useAndroidSensorJoystick = settings.useAndroidSensorJoystick();
109121
}
110122

111123
boolean isOnDeviceJoystickRumble() {
@@ -127,6 +139,9 @@ public void pauseJoysticks() {
127139
if (onDeviceJoystickRumble) {
128140
JmeSystem.stopRumble();
129141
}
142+
if (virtualJoystick != null) {
143+
virtualJoystick.onPointerCancel(0L);
144+
}
130145

131146
}
132147

@@ -138,7 +153,6 @@ public void resumeJoysticks() {
138153
if (sensorJoyInput != null) {
139154
sensorJoyInput.resumeSensors();
140155
}
141-
142156
}
143157

144158
@Override
@@ -154,12 +168,11 @@ public boolean isInitialized() {
154168
@Override
155169
public void destroy() {
156170
initialized = false;
157-
158171
if (sensorJoyInput != null) {
159172
sensorJoyInput.destroy();
173+
sensorJoyInput = null;
160174
}
161-
162-
setView(null);
175+
view = null;
163176
}
164177

165178
@Override
@@ -191,12 +204,72 @@ public Joystick[] loadJoysticks(InputManager inputManager) {
191204
if (logger.isLoggable(Level.INFO)) {
192205
logger.log(Level.INFO, "loading joysticks for {0}", this.getClass().getName());
193206
}
194-
if (!disableSensors) {
207+
joystickList.clear();
208+
if (useJoysticks && useAndroidSensorJoystick) {
209+
if (sensorJoyInput == null) {
210+
sensorJoyInput = new AndroidSensorJoyInput(this);
211+
sensorJoyInput.setView(view);
212+
}
195213
joystickList.add(sensorJoyInput.loadJoystick(joystickList.size(), inputManager));
196214
}
215+
physicalJoystickAvailable = false;
216+
if (shouldCreateVirtualJoystick()) {
217+
virtualJoystick = new VirtualJoystick(inputManager, this, joystickList.size());
218+
virtualJoystick.setLayout(VirtualJoystick.createLayout(virtualJoystickDefaultLayout));
219+
virtualJoystick.setEnabled(false);
220+
updateVirtualJoystickAutoVisibility();
221+
joystickList.add(virtualJoystick);
222+
} else {
223+
virtualJoystick = null;
224+
}
197225
return joystickList.toArray( new Joystick[joystickList.size()] );
198226
}
199227

228+
public boolean onTouch(MotionEvent event) {
229+
VirtualJoystick joystick = virtualJoystick;
230+
if (joystick == null || inputHandler.getView() == null) {
231+
return false;
232+
}
233+
234+
boolean consumed = false;
235+
int action = event.getAction() & MotionEvent.ACTION_MASK;
236+
int pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
237+
>> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
238+
long time = event.getEventTime();
239+
240+
switch (action) {
241+
case MotionEvent.ACTION_POINTER_DOWN:
242+
case MotionEvent.ACTION_DOWN:
243+
consumed = joystick.onPointerDown(event.getPointerId(pointerIndex),
244+
toJmeX(event.getX(pointerIndex)), toJmeY(event.getY(pointerIndex)), time);
245+
break;
246+
case MotionEvent.ACTION_POINTER_UP:
247+
case MotionEvent.ACTION_UP:
248+
consumed = joystick.onPointerUp(event.getPointerId(pointerIndex),
249+
toJmeX(event.getX(pointerIndex)), toJmeY(event.getY(pointerIndex)), time);
250+
break;
251+
case MotionEvent.ACTION_CANCEL:
252+
consumed = joystick.onPointerCancel(time);
253+
break;
254+
case MotionEvent.ACTION_MOVE:
255+
for (int i = 0; i < event.getPointerCount(); i++) {
256+
consumed = joystick.onPointerMove(event.getPointerId(i),
257+
toJmeX(event.getX(i)), toJmeY(event.getY(i)), time) || consumed;
258+
}
259+
break;
260+
default:
261+
break;
262+
}
263+
return consumed;
264+
}
265+
266+
public void onKeyboardInput() {
267+
if (AppSettings.VIRTUAL_JOYSTICK_AUTO.equals(virtualJoystickMode)) {
268+
keyboardSuppressedAutoJoystick = true;
269+
updateVirtualJoystickAutoVisibility();
270+
}
271+
}
272+
200273
@Override
201274
public void update() {
202275
if (sensorJoyInput != null) {
@@ -214,7 +287,43 @@ public void update() {
214287
}
215288
}
216289
}
290+
if (virtualJoystick != null) {
291+
updateVirtualJoystickAutoVisibility();
292+
virtualJoystick.dispatchEvents(listener);
293+
}
294+
295+
}
296+
297+
private float toJmeX(float x) {
298+
return inputHandler.touchInput.getJmeX(x);
299+
}
300+
301+
private float toJmeY(float y) {
302+
return inputHandler.touchInput.invertY(inputHandler.touchInput.getJmeY(y));
303+
}
304+
305+
protected void setPhysicalJoystickAvailable(boolean available) {
306+
physicalJoystickAvailable = available;
307+
updateVirtualJoystickAutoVisibility();
308+
}
217309

310+
private boolean shouldCreateVirtualJoystick() {
311+
return useJoysticks && !AppSettings.VIRTUAL_JOYSTICK_DISABLED.equals(virtualJoystickMode);
312+
}
313+
314+
private void updateVirtualJoystickAutoVisibility() {
315+
if (virtualJoystick == null) {
316+
return;
317+
}
318+
boolean wasEnabled = virtualJoystick.isEnabled();
319+
boolean active = AppSettings.VIRTUAL_JOYSTICK_ENABLED.equals(virtualJoystickMode)
320+
|| (AppSettings.VIRTUAL_JOYSTICK_AUTO.equals(virtualJoystickMode)
321+
&& !physicalJoystickAvailable
322+
&& !keyboardSuppressedAutoJoystick
323+
&& virtualJoystick.hasInputBindings());
324+
if (wasEnabled != active) {
325+
virtualJoystick.setEnabled(active);
326+
}
218327
}
219328

220329
}

jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput14.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,12 @@ public void destroy() {
9090

9191
@Override
9292
public Joystick[] loadJoysticks(InputManager inputManager) {
93-
// load the simulated joystick for device orientation
93+
// load virtual joystick if enabled
9494
super.loadJoysticks(inputManager);
9595
// load physical gamepads/joysticks
96+
int beforePhysicalJoysticks = joystickList.size();
9697
joystickList.addAll(joystickJoyInput.loadJoysticks(joystickList.size(), inputManager));
98+
setPhysicalJoystickAvailable(joystickList.size() > beforePhysicalJoysticks);
9799
// return the list of joysticks back to InputManager
98100
return joystickList.toArray( new Joystick[joystickList.size()] );
99101
}

jme3-core/src/main/java/com/jme3/app/SimpleApplication.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,13 @@
3737
import com.jme3.font.BitmapFont;
3838
import com.jme3.font.BitmapText;
3939
import com.jme3.input.FlyByCamera;
40+
import com.jme3.input.Joystick;
4041
import com.jme3.input.KeyInput;
4142
import com.jme3.input.controls.ActionListener;
4243
import com.jme3.input.controls.KeyTrigger;
44+
import com.jme3.input.virtual.VirtualJoystick;
4345
import com.jme3.profile.AppStep;
46+
import com.jme3.renderer.Camera;
4447
import com.jme3.renderer.RenderManager;
4548
import com.jme3.renderer.queue.RenderQueue.Bucket;
4649
import com.jme3.scene.Node;
@@ -336,6 +339,7 @@ public void update() {
336339
if (prof != null) {
337340
prof.appStep(AppStep.SpatialUpdate);
338341
}
342+
updateVirtualJoystickVisuals(tpf);
339343
rootNode.updateLogicalState(tpf);
340344
guiNode.updateLogicalState(tpf);
341345

@@ -410,4 +414,22 @@ public void simpleUpdate(float tpf) {
410414
public void simpleRender(RenderManager rm) {
411415
// Default empty implementation; subclasses can override
412416
}
417+
418+
private void updateVirtualJoystickVisuals(float tpf) {
419+
if (inputManager == null || assetManager == null || guiViewPort == null) {
420+
return;
421+
}
422+
Joystick[] joysticks = inputManager.getJoysticks();
423+
if (joysticks == null) {
424+
return;
425+
}
426+
Camera guiCamera = guiViewPort.getCamera();
427+
int width = guiCamera != null ? guiCamera.getWidth() : settings.getWidth();
428+
int height = guiCamera != null ? guiCamera.getHeight() : settings.getHeight();
429+
for (Joystick joystick : joysticks) {
430+
if (joystick instanceof VirtualJoystick) {
431+
((VirtualJoystick) joystick).updateVisuals(guiNode, assetManager, width, height, tpf);
432+
}
433+
}
434+
}
413435
}

0 commit comments

Comments
 (0)