Se supporti i controller di gioco nel tuo gioco, è tua responsabilità assicurarti che il gioco risponda ai controller in modo coerente sui dispositivi che eseguono versioni diverse di Android. In questo modo, il tuo gioco può raggiungere un pubblico più ampio e i giocatori possono godersi un'esperienza di gioco fluida con i loro controller anche quando cambiano o aggiornano i loro dispositivi Android.
Questa lezione mostra come utilizzare le API disponibili in Android 4.1 e versioni successive in modo compatibile con le versioni precedenti, consentendo al tuo gioco di supportare le seguenti funzionalità sui dispositivi che eseguono Android 3.1 e versioni successive:
- Il gioco può rilevare se un nuovo controller di gioco viene aggiunto, modificato o rimosso.
- Il gioco può eseguire query sulle funzionalità di un controller di gioco.
- Il gioco può riconoscere gli eventi di movimento in entrata da un controller di gioco.
Prepararsi ad astrarre le API per il supporto dei controller di gioco
Supponiamo che tu voglia essere in grado di determinare se lo stato della connessione di un controller di gioco è cambiato sui dispositivi che eseguono Android 3.1 (livello API 12). Tuttavia, le API sono disponibili solo in Android 4.1 (livello API 16) e versioni successive, quindi devi fornire un'implementazione che supporti Android 4.1 e versioni successive, fornendo al contempo un meccanismo di fallback che supporti Android 3.1 fino ad Android 4.0.
Per aiutarti a determinare quali funzionalità richiedono un meccanismo di fallback per le versioni precedenti, la Tabella 1 elenca le differenze nel supporto dei controller di gioco tra Android 3.1 (livello API 12) e 4.1 (livello API 16).
Tabella 1. API per il supporto dei controller di gioco in diverse versioni di Android.
| Informazioni sul controller | API controller | Livello API 12 | Livello API 16 |
|---|---|---|---|
| Identificazione del dispositivo | getInputDeviceIds() |
• | |
getInputDevice() |
• | ||
getVibrator() |
• | ||
SOURCE_JOYSTICK |
• | • | |
SOURCE_GAMEPAD |
• | • | |
| Stato della connessione | onInputDeviceAdded() |
• | |
onInputDeviceChanged() |
• | ||
onInputDeviceRemoved() |
• | ||
| Identificazione dell'evento di input | Pressione del D-pad ( KEYCODE_DPAD_UP, KEYCODE_DPAD_DOWN, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_RIGHT, KEYCODE_DPAD_CENTER) |
• | • |
Pressione del pulsante del gamepad ( BUTTON_A, BUTTON_B, BUTTON_THUMBL, BUTTON_THUMBR, BUTTON_SELECT, BUTTON_START, BUTTON_R1, BUTTON_L1, BUTTON_R2, BUTTON_L2) |
• | • | |
Movimento del joystick e del cappello ( AXIS_X, AXIS_Y, AXIS_Z, AXIS_RZ, AXIS_HAT_X, AXIS_HAT_Y) |
• | • | |
Pressione del trigger analogico ( AXIS_LTRIGGER, AXIS_RTRIGGER) |
• | • |
Puoi utilizzare l'astrazione per creare un supporto per i controller di gioco che riconosce la versione e funziona su tutte le piattaforme. Questo approccio prevede i seguenti passaggi:
- Definisci un'interfaccia Java intermedia che astragga l'implementazione delle funzionalità del controller di gioco richieste dal tuo gioco.
- Crea un'implementazione proxy dell'interfaccia che utilizza le API in Android 4.1 e versioni successive.
- Crea un'implementazione personalizzata dell'interfaccia che utilizza le API disponibili tra Android 3.1 e Android 4.0.
- Crea la logica per passare da un'implementazione all'altra in fase di runtime e inizia a utilizzare l'interfaccia nel gioco.
Per una panoramica su come utilizzare l'astrazione per verificare che le applicazioni possano funzionare in modo compatibile con le versioni precedenti su diverse versioni di Android, consulta Creare UI compatibili con le versioni precedenti.
Aggiungere un'interfaccia per la compatibilità con le versioni precedenti
Per fornire la compatibilità con le versioni precedenti, puoi creare un'interfaccia personalizzata e poi aggiungere implementazioni specifiche per la versione. Uno dei vantaggi di questo approccio è che ti consente di eseguire il mirroring delle interfacce pubbliche su Android 4.1 (livello API 16) che supportano i controller di gioco.
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);
}
...
}
L'interfaccia InputManagerCompat fornisce i seguenti metodi:
getInputDevice()- Esegue il mirroring di
getInputDevice(). Ottiene l'oggettoInputDeviceche rappresenta le funzionalità di un controller di gioco. getInputDeviceIds()- Esegue il mirroring di
getInputDeviceIds(). Restituisce un array di numeri interi, ognuno dei quali è un ID per un dispositivo di input diverso. Questa opzione è utile se stai creando un gioco che supporta più giocatori e vuoi rilevare quanti controller sono connessi. registerInputDeviceListener()- Esegue il mirroring di
registerInputDeviceListener(). Ti consente di registrarti per ricevere una notifica quando un nuovo dispositivo viene aggiunto, modificato o rimosso. unregisterInputDeviceListener()- Esegue il mirroring di
unregisterInputDeviceListener(). Annulla la registrazione di un listener del dispositivo di input. onGenericMotionEvent()- Esegue il mirroring di
onGenericMotionEvent(). Consente al tuo gioco di intercettare e gestire gli oggettiMotionEvente i valori degli assi che rappresentano eventi come i movimenti del joystick e le pressioni dei trigger analogici. onPause()- Interrompe il polling degli eventi del controller di gioco quando l'attività principale viene messa in pausa o quando il gioco non ha più lo stato attivo.
onResume()- Avvia il polling degli eventi del controller di gioco quando l'attività principale viene ripresa o quando il gioco viene avviato ed è in esecuzione in primo piano.
InputDeviceListener- Esegue il mirroring dell'interfaccia
InputManager.InputDeviceListener. Consente al tuo gioco di sapere quando un controller di gioco è stato aggiunto, modificato o rimosso.
Poi, crea implementazioni per InputManagerCompat che funzionino su diverse versioni della piattaforma. Se il gioco è in esecuzione su Android 4.1 o
versioni successive e chiama un metodo InputManagerCompat, l'implementazione proxy
chiama il metodo equivalente in InputManager.
Tuttavia, se il gioco è in esecuzione su Android 3.1 fino ad Android 4.0, l'implementazione personalizzata elabora le chiamate ai metodi InputManagerCompat utilizzando solo le API introdotte non oltre Android 3.1. Indipendentemente dall'implementazione specifica della versione utilizzata in fase di runtime, l'implementazione passa i risultati della chiamata al gioco in modo trasparente.
Implementare l'interfaccia su Android 4.1 e versioni successive
InputManagerCompatV16 è un'implementazione dell'InputManagerCompat
interfaccia che esegue il proxy delle chiamate ai metodi a un
InputManager e a un
InputManager.InputDeviceListener effettivi.
InputManager viene
ottenuto da Context di sistema.
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
}
}
Implementare l'interfaccia su Android 3.1 fino ad Android 4.0
Per creare un'implementazione di InputManagerCompat che supporti Android 3.1 fino ad Android 4.0, puoi utilizzare i seguenti oggetti:
- Un
SparseArraydi ID dispositivo per monitorare i controller di gioco connessi al dispositivo. - Un
Handlerper elaborare gli eventi del dispositivo. Quando un'app viene avviata o ripresa, ilHandlerriceve un messaggio per avviare il polling per la disconnessione del controller di gioco.Handleravvierà un ciclo per controllare ogni controller di gioco connesso noto e verificare se viene restituito un ID dispositivo. Un valore di ritornonullindica che il controller di gioco è disconnesso. TheHandlerinterrompe il polling quando l'app viene messa in pausa. Una
MapdiInputManagerCompat.InputDeviceListeneroggetti. Utilizzerai i listener per aggiornare lo stato della connessione dei controller di gioco monitorati.
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);
}
}
Implementa un oggetto PollingMessageHandler che estende
Handler ed esegui l'override del
handleMessage()
metodo. Questo metodo verifica se un controller di gioco collegato è stato disconnesso e invia una notifica ai listener registrati.
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;
}
}
}
Per avviare e interrompere il polling per la disconnessione del controller di gioco, esegui l'override di questi metodi:
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);
}
Per rilevare che è stato aggiunto un dispositivo di input, esegui l'override del metodo onGenericMotionEvent(). Quando il sistema segnala un evento di movimento, controlla se questo evento proviene da un ID dispositivo già monitorato o da un nuovo ID dispositivo. Se l'ID dispositivo è nuovo, invia una notifica ai listener registrati.
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;
}
La notifica dei listener viene implementata utilizzando l'
Handler per inviare un DeviceEvent
Runnable alla coda dei messaggi.
DeviceEvent contiene un riferimento a un InputManagerCompat.InputDeviceListener. Quando DeviceEvent viene eseguito, viene chiamato il metodo di callback appropriato del listener per segnalare se il controller di gioco è stato aggiunto, modificato o rimosso.
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);
}
}
Ora hai due implementazioni di InputManagerCompat: una che funziona su dispositivi che eseguono Android 4.1 e versioni successive e un'altra che funziona su dispositivi che eseguono Android 3.1 fino ad Android 4.0.
Utilizzare l'implementazione specifica della versione
La logica di cambio specifica della versione viene implementata in una classe che funge da a 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();
}
}
}
Ora puoi creare un'istanza di un oggetto InputManagerCompat e registrare un
InputManagerCompat.InputDeviceListener nel tuo
View principale. Grazie alla logica di cambio di versione che hai configurato, il gioco utilizza automaticamente l'implementazione appropriata per la versione di Android in esecuzione sul dispositivo.
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);
...
}
}
Poi, esegui l'override del metodo onGenericMotionEvent() nella visualizzazione principale, come
descritto in Gestire un MotionEvent da un controller di gioco e versioni successive. Il gioco dovrebbe ora essere in grado di elaborare gli eventi del controller di gioco in modo coerente sui dispositivi che eseguono Android 3.1 (livello 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);
}
Puoi trovare un'implementazione completa di questo codice di compatibilità nella classe GameView fornita nel file di esempio ControllerSample.zip disponibile per il download.