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.
Pelajaran ini menunjukkan cara menggunakan API yang tersedia di Android 4.1 dan yang lebih tinggi dengan cara yang kompatibel dengan versi sebelumnya sehingga memungkinkan game Anda mendukung fitur berikut di perangkat yang menjalankan Android 3.1 dan yang lebih tinggi:
- Game dapat mendeteksi apakah pengontrol game baru ditambahkan, diubah, atau dihapus.
- Game dapat mengkueri kemampuan pengontrol game.
- Game dapat mengenali peristiwa gerakan yang akan terjadi dari pengontrol game.
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 untuk versi yang lebih rendah, Tabel 1 mencantumkan perbedaan dalam dukungan pengontrol game antara Android 3.1 (API level 12) dan 4.1 (API level 16).
Tabel 1. API untuk dukungan pengontrol game di versi Android yang berbeda.
| 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 memverifikasi bahwa aplikasi dapat berfungsi secara kompatibel dengan versi sebelumnya di berbagai versi Android, lihat Membuat UI yang Kompatibel dengan Versi Sebelumnya.
Menambahkan antarmuka untuk kompatibilitas dengan versi sebelumnya
Untuk memberikan kompatibilitas dengan versi sebelumnya, Anda dapat membuat antarmuka kustom, lalu menambahkan implementasi khusus versi. Salah satu keuntungan pendekatan ini adalah, pendekatan ini memungkinkan Anda mencerminkan antarmuka publik di Android 4.1 (level API 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 objekInputDeviceyang 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 objekMotionEventdan 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 adalah 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:
SparseArrayID perangkat untuk melacak pengontrol game yang tersambung ke perangkat.Handleruntuk memproses peristiwa perangkat. Saat aplikasi dimulai atau dilanjutkan,Handlermenerima pesan untuk memulai polling pemutusan sambungan pengontrol game.Handlerakan memulai loop untuk memeriksa setiap pengontrol game tersambung yang dikenali dan melihat apakah ID perangkat ditampilkan. Nilai yang ditampilkan sebesarnullmenunjukkan bahwa pengontrol game terputus sambungannya.Handlermenghentikan polling saat aplikasi dijeda.MapobjekInputManagerCompat.InputDeviceListener. Anda akan menggunakan pemroses untuk memperbarui status koneksi 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 jika pengontrol game 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();
}
}
}
Sekarang Anda dapat membuat instance objek InputManagerCompat dan mendaftarkan
InputManagerCompat.InputDeviceListener di
View utama Anda. Karena logika peralihan versi yang disiapkan, game Anda akan 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 dan yang lebih tinggi). Game Anda
sekarang seharusnya dapat memproses peristiwa pengontrol game secara konsisten di perangkat
yang menjalankan Android 3.1 (level API).
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 yang tersedia untuk
didownload.