I originally wrote this tutorial for the good folks over at Binpress.
With the release of Android TV, game developers will have a new platform to contend with. Luckily, there's only one major change that needs to be considered when building your game for Android TV: implementing the new controller.
In this post, I'll go over using the new controller in the context of a simple (i.e. it works, but isn't polished or something you'd spend hours playing) Asteroids-esque OpenGL game. All source code for this example can be found on GitHub.
The first thing to take note of when building a game for Android TV is that the application tag in AndroidManifest.xml has an attribute called 'isGame', which is set to true. This is what places your application into the Games section of the menu selection screen.
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
- android:isGame="true"
android:theme="@style/AppTheme" >
Next we need to handle input as it happens. We do this by overriding two methods in our main Activity class -
dispatchGenericMotionEvent
and dispatchKeyEvent
. In this example I simply let our controller (MVC controller, not physical controller :)) know that an event has happened, and it handles interpreting what it is and doing something with it.
MainActivity.java:
@Override
public boolean dispatchGenericMotionEvent(MotionEvent event) {
return mGame.handleMotionEvent(event);
- }
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
return mGame.handleKeyEvent(event);
- }
GameView.java:
public boolean handleMotionEvent( MotionEvent motionEvent ) {
if ( mShip != null ) {
- mShip.getController().setDeviceId( motionEvent.getDeviceId() );
mShip.getController().handleMotionEvent( motionEvent );
return true;
}
return false;
- }
public boolean handleKeyEvent( KeyEvent keyEvent ) {
if ( mShip != null ) {
mShip.getController().setDeviceId( keyEvent.getDeviceId() );
- mShip.getController().handleKeyEvent( keyEvent );
return true;
}
return false;
}
As you can see in
GameView.java
, we check to see if our ship object has been initialized, and if it has we get the controller associated with that ship. (Since this is a single player game, there's no logic to assigning one of multiple controllers to a ship). Finally, the device ID and event are passed to the game controller utility class.
The
GamepadController.java
class is where things get a bit more interesting. When the controller is initialized, we create a pair of two-dimensional arrays for storing the state of the buttons and joystick positions on the controller. Each button in mButtonState[][]
is associated with its own index and keeps track of state during the current and previous frame. Each joystick in mJoystickPositions[][]
also has its own index, but the values stored are current positions on the X and Y axes.
// The buttons on the game pad.
public static final int BUTTON_A = 0;
public static final int BUTTON_B = 1;
public static final int BUTTON_X = 2;
- public static final int BUTTON_Y = 3;
public static final int BUTTON_R1 = 4;
public static final int BUTTON_R2 = 5;
public static final int BUTTON_L1 = 6;
public static final int BUTTON_L2 = 7;
- public static final int BUTTON_COUNT = 8;
// The axes for joystick movement.
public static final int AXIS_X = 0;
public static final int AXIS_Y = 1;
- public static final int AXIS_COUNT = 2;
// Game pads usually have 2 joysticks.
public static final int JOYSTICK_1 = 0;
public static final int JOYSTICK_2 = 1;
- public static final int JOYSTICK_COUNT = 2;
// Keep track of button states for the current and previous frames.
protected static final int FRAME_INDEX_CURRENT = 0;
protected static final int FRAME_INDEX_PREVIOUS = 1;
- protected static final int FRAME_INDEX_COUNT = 2;
// Positions of the two joysticks.
private final float mJoystickPositions[][];
// The button states for the current and previous frames.
- private final boolean mButtonState[][];
public GamepadController() {
mButtonState = new boolean[BUTTON_COUNT][FRAME_INDEX_COUNT];
mJoystickPositions = new float[JOYSTICK_COUNT][AXIS_COUNT];
- resetState();//initializes values
}
With the controller arrays initialized, we can get back to handling our input.
handleMotionEvent
has two parts: getting the input from the first joystick on the controller and the second joystick (if there is any input).
public void handleMotionEvent(MotionEvent motionEvent) {
//Joystick 1:
mJoystickPositions[JOYSTICK_1][AXIS_X] = motionEvent.getAxisValue(MotionEvent.AXIS_X);
mJoystickPositions[JOYSTICK_1][AXIS_Y] = motionEvent.getAxisValue(MotionEvent.AXIS_Y);
//Joystick 2:
mJoystickPositions[JOYSTICK_2][AXIS_X] = motionEvent.getAxisValue(MotionEvent.AXIS_Z);
mJoystickPositions[JOYSTICK_2][AXIS_Y] = motionEvent.getAxisValue(MotionEvent.AXIS_RZ);
}
As you can see, the axes are mapped as X and Y for the left joystick, and Z and RZ for the second. The values from those axes are read and stored in our array for reading by our model classes.
We also save key pressed events by determining if the key action is "down," and saving that value in our button state array based on which button is being pressed.
public void handleKeyEvent(KeyEvent keyEvent) {
boolean keyIsDown = keyEvent.getAction() == KeyEvent.ACTION_DOWN;
if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BUTTON_A) {
- mButtonState[BUTTON_A][FRAME_INDEX_CURRENT] = keyIsDown;
} else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BUTTON_B) {
mButtonState[BUTTON_B][FRAME_INDEX_CURRENT] = keyIsDown;
} else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BUTTON_X) {
mButtonState[BUTTON_X][FRAME_INDEX_CURRENT] = keyIsDown;
- } else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BUTTON_Y) {
mButtonState[BUTTON_Y][FRAME_INDEX_CURRENT] = keyIsDown;
} else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BUTTON_R1 ) {
mButtonState[BUTTON_R1][FRAME_INDEX_CURRENT] = keyIsDown;
} else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BUTTON_R2 ) {
- mButtonState[BUTTON_R2][FRAME_INDEX_CURRENT] = keyIsDown;
} else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BUTTON_L1 ) {
mButtonState[BUTTON_L1][FRAME_INDEX_CURRENT] = keyIsDown;
} else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_BUTTON_L2 ) {
mButtonState[BUTTON_L2][FRAME_INDEX_CURRENT] = keyIsDown;
- }
}
Once the gamepad controller state is stored, we can access that from our object that is associated with that controller - in this case our unstoppable hero, the USS Triangle ship. The game calls the ship's update method every frame, so that's where we're going to handle updating the ship position by reading in joystick values, and wether or not the ship should fire a bullet.
public void update(float delta ) {
if ( !updateStatus( delta ) ) {
return;
}
updateShipPosition( delta );
handleKeyInput( delta );
}
updateStatus
simply checks to see if the ship is spawned, and -- if it is -- it calls the methods to update position or handle key pressed events. In updateShipPosition
, we grab the X and Y axis positions from the controller, and then use those to determine the magnitude of the joystick movement via the Pythagorean theorem (wrapped up in our Utils class methodvector2DLength
).
float newHeadingX = mController.getJoystickPosition(GamepadController.JOYSTICK_1,
GamepadController.AXIS_X);
float newHeadingY = mController.getJoystickPosition(GamepadController.JOYSTICK_1,
GamepadController.AXIS_Y);
- float magnitude = Utils.vector2DLength(newHeadingX, newHeadingY);
Once we have our headings and magnitude, we can see if that magnitude is significant enough to be used (in this case, over 10 percent movement from the center), then store the new headings in our ship's values and set the velocity for our ship. If the magnitude for the joystick is somehow greater than a set max value, then we divide our velocities by the magnitude to provide a max velocity along each axis.
if (magnitude > GamepadController.JOYSTICK_MOVEMENT_THRESHOLD) {
//Get the heading divided by how much the joystick is being used
mHeadingX = newHeadingX / magnitude;
mHeadingY = -newHeadingY / magnitude;
setVelocity( newHeadingX, -newHeadingY );
if (magnitude > 1.0f) {
//Sets a cap velocity
- mVelocityX /= magnitude;
mVelocityY /= magnitude;
}
}
Handling button pressed events is a bit more straight forward in this example - we check to see if a cool down timer has run up for firing a bullet, and we do the following if it has. We check to see if the X button is currently in a down position, reset the cool down, calculate aim based on the direction the ship is facing and create a bullet object at the ship's position.
private void handleKeyInput( float delta ) {
if( mFireTimer > 0 ) {
mFireTimer--;
return;
- }
if ( mController.isButtonDown( GamepadController.BUTTON_X ) && mFireTimer == 0 ) {
mFireTimer = FIRE_REFRESH_TIMER;
calculateAimDirection();
fireGun();
- }
}
Aim can be calculated with joystick 2 in the same way our heading is calculated for the ship, or it can simply set the aiming value as the heading if the second joystick isn't used for aiming (which is how it's used in the sample project).
Other than handling the new key events, the rest of the game is built in a standard way. This example throws together a quick OpenGL project, but the same ideas can be applied however your game is built. Hopefully this'll help some of you get rolling on getting your games onto the new Android TV platform!