如果您在游戏中支持游戏控制器,则需自行负责 确保您的游戏在不同设备上对控制器的响应一致 在不同的 Android 版本上运行。这让您的游戏覆盖更广泛的人群 并且您的玩家可以 即使他们更换或升级 Android 设备,也会使用他们的控制器。
本课演示了如何使用 Android 4.1 及更高版本中提供的 API 使游戏支持 功能:
- 游戏可以检测是否新增、更改或移除了游戏控制器。
- 游戏可以查询游戏控制器的功能。
- 游戏可以识别从游戏控制器传入的动作事件。
本课程中的示例基于参考实现
由示例 ControllerSample.zip
提供,可供下载
。此示例展示了如何实现 InputManagerCompat
来支持不同版本的 Android要编译该示例,您需要
必须使用 Android 4.1(API 级别 16)或更高版本。编译完成后,示例应用将
可在搭载 Android 3.1(API 级别 12)或更高版本的任何设备上运行 build
目标。
准备抽象化处理用于实现游戏控制器支持的 API
假设您想能够确定游戏控制器与 在搭载 Android 3.1(API 级别 12)的设备上的状态已发生更改。不过, 这些 API 仅在 Android 4.1(API 级别 16)及更高版本中提供,因此 需要提供支持 Android 4.1 及更高版本的实现,而 提供支持 Android 3.1 至 Android 4.0 的回退机制。
为了帮助您确定哪些功能需要此类后备机制 旧版本,表 1 列出了游戏控制器在支持方面的差异 介于 Android 3.1(API 级别 12)和 4.1(API 级别)之间 16)。
控制器信息 | 控制器 API | API 级别 12 | API 级别 16 |
---|---|---|---|
设备标识 | getInputDeviceIds() |
• | |
getInputDevice() |
• | ||
getVibrator() |
• | ||
SOURCE_JOYSTICK |
• | • | |
SOURCE_GAMEPAD |
• | • | |
连接状态 | onInputDeviceAdded() |
• | |
onInputDeviceChanged() |
• | ||
onInputDeviceRemoved() |
• | ||
输入事件标识 | 按方向键 (
KEYCODE_DPAD_UP ,
KEYCODE_DPAD_DOWN ,
KEYCODE_DPAD_LEFT ,
KEYCODE_DPAD_RIGHT ,
KEYCODE_DPAD_CENTER ) |
• | • |
按下游戏手柄按钮 (
BUTTON_A ,
BUTTON_B ,
BUTTON_THUMBL ,
BUTTON_THUMBR ,
BUTTON_SELECT ,
BUTTON_START ,
BUTTON_R1 ,
BUTTON_L1 ,
BUTTON_R2 ,
BUTTON_L2 ) |
• | • | |
操纵杆和帽子开关移动(
AXIS_X ,
AXIS_Y ,
AXIS_Z ,
AXIS_RZ ,
AXIS_HAT_X ,
AXIS_HAT_Y ) |
• | • | |
模拟触发器按下 (
AXIS_LTRIGGER ,
AXIS_RTRIGGER ) |
• | • |
您可以使用抽象来构建版本感知型游戏控制器支持, 可跨平台运行该方法涉及以下步骤:
- 定义一个中间 Java 接口,用于抽象化 游戏所需的游戏控制器功能。
- 为在 Android 中使用 API 的接口创建代理实现 4.1 及更高版本。
- 使用可用 API 创建自定义接口实现 从 Android 3.1 到 Android 4.0
- 创建用于在运行时在这些实现之间切换的逻辑, 并开始在游戏中使用该界面
有关如何使用抽象来确保应用 能以向后兼容的方式跨不同的 Android 版本运行,请参阅 正在创建 向后兼容的界面。
添加接口以实现向后兼容性
为了提供向后兼容性,您可以创建一个自定义接口,然后 添加特定于版本的实现。这种方法的一个优点是 让您可以镜像 Android 4.1(API 级别 16)上的公共接口, 支持游戏控制器。
Kotlin
// The InputManagerCompat interface is a reference example. // The full code is provided in the ControllerSample.zip sample. interface InputManagerCompat { val inputDeviceIds: IntArray fun getInputDevice(id: Int): InputDevice fun registerInputDeviceListener( listener: InputManager.InputDeviceListener, handler: Handler? ) fun unregisterInputDeviceListener(listener:InputManager.InputDeviceListener) fun onGenericMotionEvent(event: MotionEvent) fun onPause() fun onResume() interface InputDeviceListener { fun onInputDeviceAdded(deviceId: Int) fun onInputDeviceChanged(deviceId: Int) fun onInputDeviceRemoved(deviceId: Int) } }
Java
// The InputManagerCompat interface is a reference example. // The full code is provided in the ControllerSample.zip sample. public interface InputManagerCompat { ... public InputDevice getInputDevice(int id); public int[] getInputDeviceIds(); public void registerInputDeviceListener( InputManagerCompat.InputDeviceListener listener, Handler handler); public void unregisterInputDeviceListener( InputManagerCompat.InputDeviceListener listener); public void onGenericMotionEvent(MotionEvent event); public void onPause(); public void onResume(); public interface InputDeviceListener { void onInputDeviceAdded(int deviceId); void onInputDeviceChanged(int deviceId); void onInputDeviceRemoved(int deviceId); } ... }
InputManagerCompat
接口提供以下方法:
getInputDevice()
- 镜像
getInputDevice()
。获取InputDevice
该对象表示游戏控制器的功能。 getInputDeviceIds()
- 镜像
getInputDeviceIds()
。返回一个整数数组,其中每个 表示不同输入设备的 ID如果你要构建 一款支持多人游戏的游戏,并且您想要检测 控制器连接。 registerInputDeviceListener()
- 镜像
registerInputDeviceListener()
。让您可以注册,以便在有 设备被添加、更改或移除。 unregisterInputDeviceListener()
- 镜像
unregisterInputDeviceListener()
。 取消注册输入设备监听器。 onGenericMotionEvent()
- 镜像
onGenericMotionEvent()
。让您的游戏能够拦截和处理MotionEvent
对象和表示事件的轴值 例如操纵杆移动和模拟扳机按下操作。 onPause()
- 在以下情况下,系统会停止轮询游戏控制器事件: 主 activity 暂停时,或者游戏不再获得焦点时。
onResume()
- 在以下情况下,开始轮询游戏控制器事件: 主 activity 恢复,或游戏启动时在 。
InputDeviceListener
- 镜像
InputManager.InputDeviceListener
界面。让您的游戏可以了解游戏控制器的添加、更改或操作时间 已移除。
接下来,创建有效的 InputManagerCompat
实现
不同平台版本上的延迟。如果您的游戏在 Android 4.1 或
并调用 InputManagerCompat
方法,即代理实现
调用 InputManager
中的等效方法。
不过,如果您的游戏在 Android 3.1 到 Android 4.0 上运行,则自定义实现
使用InputManagerCompat
仅在 Android 3.1 及以上版本中引入的 API。无论
运行时使用特定于版本的实现,则实现将通过
以透明的方式将调用结果传回游戏。
在 Android 4.1 及更高版本上实现接口
InputManagerCompatV16
是
InputManagerCompat
接口,用于将方法调用
实际的 InputManager
和 InputManager.InputDeviceListener
。通过
从系统获取 InputManager
Context
。
Kotlin
// The InputManagerCompatV16 class is a reference implementation. // The full code is provided in the ControllerSample.zip sample. public class InputManagerV16( context: Context, private val inputManager: InputManager = context.getSystemService(Context.INPUT_SERVICE) as InputManager, private val listeners: MutableMap<InputManager.InputDeviceListener, V16InputDeviceListener> = mutableMapOf() ) : InputManagerCompat { override val inputDeviceIds: IntArray = inputManager.inputDeviceIds override fun getInputDevice(id: Int): InputDevice = inputManager.getInputDevice(id) override fun registerInputDeviceListener( listener: InputManager.InputDeviceListener, handler: Handler? ) { V16InputDeviceListener(listener).also { v16listener -> inputManager.registerInputDeviceListener(v16listener, handler) listeners += listener to v16listener } } // Do the same for unregistering an input device listener ... override fun onGenericMotionEvent(event: MotionEvent) { // unused in V16 } override fun onPause() { // unused in V16 } override fun onResume() { // unused in V16 } } class V16InputDeviceListener( private val idl: InputManager.InputDeviceListener ) : InputManager.InputDeviceListener { override fun onInputDeviceAdded(deviceId: Int) { idl.onInputDeviceAdded(deviceId) } // Do the same for device change and removal ... }
Java
// The InputManagerCompatV16 class is a reference implementation. // The full code is provided in the ControllerSample.zip sample. public class InputManagerV16 implements InputManagerCompat { private final InputManager inputManager; private final Map<InputManagerCompat.InputDeviceListener, V16InputDeviceListener> listeners; public InputManagerV16(Context context) { inputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE); listeners = new HashMap<InputManagerCompat.InputDeviceListener, V16InputDeviceListener>(); } @Override public InputDevice getInputDevice(int id) { return inputManager.getInputDevice(id); } @Override public int[] getInputDeviceIds() { return inputManager.getInputDeviceIds(); } static class V16InputDeviceListener implements InputManager.InputDeviceListener { final InputManagerCompat.InputDeviceListener mIDL; public V16InputDeviceListener(InputDeviceListener idl) { mIDL = idl; } @Override public void onInputDeviceAdded(int deviceId) { mIDL.onInputDeviceAdded(deviceId); } // Do the same for device change and removal ... } @Override public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) { V16InputDeviceListener v16Listener = new V16InputDeviceListener(listener); inputManager.registerInputDeviceListener(v16Listener, handler); listeners.put(listener, v16Listener); } // Do the same for unregistering an input device listener ... @Override public void onGenericMotionEvent(MotionEvent event) { // unused in V16 } @Override public void onPause() { // unused in V16 } @Override public void onResume() { // unused in V16 } }
在 Android 3.1 至 Android 4.0 上实现接口
如需创建支持 Android 3.1 至 Android 4.0 的 InputManagerCompat
实现,您可以使用
以下对象:
- 要跟踪的设备 ID
SparseArray
游戏控制器。 Handler
,用于处理设备事件。应用启动时 或已恢复,Handler
会收到一条消息以开始轮询 用于游戏控制器断开连接Handler
将启动 检查每个已知连接的游戏控制器,看看设备 ID 是否 返回。如果返回值为null
,则表示游戏控制器 已断开连接。Handler
会在应用满足以下条件时停止轮询 已暂停。- 第
Map
个,共InputManagerCompat.InputDeviceListener
个 对象的操作。您将使用监听器更新所跟踪的 游戏控制器
Kotlin
// The InputManagerCompatV9 class is a reference implementation. // The full code is provided in the ControllerSample.zip sample. class InputManagerV9( val devices: SparseArray<Array<Long>> = SparseArray(), private val listeners: MutableMap<InputManager.InputDeviceListener, Handler> = mutableMapOf() ) : InputManagerCompat { private val defaultHandler: Handler = PollingMessageHandler(this) … }
Java
// The InputManagerCompatV9 class is a reference implementation. // The full code is provided in the ControllerSample.zip sample. public class InputManagerV9 implements InputManagerCompat { private final SparseArray<long[]> devices; private final Map<InputDeviceListener, Handler> listeners; private final Handler defaultHandler; … public InputManagerV9() { devices = new SparseArray<long[]>(); listeners = new HashMap<InputDeviceListener, Handler>(); defaultHandler = new PollingMessageHandler(this); } }
实现一个 PollingMessageHandler
对象,该对象会扩展
Handler
,并替换
handleMessage()
方法。此方法会检查连接的游戏控制器是否已
并通知已注册的监听器。
Kotlin
private class PollingMessageHandler( inputManager: InputManagerV9, private val mInputManager: WeakReference<InputManagerV9> = WeakReference(inputManager) ) : Handler() { override fun handleMessage(msg: Message) { super.handleMessage(msg) when (msg.what) { MESSAGE_TEST_FOR_DISCONNECT -> { mInputManager.get()?.also { imv -> val time = SystemClock.elapsedRealtime() val size = imv.devices.size() for (i in 0 until size) { imv.devices.valueAt(i)?.also { lastContact -> if (time - lastContact[0] > CHECK_ELAPSED_TIME) { // check to see if the device has been // disconnected val id = imv.devices.keyAt(i) if (null == InputDevice.getDevice(id)) { // Notify the registered listeners // that the game controller is disconnected imv.devices.remove(id) } else { lastContact[0] = time } } } } sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT, CHECK_ELAPSED_TIME) } } } } }
Java
private static class PollingMessageHandler extends Handler { private final WeakReference<InputManagerV9> inputManager; PollingMessageHandler(InputManagerV9 im) { inputManager = new WeakReference<InputManagerV9>(im); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case MESSAGE_TEST_FOR_DISCONNECT: InputManagerV9 imv = inputManager.get(); if (null != imv) { long time = SystemClock.elapsedRealtime(); int size = imv.devices.size(); for (int i = 0; i < size; i++) { long[] lastContact = imv.devices.valueAt(i); if (null != lastContact) { if (time - lastContact[0] > CHECK_ELAPSED_TIME) { // check to see if the device has been // disconnected int id = imv.devices.keyAt(i); if (null == InputDevice.getDevice(id)) { // Notify the registered listeners // that the game controller is disconnected imv.devices.remove(id); } else { lastContact[0] = time; } } } } sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT, CHECK_ELAPSED_TIME); } break; } } }
如需开始和停止轮询游戏控制器断开连接情况,请替换 方法:
Kotlin
private const val MESSAGE_TEST_FOR_DISCONNECT = 101 private const val CHECK_ELAPSED_TIME = 3000L class InputManagerV9( val devices: SparseArray<Array<Long>> = SparseArray(), private val listeners: MutableMap<InputManager.InputDeviceListener, Handler> = mutableMapOf() ) : InputManagerCompat { ... override fun onPause() { defaultHandler.removeMessages(MESSAGE_TEST_FOR_DISCONNECT) } override fun onResume() { defaultHandler.sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT, CHECK_ELAPSED_TIME) } ... }
Java
private static final int MESSAGE_TEST_FOR_DISCONNECT = 101; private static final long CHECK_ELAPSED_TIME = 3000L; @Override public void onPause() { defaultHandler.removeMessages(MESSAGE_TEST_FOR_DISCONNECT); } @Override public void onResume() { defaultHandler.sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT, CHECK_ELAPSED_TIME); }
如需检测是否添加了输入设备,请替换
onGenericMotionEvent()
方法结合使用。当系统报告动作事件时,
检查此事件是来自已跟踪的设备 ID,还是来自
新的设备 ID。如果来自新的设备 ID,请向已注册的监听器发出通知。
Kotlin
override fun onGenericMotionEvent(event: MotionEvent) { // detect new devices val id = event.deviceId val timeArray: Array<Long> = mDevices.get(id) ?: run { // Notify the registered listeners that a game controller is added ... arrayOf<Long>().also { mDevices.put(id, it) } } timeArray[0] = SystemClock.elapsedRealtime() }
Java
@Override public void onGenericMotionEvent(MotionEvent event) { // detect new devices int id = event.getDeviceId(); long[] timeArray = mDevices.get(id); if (null == timeArray) { // Notify the registered listeners that a game controller is added ... timeArray = new long[1]; mDevices.put(id, timeArray); } long time = SystemClock.elapsedRealtime(); timeArray[0] = time; }
监听器的通知是通过使用
Handler
对象,用于发送 DeviceEvent
Runnable
对象添加到消息队列中。DeviceEvent
包含对 InputManagerCompat.InputDeviceListener
的引用。时间
DeviceEvent
运行后,监听器的相应回调方法
来指明是添加、更改还是移除了游戏控制器。
Kotlin
class InputManagerV9( val devices: SparseArray<Array<Long>> = SparseArray(), private val listeners: MutableMap<InputManager.InputDeviceListener, Handler> = mutableMapOf() ) : InputManagerCompat { ... override fun registerInputDeviceListener( listener: InputManager.InputDeviceListener, handler: Handler? ) { listeners[listener] = handler ?: defaultHandler } override fun unregisterInputDeviceListener(listener: InputManager.InputDeviceListener) { listeners.remove(listener) } private fun notifyListeners(why: Int, deviceId: Int) { // the state of some device has changed listeners.forEach { listener, handler -> DeviceEvent.getDeviceEvent(why, deviceId, listener).also { handler?.post(it) } } } ... } private val sObjectQueue: Queue<DeviceEvent> = ArrayDeque<DeviceEvent>() private class DeviceEvent( private var mMessageType: Int, private var mId: Int, private var mListener: InputManager.InputDeviceListener ) : Runnable { companion object { fun getDeviceEvent(messageType: Int, id: Int, listener: InputManager.InputDeviceListener) = sObjectQueue.poll()?.apply { mMessageType = messageType mId = id mListener = listener } ?: DeviceEvent(messageType, id, listener) } override fun run() { when(mMessageType) { ON_DEVICE_ADDED -> mListener.onInputDeviceAdded(mId) ON_DEVICE_CHANGED -> mListener.onInputDeviceChanged(mId) ON_DEVICE_REMOVED -> mListener.onInputDeviceChanged(mId) else -> { // Handle unknown message type } } } }
Java
@Override public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) { listeners.remove(listener); if (handler == null) { handler = defaultHandler; } listeners.put(listener, handler); } @Override public void unregisterInputDeviceListener(InputDeviceListener listener) { listeners.remove(listener); } private void notifyListeners(int why, int deviceId) { // the state of some device has changed if (!listeners.isEmpty()) { for (InputDeviceListener listener : listeners.keySet()) { Handler handler = listeners.get(listener); DeviceEvent odc = DeviceEvent.getDeviceEvent(why, deviceId, listener); handler.post(odc); } } } private static class DeviceEvent implements Runnable { private int mMessageType; private int mId; private InputDeviceListener mListener; private static Queue<DeviceEvent> sObjectQueue = new ArrayDeque<DeviceEvent>(); ... static DeviceEvent getDeviceEvent(int messageType, int id, InputDeviceListener listener) { DeviceEvent curChanged = sObjectQueue.poll(); if (null == curChanged) { curChanged = new DeviceEvent(); } curChanged.mMessageType = messageType; curChanged.mId = id; curChanged.mListener = listener; return curChanged; } @Override public void run() { switch (mMessageType) { case ON_DEVICE_ADDED: mListener.onInputDeviceAdded(mId); break; case ON_DEVICE_CHANGED: mListener.onInputDeviceChanged(mId); break; case ON_DEVICE_REMOVED: mListener.onInputDeviceRemoved(mId); break; default: // Handle unknown message type ... break; } // Put this runnable back in the queue sObjectQueue.offer(this); } }
现在,您有两个 InputManagerCompat
实现:一个用于
适用于搭载 Android 4.1 及更高版本的设备,
适用于搭载 Android 3.1 至 Android 4.0 的设备。
使用特定于版本的实现
特定于版本的切换逻辑是在充当 工厂。
Kotlin
object Factory { fun getInputManager(context: Context): InputManagerCompat = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { InputManagerV16(context) } else { InputManagerV9() } }
Java
public static class Factory { public static InputManagerCompat getInputManager(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { return new InputManagerV16(context); } else { return new InputManagerV9(); } } }
现在,您只需实例化 InputManagerCompat
对象并
在您的主服务器中注册一个InputManagerCompat.InputDeviceListener
View
。根据您设置的版本切换逻辑
您的游戏会自动使用适合应用场景的实现方法
设备所运行的 Android 版本。
Kotlin
class GameView(context: Context) : View(context), InputManager.InputDeviceListener { private val inputManager: InputManagerCompat = Factory.getInputManager(context).apply { registerInputDeviceListener(this@GameView, null) ... } ... }
Java
public class GameView extends View implements InputDeviceListener { private InputManagerCompat inputManager; ... public GameView(Context context, AttributeSet attrs) { inputManager = InputManagerCompat.Factory.getInputManager(this.getContext()); inputManager.registerInputDeviceListener(this, null); ... } }
接下来,替换
onGenericMotionEvent()
方法,如
处理游戏中的 MotionEvent
控制器。您的游戏现在应该能够处理游戏控制器事件了
在搭载 Android 3.1(API 级别 12)及更高版本的设备上保持一致。
Kotlin
override fun onGenericMotionEvent(event: MotionEvent): Boolean { inputManager.onGenericMotionEvent(event) // Handle analog input from the controller as normal ... return super.onGenericMotionEvent(event) }
Java
@Override public boolean onGenericMotionEvent(MotionEvent event) { inputManager.onGenericMotionEvent(event); // Handle analog input from the controller as normal ... return super.onGenericMotionEvent(event); }
您可以在
示例 ControllerSample.zip
中提供的 GameView
类
可在上面下载。