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

如果您在遊戲中支援遊戲控制器,則您有責任確保遊戲在不同 Android 版本上執行的裝置都能以一致的方式回應控制器。這樣一來,您的遊戲就能觸及更多目標對象,而玩家即使切換或升級 Android 裝置,也能使用控制器享受流暢的遊戲體驗。

本課程將說明如何以回溯相容的方式使用 Android 4.1 以上版本提供的 API,讓您的遊戲在搭載 Android 3.1 以上版本的裝置上支援下列功能:

  • 這個遊戲可以偵測是否新增、變更或移除新的遊戲控制器。
  • 這個遊戲可以查詢遊戲控制器的功能。
  • 遊戲可以辨識遊戲控制器傳入的動作事件。

本課程的範例是根據 ControllerSample.zip 範例 (可供下載) 提供的參考實作結果而定。本範例說明如何實作 InputManagerCompat 介面,以支援不同版本的 Android。如要編譯範例,必須使用 Android 4.1 (API 級別 16) 以上版本。編譯完成後,範例應用程式就會在任何搭載 Android 3.1 (API 級別 12) 以上版本的裝置上執行,做為建構目標。

準備將 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) 之間的遊戲控制器支援差異。

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

控制器資訊 Controller API API 級別 12 API 級別 16
裝置 ID getInputDeviceIds()  
getInputDevice()  
getVibrator()  
SOURCE_JOYSTICK
SOURCE_GAMEPAD
連線狀態 onInputDeviceAdded()  
onInputDeviceChanged()  
onInputDeviceRemoved()  
輸入事件識別 D-Pad 按下 (KEYCODE_DPAD_UPKEYCODE_DPAD_DOWNKEYCODE_DPAD_LEFTKEYCODE_DPAD_RIGHTKEYCODE_DPAD_CENTER)
按下遊戲手把按鈕 (BUTTON_ABUTTON_BBUTTON_THUMBLBUTTON_THUMBRBUTTON_SELECTBUTTON_STARTBUTTON_R1BUTTON_L1BUTTON_R2BUTTON_L2)
搖桿和帽子切換動作 ( AXIS_XAXIS_YAXIS_ZAXIS_RZAXIS_HAT_XAXIS_HAT_Y)
類比觸發條件按下 (AXIS_LTRIGGERAXIS_RTRIGGER)

您可以使用抽象化功能,建構適用於不同平台的版本感知遊戲控制器支援。這個方法包含以下步驟:

  1. 定義一個中介 Java 介面,該介面簡化遊戲所需的遊戲控制器功能的實作。
  2. 在 Android 4.1 以上版本中建立使用 API 的介面 Proxy 實作。
  3. 使用 Android 3.1 至 Android 4.0 之間可用的 API 建立自訂介面實作。
  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 介面的鏡像。讓遊戲知道已新增、變更或移除遊戲控制器。

接著,建立可在不同平台版本上運作的 InputManagerCompat 實作。如果遊戲是在 Android 4.1 以上版本執行,並呼叫 InputManagerCompat 方法,Proxy 實作會呼叫 InputManager 中的對等方法,不過,如果您的遊戲是在 Android 3.1 至 Android 4.0 上運作,則自訂實作只會使用 Android 3.1 以上版本所推出的 API 來處理 InputManagerCompat 方法的呼叫。無論執行階段使用的是哪一種版本專屬實作,實作都會以公開透明的方式將呼叫結果傳回遊戲。

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

在 Android 4.1 以上版本實作介面

InputManagerCompatV16InputManagerCompat 介面的實作,可透過 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 3.1 至 Android 4.0 的 InputManagerCompat 實作,您可以使用下列物件:

  • SparseArray 的裝置 ID,用於追蹤連線至裝置的遊戲控制器。
  • 用於處理裝置事件的 Handler。應用程式啟動或重新啟用時,Handler 會收到輪詢遊戲控制器中斷連線的訊息。Handler 會啟動迴圈,藉此檢查每個已知的已連線遊戲控制器,檢查是否傳回裝置 ID。null 傳回值表示遊戲控制器已中斷連線。應用程式暫停後,Handler 會停止輪詢。
  • InputManagerCompat.InputDeviceListener 物件的 Map。您將使用事件監聽器更新已追蹤遊戲控制器的連線狀態。

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);
    }
}

實作擴充 HandlerPollingMessageHandler 物件,並覆寫 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 是新 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 物件例項化,並在主要 View 中註冊 InputManagerCompat.InputDeviceListener。由於您設定的版本切換邏輯,遊戲會自動採用適用於裝置執行 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);
        ...
    }
}

接著,按照從遊戲控制器處理 MotionEvent 的說明,覆寫主要檢視畫面中的 onGenericMotionEvent() 方法。您的遊戲現在應可在搭載 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 類別中找到這個相容性程式碼的完整實作。