Jeśli w swojej grze korzystasz z kontrolerów do gier, Twoim obowiązkiem jest upewnienie się, że gra zachowuje spójność na różnych urządzeniach z różnymi wersjami Androida. Dzięki temu Twoja gra może dotrzeć do szerszego grona odbiorców, a gracze mogą płynnie grać za pomocą kontrolerów, nawet jeśli przejdą na inne urządzenie z Androidem lub zmodernizują swoje urządzenie.
Ta lekcja pokazuje, jak korzystać z interfejsów API dostępnych na Androidzie 4.1 i nowszych w sposób zgodny wstecznie, tak by gra obsługiwała te funkcje na urządzeniach z Androidem 3.1 lub nowszym:
- Gra może wykryć, czy został dodany, zmieniony czy usunięty.
- Gra może sprawdzać możliwości kontrolera do gier.
- Gra może rozpoznawać przychodzące zdarzenia ruchu z kontrolera do gier.
Przykłady w tej lekcji opierają się na referencyjnej implementacji, którą znajdziesz powyżej w przykładzie ControllerSample.zip
. Ten przykład pokazuje, jak wdrożyć interfejs InputManagerCompat
do obsługi różnych wersji Androida. Aby skompilować przykład, musisz używać Androida 4.1 (poziom interfejsu API 16) lub nowszego. Po skompilowaniu przykładowa aplikacja działa na dowolnym urządzeniu z Androidem 3.1 (poziom interfejsu API 12) lub nowszym.
Przygotuj się do abstrakcyjnych interfejsów API do obsługi kontrolera gier
Załóżmy, że chcesz mieć możliwość sprawdzenia, czy stan połączenia kontrolera gier na urządzeniach z Androidem 3.1 (poziom interfejsu API 12) zmienił się. Jednak interfejsy API są dostępne tylko na Androidzie 4.1 (poziom interfejsu API 16) i nowszych, więc musisz udostępnić implementację, która obsługuje Androida 4.1 i nowsze wersje, oraz udostępniać mechanizm kreacji zastępczej, który obsługuje Androida od 3.1 do 4.0.
Aby ułatwić określenie, które funkcje wymagają takiego mechanizmu kreacji zastępczej w przypadku starszych wersji, w tabeli 1 znajdziesz różnice w obsłudze kontrolerów gier między Androidem 3.1 (poziom interfejsu API 12) a 4.1 (poziom interfejsu API 16).
Informacje o administratorze | Interfejs API kontrolera | Poziom API 12 | Poziom API 16 |
---|---|---|---|
Identyfikacja urządzenia | getInputDeviceIds() |
• | |
getInputDevice() |
• | ||
getVibrator() |
• | ||
SOURCE_JOYSTICK |
• | • | |
SOURCE_GAMEPAD |
• | • | |
Stan połączenia | onInputDeviceAdded() |
• | |
onInputDeviceChanged() |
• | ||
onInputDeviceRemoved() |
• | ||
Identyfikacja zdarzeń wejściowych | Naciśnij na padzie kierunkowym (KEYCODE_DPAD_UP , KEYCODE_DPAD_DOWN , KEYCODE_DPAD_LEFT , KEYCODE_DPAD_RIGHT , KEYCODE_DPAD_CENTER ) |
• | • |
Naciśnięcie przycisku na padzie do gier (BUTTON_A ,
BUTTON_B ,
BUTTON_THUMBL ,
BUTTON_THUMBR ,
BUTTON_SELECT ,
BUTTON_START ,
BUTTON_R1 ,
BUTTON_L1 ,
BUTTON_R2 ,
BUTTON_L2 ) |
• | • | |
Ruch joysticka i przełącznika kapelusza (AXIS_X ,
AXIS_Y ,
AXIS_Z ,
AXIS_RZ ,
AXIS_HAT_X ,
AXIS_HAT_Y ) |
• | • | |
Naciśnięcie aktywatora analogowego (AXIS_LTRIGGER , AXIS_RTRIGGER ) |
• | • |
Za pomocą abstrakcji możesz zbudować obsługę kontrolera do gier z uwzględnieniem wersji, która działa na różnych platformach. Oto czynności, które musisz wykonać:
- Zdefiniuj pośredni interfejs Java, który wyodrębnia implementację funkcji kontrolera do gier wymaganych przez Twoją grę.
- Utwórz pośredniczącą implementację interfejsu, która używa interfejsów API na Androidzie 4.1 lub nowszym.
- Utwórz niestandardową implementację swojego interfejsu, która korzysta z interfejsów API dostępnych między Androidem 3.1 a 4.0.
- Utwórz logikę przełączania między tymi implementacjami w czasie działania i zacznij korzystać z interfejsu w grze.
Omówienie sposobu wykorzystania abstrakcji do zapewnienia zgodności wstecznej aplikacji w różnych wersjach Androida znajdziesz w artykule o tworzeniu interfejsów zgodnych wstecznie.
Dodawanie interfejsu na potrzeby zgodności wstecznej
Aby zapewnić zgodność wsteczną, możesz utworzyć interfejs niestandardowy i dodać implementacje związane z konkretną wersją. Jedną z zalet tego podejścia jest możliwość powielania interfejsów publicznych na Androidzie 4.1 (poziom API 16), które obsługują kontrolery gier.
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); } ... }
Interfejs InputManagerCompat
udostępnia te metody:
getInputDevice()
- Odbicia lustrzane
getInputDevice()
. Pobiera obiektInputDevice
, który reprezentuje możliwości kontrolera gier. getInputDeviceIds()
- Odbicia lustrzane
getInputDeviceIds()
. Zwraca tablicę liczb całkowitych, z których każda jest identyfikatorem innego urządzenia wejściowego. Jest to przydatne, gdy tworzysz grę, w której może grać wielu graczy i chcesz sprawdzić, ile kontrolerów jest podłączonych. registerInputDeviceListener()
- Odbicia lustrzane
registerInputDeviceListener()
. Dzięki tej funkcji możesz się zarejestrować, aby otrzymywać informacje o dodaniu, zmianie lub usunięciu nowego urządzenia. unregisterInputDeviceListener()
- Odbicia lustrzane
unregisterInputDeviceListener()
. Wyrejestrowuje detektor urządzenia wejściowego. onGenericMotionEvent()
- Odbicia lustrzane
onGenericMotionEvent()
. Pozwala grze na przechwytywanie i obsługę obiektówMotionEvent
oraz wartości osi, które reprezentują zdarzenia, takie jak ruchy joysticka czy naciśnięcia spustu analogowego. onPause()
- Zatrzymuje odpytywanie o zdarzenia na kontrolerze do gier, gdy główna aktywność zostanie wstrzymana lub gra nie będzie już koncentrować się na grze.
onResume()
- Rozpoczyna odpytywanie zdarzeń kontrolera gier, gdy zostanie wznowiona główna aktywność lub gdy gra już się rozpoczęła i działa na pierwszym planie.
InputDeviceListener
- Odzwierciedla interfejs
InputManager.InputDeviceListener
. Informuje grę o dodaniu, zmianie lub usunięciu kontrolera.
Następnie utwórz implementacje InputManagerCompat
, które działają na różnych wersjach platformy. Jeśli Twoja gra działa na Androidzie 4.1 lub nowszym i wywołuje metodę InputManagerCompat
, implementacja serwera proxy wywołuje jej odpowiednik w InputManager
.
Jeśli jednak gra działa na Androidzie od 3.1 do 4.0, implementacja niestandardowa przetwarza wywołania metod InputManagerCompat
przy użyciu tylko interfejsów API wprowadzonych nie później niż w Androidzie 3.1. Niezależnie od tego, która implementacja dla konkretnej wersji jest używana w czasie działania, implementacja przekazuje wyniki wywołania z powrotem do gry w przejrzysty sposób.
Wdrażanie interfejsu na Androidzie 4.1 lub nowszym
InputManagerCompatV16
to implementacja interfejsu InputManagerCompat
, który powoduje wywołanie metody proxy do rzeczywistych InputManager
i InputManager.InputDeviceListener
. Wartość InputManager
jest pobierana z systemu 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 } }
Implementacja interfejsu na urządzeniach z Androidem od 3.1 do 4.0
Aby utworzyć implementację InputManagerCompat
obsługującą Androida od 3.1 do 4.0, możesz użyć tych obiektów:
SparseArray
identyfikatorów urządzeń służących do śledzenia kontrolerów gier połączonych z urządzeniem.Handler
do przetwarzania zdarzeń dotyczących urządzenia. Po uruchomieniu lub wznowieniu aplikacjiHandler
otrzyma wiadomość z prośbą o rozpoczęcie odpytywania o odłączenie kontrolera gier.Handler
rozpocznie pętlę, aby sprawdzić każdy połączony kontroler gier i sprawdzić, czy zwracany jest identyfikator urządzenia. Zwracana wartośćnull
wskazuje, że kontroler gier jest odłączony.Handler
przerywa odpytywanie, gdy aplikacja jest wstrzymana.Map
zInputManagerCompat.InputDeviceListener
obiektów. Detektory będą aktualizować stan połączenia monitorowanych kontrolerów gier.
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); } }
Zaimplementuj obiekt PollingMessageHandler
, który rozszerza zakres Handler
, i zastąp metodę handleMessage()
. Ta metoda sprawdza, czy podłączony kontroler do gier został odłączony, i powiadamia zarejestrowane detektory.
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; } } }
Aby rozpocząć i zatrzymać odpytywanie w celu odłączenia kontrolera do gier, zastąp te metody:
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); }
Aby wykryć, że zostało dodane urządzenie wejściowe, zastąp metodę onGenericMotionEvent()
. Gdy system zgłasza zdarzenie ruchu, sprawdź, czy pochodzi ono z identyfikatora urządzenia, które jest już śledzone, czy z nowego identyfikatora urządzenia. Jeśli identyfikator urządzenia jest nowy, powiadom zarejestrowane detektory.
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; }
Powiadomienie detektorów wykorzystuje obiekt Handler
do wysyłania obiektu DeviceEvent
Runnable
do kolejki wiadomości. DeviceEvent
zawiera odwołanie do InputManagerCompat.InputDeviceListener
. Po uruchomieniu DeviceEvent
następuje wywołanie odpowiedniej metody wywołania zwrotnego detektora, aby zasygnalizować, czy kontroler gry został dodany, zmieniony lub usunięty.
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); } }
Masz teraz 2 implementacje pakietu InputManagerCompat
: jedną, która działa na urządzeniach z Androidem 4.1 lub nowszym, a druga, która działa na urządzeniach z Androidem w wersji od 3.1 do 4.0.
Używanie implementacji zależnie od wersji
Logika przełączania specyficzna dla wersji jest zaimplementowana w klasie, która działa jako fabryka.
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(); } } }
Teraz możesz po prostu utworzyć instancję obiektu InputManagerCompat
i zarejestrować InputManagerCompat.InputDeviceListener
w głównym View
. Ze względu na logikę zmiany wersji gra automatycznie korzysta z implementacji odpowiedniej dla wersji Androida, na której działa urządzenie.
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); ... } }
Następnie zastąp metodę onGenericMotionEvent()
w widoku głównym zgodnie z opisem w sekcji Obsługa zdarzeń MotionEvent z kontrolera gier. Gra powinna teraz przetwarzać w spójny sposób zdarzenia kontrolera do gier na urządzeniach z Androidem 3.1 (poziom interfejsu API 12) lub nowszym.
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); }
Pełną implementację tego kodu zgodności znajdziesz w klasie GameView
w przykładowym ControllerSample.zip
dostępnym do pobrania powyżej.