Se supporti i controller di gioco nel tuo gioco, è una tua responsabilità. per assicurarti che il gioco risponda ai controller in modo coerente su tutti i dispositivi su diverse versioni di Android. In questo modo il tuo gioco avrà una copertura più ampia e i giocatori possono godersi un'esperienza di gameplay senza interruzioni con i controller anche quando cambiano dispositivi Android o eseguono l'upgrade dei loro dispositivi.
Questa lezione mostra come utilizzare le API disponibili in Android 4.1 e versioni successive. in modo compatibile con le versioni precedenti, permettendo al gioco di supportare quanto segue funzioni su dispositivi con Android 3.1 e versioni successive:
- Il gioco può rilevare se un nuovo controller di gioco viene aggiunto, modificato o rimosso.
- Il gioco può interrogare le funzionalità di un controller di gioco.
- Il gioco può riconoscere gli eventi di movimento in arrivo da un controller di gioco.
Gli esempi di questa lezione si basano sull'implementazione di riferimento
fornita dall'esempio di ControllerSample.zip
disponibile per il download
in alto. Questo esempio mostra come implementare l'InputManagerCompat
per supportare diverse versioni di Android. Per compilare l'esempio,
Deve utilizzare Android 4.1 (livello API 16) o versioni successive. Una volta compilata, l'app di esempio
funziona su qualsiasi dispositivo con Android 3.1 (livello API 12) o versioni successive come build
target.
Preparati ad astrarre le API per il supporto dei controller di gioco
Supponiamo di voler capire se la connessione di un controller di gioco lo stato è cambiato sui dispositivi con Android 3.1 (livello API 12). Tuttavia, le API sono disponibili solo in Android 4.1 (livello API 16) e versioni successive, quindi fornire un'implementazione che supporti Android 4.1 o versioni successive che fornisce un meccanismo di riserva che supporta Android 3.1 fino ad Android 4.0.
Per aiutarti a determinare quali funzionalità richiedono un meccanismo di riserva per 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 12) 16).
Informazioni sul titolare | API Controller | Livello API 12 | Livello API 16 |
---|---|---|---|
Identificazione del dispositivo | getInputDeviceIds() |
• | |
getInputDevice() |
• | ||
getVibrator() |
• | ||
SOURCE_JOYSTICK |
• | • | |
SOURCE_GAMEPAD |
• | • | |
Stato connessione | onInputDeviceAdded() |
• | |
onInputDeviceChanged() |
• | ||
onInputDeviceRemoved() |
• | ||
Identificazione 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 analogica (
AXIS_LTRIGGER ,
AXIS_RTRIGGER ) |
• | • |
Puoi usare l'astrazione per creare il supporto di un controller di gioco sensibile alla versione che funziona su più piattaforme. Questo approccio comporta i seguenti passaggi:
- Definire un'interfaccia Java intermedia che astrae l'implementazione di le funzionalità del controller di gioco richieste dal gioco.
- Crea un'implementazione proxy della tua interfaccia che utilizza le API in Android 4.1 e versioni successive.
- Crea un'implementazione personalizzata della tua interfaccia che utilizzi le API disponibili tra Android 3.1 e Android 4.0.
- Crea la logica per il passaggio da un'implementazione all'altra in fase di runtime, e iniziare a utilizzare l'interfaccia nel gioco.
Per una panoramica di come utilizzare l'astrazione per garantire che le applicazioni possono funzionare in modo compatibile con le versioni precedenti di diverse versioni di Android, vedi Creazione in corso UI compatibili con le versioni precedenti.
Aggiungi un'interfaccia per la compatibilità con le versioni precedenti
Per garantire la compatibilità con le versioni precedenti, puoi creare un'interfaccia personalizzata, quindi aggiungere implementazioni specifiche della versione. Un vantaggio di questo approccio è che permette di eseguire il mirroring delle interfacce pubbliche su Android 4.1 (livello API 16) 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()
- Specchi
getInputDevice()
. Ottieni ilInputDevice
che rappresenta le funzionalità di un controller di gioco. getInputDeviceIds()
- Specchi
getInputDeviceIds()
. Restituisce un array di numeri interi, ciascuno che è un ID per un altro dispositivo di input. È utile se stai creando un gioco che supporta più giocatori e vuoi sapere quanti che siano connessi. registerInputDeviceListener()
- Specchi
registerInputDeviceListener()
. Ti consente di registrarti per essere informato quando un nuovo viene aggiunto, modificato o rimosso. unregisterInputDeviceListener()
- Specchi
unregisterInputDeviceListener()
. Annulla la registrazione di un listener del dispositivo di input. onGenericMotionEvent()
- Specchi
onGenericMotionEvent()
. Consente al gioco di intercettare e gestireMotionEvent
di oggetti e valori dell'asse che rappresentano gli eventi ad esempio movimenti del joystick e pressioni dei grilletti analogici. onPause()
- Interrompe il polling per gli eventi del controller di gioco quando l'attività principale viene messa in pausa o quando il gioco non è più attivo.
onResume()
- Avvia il polling per gli eventi del controller di gioco quando l'attività principale viene ripresa o quando il gioco viene avviato e viene eseguito in primo piano.
InputDeviceListener
- Rispecchia
InputManager.InputDeviceListener
a riga di comando. Consente al gioco di sapere quando un controller di gioco è stato aggiunto, modificato o rimosso.
Ora crea implementazioni per InputManagerCompat
che funzionino
su diverse versioni della piattaforma. Se il gioco utilizza Android 4.1 o
e richiama un metodo InputManagerCompat
, l'implementazione del proxy
chiama il metodo equivalente in InputManager
.
Tuttavia, se il gioco utilizza Android 3.1 fino ad Android 4.0, l'implementazione personalizzata
elabora le chiamate ai metodi InputManagerCompat
utilizzando
solo API introdotte non dopo Android 3.1. Indipendentemente da quale
un'implementazione specifica per la versione
viene usata in fase di runtime,
la chiamata restituisce in modo trasparente al gioco.
Implementazione dell'interfaccia su Android 4.1 e versioni successive
InputManagerCompatV16
è un'implementazione del
interfaccia InputManagerCompat
che esegue il proxy delle chiamate di metodo a un
InputManager
e InputManager.InputDeviceListener
effettivi. La
InputManager
ottenuto dal sistema
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 ... }
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 } }
Implementazione dell'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 usare
i seguenti oggetti:
- Un
SparseArray
di ID dispositivo per monitorare controller di gioco collegati al dispositivo. - Un
Handler
per elaborare gli eventi del dispositivo. Quando viene avviata un'app o ripristinato,Handler
riceve un messaggio per avviare il polling per la disconnessione del controller di gioco. IlHandler
avvierà per controllare ogni controller di gioco connesso noto e vedere se un ID dispositivo è restituito. Un valore restituitonull
indica che il controller di gioco è disconnesso.Handler
interrompe il polling quando l'app viene in pausa. - Un
Map
diInputManagerCompat.InputDeviceListener
di oggetti strutturati. Utilizzerai i listener per aggiornare lo stato della connessione del controller di gioco.
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 si estende
Handler
e sostituisci il
handleMessage()
. Questo metodo verifica se un controller di gioco collegato è stato
disconnesso e avvisa gli ascoltatori 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, sostituisci 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, sostituisci il
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, avvisa gli ascoltatori 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
Handler
oggetto per inviare DeviceEvent
Runnable
alla coda di messaggi. DeviceEvent
contiene un riferimento a un InputManagerCompat.InputDeviceListener
. Quando
viene eseguito DeviceEvent
, il metodo di callback appropriato del listener
viene chiamato 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 con Android 4.1 e versioni successive e
che funziona su dispositivi con sistema operativo Android 3.1 fino ad Android 4.0.
Utilizza l'implementazione specifica per la versione
La logica di passaggio specifica per la versione viene implementata in una classe che agisce una fabbrica.
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 semplicemente creare un'istanza per un oggetto InputManagerCompat
e
registra un InputManagerCompat.InputDeviceListener
nella tua
View
. A causa della logica di commutazione di versione impostata
il gioco utilizza automaticamente l'implementazione appropriata per
versione di Android installata 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); ... } }
Quindi, sostituisci
onGenericMotionEvent()
nella visualizzazione principale, come descritto in
Gestire un evento di movimento da un gioco
dell'assistenza clienti. Ora il gioco dovrebbe essere in grado di elaborare gli eventi del controller di gioco
in modo coerente sui dispositivi con Android 3.1 (livello API 12) e versioni successive.
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à nei
GameView
corso fornito nell'esempio ControllerSample.zip
disponibile per il download qui sopra.