Fix joystick axis mapping issues with NVIDIA shield. Probably others.

Issues addressed:

a) Axis mappings were including virtual mouse axes on NVIDIA Shield TV.

The virtual mouse axes have the same axis numbers as the normal analog stick numbers. This was completely breaking joypad support on NVIDIA Shield TV.

b) Joypads were being tracked in a List with the index in the list being treated as the Godot device id.

If a device were to be removed, any device later in the list would be shifted, potentially causing future events with the shifted joypads to have incorrect IDs according to the Godot engine.

c) Unnecessary events were being sent to the Godot engine.

A check was added (per Joystick) that will prevent sending events for all axes when only a single axis value changed.
A similar check was added for "HATs".

See #45712
This commit is contained in:
Michael Conrad 2021-02-06 12:54:09 -05:00
parent fa2f7693bb
commit cc2547a9e9
2 changed files with 153 additions and 99 deletions

View file

@ -38,6 +38,8 @@ import org.godotengine.godot.input.InputManagerCompat.InputDeviceListener;
import android.os.Build;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.view.InputDevice;
import android.view.InputDevice.MotionRange;
import android.view.KeyEvent;
@ -46,17 +48,24 @@ import android.view.MotionEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Handles input related events for the {@link GodotRenderView} view.
*/
public class GodotInputHandler implements InputDeviceListener {
private final ArrayList<Joystick> mJoysticksDevices = new ArrayList<Joystick>();
private final GodotRenderView mRenderView;
private final InputManagerCompat mInputManager;
private final String tag = this.getClass().getSimpleName();
private final SparseIntArray mJoystickIds = new SparseIntArray(4);
private final SparseArray<Joystick> mJoysticksDevices = new SparseArray<Joystick>(4);
public GodotInputHandler(GodotRenderView godotView) {
mRenderView = godotView;
mInputManager = InputManagerCompat.Factory.getInputManager(mRenderView.getView().getContext());
@ -82,19 +91,20 @@ public class GodotInputHandler implements InputDeviceListener {
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
return false;
};
}
int source = event.getSource();
if (isKeyEvent_GameDevice(source)) {
final int button = getGodotButton(keyCode);
final int device_id = findJoystickDevice(event.getDeviceId());
// Check if the device exists
if (device_id > -1) {
final int deviceId = event.getDeviceId();
if (mJoystickIds.indexOfKey(deviceId) >= 0) {
final int button = getGodotButton(keyCode);
final int godotJoyId = mJoystickIds.get(deviceId);
queueEvent(new Runnable() {
@Override
public void run() {
GodotLib.joybutton(device_id, button, false);
GodotLib.joybutton(godotJoyId, button, false);
}
});
}
@ -107,7 +117,7 @@ public class GodotInputHandler implements InputDeviceListener {
GodotLib.key(keyCode, scanCode, chr, false);
}
});
};
}
return true;
}
@ -122,24 +132,25 @@ public class GodotInputHandler implements InputDeviceListener {
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
return false;
};
}
int source = event.getSource();
//Log.e(TAG, String.format("Key down! source %d, device %d, joystick %d, %d, %d", event.getDeviceId(), source, (source & InputDevice.SOURCE_JOYSTICK), (source & InputDevice.SOURCE_DPAD), (source & InputDevice.SOURCE_GAMEPAD)));
final int deviceId = event.getDeviceId();
// Check if source is a game device and that the device is a registered gamepad
if (isKeyEvent_GameDevice(source)) {
if (event.getRepeatCount() > 0) // ignore key echo
return true;
final int button = getGodotButton(keyCode);
final int device_id = findJoystickDevice(event.getDeviceId());
if (mJoystickIds.indexOfKey(deviceId) >= 0) {
final int button = getGodotButton(keyCode);
final int godotJoyId = mJoystickIds.get(deviceId);
// Check if the device exists
if (device_id > -1) {
queueEvent(new Runnable() {
@Override
public void run() {
GodotLib.joybutton(device_id, button, true);
GodotLib.joybutton(godotJoyId, button, true);
}
});
}
@ -152,7 +163,7 @@ public class GodotInputHandler implements InputDeviceListener {
GodotLib.key(keyCode, scanCode, chr, true);
}
});
};
}
return true;
}
@ -203,38 +214,52 @@ public class GodotInputHandler implements InputDeviceListener {
}
public boolean onGenericMotionEvent(MotionEvent event) {
if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK && event.getAction() == MotionEvent.ACTION_MOVE) {
final int device_id = findJoystickDevice(event.getDeviceId());
if (event.isFromSource(InputDevice.SOURCE_JOYSTICK) && event.getAction() == MotionEvent.ACTION_MOVE) {
// Check if the device exists
if (device_id > -1) {
Joystick joy = mJoysticksDevices.get(device_id);
final int deviceId = event.getDeviceId();
if (mJoystickIds.indexOfKey(deviceId) >= 0) {
final int godotJoyId = mJoystickIds.get(deviceId);
Joystick joystick = mJoysticksDevices.get(deviceId);
for (int i = 0; i < joy.axes.size(); i++) {
InputDevice.MotionRange range = joy.axes.get(i);
final float value = (event.getAxisValue(range.getAxis()) - range.getMin()) / range.getRange() * 2.0f - 1.0f;
final int idx = i;
queueEvent(new Runnable() {
@Override
public void run() {
GodotLib.joyaxis(device_id, idx, value);
}
});
for (int i = 0; i < joystick.axes.size(); i++) {
final int axis = joystick.axes.get(i);
final float value = event.getAxisValue(axis);
/**
* As all axes are polled for each event, only fire an axis event if the value has actually changed.
* Prevents flooding Godot with repeated events.
*/
if (joystick.axesValues.indexOfKey(axis) < 0 || (float)joystick.axesValues.get(axis) != value) {
// save value to prevent repeats
joystick.axesValues.put(axis, value);
final int godotAxisIdx = i;
queueEvent(new Runnable() {
@Override
public void run() {
GodotLib.joyaxis(godotJoyId, godotAxisIdx, value);
//Log.i(tag, "GodotLib.joyaxis("+godotJoyId+", "+godotAxisIdx+", "+value+");");
}
});
}
}
for (int i = 0; i < joy.hats.size(); i += 2) {
final int hatX = Math.round(event.getAxisValue(joy.hats.get(i).getAxis()));
final int hatY = Math.round(event.getAxisValue(joy.hats.get(i + 1).getAxis()));
queueEvent(new Runnable() {
@Override
public void run() {
GodotLib.joyhat(device_id, hatX, hatY);
}
});
if (joystick.hasAxisHat) {
final int hatX = Math.round(event.getAxisValue(MotionEvent.AXIS_HAT_X));
final int hatY = Math.round(event.getAxisValue(MotionEvent.AXIS_HAT_Y));
if (joystick.hatX != hatX || joystick.hatY != hatY) {
joystick.hatX = hatX;
joystick.hatY = hatY;
queueEvent(new Runnable() {
@Override
public void run() {
GodotLib.joyhat(godotJoyId, hatX, hatY);
//Log.i(tag, "GodotLib.joyhat("+godotJoyId+", "+hatX+", "+hatY+");");
}
});
}
}
return true;
}
} else if ((event.getSource() & InputDevice.SOURCE_STYLUS) == InputDevice.SOURCE_STYLUS) {
} else if (event.isFromSource(InputDevice.SOURCE_STYLUS)) {
final float x = event.getX();
final float y = event.getY();
final int type = event.getAction();
@ -245,6 +270,7 @@ public class GodotInputHandler implements InputDeviceListener {
}
});
return true;
} else if (event.isFromSource(InputDevice.SOURCE_MOUSE) || event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return handleMouseEvent(event);
@ -266,67 +292,98 @@ public class GodotInputHandler implements InputDeviceListener {
}
}
private int assignJoystickIdNumber(int deviceId) {
int godotJoyId = 0;
while (mJoystickIds.indexOfValue(godotJoyId) >= 0) {
godotJoyId++;
}
mJoystickIds.put(deviceId, godotJoyId);
return godotJoyId;
}
@Override
public void onInputDeviceAdded(int deviceId) {
int id = findJoystickDevice(deviceId);
// Check if the device has not been already added
if (id < 0) {
InputDevice device = mInputManager.getInputDevice(deviceId);
//device can be null if deviceId is not found
if (device != null) {
int sources = device.getSources();
if (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
((sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK)) {
id = mJoysticksDevices.size();
Joystick joy = new Joystick();
joy.device_id = deviceId;
joy.name = device.getName();
joy.axes = new ArrayList<InputDevice.MotionRange>();
joy.hats = new ArrayList<InputDevice.MotionRange>();
if (mJoystickIds.indexOfKey(deviceId) >= 0) {
return;
}
List<InputDevice.MotionRange> ranges = device.getMotionRanges();
Collections.sort(ranges, new RangeComparator());
InputDevice device = mInputManager.getInputDevice(deviceId);
//device can be null if deviceId is not found
if (device == null) {
return;
}
for (InputDevice.MotionRange range : ranges) {
if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {
joy.hats.add(range);
} else {
joy.axes.add(range);
}
}
int sources = device.getSources();
mJoysticksDevices.add(joy);
// Device may not be a joystick or gamepad
if ((sources & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD &&
(sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {
return;
}
final int device_id = id;
final String name = joy.name;
queueEvent(new Runnable() {
@Override
public void run() {
GodotLib.joyconnectionchanged(device_id, true, name);
}
});
// Assign first available number. Re-use numbers where possible.
final int id = assignJoystickIdNumber(deviceId);
final Joystick joystick = new Joystick();
joystick.device_id = deviceId;
joystick.name = device.getName();
//Helps with creating new joypad mappings.
Log.i(tag, "=== New Input Device: " + joystick.name);
Set<Integer> already = new HashSet<Integer>();
for (InputDevice.MotionRange range : device.getMotionRanges()) {
boolean isJoystick = range.isFromSource(InputDevice.SOURCE_JOYSTICK);
boolean isGamepad = range.isFromSource(InputDevice.SOURCE_GAMEPAD);
//Log.i(tag, "axis: "+range.getAxis()+ ", isJoystick: "+isJoystick+", isGamepad: "+isGamepad);
if (!isJoystick && !isGamepad) {
continue;
}
final int axis = range.getAxis();
if (axis == MotionEvent.AXIS_HAT_X || axis == MotionEvent.AXIS_HAT_Y) {
joystick.hasAxisHat = true;
} else {
if (!already.contains(axis)) {
already.add(axis);
joystick.axes.add(axis);
} else {
Log.w(tag, " - DUPLICATE AXIS VALUE IN LIST: " + axis);
}
}
}
Collections.sort(joystick.axes);
for (int idx = 0; idx < joystick.axes.size(); idx++) {
//Helps with creating new joypad mappings.
Log.i(tag, " - Mapping Android axis " + joystick.axes.get(idx) + " to Godot axis " + idx);
}
mJoysticksDevices.put(deviceId, joystick);
queueEvent(new Runnable() {
@Override
public void run() {
GodotLib.joyconnectionchanged(id, true, joystick.name);
}
});
}
@Override
public void onInputDeviceRemoved(int deviceId) {
final int device_id = findJoystickDevice(deviceId);
// Check if the evice has not been already removed
if (device_id > -1) {
mJoysticksDevices.remove(device_id);
queueEvent(new Runnable() {
@Override
public void run() {
GodotLib.joyconnectionchanged(device_id, false, "");
}
});
// Check if the device has not been already removed
if (mJoystickIds.indexOfKey(deviceId) < 0) {
return;
}
final int godotJoyId = mJoystickIds.get(deviceId);
mJoystickIds.delete(deviceId);
mJoysticksDevices.delete(deviceId);
queueEvent(new Runnable() {
@Override
public void run() {
GodotLib.joyconnectionchanged(godotJoyId, false, "");
}
});
}
@Override
@ -407,16 +464,6 @@ public class GodotInputHandler implements InputDeviceListener {
return button;
}
private int findJoystickDevice(int device_id) {
for (int i = 0; i < mJoysticksDevices.size(); i++) {
if (mJoysticksDevices.get(i).device_id == device_id) {
return i;
}
}
return -1;
}
private boolean handleMouseEvent(final MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_HOVER_ENTER:

View file

@ -30,9 +30,10 @@
package org.godotengine.godot.input;
import android.view.InputDevice.MotionRange;
import android.util.SparseArray;
import java.util.ArrayList;
import java.util.List;
/**
* POJO class to represent a Joystick input device.
@ -40,6 +41,12 @@ import java.util.ArrayList;
class Joystick {
int device_id;
String name;
ArrayList<MotionRange> axes;
ArrayList<MotionRange> hats;
List<Integer> axes = new ArrayList<Integer>();
protected boolean hasAxisHat = false;
/*
* Keep track of values so we can prevent flooding the engine with useless events.
*/
protected final SparseArray axesValues = new SparseArray<Float>(4);
protected int hatX;
protected int hatY;
}