Jika game Anda mendukung pengontrol game, Anda bertanggung jawab untuk memastikan bahwa game merespons pengontrol secara konsisten di seluruh perangkat yang menjalankan versi Android yang berbeda. Dengan begitu, game Anda dapat menjangkau audiens yang lebih luas, dan pemain dapat menikmati pengalaman gameplay terbaik dengan pengontrolnya, bahkan saat mereka mengganti atau mengupgrade perangkat Android.
Tutorial ini menunjukkan cara menggunakan API yang tersedia di Android 4.1 dan versi yang lebih tinggi dengan cara yang kompatibel dengan versi sebelumnya sehingga game Anda dapat mendukung fitur berikut di perangkat yang menjalankan Android 3.1 dan versi yang lebih tinggi:
- Game dapat mendeteksi saat ada pengontrol game baru yang ditambahkan, diubah, atau dihapus.
- Game dapat mengkueri kemampuan pengontrol game.
- Game dapat mengenali peristiwa gerakan yang akan terjadi dari pengontrol game.
Contoh di tutorial ini didasarkan pada implementasi referensi
yang diberikan oleh controh ControllerSample.zip
yang tersedia untuk didownload
di atas. Contoh ini menunjukkan cara mengimplementasikan antarmuka InputManagerCompat
untuk mendukung berbagai versi Android. Untuk mengompilasi contoh, Anda
harus menggunakan Android 4.1 (API level 16) atau versi yang lebih tinggi. Setelah dikompilasi, aplikasi contoh
berjalan di semua perangkat yang menjalankan Android 3.1 (API level 12) atau versi yang lebih tinggi sebagai target
build.
Menyiapkan abstraksi API untuk dukungan pengontrol game
Misalnya, Anda ingin agar dapat menentukan saat status sambungan pengontrol game telah berubah di perangkat yang berjalan di Android 3.1 (API level 12). Namun, API hanya tersedia di Android 4.1 (API level 16) dan versi yang lebih tinggi, jadi Anda perlu memberikan implementasi yang mendukung Android 4.1 dan versi yang lebih tinggi, selagi memberikan mekanisme penggantian yang mendukung Android 3.1 hingga Android 4.0.
Untuk membantu Anda menentukan fitur mana yang memerlukan mekanisme penggantian semacam itu untuk versi yang lebih lama, tabel 1 mencantumkan perbedaan dalam dukungan pengontrol game antara Android 3.1 (API level 12) dan 4.1 (API level 16).
Informasi Pengontrol | Controller API | API level 12 | API level 16 |
---|---|---|---|
Identifikasi Perangkat | getInputDeviceIds() |
• | |
getInputDevice() |
• | ||
getVibrator() |
• | ||
SOURCE_JOYSTICK |
• | • | |
SOURCE_GAMEPAD |
• | • | |
Status Sambungan | onInputDeviceAdded() |
• | |
onInputDeviceChanged() |
• | ||
onInputDeviceRemoved() |
• | ||
Identifikasi Peristiwa Input | Penekanan D-pad (
KEYCODE_DPAD_UP ,
KEYCODE_DPAD_DOWN ,
KEYCODE_DPAD_LEFT ,
KEYCODE_DPAD_RIGHT ,
KEYCODE_DPAD_CENTER ) |
• | • |
Penekanan tombol gamepad (
BUTTON_A ,
BUTTON_B ,
BUTTON_THUMBL ,
BUTTON_THUMBR ,
BUTTON_SELECT ,
BUTTON_START ,
BUTTON_R1 ,
BUTTON_L1 ,
BUTTON_R2 ,
BUTTON_L2 ) |
• | • | |
Pergerakan joystick dan hat switch (
AXIS_X ,
AXIS_Y ,
AXIS_Z ,
AXIS_RZ ,
AXIS_HAT_X ,
AXIS_HAT_Y ) |
• | • | |
Penekanan pemicu analog (
AXIS_LTRIGGER ,
AXIS_RTRIGGER ) |
• | • |
Anda dapat menggunakan abstraksi untuk membuat dukungan pengontrol game berbasis versi yang dapat berfungsi di seluruh platform. Pendekatan ini mencakup langkah-langkah berikut:
- Tentukan antarmuka Java perantara yang mengabstraksikan implementasi fitur pengontrol game yang diperlukan oleh game Anda.
- Buat implementasi proxy antarmuka Anda menggunakan API di Android versi 4.1 dan versi yang lebih tinggi.
- Buat implementasi kustom antarmuka Anda menggunakan API yang tersedia di antara Android 3.1 hingga Android 4.0.
- Buat logika untuk peralihan antar-implementasi ini pada waktu proses, dan mulai gunakan antarmuka tersebut di game Anda.
Untuk ringkasan terkait penggunaan abstraksi untuk memastikan bahwa aplikasi kompatibel dengan semua versi Android sebelumnya, buka Membuat UI yang Kompatibel dengan Versi Sebelumnya.
Menambahkan antarmuka untuk kompatibilitas mundur
Untuk memberikan kompatibilitas mundur, Anda dapat membuat antarmuka kustom, kemudian menambahkan implementasi khusus versi. Salah satu keuntungan pendekatan ini adalah, pendekatan ini memungkinkan Anda mencerminkan antarmuka publik di Android 4.1 (API level 16) yang mendukung pengontrol game.
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); } ... }
Antarmuka InputManagerCompat
memberikan metode berikut:
getInputDevice()
- Mencerminkan
getInputDevice()
. Mendapatkan objekInputDevice
yang mewakili kemampuan pengontrol game. getInputDeviceIds()
- Mencerminkan
getInputDeviceIds()
. Menampilkan array integer, yang masing-masing merupakan ID untuk perangkat input yang berbeda. Metode ini berguna jika Anda membuat game yang mendukung multiplayer dan Anda ingin mendeteksi jumlah pengontrol yang tersambung. registerInputDeviceListener()
- Mencerminkan
registerInputDeviceListener()
. Memungkinkan Anda mendaftar agar diberi tahu saat ada perangkat baru yang ditambahkan, diubah, atau dihapus. unregisterInputDeviceListener()
- Mencerminkan
unregisterInputDeviceListener()
. Membatalkan pendaftaran pemroses perangkat input. onGenericMotionEvent()
- Mencerminkan
onGenericMotionEvent()
. Memungkinkan game Anda mencegat dan menangani objekMotionEvent
dan nilai sumbu yang mewakili peristiwa seperti pergerakan joystick dan penekanan trigger analog. onPause()
- Menghentikan polling peristiwa pengontrol game saat aktivitas utama dijeda, atau saat game tidak lagi memiliki fokus.
onResume()
- Memulai polling peristiwa pengontrol game saat aktivitas utama dilanjutkan, atau saat game dimulai dan berjalan di latar depan.
InputDeviceListener
- Mencerminkan
antarmuka
InputManager.InputDeviceListener
. Memungkinkan game Anda mengetahui saat ada pengontrol game yang ditambahkan, diubah, atau dihapus.
Selanjutnya, buat implementasi untuk InputManagerCompat
yang dapat berfungsi
di berbagai versi platform. Jika game Anda berjalan di Android 4.1 atau
yang lebih tinggi dan memanggil metode InputManagerCompat
, implementasi proxy
akan memanggil metode yang setara di InputManager
.
Namun, jika game Anda berjalan di Android 3.1 hingga Android 4.0, proses implementasi kustom
akan memanggil metode InputManagerCompat
dengan menggunakan
API yang diperkenalkan di Android dengan versi yang tidak lebih baru dari 3.1 saja. Terlepas dari implementasi
khusus versi mana pun yang digunakan pada waktu proses, implementasi tersebut akan meneruskan
kembali hasil panggilan secara transparan ke game.
Menerapkan antarmuka di Android 4.1 dan versi yang lebih tinggi
InputManagerCompatV16
merupakan implementasi antarmuka
InputManagerCompat
yang mewakili panggilan metode ke
InputManager
dan InputManager.InputDeviceListener
yang sebenarnya. InputManager
diperoleh dari
Context
sistem.
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 } }
Mengimplementasikan antarmuka di Android 3.1 hingga Android 4.0
Untuk membuat implementasi InputManagerCompat
yang mendukung Android 3.1 hingga Android 4.0, Anda dapat menggunakan
objek berikut:
SparseArray
ID perangkat untuk melacak pengontrol game yang tersambung ke perangkat.Handler
untuk memproses peristiwa perangkat. Saat aplikasi dimulai atau dilanjutkan,Handler
menerima pesan untuk memulai polling pemutusan sambungan pengontrol game. KodeHandler
akan memulai loop untuk memeriksa setiap pengontrol game tersambung yang dikenali dan melihat apakah ID perangkat ditampilkan. Nilai ditampilkan yang berupanull
menunjukkan bahwa pengontrol game terputus sambungannya.Handler
menghentikan polling saat aplikasi dijeda.Map
objekInputManagerCompat.InputDeviceListener
. Anda akan menggunakan pemroses untuk memperbarui status sambungan pengontrol game yang dilacak.
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); } }
Menerapkan objek PollingMessageHandler
yang memperluas
Handler
, dan menggantikan
metode
handleMessage()
. Metode ini memeriksa apakah pengontrol game yang terpasang
terputus sambungannya dan memberi tahu pemroses yang terdaftar.
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; } } }
Guna memulai dan menghentikan polling untuk pemutusan sambungan pengontrol game, ganti metode ini:
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); }
Untuk mendeteksi saat ada perangkat input yang telah ditambahkan, ganti
metode onGenericMotionEvent()
. Saat sistem melaporkan peristiwa gerakan,
periksa apakah peristiwa ini berasal dari ID perangkat yang telah dilacak, atau dari
ID perangkat yang baru. Jika berasal dari ID perangkat baru, beri tahu pemroses yang terdaftar.
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; }
Notifikasi pemroses diimplementasikan dengan menggunakan
objek Handler
untuk mengirim objek DeviceEvent
Runnable
ke antrean pesan. DeviceEvent
berisi referensi ke InputManagerCompat.InputDeviceListener
. Saat
DeviceEvent
berjalan, metode callback pemroses yang sesuai
dipanggil untuk menandakan saat ada pengontrol game yang ditambahkan, diubah, atau dihapus.
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); } }
Kini, Anda memiliki dua implementasi InputManagerCompat
: satu
berfungsi di perangkat yang menjalankan Android 4.1 dan versi yang lebih tinggi, dan satu lagi
berfungsi di perangkat yang menjalankan Android 3.1 hingga Android 4.0.
Menggunakan implementasi khusus versi
Logika peralihan khusus versi diimplementasikan di class yang berperan sebagai 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(); } } }
Kini Anda cukup membuat instance objek InputManagerCompat
dan
mendaftarkan InputManagerCompat.InputDeviceListener
di
View
utama Anda. Karena logika peralihan versi yang disiapkan,
game Anda akan secara otomatis menggunakan implementasi yang sesuai untuk
versi Android yang dijalankan perangkat.
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); ... } }
Selanjutnya, ganti
metode onGenericMotionEvent()
di tampilan utama Anda, seperti yang dijelaskan di
Menangani MotionEvent dari Pengontrol
Game. Kini game Anda seharusnya dapat memproses peristiwa pengontrol game
secara konsisten di perangkat yang menjalankan Android 3.1 (API level 12) dan versi yang lebih tinggi.
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); }
Anda dapat menemukan implementasi lengkap dari kode kompatibilitas ini di
class GameView
yang disediakan di contoh ControllerSample.zip
,
tersedia untuk didownload di atas.