- Mastering Android Game Development
- Raul Portales
- 2166字
- 2021-07-16 13:59:09
Physical controllers
Time to move into a type of controller that hardcore gamers love: physical ones.
There are a few devices that have a controller included as a part of their hardware. Some notable examples are XPeria Play—one of the pioneer phones that had a sliding gamepad—and Nvidia Shield, the latest in this category.
XPeria Play was one of the first devices with an integrated gamepad:

Nvidia Shield is one of the most powerful Android devices with a gamepad:

On the other hand, there are many brands that build game controllers for smartphones and they have been fairly popular among traditional gamers. All of them are Bluetooth controllers that can be connected to your phone or tablet. Some of them are designed to make your phone fit into it like Gametel (another pioneer) and most of the MOGA models.

MOGA controller with an adjustable strip to hold your phone
There are also a few Android-powered devices that use controllers as the main input source. Here, we are talking about microconsoles such as the OUYA or other TV-like devices such as the Amazon FireTV or Android TV.

The OUYA was the first of the Android microconsoles
These devices work in a very similar way to HID devices, either in the form of keyboards or as directional controls based on an axis (analog joysticks). All we have to do to handle them is to set the correct listeners.
Some controllers do have their own proprietary library. We won't cover this, since they are very specific and they provide detailed documentation on how to integrate them. This is the case with the MOGA Pocket (more advanced MOGA controllers support two modes: proprietary and HID).
Note
Most controllers work as HID, which is standard.
We can set listeners for the controller at the Activity
level or View
level. In any case, we will need to extend the class. There is no way to add a listener for these methods, they must be overridden. Since we are already extending the Activity
class, we'll do it this way.
Note
We listen to KeyEvent
and MotionEvent
inside the Activity.
There are two types of events we need to listen for. They are as follows:
KeyEvent
: For all the button presses and, in some gamepads, also the directional crossMotionEvent
: Events related to the movement along an axis: joysticks
We want to have the input controller separated from the Activity
, so we will make a special listener that combines the two events we need and then make the Activity
delegate on it.
The interface we need is very simple, we will call it GamepadControllerListener
:
public interface GamepadControllerListener { boolean dispatchGenericMotionEvent(MotionEvent event); boolean dispatchKeyEvent(KeyEvent event); }
Inside the Activity
, we create a method to set a listener of GamepadControllerListener
type. Since we only require one listener at a time, the method is set instead of add. To remove the listener, we just need to set it to null:
public void setGamepadControllerListener(GamepadControllerListener listener) { mGamepadControllerListener = listener; }
Finally, we have to override dispatchGenericMotionEvent
and dispatchKeyEvent
inside our Activity
:
@Override public boolean dispatchGenericMotionEvent(MotionEvent event) { if (mGamepadControllerListener != null) { if (mGamepadControllerListener.dispatchGenericMotionEvent(event)) { return true; } } return super.dispatchGenericMotionEvent(event); } @Override public boolean dispatchKeyEvent (KeyEvent event) { if (mGamepadControllerListener != null) { if (mGamepadControllerListener.dispatchKeyEvent(event)) { return true; } } return super.dispatchKeyEvent(event); }
Note that this method uses the convention of returning true if the event was consumed and false if it was not. In our case, we will return true only if the event was consumed by the listener. In other cases, we will return the result to delegate the event to the base class.
It is very important to call the corresponding method in the super class, as there is a lot of processing done inside the Activity
class that we do not want to discard accidentally.
With these components in place, we can proceed to create our GamepadInputController
, which will extend InputController
and implement GamepadControllerListener
:
public class GamepadInputController extends InputController implements GamepadControllerListener { public GamepadInputController(YassActivity activity) { mActivity = activity; } @Override public void onStart() { mActivity.setGamepadControllerListener(this); } @Override public void onStop() { mActivity.setGamepadControllerListener(null); } [...] }
Hooking into the key and motion events of the Activity
is something we want to limit as much as possible. This is why we override the onStop
and onStart
methods of the InputController
to only set the listener while a game is running.
There are several possible controller layouts. In general, they usually have a cross control and/or an analog joystick. Some have several analog joysticks and, of course, buttons. All in all there are some important details about the events and how different gamepads may be configured:

- The cross control can be made of buttons or it can be another analog joystick. If it has buttons, it will be handled as a
KeyEvent
with the same constants as a D-Pad. If it is an analog joystick, it will useAXIS_HAT_X
andAXIS_HAT_Y
. - Analog joysticks are handled via
MotionEvent
and we can read them using thegetAxisValue
method of theMotionEvent
. The default joystick will useAXIS_X
andAXIS_Y
. - We are not going to map the second analog joystick for this game, but it is mapped on the
AXIS_Z
andAXIS_RZ
. - The buttons are mapped as a
KeyEvent
with the name of each button.
Handling MotionEvents
When we receive a MotionEvent
, we need to first validate that the event is from a source we should read.
The source is part of the event and it is a composition of flags. The ones we are interested in are:
SOURCE_GAMEPAD
: It indicates that the device has gamepad buttons such as A, B, X, or Y.SOURCE_DPAD
: It indicates that the device has a D-Pad.SOURCE_JOYSTICK
: It indicates that the device has analog control sticks.
The only motion events we should process are the ones in which the source has the joystick flag set. Both gamepad and D-Pad sources will be sent as a KeyEvent
.
The handling of receiving a MotionEvent
is as follows:
@Override public boolean dispatchGenericMotionEvent(MotionEvent event) { int source = event.getSource(); if ((source & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) { return false } mHorizontalFactor = event.getAxisValue(MotionEvent.AXIS_X); mVerticalFactor = event.getAxisValue(MotionEvent.AXIS_Y); InputDevice device = event.getDevice(); MotionRange rangeX = device.getMotionRange(MotionEvent.AXIS_X, source); if (Math.abs(mHorizontalFactor) <= rangeX.getFlat()) { mHorizontalFactor = event.getAxisValue(MotionEvent.AXIS_HAT_X); MotionRange rangeHatX = device.getMotionRange(MotionEvent.AXIS_HAT_X, source); if (Math.abs(mHorizontalFactor) <= rangeHatX.getFlat()) { mHorizontalFactor = 0; } } MotionRange rangeY = device.getMotionRange(MotionEvent.AXIS_Y, source); if (Math.abs(mVerticalFactor) <= rangeY.getFlat()) { mVerticalFactor = event.getAxisValue(MotionEvent.AXIS_HAT_Y); MotionRange rangeHatY = device.getMotionRange(MotionEvent.AXIS_HAT_Y, source); if (Math.abs(mVerticalFactor) <= rangeHatY.getFlat()) { mVerticalFactor = 0; } } return true; }
First, we check the source. If it is not from a joystick, we just return false
, since we won't be consuming this event.
Then we read the axis values for MotionEvent.AXIS_X
and MotionEvent.AXIS_Y
and assign them to our variables. This is meant to read the default joystick. But we are not done. It is possible that the controller has a cross that works as an analog joystick.
To decide whether we read the secondary joystick, we check if there was input on the default one. If not, we assign the value from the secondary one to our variables.
It is important to note that most analog joysticks are not perfectly aligned at 0, so comparing the value of mHorizontalFactor
and mVerticalFactor
with 0 is not a valid way to detect if the joystick was moved.
Note
Analog joysticks are not perfectly centered at 0.
What we need to do is to read the flat value of the motion range of the device. It is much simpler than it sounds, since all this information is a part of the MotionEvent
.
Then, if there was no input from the default axis, we assign the value of AXIS_HAT_X
and AXIS_HAT_Y
to our variables. We also check whether the input of the axis is above its flat value and set it to 0 if it is not. We need to do this or the spaceship will move very slowly without any input at all.
Finally, we return true
to indicate that we have consumed the event.
Handling KeyEvents
The implementation of dispatchKeyEvent
is very similar to the one we did for the basic controller with buttons on the screen:
@Override public boolean dispatchKeyEvent(KeyEvent event) { int action = event.getAction(); int keyCode = event.getKeyCode(); if (action == MotionEvent.ACTION_DOWN) { if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { mVerticalFactor -= 1; return true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { mVerticalFactor += 1; return true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { mHorizontalFactor -= 1; return true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { mHorizontalFactor += 1; return true; } else if (keyCode == KeyEvent.KEYCODE_BUTTON_A) { mIsFiring = true; return true; } } else if (action == MotionEvent.ACTION_UP) { if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { mVerticalFactor += 1; return true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { mVerticalFactor -= 1; return true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { mHorizontalFactor += 1; return true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { mHorizontalFactor -= 1; return true; } else if (keyCode == KeyEvent.KEYCODE_BUTTON_A) { mIsFiring = false; return true; } else if (keyCode == KeyEvent.KEYCODE_BUTTON_B) { mActivity.onBackPressed(); return true; } } return false; }
The only significant difference is that we are comparing key codes to the constants for the D-Pad instead of view ID. But apart from this, the logic is exactly the same.
We also have to take care of mapping the button B to act as the back key. While this is already done on the latest versions of Android, it was not always the case, so we need to handle it. For this, we use the onBackPressed
callback we already created in YassActivity
.
Also, on Android 4.2 (API level 17) and before it, the system treated BUTTON_A
as the Android back key by default. This is why we should always use BUTTON_A
as the primary game action.
Detecting gamepads
It is a good practice to check whether a controller is connected when we launch the game. This allows us to display a help screen on how to play with a controller before the user starts playing. We should also check whether the controller is disconnected while the game is already running to pause it.
While checking for controllers can be done via the InputDevice
class, checking for changes in the controllers was only introduced in API level 16 (we are using minSDK=15).
Note
Detecting changes in the controllers being connected or disconnected was only introduced in Jelly Bean.
We are not going to provide a backward-compatible solution to detect the connection and disconnection of controllers. If you need to do it, there are detailed steps in the official documentation at http://developer.android.com/training/game-controllers/compatibility.html; these basically use a polling mechanism over input devices and check for changes in the list.
We are going to check for gamepads during the onResume
of the MainMenuFragment
. The first time a controller is detected, we will show an AlertDialog
that shows how to use the gamepad:
@Override public void onResume() { super.onResume(); if (isGameControllerConnected() && shouldDisplayGamepadHelp()) { displayGamepadHelp(); // Do not show the dialog again PreferenceManager.getDefaultSharedPreferences(getActivity()) .edit() .putBoolean(PREF_SHOULD_DISPLAY_GAMEPAD_HELP, false) .commit(); } } private boolean shouldDisplayGamepadHelp() { return PreferenceManager.getDefaultSharedPreferences(getActivity()) .getBoolean(PREF_SHOULD_DISPLAY_GAMEPAD_HELP, true); }
We are using default shared preferences to store whether we have displayed the dialog already or not. Once it is displayed we set the value to false, so it is not shown again.
The method to check if there is a controller connected is as follows:
public boolean isGameControllerConnected() { int[] deviceIds = InputDevice.getDeviceIds(); for (int deviceId : deviceIds) { InputDevice dev = InputDevice.getDevice(deviceId); int sources = dev.getSources(); if (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) || ((sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK)) { return true; } } return false; }
We iterate over the input devices and, if the source of any of them is a gamepad or a joystick, we return true.
If no device is found with these sources, we return false.
Note that each InputDevice
also has a name. This can be useful to identify specific gamepads in case you want to show different help screens such as for Nvidia Shield.
To check whether a controller gets disconnected during gameplay, we need to register an InputDeviceListener
on the InputManager
and process the events. We will make GameFragment
implement InputDeviceListener
.
We do the registration right after creating the GameEngine
and the unregistration after stopping the game in onDestroy
. You need to either add some annotations to prevent int from giving an error for the method not being available on the minimum SDK or wrap it into an if
block that checks the version, as we did before.
Then, it is as simple as pausing the game when a device is disconnected:
@Override public void onInputDeviceRemoved(int deviceId) { if (!mGameEngine.isRunning()) { pauseGameAndShowPauseDialog(); } }
Note that this pauses the game when any device gets disconnected. It is unlikely that a device that is not a controller gets disconnected, but we could make sure it is a controller just by checking the source, as we did for isGameControllerConnected
.
- C語言程序設(shè)計(jì)實(shí)踐教程
- 零基礎(chǔ)學(xué)Java(第4版)
- 編譯系統(tǒng)透視:圖解編譯原理
- 常用工具軟件立體化教程(微課版)
- PLC應(yīng)用技術(shù)(三菱FX2N系列)
- SQL Server實(shí)用教程(SQL Server 2008版)
- C語言從入門到精通
- JavaScript程序設(shè)計(jì)(第2版)
- SQL Server 2008 R2數(shù)據(jù)庫技術(shù)及應(yīng)用(第3版)
- uni-app跨平臺(tái)開發(fā)與應(yīng)用從入門到實(shí)踐
- C++ System Programming Cookbook
- C語言程序設(shè)計(jì)實(shí)踐
- Getting Started with Electronic Projects
- Java程序設(shè)計(jì)教程
- Java 9:Building Robust Modular Applications