如果您要支援遊戲中的遊戲控制器,就必須負責 確保遊戲能在所有裝置上一致地回應控制器 在不同版本的 Android 上運作如此一來 您的玩家和玩家 控制器。
本課程說明如何使用 Android 4.1 以上版本提供的 API 回溯相容,讓遊戲能夠支援下列平台: 功能:
- 遊戲可以偵測是否有新增、變更或移除新的遊戲控制器。
- 該遊戲可以查詢遊戲控制器的功能。
- 該遊戲可辨識從遊戲控制器傳入的動作事件。
本課程中的範例是以參考實作為基礎
「ControllerSample.zip
」範例提供的可供下載
。這個範例說明如何實作 InputManagerCompat
介面,支援不同版本的 Android。如要編譯範例,您必須
必須使用 Android 4.1 (API 級別 16) 以上版本。編譯完成後,範例應用程式
會在執行 Android 3.1 (API 級別 12) 或以上版本的任何裝置中執行
目標。
準備抽象化 API,以便支援遊戲控制器
假設您想要能夠判斷遊戲控制器的連線狀態 在搭載 Android 3.1 (API 級別 12) 的裝置上,狀態已變更。不過 只有 Android 4.1 (API 級別 16) 及以上版本提供這些 API,因此您 您需要提供支援 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() |
• | ||
輸入事件識別 | D-pad 按下 (
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 的介面 Proxy 實作 4.1 以及更高版本。
- 使用可用的 API 建立自訂介面 介於 Android 3.1 到 Android 4.0 之間
- 建立在執行階段在這些實作項目間切換的邏輯。 即可開始在遊戲中使用這個介面
概略瞭解如何運用抽象化機制確保應用程式 可在不同版本的 Android 上以回溯相容方式運作,詳情請參閱 建立中 回溯相容的 UI。
新增具回溯相容性的介面
如要提供回溯相容性,您可以建立自訂介面,然後 以及新增特定版本的實作。這種做法的優點之一 可讓您在 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()
- 當遊戲控制器發生以下事件時,系統會停止輪詢 或遊戲失去焦點時。
onResume()
- 當遊戲控制器事件出現 繼續主要活動,或遊戲開始並在 前景。
InputDeviceListener
- 鏡像
InputManager.InputDeviceListener
存取 API讓遊戲在新增、變更或變更遊戲控制器時通知 已移除
接下來,建立可運作的 InputManagerCompat
實作方式
在不同平台版本之間放送如果您的遊戲執行 Android 4.1 版或
並呼叫 InputManagerCompat
方法,
會呼叫 InputManager
中的對等方法。
不過,如果遊戲是在 Android 3.1 以上版本執行 Android 4.0,則自訂實作
處理對 InputManagerCompat
方法的呼叫
只有 Android 3.1 以下版本的 API無論哪個
版本專屬的實作項目會在執行階段使用,
以公開透明的方式傳回遊戲結果
在 Android 4.1 以上版本中實作介面
InputManagerCompatV16
是網頁的
InputManagerCompat
介面,可透過 Proxy 向
實際的 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
實作項目,您可以使用
下列物件:
- 要追蹤的
SparseArray
裝置 ID 提供與裝置連線的遊戲控制器。 - 用於處理裝置事件的
Handler
。應用程式啟動時 或已繼續,Handler
會收到開始輪詢的訊息 才能連結遊戲控制器Handler
會啟動 迴圈,檢查每個已知的連結遊戲控制器,並查看裝置 ID 是否 。null
回傳值表示遊戲控制器是 已中斷連線。Handler
會在應用程式發生時停止輪詢 已暫停 InputManagerCompat.InputDeviceListener
的Map
如需儲存大量結構化物件 建議使用 Cloud Bigtable您將使用事件監聽器更新追蹤項目的連線狀態 遊戲控制器
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; }
使用
用於傳送 DeviceEvent
的 Handler
物件
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
類別
即可在上方下載。