官术网_书友最值得收藏!

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 cross
  • MotionEvent: 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 use AXIS_HAT_X and AXIS_HAT_Y.
  • Analog joysticks are handled via MotionEvent and we can read them using the getAxisValue method of the MotionEvent. The default joystick will use AXIS_X and AXIS_Y.
  • We are not going to map the second analog joystick for this game, but it is mapped on the AXIS_Z and AXIS_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.

主站蜘蛛池模板: 白银市| 天台县| 西乌| 盘锦市| 阜城县| 玛沁县| 白朗县| 郧西县| 淮安市| 阳高县| 洱源县| 濉溪县| 客服| 绵阳市| 明光市| 织金县| 西乡县| 洛宁县| 拜城县| 松溪县| 曲靖市| 抚松县| 巧家县| 乡城县| 鄂托克旗| 淅川县| 余庆县| 安西县| 焉耆| 新兴县| 册亨县| 靖州| 阳原县| 左权县| 通榆县| 巩留县| 平潭县| 尉氏县| 石林| 措勤县| 衢州市|