ゲームでゲーム コントローラをサポートする場合は、異なるバージョンの Android を搭載したデバイス間でもゲームがコントローラに一貫して反応するようにする必要があります。これにより、より多くのユーザーにリーチでき、プレーヤーは Android デバイスの切り替えやアップグレードを行ったとしても、コントローラでシームレスなゲームプレイ エクスペリエンスを実現できます。
このレッスンでは、Android 4.1 以降で利用可能な API を下位互換性のある方法で使用し、Android 3.1 以降を搭載したデバイスでゲームが次の機能をサポートする方法について説明します。
- ゲーム コントローラが追加、変更、削除されたかどうかをゲームで検出する。
- ゲームでゲーム コントローラの機能を照会する。
- ゲーム コントローラから送信されるモーション イベントをゲームで認識する。
このレッスンの例は、上記でダウンロードできるサンプル ControllerSample.zip
に用意されているリファレンス実装に基づいています。このサンプルは、さまざまなバージョンの Android をサポートする InputManagerCompat
インターフェースの実装方法を示しています。サンプルをコンパイルするには、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 4.0 から Android 3.1 までをサポートするフォールバック メカニズムを提供する必要があります。
以前のバージョンでそのようなフォールバック メカニズムが必要な機能を判断するのに役立つように、Android 3.1(API レベル 12)と 4.1(API レベル 16)におけるゲーム コントローラのサポートの違いを表 1 に示します。
コントローラの情報 | コントローラ向け 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 ) |
• | • |
抽象化により、プラットフォーム間で動作するバージョン対応のゲーム コントローラのサポートを構築できます。このアプローチでは次の手順を実施します。
- ゲームに必要なゲーム コントローラ機能の実装を抽象化する中間 Java インターフェースを定義します。
- Android 4.1 以降で、API を使用するインターフェースのプロキシ実装を作成します。
- Android 3.1 から Android 4.0 までの間で利用可能な API を使用するインターフェースのカスタム実装を作成します。
- ランタイムにこれらの実装を切り替えるロジックを作成し、ゲームでインターフェースの使用を開始します。
抽象化を使用して、異なるバージョンの 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
メソッドを呼び出すと、プロキシ実装は InputManager
の同等のメソッドを呼び出します。ただし、ゲームが Android 3.1 から Android 4.0 までで実行されている場合、カスタム実装は、Android 3.1 以降に導入された API のみを使用して InputManagerCompat
メソッドの呼び出しを処理します。どのバージョン固有の実装がランタイムで使用されるかにかかわらず、この実装は呼び出し結果を透過的にゲームに返します。
Android 4.1 以降にインターフェースを実装する
InputManagerCompatV16
は、メソッド呼び出しを実際の InputManager
と InputManager.InputDeviceListener
にプロキシする InputManagerCompat
インターフェースの実装です。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.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); } }
Handler
を拡張する PollingMessageHandler
オブジェクトを実装し、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
の実装が 2 つになりました。1 つは Android 4.1 以降を搭載するデバイスで動作し、もう 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
クラスにあります。