Jeśli Twoja gra obsługuje kontrolery do gier, ponosisz odpowiedzialność aby gra spójnie reaguje na kontrolery na różnych urządzeniach na różnych wersjach Androida. Dzięki temu gra może dotrzeć do szerszego grona odbiorców a gracze mogą cieszyć się płynną rozgrywką dzięki nawet w przypadku zmiany urządzenia z Androidem na nowszą wersję.
Z tej lekcji dowiesz się, jak korzystać z interfejsów API dostępnych w Androidzie 4.1 i nowszych w sposób zgodny wstecznie, dzięki czemu gra będzie obsługiwać te funkcje na urządzeniach z Androidem 3.1 lub nowszym:
- Gra może wykryć, czy nowy kontroler został dodany, zmieniony lub usunięty.
- Gra może wysyłać zapytania o możliwości kontrolera do gier.
- Gra może rozpoznawać przychodzące zdarzenia ruchu z kontrolera gry.
Przykłady w tej lekcji wykorzystują referencyjną implementację
podane w próbce ControllerSample.zip
dostępnej do pobrania
powyżej. Ten przykład pokazuje, jak wdrożyć InputManagerCompat
obsługę różnych wersji Androida. Aby skompilować przykład,
musi używać Androida 4.1 (poziom interfejsu API 16) lub nowszego. Po skompilowaniu aplikacja przykładowa
działa na każdym urządzeniu z Androidem 3.1 (poziom interfejsu API 12) lub nowszym.
cel.
Przygotowanie do abstrakcyjnych interfejsów API do obsługi kontrolera gier
Załóżmy, że chcesz móc określić, czy połączenie kontrolera do gier zmienił się stan urządzeń z Androidem 3.1 (poziom interfejsu API 12). Pamiętaj jednak: interfejsy API są dostępne tylko w Androidzie 4.1 (poziom API 16) i nowszych, muszą zapewnić implementację, która obsługuje Androida 4.1 lub nowszego, udostępniając mechanizm kreacji zastępczych, który obsługuje Androida od 3.1 do 4.0.
Pomaga określić, które funkcje wymagają takiego mechanizmu awaryjnego starszych wersji w tabeli 1 przedstawiliśmy 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 interfejsu API 16 |
---|---|---|---|
Identyfikacja urządzenia | getInputDeviceIds() |
• | |
getInputDevice() |
• | ||
getVibrator() |
• | ||
SOURCE_JOYSTICK |
• | • | |
SOURCE_GAMEPAD |
• | • | |
Stan połączenia | onInputDeviceAdded() |
• | |
onInputDeviceChanged() |
• | ||
onInputDeviceRemoved() |
• | ||
Identyfikacja zdarzenia wejściowego | Naciśnij pad kierunkowy (
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 przełącznikiem joysticka i kapelusza (
AXIS_X ,
AXIS_Y ,
AXIS_Z ,
AXIS_RZ ,
AXIS_HAT_X ,
AXIS_HAT_Y ) |
• | • | |
Naciśnięcie aktywatora analogowego (
AXIS_LTRIGGER ,
AXIS_RTRIGGER ). |
• | • |
Możesz wykorzystać abstrakcję, aby utworzyć obsługę kontrolera do gier z uwzględnieniem wersji, która działa na różnych platformach. To podejście obejmuje te kroki:
- Zdefiniuj pośredni interfejs Java, który abstrakcyjna będzie implementacja funkcje kontrolera do gier wymagane przez grę.
- Utwórz implementację proxy interfejsu korzystającego z interfejsów API w Androidzie. w wersji 4.1 i nowszych.
- Tworzenie niestandardowej implementacji interfejsu wykorzystującej dostępne interfejsy API od Androida 3.1 do 4.0.
- Utwórz logikę przełączania między tymi implementacjami w czasie działania, i zacznij korzystać z interfejsu w grze.
Omówienie sposobów wykorzystania abstrakcji do zapewnienia, że aplikacje mogą działać w sposób zgodny wstecznie na różnych wersjach Androida, patrz Tworzenie Interfejsy API zgodne wstecz
Dodaj interfejs, aby zapewnić zgodność wsteczną
Aby zapewnić zgodność wsteczną, możesz utworzyć niestandardowy interfejs, a następnie i dodawać implementacje w określonej wersji. Jedną z zalet tego podejścia jest to, pozwala powielać publiczne interfejsy Androida 4.1 (poziom API 16), obsługują kontrolery do 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()
- Odzwierciedla
getInputDevice()
. PobieraInputDevice
reprezentujący możliwości kontrolera gry. getInputDeviceIds()
- Odzwierciedla
getInputDeviceIds()
. Zwraca tablicę liczb całkowitych, z których każda który jest identyfikatorem innego urządzenia wejściowego. Jest to przydatne, jeśli tworzysz To gra dla wielu graczy i chcesz określić, ilu są podłączone. registerInputDeviceListener()
- Odzwierciedla
registerInputDeviceListener()
. Pozwala zarejestrować się, aby otrzymywać informacje o nowych po dodaniu, zmianie lub usunięciu urządzenia. unregisterInputDeviceListener()
- Odzwierciedla
unregisterInputDeviceListener()
. Wyrejestrowuje detektor urządzenia wejściowego. onGenericMotionEvent()
- Odzwierciedla
onGenericMotionEvent()
. Przechwytywanie i obsługa gryMotionEvent
obiektów i wartości osi, które reprezentują zdarzenia takie jak ruchy joysticka i naciśnięcia spustu analogowego. onPause()
- Przestaje odpytywać zdarzenia kontrolera gier, gdy główna aktywność jest wstrzymana lub gra nie jest już skupiona.
onResume()
- Rozpoczyna odpytywanie zdarzeń kontrolera gier, gdy główna aktywność zostanie wznowiona lub po rozpoczęciu gry na pierwszym planie.
InputDeviceListener
- Odzwierciedla
InputManager.InputDeviceListener
za pomocą prostego interfejsu online. Informuje grę, gdy kontroler został dodany, zmieniony lub usunięto.
Następnie utwórz działające implementacje interfejsu InputManagerCompat
na różnych wersjach platformy. Jeśli gra działa na Androidzie 4.1 lub
i wywołuje metodę InputManagerCompat
, implementację serwera proxy
wywołuje równoważną metodę w InputManager
.
Jeśli jednak gra działa na Androidzie od 3.1 do 4.0, niestandardowa implementacja
przetwarza wywołania metod InputManagerCompat
za pomocą
tylko interfejsy API wprowadzone nie później niż do Androida 3.1. Bez względu na to
jest używana w czasie wykonywania, implementacja musi
a wyniki rozmowy
w przejrzysty sposób zostaną zwrócone do gry.
Implementacja interfejsu w Androidzie 4.1 i nowszych
InputManagerCompatV16
to implementacja
InputManagerCompat
, który pośredniczy w wywołaniach metody
wartości rzeczywiste 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 w systemach Android od 3.1 do 4.0
Aby utworzyć implementację InputManagerCompat
, która obsługuje Androida od 3.1 do 4.0, możesz użyć
następujące obiekty:
SparseArray
identyfikatorów urządzeń do śledzenia kontrolerów gier podłączonych do urządzenia.Handler
do przetwarzania zdarzeń dotyczących urządzenia. Po uruchomieniu aplikacji lub wznowione,Handler
otrzymuje wiadomość z prośbą o rozpoczęcie odpytywania .Handler
rozpocznie w pętli, aby sprawdzić każdy znany podłączony kontroler do gier i sprawdzić, czy identyfikator urządzenia . Zwracana wartośćnull
oznacza, że kontroler do gier jest – rozłączono.Handler
przestaje odpytywać, gdy aplikacja jest wstrzymane.Map
zInputManagerCompat.InputDeviceListener
obiektów. Będziesz używać detektorów do aktualizowania stanu połączenia śledzonego kontrolerach do 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 rozciąga się
Handler
i zastąp
handleMessage()
. Ta metoda sprawdza, czy podłączony kontroler do gier został
jest rozłączony i wysyła powiadomienia do zarejestrowanych detektorów.
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 o odłączenie kontrolera gry, zastąp ustawienie 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 dodano urządzenie wejściowe, zastąp
Metoda onGenericMotionEvent()
. Gdy system zgłosi zdarzenie ruchu,
Sprawdź, czy zdarzenie pochodzi z identyfikatora urządzenia, które jest już śledzone
nowego identyfikatora urządzenia. Jeśli identyfikator urządzenia jest nowy, powiadom zarejestrowanych słuchaczy.
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; }
Powiadomienia detektorów są implementowane za pomocą polecenia
Handler
obiekt do wysłania DeviceEvent
Runnable
obiekt do kolejki wiadomości. DeviceEvent
zawiera odwołanie do elementu InputManagerCompat.InputDeviceListener
. Kiedy
gdy uruchomi się DeviceEvent
, odpowiednia metoda wywołania zwrotnego detektora
jest wywoływana, aby zasygnalizować, czy kontroler do gier został dodany, zmieniony czy 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 kodu InputManagerCompat
: jedną, która
działa na urządzeniach z Androidem 4.1 i nowszym oraz na innych
który działa na urządzeniach z Androidem w wersji od 3.1 do 4.0.
Użyj implementacji w zależności od wersji
Logika przełączania określonej 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
zarejestruj InputManagerCompat.InputDeviceListener
w swojej głównej
View
Ze względu na ustawiony przez Ciebie mechanizm przełączania wersji
gra automatycznie stosuje implementację, która pasuje do
wersji Androida zainstalowanej na urządzeniu.
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
onGenericMotionEvent()
w widoku głównym, tak jak to opisano w
Obsługa zdarzenia MotionEvent z gry
Kontroler. Twoja gra powinna teraz przetwarzać zdarzenia kontrolera
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
GameView
klasa podana w przykładzie ControllerSample.zip
dostępne do pobrania powyżej.