게임에서 게임 컨트롤러를 지원하는 것은 개발자의 책임입니다. 게임이 여러 기기에서 컨트롤러에 일관되게 반응하는지 확인 실행할 수도 있습니다 이렇게 하면 게임의 도달범위가 더 넓어지고 개발자는 원활한 게임플레이 환경을 제공할 수 있는 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 수준) 사이 참조).
컨트롤러 정보 | 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 ) |
• | • |
추상화를 사용하여 버전 인식 게임 컨트롤러 지원을 빌드할 수 있습니다. 다양한 플랫폼에서 작동합니다 이 접근 방법은 다음 단계를 포함합니다.
- 애플리케이션의 구현을 추상화하는 중간 Java 인터페이스를 게임에 필요한 게임 컨트롤러 기능을 정의합니다.
- Android에서 API를 사용하는 인터페이스의 프록시 구현 만들기 4.1 이상
- 사용 가능한 API를 사용하는 인터페이스의 맞춤 구현을 만듭니다. 있습니다.
- 런타임 시 이러한 구현 간에 전환하는 로직을 만듭니다. 게임에서 인터페이스 사용을 시작할 수 있습니다.
애플리케이션이 추상화를 사용하도록 하는 방법에 대한 개요는 여러 Android 버전에서 이전 버전과 호환되는 방식으로 작동할 수 있습니다. 생성 중 이전 버전과 호환되는 UI를 참조하세요.
이전 버전과의 호환성을 위한 인터페이스 추가
이전 버전과의 호환성을 제공하려면 맞춤 인터페이스를 생성한 다음 버전별 구현을 추가합니다. 이 접근 방식의 한 가지 장점은 를 사용하면 공개 인터페이스를 미러링할 수 있는 게임 컨트롤러를 지원해야 합니다.
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입니다. 이는 Kubernetes와 여러 명의 플레이어를 지원하는 게임에서 얼마나 많은 사람이 컨트롤러가 연결되어 있습니다.registerInputDeviceListener()
registerInputDeviceListener()
를 미러링합니다. 새로운 또는 기기가 추가, 변경 또는 삭제될 때 발생합니다.unregisterInputDeviceListener()
unregisterInputDeviceListener()
를 미러링합니다. 입력 기기 리스너를 등록 취소합니다.onGenericMotionEvent()
onGenericMotionEvent()
를 미러링합니다. 게임에서MotionEvent
객체 및 이벤트를 나타내는 축 값 조이스틱 이동, 아날로그 트리거 누르기 등입니다onPause()
- 다음의 경우 게임 컨트롤러 이벤트 폴링을 중지합니다. 기본 활동이 일시중지되거나 게임에 더 이상 포커스가 없을 때
onResume()
- 기본 활동이 재개되거나 포그라운드에 있습니다.
InputDeviceListener
InputManager.InputDeviceListener
미러링 인터페이스에 추가되었습니다. 게임 컨트롤러가 추가, 변경 또는 변경된 경우 이를 게임에 알립니다. 삭제되었습니다.
다음으로, 작동하는 InputManagerCompat
의 구현을 만듭니다.
서로 다른 플랫폼 버전을
확인할 수 있습니다 게임이 Android 4.1 또는
InputManagerCompat
메서드를 호출합니다. 이는 프록시 구현입니다.
InputManager
에서 상응하는 메서드를 호출합니다.
하지만 게임이 Android 3.1~4.0에서 실행되는 경우 맞춤 구현은
InputManagerCompat
메서드 호출을 처리합니다.
Android 3.1 이전에 도입된 API에서만 지원됩니다. 어떤 경우에도
버전별 구현은 런타임에 사용되며, 구현은 다음을 전달합니다.
호출 결과를 게임에 투명하게 반환합니다.
Android 4.1 이상에서 인터페이스 구현
InputManagerCompatV16
는
InputManagerCompat
:
실제 InputManager
및 InputManager.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 ... }
자바
// 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.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); } }
다음을 확장하는 PollingMessageHandler
객체 구현
Handler
로 설정하고,
<ph type="x-smartling-placeholder">handleMessage()
</ph>
메서드를 사용하여 축소하도록 요청합니다. 이 메서드는 연결된 게임 컨트롤러가
연결 해제되고 등록된 리스너에 알립니다.
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
전송을 위한 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 } } } }
자바
@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
객체를 간단히 인스턴스화하고
기본 앱에 InputManagerCompat.InputDeviceListener
를 등록하고
View
입니다. 개발자가 설정한 버전 전환 로직 때문에
게임에 적합한 구현 방식을 자동으로 사용하여
확인해야 합니다
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); ... } }
그런 다음
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) }
자바
@Override public boolean onGenericMotionEvent(MotionEvent event) { inputManager.onGenericMotionEvent(event); // Handle analog input from the controller as normal ... return super.onGenericMotionEvent(event); }
이 호환성 코드의 전체 구현은
샘플 ControllerSample.zip
에 제공된 GameView
클래스
위에서 다운로드할 수 있습니다.