게임 컨트롤러를 지원하는 게임은 다양한 버전의 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 3.1에서 Android 4.0까지 지원하는 대체 메커니즘을 제공해야 합니다.
이전 버전용 대체 메커니즘이 필요한 기능을 결정할 수 있도록 표 1에 Android 3.1(API 레벨 12)과 4.1(API 레벨 16) 사이의 게임 컨트롤러 지원에 관한 차이점이 나열되어 있습니다.
표 1. 다양한 버전의 Android에서 게임 컨트롤러를 지원하는 API
컨트롤러 정보 | Controller API | API 레벨 12 | API 수준 16 |
---|---|---|---|
기기 식별 | getInputDeviceIds() |
• | |
getInputDevice() |
• | ||
getVibrator() |
• | ||
SOURCE_JOYSTICK |
• | • | |
SOURCE_GAMEPAD |
• | • | |
연결 상태 | onInputDeviceAdded() |
• | |
onInputDeviceChanged() |
• | ||
onInputDeviceRemoved() |
• | ||
입력 이벤트 식별 | D패드 누르기(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 ) |
• | • |
추상화를 사용하여 다양한 플랫폼에서 작동하는 버전 인식 게임 컨트롤러를 지원하도록 빌드할 수 있습니다. 이 접근 방법은 다음 단계를 포함합니다.
- 게임에 필요한 게임 컨트롤러 기능을 추상화하여 구현한 중개 자바 인터페이스를 정의합니다.
- 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) } }
자바
// 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
메서드 호출을 처리합니다. 런타임에 사용되는 버전별 구현과 관계없이 구현은 호출 결과를 게임에 투명하게 전달합니다.

그림 1. 인터페이스 및 버전별 구현의 클래스 다이어그램입니다.
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 ... }
자바
// 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) … }
자바
// 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) } } } } }
자바
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) } ... }
자바
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() }
자바
@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
Runnable
객체를 메시지 큐로 보내기 위해 Handler
객체를 사용하여 구현합니다. 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 } } } }
자바
@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() } }
자바
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) ... } ... }
자바
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) }
자바
@Override public boolean onGenericMotionEvent(MotionEvent event) { inputManager.onGenericMotionEvent(event); // Handle analog input from the controller as normal ... return super.onGenericMotionEvent(event); }
이 호환 코드의 전체 구현은 위의 다운로드 가능한 샘플 ControllerSample.zip
에서 제공하는 GameView
클래스에서 찾을 수 있습니다.