diff --git a/.gitignore b/.gitignore index 06d7e04..5775f3e 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,8 @@ build/ ## OS Specific .DS_Store +.worktrees/ + local.properties diff --git a/gdx-controllers-android/src/com/badlogic/gdx/controllers/android/AndroidControllerEvent.java b/gdx-controllers-android/src/com/badlogic/gdx/controllers/android/AndroidControllerEvent.java deleted file mode 100644 index 2701f8d..0000000 --- a/gdx-controllers-android/src/com/badlogic/gdx/controllers/android/AndroidControllerEvent.java +++ /dev/null @@ -1,35 +0,0 @@ -/******************************************************************************* - * Copyright 2011 See AUTHORS file. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - ******************************************************************************/ - -package com.badlogic.gdx.controllers.android; - -class AndroidControllerEvent { - public static final int BUTTON_DOWN = 0; - public static final int BUTTON_UP = 1; - public static final int AXIS = 2; - public static final int CONNECTED = 4; - public static final int DISCONNECTED = 5; - - /** the controller the event belongs to **/ - public AndroidController controller; - /** the event type, see constants above **/ - public int type; - /** the code for the even source, e.g. button keycode, axis index **/ - public int code; - /** the axis value if this is an #AXIS event **/ - public float axisValue; - -} \ No newline at end of file diff --git a/gdx-controllers-android/src/com/badlogic/gdx/controllers/android/AndroidControllers.java b/gdx-controllers-android/src/com/badlogic/gdx/controllers/android/AndroidControllers.java index a8f0bca..ce24c58 100644 --- a/gdx-controllers-android/src/com/badlogic/gdx/controllers/android/AndroidControllers.java +++ b/gdx-controllers-android/src/com/badlogic/gdx/controllers/android/AndroidControllers.java @@ -23,29 +23,25 @@ import android.view.View.OnGenericMotionListener; import android.view.View.OnKeyListener; -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.LifecycleListener; -import com.badlogic.gdx.backends.android.AndroidInput; -import com.badlogic.gdx.controllers.AbstractControllerManager; -import com.badlogic.gdx.controllers.ControllerListener; -import com.badlogic.gdx.utils.Array; -import com.badlogic.gdx.utils.IntMap; -import com.badlogic.gdx.utils.IntMap.Entry; -import com.badlogic.gdx.utils.Pool; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.LifecycleListener; +import com.badlogic.gdx.backends.android.AndroidInput; +import com.badlogic.gdx.controllers.AbstractControllerManager; +import com.badlogic.gdx.controllers.ControllerListener; +import com.badlogic.gdx.controllers.event.ControllerEvent; +import com.badlogic.gdx.controllers.event.ControllerEventQueue; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.IntMap; +import com.badlogic.gdx.utils.IntMap.Entry; public class AndroidControllers extends AbstractControllerManager implements LifecycleListener, OnKeyListener, OnGenericMotionListener { private final static String TAG = "AndroidControllers"; public static boolean ignoreNoGamepadButtons = true; public static boolean useNewAxisLogic = true; - private final IntMap controllerMap = new IntMap(); - private final Array listeners = new Array(); - private final Array eventQueue = new Array(); - private final Pool eventPool = new Pool() { - @Override - protected AndroidControllerEvent newObject () { - return new AndroidControllerEvent(); - } - }; + private final IntMap controllerMap = new IntMap(); + private final Array listeners = new Array(); + private final ControllerEventQueue eventQueue = new ControllerEventQueue(); + private final Object dispatchLock = new Object(); public AndroidControllers() { listeners.add(new ManageCurrentControllerListener()); @@ -66,198 +62,150 @@ public AndroidControllers() { } } - private void setupEventQueue() { - new Runnable() { - @SuppressWarnings("synthetic-access") - @Override - public void run () { - synchronized(eventQueue) { - for(AndroidControllerEvent event: eventQueue) { - switch(event.type) { - case AndroidControllerEvent.CONNECTED: - controllers.add(event.controller); - for(ControllerListener listener: listeners) { - listener.connected(event.controller); - } - break; - case AndroidControllerEvent.DISCONNECTED: - controllers.removeValue(event.controller, true); - for(ControllerListener listener: listeners) { - listener.disconnected(event.controller); - } - for(ControllerListener listener: event.controller.getListeners()) { - listener.disconnected(event.controller); - } - break; - case AndroidControllerEvent.BUTTON_DOWN: - event.controller.buttons.put(event.code, event.code); - for(ControllerListener listener: listeners) { - if(listener.buttonDown(event.controller, event.code)) break; - } - for(ControllerListener listener: event.controller.getListeners()) { - if(listener.buttonDown(event.controller, event.code)) break; - } - break; - case AndroidControllerEvent.BUTTON_UP: - event.controller.buttons.remove(event.code, 0); - for(ControllerListener listener: listeners) { - if(listener.buttonUp(event.controller, event.code)) break; - } - for(ControllerListener listener: event.controller.getListeners()) { - if(listener.buttonUp(event.controller, event.code)) break; - } - break; - case AndroidControllerEvent.AXIS: - event.controller.axes[event.code] = event.axisValue; - for(ControllerListener listener: listeners) { - if(listener.axisMoved(event.controller, event.code, event.axisValue)) break; - } - for(ControllerListener listener: event.controller.getListeners()) { - if(listener.axisMoved(event.controller, event.code, event.axisValue)) break; - } - break; - default: - } - } - eventPool.freeAll(eventQueue); - eventQueue.clear(); - } - Gdx.app.postRunnable(this); - } - }.run(); - } + private void setupEventQueue() { + new Runnable() { + @SuppressWarnings("synthetic-access") + @Override + public void run () { + synchronized (dispatchLock) { + eventQueue.drain(new ControllerEventQueue.ControllerEventConsumer() { + @Override + public void consume(ControllerEvent event) { + switch(event.type) { + case ControllerEvent.CONNECTED: + controllers.add(event.controller); + for(ControllerListener listener: listeners) { + listener.connected(event.controller); + } + break; + case ControllerEvent.DISCONNECTED: + controllers.removeValue(event.controller, true); + for(ControllerListener listener: listeners) { + listener.disconnected(event.controller); + } + AndroidController disconnectedController = (AndroidController)event.controller; + for(ControllerListener listener: disconnectedController.getListeners()) { + listener.disconnected(disconnectedController); + } + break; + case ControllerEvent.BUTTON_DOWN: + AndroidController buttonDownController = (AndroidController)event.controller; + buttonDownController.buttons.put(event.code, event.code); + for(ControllerListener listener: listeners) { + if(listener.buttonDown(buttonDownController, event.code)) break; + } + for(ControllerListener listener: buttonDownController.getListeners()) { + if(listener.buttonDown(buttonDownController, event.code)) break; + } + break; + case ControllerEvent.BUTTON_UP: + AndroidController buttonUpController = (AndroidController)event.controller; + buttonUpController.buttons.remove(event.code, 0); + for(ControllerListener listener: listeners) { + if(listener.buttonUp(buttonUpController, event.code)) break; + } + for(ControllerListener listener: buttonUpController.getListeners()) { + if(listener.buttonUp(buttonUpController, event.code)) break; + } + break; + case ControllerEvent.AXIS: + AndroidController axisController = (AndroidController)event.controller; + axisController.axes[event.code] = event.amount; + for(ControllerListener listener: listeners) { + if(listener.axisMoved(axisController, event.code, event.amount)) break; + } + for(ControllerListener listener: axisController.getListeners()) { + if(listener.axisMoved(axisController, event.code, event.amount)) break; + } + break; + default: + } + } + }); + } + Gdx.app.postRunnable(this); + } + }.run(); + } @Override public boolean onGenericMotion (View view, MotionEvent motionEvent) { if((motionEvent.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0) return false; AndroidController controller = controllerMap.get(motionEvent.getDeviceId()); if(controller != null) { - synchronized(eventQueue) { - if (controller.hasPovAxis()) { - float povX = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_X); - float povY = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_Y); - // map axis movement to dpad buttons - if (povX != controller.povX) { - if (controller.povX == 1f) { - AndroidControllerEvent event = eventPool.obtain(); - event.controller = controller; - event.type = AndroidControllerEvent.BUTTON_UP; - event.code = KeyEvent.KEYCODE_DPAD_RIGHT; - eventQueue.add(event); - } else if (controller.povX == -1f) { - AndroidControllerEvent event = eventPool.obtain(); - event.controller = controller; - event.type = AndroidControllerEvent.BUTTON_UP; - event.code = KeyEvent.KEYCODE_DPAD_LEFT; - eventQueue.add(event); - } - - if (povX == 1f) { - AndroidControllerEvent event = eventPool.obtain(); - event.controller = controller; - event.type = AndroidControllerEvent.BUTTON_DOWN; - event.code = KeyEvent.KEYCODE_DPAD_RIGHT; - eventQueue.add(event); - } else if (povX == -1f) { - AndroidControllerEvent event = eventPool.obtain(); - event.controller = controller; - event.type = AndroidControllerEvent.BUTTON_DOWN; - event.code = KeyEvent.KEYCODE_DPAD_LEFT; - eventQueue.add(event); - } - controller.povX = povX; - } - - if (povY != controller.povY) { - if (controller.povY == 1f) { - AndroidControllerEvent event = eventPool.obtain(); - event.controller = controller; - event.type = AndroidControllerEvent.BUTTON_UP; - event.code = KeyEvent.KEYCODE_DPAD_DOWN; - eventQueue.add(event); - } else if (controller.povY == -1f) { - AndroidControllerEvent event = eventPool.obtain(); - event.controller = controller; - event.type = AndroidControllerEvent.BUTTON_UP; - event.code = KeyEvent.KEYCODE_DPAD_UP; - eventQueue.add(event); - } - - if (povY == 1f) { - AndroidControllerEvent event = eventPool.obtain(); - event.controller = controller; - event.type = AndroidControllerEvent.BUTTON_DOWN; - event.code = KeyEvent.KEYCODE_DPAD_DOWN; - eventQueue.add(event); - } else if (povY == -1f) { - AndroidControllerEvent event = eventPool.obtain(); - event.controller = controller; - event.type = AndroidControllerEvent.BUTTON_DOWN; - event.code = KeyEvent.KEYCODE_DPAD_UP; - eventQueue.add(event); - } - controller.povY = povY; + synchronized(dispatchLock) { + if (controller.hasPovAxis()) { + float povX = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_X); + float povY = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_Y); + // map axis movement to dpad buttons + if (povX != controller.povX) { + if (controller.povX == 1f) { + eventQueue.enqueueButtonUp(controller, KeyEvent.KEYCODE_DPAD_RIGHT, 0f); + } else if (controller.povX == -1f) { + eventQueue.enqueueButtonUp(controller, KeyEvent.KEYCODE_DPAD_LEFT, 0f); + } + + if (povX == 1f) { + eventQueue.enqueueButtonDown(controller, KeyEvent.KEYCODE_DPAD_RIGHT, 1f); + } else if (povX == -1f) { + eventQueue.enqueueButtonDown(controller, KeyEvent.KEYCODE_DPAD_LEFT, 1f); + } + controller.povX = povX; + } + + if (povY != controller.povY) { + if (controller.povY == 1f) { + eventQueue.enqueueButtonUp(controller, KeyEvent.KEYCODE_DPAD_DOWN, 0f); + } else if (controller.povY == -1f) { + eventQueue.enqueueButtonUp(controller, KeyEvent.KEYCODE_DPAD_UP, 0f); + } + + if (povY == 1f) { + eventQueue.enqueueButtonDown(controller, KeyEvent.KEYCODE_DPAD_DOWN, 1f); + } else if (povY == -1f) { + eventQueue.enqueueButtonDown(controller, KeyEvent.KEYCODE_DPAD_UP, 1f); + } + controller.povY = povY; } } - if (controller.hasTriggerAxis()){ - float lTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_LTRIGGER); - float rTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_RTRIGGER); - //map axis movement to trigger buttons - if (lTrigger != controller.lTrigger){ - if (lTrigger == 1){ - AndroidControllerEvent event = eventPool.obtain(); - event.controller = controller; - event.type = AndroidControllerEvent.BUTTON_DOWN; - event.code = KeyEvent.KEYCODE_BUTTON_L2; - eventQueue.add(event); - } else if (lTrigger == 0){ - AndroidControllerEvent event = eventPool.obtain(); - event.controller = controller; - event.type = AndroidControllerEvent.BUTTON_UP; - event.code = KeyEvent.KEYCODE_BUTTON_L2; - eventQueue.add(event); - } - controller.lTrigger = lTrigger; + if (controller.hasTriggerAxis()){ + float lTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_LTRIGGER); + float rTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_RTRIGGER); + //map axis movement to trigger buttons + if (lTrigger != controller.lTrigger){ + if (lTrigger == 1){ + eventQueue.enqueueButtonDown(controller, KeyEvent.KEYCODE_BUTTON_L2, 1f); + } else if (lTrigger == 0){ + eventQueue.enqueueButtonUp(controller, KeyEvent.KEYCODE_BUTTON_L2, 0f); + } + controller.lTrigger = lTrigger; } - if (rTrigger != controller.rTrigger){ - if (rTrigger == 1){ - AndroidControllerEvent event = eventPool.obtain(); - event.controller = controller; - event.type = AndroidControllerEvent.BUTTON_DOWN; - event.code = KeyEvent.KEYCODE_BUTTON_R2; - eventQueue.add(event); - } else if (rTrigger == 0){ - AndroidControllerEvent event = eventPool.obtain(); - event.controller = controller; - event.type = AndroidControllerEvent.BUTTON_UP; - event.code = KeyEvent.KEYCODE_BUTTON_R2; - eventQueue.add(event); - } - controller.rTrigger = rTrigger; + if (rTrigger != controller.rTrigger){ + if (rTrigger == 1){ + eventQueue.enqueueButtonDown(controller, KeyEvent.KEYCODE_BUTTON_R2, 1f); + } else if (rTrigger == 0){ + eventQueue.enqueueButtonUp(controller, KeyEvent.KEYCODE_BUTTON_R2, 0f); + } + controller.rTrigger = rTrigger; } } - int axisIndex = 0; - for (int axisId: controller.axesIds) { - float axisValue = motionEvent.getAxisValue(axisId); - if(controller.getAxis(axisIndex) == axisValue) { - axisIndex++; - continue; - } - AndroidControllerEvent event = eventPool.obtain(); - event.type = AndroidControllerEvent.AXIS; - event.controller = controller; - event.code = axisIndex; - event.axisValue = axisValue; - eventQueue.add(event); - axisIndex++; - } - } + int axisIndex = 0; + for (int axisId: controller.axesIds) { + float axisValue = motionEvent.getAxisValue(axisId); + if(controller.getAxis(axisIndex) == axisValue) { + axisIndex++; + continue; + } + eventQueue.enqueueAxis(controller, axisIndex, axisValue); + axisIndex++; + } + } return true; } return false; @@ -277,17 +225,13 @@ public boolean onKey (View view, int keyCode, KeyEvent keyEvent) { if (controller.hasTriggerAxis() && (keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2)){ return true; } - synchronized(eventQueue) { - AndroidControllerEvent event = eventPool.obtain(); - event.controller = controller; - if(keyEvent.getAction() == KeyEvent.ACTION_DOWN) { - event.type = AndroidControllerEvent.BUTTON_DOWN; - } else { - event.type = AndroidControllerEvent.BUTTON_UP; - } - event.code = keyCode; - eventQueue.add(event); - } + synchronized(dispatchLock) { + if(keyEvent.getAction() == KeyEvent.ACTION_DOWN) { + eventQueue.enqueueButtonDown(controller, keyCode, 1f); + } else { + eventQueue.enqueueButtonUp(controller, keyCode, 0f); + } + } return keyCode != KeyEvent.KEYCODE_BACK || Gdx.input.isCatchKey(keyCode); } else { return false; @@ -319,16 +263,13 @@ protected void addController(int deviceId, boolean sendEvent) { if (!isController(device)) return; String name = device.getName(); AndroidController controller = new AndroidController(deviceId, name); - controllerMap.put(deviceId, controller); - if (sendEvent) { - synchronized (eventQueue) { - AndroidControllerEvent event = eventPool.obtain(); - event.type = AndroidControllerEvent.CONNECTED; - event.controller = controller; - eventQueue.add(event); - } - } else { - controllers.add(controller); + controllerMap.put(deviceId, controller); + if (sendEvent) { + synchronized (dispatchLock) { + eventQueue.enqueueConnected(controller); + } + } else { + controllers.add(controller); } Gdx.app.log(TAG, "added controller '" + name + "'"); } catch (RuntimeException e) { @@ -339,18 +280,15 @@ protected void addController(int deviceId, boolean sendEvent) { } } - protected void removeController(int deviceId) { - AndroidController controller = controllerMap.remove(deviceId); - if(controller != null) { - synchronized(eventQueue) { - AndroidControllerEvent event = eventPool.obtain(); - controller.connected = false; - event.type = AndroidControllerEvent.DISCONNECTED; - event.controller = controller; - eventQueue.add(event); - } - Gdx.app.log(TAG, "removed controller '" + controller.getName() + "'"); - } + protected void removeController(int deviceId) { + AndroidController controller = controllerMap.remove(deviceId); + if(controller != null) { + synchronized(dispatchLock) { + controller.connected = false; + eventQueue.enqueueDisconnected(controller); + } + Gdx.app.log(TAG, "removed controller '" + controller.getName() + "'"); + } } private boolean isController(InputDevice device) { @@ -360,19 +298,19 @@ private boolean isController(InputDevice device) { && !"uinput-fpc".equals(device.getName()); } - @Override - public void addListener (ControllerListener listener) { - synchronized(eventQueue) { - listeners.add(listener); - } - } - - @Override - public void removeListener (ControllerListener listener) { - synchronized(eventQueue) { - listeners.removeValue(listener, true); - } - } + @Override + public void addListener (ControllerListener listener) { + synchronized(dispatchLock) { + listeners.add(listener); + } + } + + @Override + public void removeListener (ControllerListener listener) { + synchronized(dispatchLock) { + listeners.removeValue(listener, true); + } + } @Override public void pause () { @@ -399,4 +337,4 @@ public void clearListeners () { public Array getListeners () { return listeners; } -} \ No newline at end of file +} diff --git a/gdx-controllers-core/build.gradle b/gdx-controllers-core/build.gradle index c439403..349a4fc 100644 --- a/gdx-controllers-core/build.gradle +++ b/gdx-controllers-core/build.gradle @@ -6,6 +6,7 @@ eclipse { dependencies { implementation "com.badlogicgames.gdx:gdx:$gdxVersion" + testImplementation 'junit:junit:4.13.2' } targetCompatibility = 1.7 @@ -15,4 +16,3 @@ sourceCompatibility = 1.7 ext { ARTIFACTID = 'gdx-controllers-core' } - diff --git a/gdx-controllers-core/src/main/java/com/badlogic/gdx/controllers/event/ControllerEvent.java b/gdx-controllers-core/src/main/java/com/badlogic/gdx/controllers/event/ControllerEvent.java new file mode 100644 index 0000000..c8c2a4d --- /dev/null +++ b/gdx-controllers-core/src/main/java/com/badlogic/gdx/controllers/event/ControllerEvent.java @@ -0,0 +1,25 @@ +package com.badlogic.gdx.controllers.event; + +import com.badlogic.gdx.controllers.Controller; +import com.badlogic.gdx.utils.Pool; + +public final class ControllerEvent implements Pool.Poolable { + public static final int CONNECTED = 1; + public static final int DISCONNECTED = 2; + public static final int BUTTON_DOWN = 3; + public static final int BUTTON_UP = 4; + public static final int AXIS = 5; + + public Controller controller; + public int type; + public int code; + public float amount; + + @Override + public void reset() { + controller = null; + type = 0; + code = 0; + amount = 0f; + } +} diff --git a/gdx-controllers-core/src/main/java/com/badlogic/gdx/controllers/event/ControllerEventQueue.java b/gdx-controllers-core/src/main/java/com/badlogic/gdx/controllers/event/ControllerEventQueue.java new file mode 100644 index 0000000..fd3a2dd --- /dev/null +++ b/gdx-controllers-core/src/main/java/com/badlogic/gdx/controllers/event/ControllerEventQueue.java @@ -0,0 +1,60 @@ +package com.badlogic.gdx.controllers.event; + +import com.badlogic.gdx.controllers.Controller; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.Pool; + +public final class ControllerEventQueue { + private final Array queue = new Array(); + private final Pool eventPool = new Pool() { + @Override + protected ControllerEvent newObject() { + return new ControllerEvent(); + } + }; + + public void enqueueConnected(Controller controller) { + enqueue(controller, ControllerEvent.CONNECTED, 0, 0f); + } + + public void enqueueDisconnected(Controller controller) { + enqueue(controller, ControllerEvent.DISCONNECTED, 0, 0f); + } + + public void enqueueButtonDown(Controller controller, int code, float amount) { + enqueue(controller, ControllerEvent.BUTTON_DOWN, code, amount); + } + + public void enqueueButtonUp(Controller controller, int code, float amount) { + enqueue(controller, ControllerEvent.BUTTON_UP, code, amount); + } + + public void enqueueAxis(Controller controller, int axis, float amount) { + enqueue(controller, ControllerEvent.AXIS, axis, amount); + } + + public void drain(ControllerEventConsumer consumer) { + synchronized (queue) { + for (ControllerEvent event : queue) { + consumer.consume(event); + } + eventPool.freeAll(queue); + queue.clear(); + } + } + + private void enqueue(Controller controller, int type, int code, float amount) { + synchronized (queue) { + ControllerEvent event = eventPool.obtain(); + event.controller = controller; + event.type = type; + event.code = code; + event.amount = amount; + queue.add(event); + } + } + + public interface ControllerEventConsumer { + void consume(ControllerEvent event); + } +} diff --git a/gdx-controllers-core/src/test/java/com/badlogic/gdx/controllers/event/ControllerEventQueueTest.java b/gdx-controllers-core/src/test/java/com/badlogic/gdx/controllers/event/ControllerEventQueueTest.java new file mode 100644 index 0000000..5cdc748 --- /dev/null +++ b/gdx-controllers-core/src/test/java/com/badlogic/gdx/controllers/event/ControllerEventQueueTest.java @@ -0,0 +1,193 @@ +package com.badlogic.gdx.controllers.event; + +import com.badlogic.gdx.controllers.Controller; +import com.badlogic.gdx.controllers.ControllerListener; +import com.badlogic.gdx.controllers.ControllerMapping; +import com.badlogic.gdx.controllers.ControllerPowerLevel; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +public class ControllerEventQueueTest { + + @Test + public void keepsInsertionOrder() { + ControllerEventQueue queue = new ControllerEventQueue(); + Controller controller = new StubController("pad-1"); + + queue.enqueueConnected(controller); + queue.enqueueButtonDown(controller, 4, 1f); + + List events = drain(queue); + + Assert.assertEquals(2, events.size()); + Assert.assertEquals(ControllerEvent.CONNECTED, events.get(0).type); + Assert.assertEquals(ControllerEvent.BUTTON_DOWN, events.get(1).type); + } + + @Test + public void canDrainRepeatedly() { + ControllerEventQueue queue = new ControllerEventQueue(); + Controller controller = new StubController("pad-1"); + + queue.enqueueConnected(controller); + List firstDrain = drain(queue); + List secondDrain = drain(queue); + + queue.enqueueDisconnected(controller); + List thirdDrain = drain(queue); + + Assert.assertEquals(1, firstDrain.size()); + Assert.assertEquals(ControllerEvent.CONNECTED, firstDrain.get(0).type); + Assert.assertEquals(0, secondDrain.size()); + Assert.assertEquals(1, thirdDrain.size()); + Assert.assertEquals(ControllerEvent.DISCONNECTED, thirdDrain.get(0).type); + } + + @Test + public void keepsPayloadValues() { + ControllerEventQueue queue = new ControllerEventQueue(); + Controller controller = new StubController("pad-1"); + + queue.enqueueButtonUp(controller, 8, 0.5f); + queue.enqueueAxis(controller, 2, -0.25f); + + List events = drain(queue); + + Assert.assertEquals(2, events.size()); + assertEvent(events.get(0), ControllerEvent.BUTTON_UP, controller, 8, 0.5f); + assertEvent(events.get(1), ControllerEvent.AXIS, controller, 2, -0.25f); + } + + private static List drain(ControllerEventQueue queue) { + final List snapshots = new ArrayList(); + + queue.drain(new ControllerEventQueue.ControllerEventConsumer() { + @Override + public void consume(ControllerEvent event) { + EventSnapshot snapshot = new EventSnapshot(); + snapshot.type = event.type; + snapshot.controller = event.controller; + snapshot.code = event.code; + snapshot.amount = event.amount; + snapshots.add(snapshot); + } + }); + + return snapshots; + } + + private static void assertEvent(EventSnapshot event, int type, Controller controller, int code, float amount) { + Assert.assertEquals(type, event.type); + Assert.assertSame(controller, event.controller); + Assert.assertEquals(code, event.code); + Assert.assertEquals(amount, event.amount, 0.0001f); + } + + private static class EventSnapshot { + int type; + Controller controller; + int code; + float amount; + } + + private static class StubController implements Controller { + private final String id; + + private StubController(String id) { + this.id = id; + } + + @Override + public boolean getButton(int buttonCode) { + return false; + } + + @Override + public float getAxis(int axisCode) { + return 0; + } + + @Override + public String getName() { + return id; + } + + @Override + public String getUniqueId() { + return id; + } + + @Override + public int getMinButtonIndex() { + return 0; + } + + @Override + public int getMaxButtonIndex() { + return 0; + } + + @Override + public int getAxisCount() { + return 0; + } + + @Override + public boolean isConnected() { + return true; + } + + @Override + public boolean canVibrate() { + return false; + } + + @Override + public boolean isVibrating() { + return false; + } + + @Override + public void startVibration(int duration, float strength) { + } + + @Override + public void cancelVibration() { + } + + @Override + public boolean supportsPlayerIndex() { + return false; + } + + @Override + public int getPlayerIndex() { + return PLAYER_IDX_UNSET; + } + + @Override + public void setPlayerIndex(int index) { + } + + @Override + public ControllerMapping getMapping() { + return null; + } + + @Override + public ControllerPowerLevel getPowerLevel() { + return ControllerPowerLevel.POWER_UNKNOWN; + } + + @Override + public void addListener(ControllerListener listener) { + } + + @Override + public void removeListener(ControllerListener listener) { + } + } +} diff --git a/gdx-controllers-desktop/src/main/java/com/badlogic/gdx/controllers/desktop/JamepadControllerManager.java b/gdx-controllers-desktop/src/main/java/com/badlogic/gdx/controllers/desktop/JamepadControllerManager.java index d7706b2..84e843a 100644 --- a/gdx-controllers-desktop/src/main/java/com/badlogic/gdx/controllers/desktop/JamepadControllerManager.java +++ b/gdx-controllers-desktop/src/main/java/com/badlogic/gdx/controllers/desktop/JamepadControllerManager.java @@ -20,6 +20,7 @@ public class JamepadControllerManager extends AbstractControllerManager implemen private static com.studiohartman.jamepad.ControllerManager controllerManager; private final CompositeControllerListener compositeListener = new CompositeControllerListener(); + private JamepadControllerMonitor monitor; public JamepadControllerManager() { compositeListener.addListener(new ManageControllers()); @@ -32,10 +33,10 @@ public JamepadControllerManager() { controllerManager = new com.studiohartman.jamepad.ControllerManager(jamepadConfiguration); controllerManager.initSDLGamepad(); - JamepadControllerMonitor monitor = new JamepadControllerMonitor(controllerManager, compositeListener); - monitor.run(); + monitor = new JamepadControllerMonitor(controllerManager, compositeListener); + monitor.start(); - Gdx.app.addLifecycleListener(new JamepadShutdownHook(controllerManager)); + Gdx.app.addLifecycleListener(new JamepadShutdownHook(controllerManager, monitor)); nativeLibInitialized = true; } @@ -66,6 +67,10 @@ public void clearListeners() { @Override public void dispose() { + if (monitor != null) { + monitor.stop(); + monitor = null; + } controllerManager.quitSDLGamepad(); } diff --git a/gdx-controllers-desktop/src/main/java/com/badlogic/gdx/controllers/desktop/support/JamepadControllerMonitor.java b/gdx-controllers-desktop/src/main/java/com/badlogic/gdx/controllers/desktop/support/JamepadControllerMonitor.java index 4694e81..204f977 100644 --- a/gdx-controllers-desktop/src/main/java/com/badlogic/gdx/controllers/desktop/support/JamepadControllerMonitor.java +++ b/gdx-controllers-desktop/src/main/java/com/badlogic/gdx/controllers/desktop/support/JamepadControllerMonitor.java @@ -10,12 +10,19 @@ import com.studiohartman.jamepad.ControllerUnpluggedException; public class JamepadControllerMonitor implements Runnable { + private static final long POLL_SLEEP_MS = 4L; + private final ControllerManager controllerManager; private final ControllerListener listener; private final IntMap indexToController = new IntMap<>(JamepadControllerManager.jamepadConfiguration.maxNumControllers); // temporary array for delaying connect messages private final Array connectedControllers = new Array(); + private final Object stateLock = new Object(); + + private volatile boolean running = true; + private volatile boolean pendingReconcile = false; + private Thread pollingThread; public JamepadControllerMonitor(ControllerManager controllerManager, ControllerListener listener) { this.controllerManager = controllerManager; @@ -24,17 +31,67 @@ public JamepadControllerMonitor(ControllerManager controllerManager, ControllerL reconcileControllers(); } + public void start() { + pollingThread = new Thread(new Runnable() { + @Override + public void run() { + while (running) { + boolean controllersChanged; + + synchronized (stateLock) { + controllersChanged = controllerManager.update(); + } + + if (controllersChanged) { + pendingReconcile = true; + } + + try { + Thread.sleep(POLL_SLEEP_MS); + } catch (InterruptedException ignored) { + // stop requested + } + } + } + }, "gdx-jamepad-monitor"); + pollingThread.setDaemon(true); + pollingThread.start(); + + Gdx.app.postRunnable(this); + } + + public void stop() { + running = false; + + if (pollingThread != null) { + pollingThread.interrupt(); + try { + pollingThread.join(); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + pollingThread = null; + } + } + @Override public void run() { - boolean controllersChanged = controllerManager.update(); + if (!running) { + return; + } - if (controllersChanged) { - reconcileControllers(); + if (pendingReconcile) { + synchronized (stateLock) { + reconcileControllers(); + pendingReconcile = false; + } } update(); - Gdx.app.postRunnable(this); + if (running) { + Gdx.app.postRunnable(this); + } } private void reconcileControllers() { diff --git a/gdx-controllers-desktop/src/main/java/com/badlogic/gdx/controllers/desktop/support/JamepadShutdownHook.java b/gdx-controllers-desktop/src/main/java/com/badlogic/gdx/controllers/desktop/support/JamepadShutdownHook.java index 9d930e5..f9ed1d3 100644 --- a/gdx-controllers-desktop/src/main/java/com/badlogic/gdx/controllers/desktop/support/JamepadShutdownHook.java +++ b/gdx-controllers-desktop/src/main/java/com/badlogic/gdx/controllers/desktop/support/JamepadShutdownHook.java @@ -5,9 +5,11 @@ public class JamepadShutdownHook implements LifecycleListener { private final ControllerManager controllerManager; + private final JamepadControllerMonitor monitor; - public JamepadShutdownHook(ControllerManager controllerManager) { + public JamepadShutdownHook(ControllerManager controllerManager, JamepadControllerMonitor monitor) { this.controllerManager = controllerManager; + this.monitor = monitor; } @Override @@ -22,6 +24,7 @@ public void resume() { @Override public void dispose() { + monitor.stop(); controllerManager.quitSDLGamepad(); } } diff --git a/gdx-controllers-gwt/src/main/java/com/badlogic/gdx/controllers/gwt/GwtControllerEvent.java b/gdx-controllers-gwt/src/main/java/com/badlogic/gdx/controllers/gwt/GwtControllerEvent.java deleted file mode 100644 index a203639..0000000 --- a/gdx-controllers-gwt/src/main/java/com/badlogic/gdx/controllers/gwt/GwtControllerEvent.java +++ /dev/null @@ -1,36 +0,0 @@ -/******************************************************************************* - * Copyright 2011 See AUTHORS file. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - ******************************************************************************/ - -package com.badlogic.gdx.controllers.gwt; - -public class GwtControllerEvent { - - public static final int BUTTON_DOWN = 0; - public static final int BUTTON_UP = 1; - public static final int AXIS = 2; - public static final int CONNECTED = 4; - public static final int DISCONNECTED = 5; - - /** the controller the event belongs to **/ - public GwtController controller; - /** the event type, see constants above **/ - public int type; - /** the code for the even source, e.g. button keycode, axis index **/ - public int code; - /** the value if this is an #AXIS, a #BUTTON_DOWN, or a #BUTTON_UP event **/ - public float amount; - -} \ No newline at end of file diff --git a/gdx-controllers-gwt/src/main/java/com/badlogic/gdx/controllers/gwt/GwtControllers.java b/gdx-controllers-gwt/src/main/java/com/badlogic/gdx/controllers/gwt/GwtControllers.java index f444b92..e9cc8ce 100644 --- a/gdx-controllers-gwt/src/main/java/com/badlogic/gdx/controllers/gwt/GwtControllers.java +++ b/gdx-controllers-gwt/src/main/java/com/badlogic/gdx/controllers/gwt/GwtControllers.java @@ -19,13 +19,14 @@ import com.badlogic.gdx.Gdx; import com.badlogic.gdx.controllers.AbstractControllerManager; import com.badlogic.gdx.controllers.ControllerListener; +import com.badlogic.gdx.controllers.event.ControllerEvent; +import com.badlogic.gdx.controllers.event.ControllerEventQueue; import com.badlogic.gdx.controllers.gwt.support.Gamepad; import com.badlogic.gdx.controllers.gwt.support.GamepadButton; import com.badlogic.gdx.controllers.gwt.support.GamepadSupport; import com.badlogic.gdx.controllers.gwt.support.GamepadSupportListener; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.IntMap; -import com.badlogic.gdx.utils.Pool; import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.JsArrayNumber; @@ -33,13 +34,8 @@ public class GwtControllers extends AbstractControllerManager implements Gamepad private final IntMap controllerMap = new IntMap(); private final Array listeners = new Array(); - private final Array eventQueue = new Array(); - private final Pool eventPool = new Pool() { - @Override - protected GwtControllerEvent newObject () { - return new GwtControllerEvent(); - } - }; + private final ControllerEventQueue eventQueue = new ControllerEventQueue(); + private final Object dispatchLock = new Object(); public GwtControllers () { GamepadSupport.init(this); @@ -52,56 +48,61 @@ public void setupEventQueue () { @SuppressWarnings("synthetic-access") @Override public void run () { - synchronized (eventQueue) { - for (GwtControllerEvent event : eventQueue) { - switch (event.type) { - case GwtControllerEvent.CONNECTED: - controllers.add(event.controller); - for (ControllerListener listener : listeners) { - listener.connected(event.controller); - } - break; - case GwtControllerEvent.DISCONNECTED: - controllers.removeValue(event.controller, true); - for (ControllerListener listener : listeners) { - listener.disconnected(event.controller); - } - for (ControllerListener listener : event.controller.getListeners()) { - listener.disconnected(event.controller); - } - break; - case GwtControllerEvent.BUTTON_DOWN: - event.controller.buttons.put(event.code, event.amount); - for (ControllerListener listener : listeners) { - if (listener.buttonDown(event.controller, event.code)) break; - } - for (ControllerListener listener : event.controller.getListeners()) { - if (listener.buttonDown(event.controller, event.code)) break; - } - break; - case GwtControllerEvent.BUTTON_UP: - event.controller.buttons.remove(event.code, event.amount); - for (ControllerListener listener : listeners) { - if (listener.buttonUp(event.controller, event.code)) break; + synchronized (dispatchLock) { + eventQueue.drain(new ControllerEventQueue.ControllerEventConsumer() { + @Override + public void consume(ControllerEvent event) { + switch (event.type) { + case ControllerEvent.CONNECTED: + controllers.add(event.controller); + for (ControllerListener listener : listeners) { + listener.connected(event.controller); + } + break; + case ControllerEvent.DISCONNECTED: + controllers.removeValue(event.controller, true); + for (ControllerListener listener : listeners) { + listener.disconnected(event.controller); + } + GwtController disconnectedController = (GwtController)event.controller; + for (ControllerListener listener : disconnectedController.getListeners()) { + listener.disconnected(disconnectedController); + } + break; + case ControllerEvent.BUTTON_DOWN: + GwtController buttonDownController = (GwtController)event.controller; + buttonDownController.buttons.put(event.code, event.amount); + for (ControllerListener listener : listeners) { + if (listener.buttonDown(buttonDownController, event.code)) break; + } + for (ControllerListener listener : buttonDownController.getListeners()) { + if (listener.buttonDown(buttonDownController, event.code)) break; + } + break; + case ControllerEvent.BUTTON_UP: + GwtController buttonUpController = (GwtController)event.controller; + buttonUpController.buttons.remove(event.code, event.amount); + for (ControllerListener listener : listeners) { + if (listener.buttonUp(buttonUpController, event.code)) break; + } + for (ControllerListener listener : buttonUpController.getListeners()) { + if (listener.buttonUp(buttonUpController, event.code)) break; + } + break; + case ControllerEvent.AXIS: + GwtController axisController = (GwtController)event.controller; + axisController.axes[event.code] = event.amount; + for (ControllerListener listener : listeners) { + if (listener.axisMoved(axisController, event.code, event.amount)) break; + } + for (ControllerListener listener : axisController.getListeners()) { + if (listener.axisMoved(axisController, event.code, event.amount)) break; + } + break; + default: } - for (ControllerListener listener : event.controller.getListeners()) { - if (listener.buttonUp(event.controller, event.code)) break; - } - break; - case GwtControllerEvent.AXIS: - event.controller.axes[event.code] = event.amount; - for (ControllerListener listener : listeners) { - if (listener.axisMoved(event.controller, event.code, event.amount)) break; - } - for (ControllerListener listener : event.controller.getListeners()) { - if (listener.axisMoved(event.controller, event.code, event.amount)) break; - } - break; - default: } - } - eventPool.freeAll(eventQueue); - eventQueue.clear(); + }); } Gdx.app.postRunnable(this); } @@ -110,14 +111,14 @@ public void run () { @Override public void addListener (ControllerListener listener) { - synchronized (eventQueue) { + synchronized (dispatchLock) { listeners.add(listener); } } @Override public void removeListener (ControllerListener listener) { - synchronized (eventQueue) { + synchronized (dispatchLock) { listeners.removeValue(listener, true); } } @@ -127,11 +128,8 @@ public void onGamepadConnected (int index) { Gamepad gamepad = Gamepad.getGamepad(index); GwtController controller = new GwtController(gamepad.getIndex(), gamepad.getId()); controllerMap.put(index, controller); - synchronized (eventQueue) { - GwtControllerEvent event = eventPool.obtain(); - event.type = GwtControllerEvent.CONNECTED; - event.controller = controller; - eventQueue.add(event); + synchronized (dispatchLock) { + eventQueue.enqueueConnected(controller); } } @@ -139,12 +137,9 @@ public void onGamepadConnected (int index) { public void onGamepadDisconnected (int index) { GwtController controller = controllerMap.remove(index); if (controller != null) { - synchronized (eventQueue) { + synchronized (dispatchLock) { controller.connected = false; - GwtControllerEvent event = eventPool.obtain(); - event.type = GwtControllerEvent.DISCONNECTED; - event.controller = controller; - eventQueue.add(event); + eventQueue.enqueueDisconnected(controller); } } } @@ -157,17 +152,12 @@ public void onGamepadUpdated (int index) { // Determine what changed JsArrayNumber axes = gamepad.getAxes(); JsArray buttons = gamepad.getButtons(); - synchronized (eventQueue) { + synchronized (dispatchLock) { for (int i = 0, j = axes.length(); i < j; i++) { float oldAxis = controller.getAxis(i); float newAxis = (float)axes.get(i); if (oldAxis != newAxis) { - GwtControllerEvent event = eventPool.obtain(); - event.type = GwtControllerEvent.AXIS; - event.controller = controller; - event.code = i; - event.amount = newAxis; - eventQueue.add(event); + eventQueue.enqueueAxis(controller, i, newAxis); } } for (int i = 0, j = buttons.length(); i < j; i++) { @@ -179,12 +169,11 @@ public void onGamepadUpdated (int index) { continue; } - GwtControllerEvent event = eventPool.obtain(); - event.type = newButton >= 0.5f ? GwtControllerEvent.BUTTON_DOWN : GwtControllerEvent.BUTTON_UP; - event.controller = controller; - event.code = i; - event.amount = newButton; - eventQueue.add(event); + if (newButton >= 0.5f) { + eventQueue.enqueueButtonDown(controller, i, newButton); + } else { + eventQueue.enqueueButtonUp(controller, i, newButton); + } } } } diff --git a/gdx-controllers-ios/src/com/badlogic/gdx/controllers/IosController.java b/gdx-controllers-ios/src/com/badlogic/gdx/controllers/IosController.java index aed7649..2e59552 100644 --- a/gdx-controllers-ios/src/com/badlogic/gdx/controllers/IosController.java +++ b/gdx-controllers-ios/src/com/badlogic/gdx/controllers/IosController.java @@ -45,9 +45,9 @@ public class IosController extends AbstractController { private final float[] axisValues; private long lastPausePressedMs = 0; - private CHHapticEngine hapticEngine; - private CHHapticPatternPlayer playingHapticPattern; - private long vibrationEndMs; + private CHHapticEngine hapticEngine; + private CHHapticPatternPlayer playingHapticPattern; + private long vibrationEndMs; public IosController(GCController controller) { this.controller = controller; @@ -80,15 +80,23 @@ public void invoke(GCGamepad gcGamepad, GCControllerElement gcControllerElement) } }); - if (Foundation.getMajorSystemVersion() >= 14) try { - if (controller.getHaptics()!=null) { - hapticEngine = controller.getHaptics().createEngine(GCHapticsLocality.Default); - hapticEngine.retain(); - } - } catch (Throwable t) { - Gdx.app.error("Controllers", "Failed to create haptics engine", t); - } - } + // lazily initialize haptics to keep connect path lightweight + } + + private void ensureHapticsEngine() { + if (hapticEngine != null || Foundation.getMajorSystemVersion() < 14) { + return; + } + + try { + if (controller.getHaptics()!=null) { + hapticEngine = controller.getHaptics().createEngine(GCHapticsLocality.Default); + hapticEngine.retain(); + } + } catch (Throwable t) { + Gdx.app.error("Controllers", "Failed to create haptics engine", t); + } + } @Override public void dispose() { @@ -378,10 +386,11 @@ public int getAxisCount() { return controller.getExtendedGamepad() != null ? 4 : 0; } - @Override - public boolean canVibrate() { - return hapticEngine != null; - } + @Override + public boolean canVibrate() { + ensureHapticsEngine(); + return hapticEngine != null; + } @Override public void startVibration(int duration, float strength) { diff --git a/gdx-controllers-ios/src/com/badlogic/gdx/controllers/IosControllerManager.java b/gdx-controllers-ios/src/com/badlogic/gdx/controllers/IosControllerManager.java index 6d2bb4a..554c403 100644 --- a/gdx-controllers-ios/src/com/badlogic/gdx/controllers/IosControllerManager.java +++ b/gdx-controllers-ios/src/com/badlogic/gdx/controllers/IosControllerManager.java @@ -1,6 +1,8 @@ package com.badlogic.gdx.controllers; import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.controllers.event.ControllerEvent; +import com.badlogic.gdx.controllers.event.ControllerEventQueue; import com.badlogic.gdx.utils.Array; import org.robovm.apple.foundation.Foundation; @@ -14,11 +16,52 @@ public class IosControllerManager extends AbstractControllerManager { private final Array listeners = new Array<>(); + private final ControllerEventQueue eventQueue = new ControllerEventQueue(); + private final Object dispatchLock = new Object(); private boolean initialized = false; private ICadeController iCadeController; public IosControllerManager() { listeners.add(new ManageCurrentControllerListener()); + setupEventQueue(); + } + + private void setupEventQueue() { + new Runnable() { + @Override + public void run() { + synchronized (dispatchLock) { + eventQueue.drain(new ControllerEventQueue.ControllerEventConsumer() { + @Override + public void consume(ControllerEvent event) { + switch (event.type) { + case ControllerEvent.CONNECTED: + controllers.add(event.controller); + synchronized (listeners) { + for (ControllerListener listener : listeners) { + listener.connected(event.controller); + } + } + break; + case ControllerEvent.DISCONNECTED: + IosController oldReference = (IosController)event.controller; + controllers.removeValue(oldReference, true); + synchronized (listeners) { + for (ControllerListener listener : listeners) { + listener.disconnected(oldReference); + } + } + oldReference.dispose(); + break; + default: + } + } + }); + } + + Gdx.app.postRunnable(this); + } + }.run(); } public static void enableICade(UIViewController controller, Selector action) { @@ -45,11 +88,9 @@ private void handleKeyPressed(UIKeyCommand sender) { Gdx.app.log("Controllers", "iCade key was pressed, adding iCade controller."); iCadeController = new ICadeController(); - controllers.add(iCadeController); - synchronized (listeners) { - for (ControllerListener listener : listeners) - listener.connected(iCadeController); + synchronized (dispatchLock) { + eventQueue.enqueueConnected(iCadeController); } } @@ -114,11 +155,9 @@ protected void onControllerConnect(GCController gcController) { if (!alreadyInList) { IosController iosController = new IosController(gcController); - controllers.add(iosController); - synchronized (listeners) { - for (ControllerListener listener : listeners) - listener.connected(iosController); + synchronized (dispatchLock) { + eventQueue.enqueueConnected(iosController); } } } @@ -132,14 +171,9 @@ protected void onControllerDisconnect(GCController gcController) { } if (oldReference != null) { - controllers.removeValue(oldReference, true); - - synchronized (listeners) { - for (ControllerListener listener : listeners) - listener.disconnected(oldReference); + synchronized (dispatchLock) { + eventQueue.enqueueDisconnected(oldReference); } - - oldReference.dispose(); } } @@ -173,4 +207,3 @@ public void clearListeners() { } } } -