複数の 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)で変更されました。ただし、 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 を 同期できます。

コントローラの情報 コントローラ向け API API レベル 12 API レベル 16
デバイス ID getInputDeviceIds()  
getInputDevice()  
getVibrator()  
SOURCE_JOYSTICK
SOURCE_GAMEPAD
接続ステータス onInputDeviceAdded()  
onInputDeviceChanged()  
onInputDeviceRemoved()  
入力イベント ID 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 を使用するインターフェースのプロキシ実装を作成する 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() をミラーリングします。登録すると、Google Chat に新規投稿された デバイスの追加、変更、削除が行われます。
unregisterInputDeviceListener()
unregisterInputDeviceListener() をミラーリングします。 入力デバイス リスナーの登録を解除します。
onGenericMotionEvent()
onGenericMotionEvent() をミラーリングします。ゲームがインターセプトして処理できるようにする イベントを表す MotionEvent オブジェクトと軸の値 たとえばジョイスティックの動きや アナログトリガーの押下などです
onPause()
ゲーム コントローラ イベントのポーリングを停止します。 メイン アクティビティが一時停止したとき、またはゲームのフォーカスがなくなったとき。
onResume()
ゲーム コントローラ イベントのポーリングを開始します。 メイン アクティビティが再開されたとき、またはゲームが開始されて
InputDeviceListener
InputManager.InputDeviceListener をミラーリングします。 行うことができます。ゲーム コントローラが追加、変更、または追加されたときにゲームに通知します 削除されます。

次に、機能する InputManagerCompat の実装を作成します さまざまなプラットフォームで利用できますゲームが Android 4.1 または InputManagerCompat メソッド(プロキシ実装)を呼び出します。 InputManager の同等のメソッドを呼び出します。 ただし、Android 3.1 から Android 4.0 までのゲームを実行している場合、カスタム実装は InputManagerCompat メソッドの呼び出しを処理するには、 Android 3.1 以降に導入された API のみ。何よりも 実行時にバージョン固有の実装が使用される場合、この実装は 呼び出しの結果がゲームに対して透過的に返されます。

図 1. インターフェースとバージョン固有のクラスの図 あります。

Android 4.1 以降にインターフェースを実装する

InputManagerCompatV16 は、 メソッド呼び出しをプロキシする InputManagerCompat インターフェース 実際の InputManagerInputManager.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 は、次の状態のときにポーリングを停止します。 一時停止しました。
  • InputManagerCompat.InputDeviceListenerMap 説明します。リスナーを使用して、トラッキング対象の接続ステータスを更新します。 使用できます。

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 の実装が 2 つになりました。 は Android 4.1 以降を搭載するデバイスで動作します。 Android 3.1 から Android 4.0 までのデバイスで動作します。

バージョン固有の実装を使用する

バージョン固有の切り替えロジックは、 Factory になります。

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

次に、kubectl の メインビューで onGenericMotionEvent() メソッドを使用します。詳しくは、 ゲームの MotionEvent を処理する Controller。これで、ゲーム コントローラ イベントをゲームで処理できるようになります。 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 クラス からダウンロードできます。