支援在各種 Android 版本上執行控制器

如果您要支援遊戲中的遊戲控制器,就必須負責 確保遊戲能在所有裝置上一致地回應控制器 在不同版本的 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)。

表 1. 支援跨雲端遊戲控制器的 API 不同的 Android 版本

控管者資訊 控制器 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)

您可以使用抽象化機制,建構可因應不同版本的遊戲控制器, 跨平台運作這個方法包含下列步驟:

  1. 定義中介 Java 介面,用於將 才能提供遊戲所需的遊戲控制器功能。
  2. 建立在 Android 中使用 API 的介面 Proxy 實作 4.1 以及更高版本。
  3. 使用可用的 API 建立自訂介面 介於 Android 3.1 到 Android 4.0 之間
  4. 建立在執行階段在這些實作項目間切換的邏輯。 即可開始在遊戲中使用這個介面

概略瞭解如何運用抽象化機制確保應用程式 可在不同版本的 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無論哪個 版本專屬的實作項目會在執行階段使用, 以公開透明的方式傳回遊戲結果

圖 1. 介面和特定版本的類別圖表 。

在 Android 4.1 以上版本中實作介面

InputManagerCompatV16 是網頁的 InputManagerCompat 介面,可透過 Proxy 向 實際的 InputManagerInputManager.InputDeviceListenerInputManager 是從系統取得 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.InputDeviceListenerMap 如需儲存大量結構化物件 建議使用 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;
}

使用 用於傳送 DeviceEventHandler 物件 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 類別 即可在上方下載。