/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.controllersample; import com.example.inputmanagercompat.InputManagerCompat; import com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Path; import android.os.Build; import android.os.SystemClock; import android.os.Vibrator; import android.util.AttributeSet; import android.util.SparseArray; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; /* * A trivial joystick based physics game to demonstrate joystick handling. If * the game controller has a vibrator, then it is used to provide feedback when * a bullet is fired or the ship crashes into an obstacle. Otherwise, the system * vibrator is used for that purpose. */ @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) public class GameView extends View implements InputDeviceListener { private static final int MAX_OBSTACLES = 12; private static final int DPAD_STATE_LEFT = 1 << 0; private static final int DPAD_STATE_RIGHT = 1 << 1; private static final int DPAD_STATE_UP = 1 << 2; private static final int DPAD_STATE_DOWN = 1 << 3; private final Random mRandom; /* * Each ship is created as an event comes in from a new Joystick device */ private final SparseArray<Ship> mShips; private final Map<String, Integer> mDescriptorMap; private final List<Bullet> mBullets; private final List<Obstacle> mObstacles; private long mLastStepTime; private final InputManagerCompat mInputManager; private final float mBaseSpeed; private final float mShipSize; private final float mBulletSize; private final float mMinObstacleSize; private final float mMaxObstacleSize; private final float mMinObstacleSpeed; private final float mMaxObstacleSpeed; public GameView(Context context, AttributeSet attrs) { super(context, attrs); mRandom = new Random(); mShips = new SparseArray<Ship>(); mDescriptorMap = new HashMap<String, Integer>(); mBullets = new ArrayList<Bullet>(); mObstacles = new ArrayList<Obstacle>(); setFocusable(true); setFocusableInTouchMode(true); float baseSize = getContext().getResources().getDisplayMetrics().density * 5f; mBaseSpeed = baseSize * 3; mShipSize = baseSize * 3; mBulletSize = baseSize; mMinObstacleSize = baseSize * 2; mMaxObstacleSize = baseSize * 12; mMinObstacleSpeed = mBaseSpeed; mMaxObstacleSpeed = mBaseSpeed * 3; mInputManager = InputManagerCompat.Factory.getInputManager(this.getContext()); mInputManager.registerInputDeviceListener(this, null); } // Iterate through the input devices, looking for controllers. Create a ship // for every device that reports itself as a gamepad or joystick. void findControllersAndAttachShips() { int[] deviceIds = mInputManager.getInputDeviceIds(); for (int deviceId : deviceIds) { InputDevice dev = mInputManager.getInputDevice(deviceId); int sources = dev.getSources(); // if the device is a gamepad/joystick, create a ship to represent it if (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) || ((sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK)) { // if the device has a gamepad or joystick getShipForId(deviceId); } } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { int deviceId = event.getDeviceId(); if (deviceId != -1) { Ship currentShip = getShipForId(deviceId); if (currentShip.onKeyDown(keyCode, event)) { step(event.getEventTime()); return true; } } return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { int deviceId = event.getDeviceId(); if (deviceId != -1) { Ship currentShip = getShipForId(deviceId); if (currentShip.onKeyUp(keyCode, event)) { step(event.getEventTime()); return true; } } return super.onKeyUp(keyCode, event); } @Override public boolean onGenericMotionEvent(MotionEvent event) { mInputManager.onGenericMotionEvent(event); // Check that the event came from a joystick or gamepad since a generic // motion event could be almost anything. API level 18 adds the useful // event.isFromSource() helper function. int eventSource = event.getSource(); if ((((eventSource & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) || ((eventSource & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK)) && event.getAction() == MotionEvent.ACTION_MOVE) { int id = event.getDeviceId(); if (-1 != id) { Ship curShip = getShipForId(id); if (curShip.onGenericMotionEvent(event)) { return true; } } } return super.onGenericMotionEvent(event); } @Override public void onWindowFocusChanged(boolean hasWindowFocus) { // Turn on and off animations based on the window focus. // Alternately, we could update the game state using the Activity // onResume() // and onPause() lifecycle events. if (hasWindowFocus) { mLastStepTime = SystemClock.uptimeMillis(); mInputManager.onResume(); } else { int numShips = mShips.size(); for (int i = 0; i < numShips; i++) { Ship currentShip = mShips.valueAt(i); if (currentShip != null) { currentShip.setHeading(0, 0); currentShip.setVelocity(0, 0); currentShip.mDPadState = 0; } } mInputManager.onPause(); } super.onWindowFocusChanged(hasWindowFocus); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // Reset the game when the view changes size. reset(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // Update the animation animateFrame(); // Draw the ships. int numShips = mShips.size(); for (int i = 0; i < numShips; i++) { Ship currentShip = mShips.valueAt(i); if (currentShip != null) { currentShip.draw(canvas); } } // Draw bullets. int numBullets = mBullets.size(); for (int i = 0; i < numBullets; i++) { final Bullet bullet = mBullets.get(i); bullet.draw(canvas); } // Draw obstacles. int numObstacles = mObstacles.size(); for (int i = 0; i < numObstacles; i++) { final Obstacle obstacle = mObstacles.get(i); obstacle.draw(canvas); } } /** * Uses the device descriptor to try to assign the same color to the same * joystick. If there are two joysticks of the same type connected over USB, * or the API is < API level 16, it will be unable to distinguish the two * devices. * * @param shipID * @return */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private Ship getShipForId(int shipID) { Ship currentShip = mShips.get(shipID); if (null == currentShip) { // do we know something about this ship already? InputDevice dev = InputDevice.getDevice(shipID); String deviceString = null; Integer shipColor = null; if (null != dev) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { deviceString = dev.getDescriptor(); } else { deviceString = dev.getName(); } shipColor = mDescriptorMap.get(deviceString); } if (null != shipColor) { int color = shipColor; int numShips = mShips.size(); // do we already have a ship with this color? for (int i = 0; i < numShips; i++) { if (mShips.valueAt(i).getColor() == color) { shipColor = null; // we won't store this value either --- if the first // controller gets disconnected/connected, it will get // the same color. deviceString = null; } } } if (null != shipColor) { currentShip = new Ship(shipColor); if (null != deviceString) { mDescriptorMap.remove(deviceString); } } else { currentShip = new Ship(getNextShipColor()); } mShips.append(shipID, currentShip); currentShip.setInputDevice(dev); if (null != deviceString) { mDescriptorMap.put(deviceString, currentShip.getColor()); } } return currentShip; } /** * Remove the ship from the array of active ships by ID. * * @param shipID */ private void removeShipForID(int shipID) { mShips.remove(shipID); } private void reset() { mShips.clear(); mBullets.clear(); mObstacles.clear(); findControllersAndAttachShips(); } private void animateFrame() { long currentStepTime = SystemClock.uptimeMillis(); step(currentStepTime); invalidate(); } private void step(long currentStepTime) { float tau = (currentStepTime - mLastStepTime) * 0.001f; mLastStepTime = currentStepTime; // Move the ships int numShips = mShips.size(); for (int i = 0; i < numShips; i++) { Ship currentShip = mShips.valueAt(i); if (currentShip != null) { currentShip.accelerate(tau); if (!currentShip.step(tau)) { currentShip.reincarnate(); } } } // Move the bullets. int numBullets = mBullets.size(); for (int i = 0; i < numBullets; i++) { final Bullet bullet = mBullets.get(i); if (!bullet.step(tau)) { mBullets.remove(i); i -= 1; numBullets -= 1; } } // Move obstacles. int numObstacles = mObstacles.size(); for (int i = 0; i < numObstacles; i++) { final Obstacle obstacle = mObstacles.get(i); if (!obstacle.step(tau)) { mObstacles.remove(i); i -= 1; numObstacles -= 1; } } // Check for collisions between bullets and obstacles. for (int i = 0; i < numBullets; i++) { final Bullet bullet = mBullets.get(i); for (int j = 0; j < numObstacles; j++) { final Obstacle obstacle = mObstacles.get(j); if (bullet.collidesWith(obstacle)) { bullet.destroy(); obstacle.destroy(); break; } } } // Check for collisions between the ship and obstacles --- this could // get slow for (int i = 0; i < numObstacles; i++) { final Obstacle obstacle = mObstacles.get(i); for (int j = 0; j < numShips; j++) { Ship currentShip = mShips.valueAt(j); if (currentShip != null) { if (currentShip.collidesWith(obstacle)) { currentShip.destroy(); obstacle.destroy(); break; } } } } // Spawn more obstacles offscreen when needed. // Avoid putting them right on top of the ship. int tries = MAX_OBSTACLES - mObstacles.size() + 10; final float minDistance = mShipSize * 4; while (mObstacles.size() < MAX_OBSTACLES && tries-- > 0) { float size = mRandom.nextFloat() * (mMaxObstacleSize - mMinObstacleSize) + mMinObstacleSize; float positionX, positionY; int edge = mRandom.nextInt(4); switch (edge) { case 0: positionX = -size; positionY = mRandom.nextInt(getHeight()); break; case 1: positionX = getWidth() + size; positionY = mRandom.nextInt(getHeight()); break; case 2: positionX = mRandom.nextInt(getWidth()); positionY = -size; break; default: positionX = mRandom.nextInt(getWidth()); positionY = getHeight() + size; break; } boolean positionSafe = true; // If the obstacle is too close to any ships, we don't want to // spawn it. for (int i = 0; i < numShips; i++) { Ship currentShip = mShips.valueAt(i); if (currentShip != null) { if (currentShip.distanceTo(positionX, positionY) < minDistance) { // try to spawn again positionSafe = false; break; } } } // if the position is safe, add the obstacle and reset the retry // counter if (positionSafe) { tries = MAX_OBSTACLES - mObstacles.size() + 10; // we can add the obstacle now since it isn't close to any ships float direction = mRandom.nextFloat() * (float) Math.PI * 2; float speed = mRandom.nextFloat() * (mMaxObstacleSpeed - mMinObstacleSpeed) + mMinObstacleSpeed; float velocityX = (float) Math.cos(direction) * speed; float velocityY = (float) Math.sin(direction) * speed; Obstacle obstacle = new Obstacle(); obstacle.setPosition(positionX, positionY); obstacle.setSize(size); obstacle.setVelocity(velocityX, velocityY); mObstacles.add(obstacle); } } } private static float pythag(float x, float y) { return (float) Math.hypot(x, y); } private static int blend(float alpha, int from, int to) { return from + (int) ((to - from) * alpha); } private static void setPaintARGBBlend(Paint paint, float alpha, int a1, int r1, int g1, int b1, int a2, int r2, int g2, int b2) { paint.setARGB(blend(alpha, a1, a2), blend(alpha, r1, r2), blend(alpha, g1, g2), blend(alpha, b1, b2)); } private static float getCenteredAxis(MotionEvent event, InputDevice device, int axis, int historyPos) { final InputDevice.MotionRange range = device.getMotionRange(axis, event.getSource()); if (range != null) { final float flat = range.getFlat(); final float value = historyPos < 0 ? event.getAxisValue(axis) : event.getHistoricalAxisValue(axis, historyPos); // Ignore axis values that are within the 'flat' region of the // joystick axis center. // A joystick at rest does not always report an absolute position of // (0,0). if (Math.abs(value) > flat) { return value; } } return 0; } /** * Any gamepad button + the spacebar or DPAD_CENTER will be used as the fire * key. * * @param keyCode * @return true of it's a fire key. */ private static boolean isFireKey(int keyCode) { return KeyEvent.isGamepadButton(keyCode) || keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_SPACE; } private abstract class Sprite { protected float mPositionX; protected float mPositionY; protected float mVelocityX; protected float mVelocityY; protected float mSize; protected boolean mDestroyed; protected float mDestroyAnimProgress; public void setPosition(float x, float y) { mPositionX = x; mPositionY = y; } public void setVelocity(float x, float y) { mVelocityX = x; mVelocityY = y; } public void setSize(float size) { mSize = size; } public float distanceTo(float x, float y) { return pythag(mPositionX - x, mPositionY - y); } public float distanceTo(Sprite other) { return distanceTo(other.mPositionX, other.mPositionY); } public boolean collidesWith(Sprite other) { // Really bad collision detection. return !mDestroyed && !other.mDestroyed && distanceTo(other) <= Math.max(mSize, other.mSize) + Math.min(mSize, other.mSize) * 0.5f; } public boolean isDestroyed() { return mDestroyed; } /** * Moves the sprite based on the elapsed time defined by tau. * * @param tau the elapsed time in seconds since the last step * @return false if the sprite is to be removed from the display */ public boolean step(float tau) { mPositionX += mVelocityX * tau; mPositionY += mVelocityY * tau; if (mDestroyed) { mDestroyAnimProgress += tau / getDestroyAnimDuration(); if (mDestroyAnimProgress >= getDestroyAnimCycles()) { return false; } } return true; } /** * Draws the sprite. * * @param canvas the Canvas upon which to draw the sprite. */ public abstract void draw(Canvas canvas); /** * Returns the duration of the destruction animation of the sprite in * seconds. * * @return the float duration in seconds of the destruction animation */ public abstract float getDestroyAnimDuration(); /** * Returns the number of cycles to play the destruction animation. A * destruction animation has a duration and a number of cycles to play * it for, so we can have an extended death sequence when a ship or * object is destroyed. * * @return the float number of cycles to play the destruction animation */ public abstract float getDestroyAnimCycles(); protected boolean isOutsidePlayfield() { final int width = GameView.this.getWidth(); final int height = GameView.this.getHeight(); return mPositionX < 0 || mPositionX >= width || mPositionY < 0 || mPositionY >= height; } protected void wrapAtPlayfieldBoundary() { final int width = GameView.this.getWidth(); final int height = GameView.this.getHeight(); while (mPositionX <= -mSize) { mPositionX += width + mSize * 2; } while (mPositionX >= width + mSize) { mPositionX -= width + mSize * 2; } while (mPositionY <= -mSize) { mPositionY += height + mSize * 2; } while (mPositionY >= height + mSize) { mPositionY -= height + mSize * 2; } } public void destroy() { mDestroyed = true; step(0); } } private static int sShipColor = 0; /** * Returns the next ship color in the sequence. Very simple. Does not in any * way guarantee that there are not multiple ships with the same color on * the screen. * * @return an int containing the index of the next ship color */ private static int getNextShipColor() { int color = sShipColor & 0x07; if (0 == color) { color++; sShipColor++; } sShipColor++; return color; } /* * Static constants associated with Ship inner class */ private static final long[] sDestructionVibratePattern = new long[] { 0, 20, 20, 40, 40, 80, 40, 300 }; private class Ship extends Sprite { private static final float CORNER_ANGLE = (float) Math.PI * 2 / 3; private static final float TO_DEGREES = (float) (180.0 / Math.PI); private final float mMaxShipThrust = mBaseSpeed * 0.25f; private final float mMaxSpeed = mBaseSpeed * 12; // The ship actually determines the speed of the bullet, not the bullet // itself private final float mBulletSpeed = mBaseSpeed * 12; private final Paint mPaint; private final Path mPath; private final int mR, mG, mB; private final int mColor; // The current device that is controlling the ship private InputDevice mInputDevice; private float mHeadingX; private float mHeadingY; private float mHeadingAngle; private float mHeadingMagnitude; private int mDPadState; /** * The colorIndex is used to create the color based on the lower three * bits of the value in the current implementation. * * @param colorIndex */ public Ship(int colorIndex) { mPaint = new Paint(); mPaint.setStyle(Style.FILL); setPosition(getWidth() * 0.5f, getHeight() * 0.5f); setVelocity(0, 0); setSize(mShipSize); mPath = new Path(); mPath.moveTo(0, 0); mPath.lineTo((float) Math.cos(-CORNER_ANGLE) * mSize, (float) Math.sin(-CORNER_ANGLE) * mSize); mPath.lineTo(mSize, 0); mPath.lineTo((float) Math.cos(CORNER_ANGLE) * mSize, (float) Math.sin(CORNER_ANGLE) * mSize); mPath.lineTo(0, 0); mR = (colorIndex & 0x01) == 0 ? 63 : 255; mG = (colorIndex & 0x02) == 0 ? 63 : 255; mB = (colorIndex & 0x04) == 0 ? 63 : 255; mColor = colorIndex; } public boolean onKeyUp(int keyCode, KeyEvent event) { // Handle keys going up. boolean handled = false; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: setHeadingX(0); mDPadState &= ~DPAD_STATE_LEFT; handled = true; break; case KeyEvent.KEYCODE_DPAD_RIGHT: setHeadingX(0); mDPadState &= ~DPAD_STATE_RIGHT; handled = true; break; case KeyEvent.KEYCODE_DPAD_UP: setHeadingY(0); mDPadState &= ~DPAD_STATE_UP; handled = true; break; case KeyEvent.KEYCODE_DPAD_DOWN: setHeadingY(0); mDPadState &= ~DPAD_STATE_DOWN; handled = true; break; default: if (isFireKey(keyCode)) { handled = true; } break; } return handled; } /* * Firing is a unique case where a ship creates a bullet. A bullet needs * to be created with a position near the ship that is firing with a * velocity that is based upon the speed of the ship. */ private void fire() { if (!isDestroyed()) { Bullet bullet = new Bullet(); bullet.setPosition(getBulletInitialX(), getBulletInitialY()); bullet.setVelocity(getBulletVelocityX(), getBulletVelocityY()); mBullets.add(bullet); vibrateController(20); } } public boolean onKeyDown(int keyCode, KeyEvent event) { // Handle DPad keys and fire button on initial down but not on // auto-repeat. boolean handled = false; if (event.getRepeatCount() == 0) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: setHeadingX(-1); mDPadState |= DPAD_STATE_LEFT; handled = true; break; case KeyEvent.KEYCODE_DPAD_RIGHT: setHeadingX(1); mDPadState |= DPAD_STATE_RIGHT; handled = true; break; case KeyEvent.KEYCODE_DPAD_UP: setHeadingY(-1); mDPadState |= DPAD_STATE_UP; handled = true; break; case KeyEvent.KEYCODE_DPAD_DOWN: setHeadingY(1); mDPadState |= DPAD_STATE_DOWN; handled = true; break; default: if (isFireKey(keyCode)) { fire(); handled = true; } break; } } return handled; } /** * Gets the vibrator from the controller if it is present. Note that it * would be easy to get the system vibrator here if the controller one * is not present, but we don't choose to do it in this case. * * @return the Vibrator for the controller, or null if it is not * present. or the API level cannot support it */ @SuppressLint("NewApi") private final Vibrator getVibrator() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && null != mInputDevice) { return mInputDevice.getVibrator(); } return null; } private void vibrateController(int time) { Vibrator vibrator = getVibrator(); if (null != vibrator) { vibrator.vibrate(time); } } private void vibrateController(long[] pattern, int repeat) { Vibrator vibrator = getVibrator(); if (null != vibrator) { vibrator.vibrate(pattern, repeat); } } /** * The ship directly handles joystick input. * * @param event * @param historyPos */ private void processJoystickInput(MotionEvent event, int historyPos) { // Get joystick position. // Many game pads with two joysticks report the position of the // second // joystick // using the Z and RZ axes so we also handle those. // In a real game, we would allow the user to configure the axes // manually. if (null == mInputDevice) { mInputDevice = event.getDevice(); } float x = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_X, historyPos); if (x == 0) { x = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_HAT_X, historyPos); } if (x == 0) { x = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_Z, historyPos); } float y = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_Y, historyPos); if (y == 0) { y = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_HAT_Y, historyPos); } if (y == 0) { y = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_RZ, historyPos); } // Set the ship heading. setHeading(x, y); GameView.this.step(historyPos < 0 ? event.getEventTime() : event .getHistoricalEventTime(historyPos)); } public boolean onGenericMotionEvent(MotionEvent event) { if (0 == mDPadState) { // Process all historical movement samples in the batch. final int historySize = event.getHistorySize(); for (int i = 0; i < historySize; i++) { processJoystickInput(event, i); } // Process the current movement sample in the batch. processJoystickInput(event, -1); } return true; } /** * Set the game controller to be used to control the ship. * * @param dev the input device that will be controlling the ship */ public void setInputDevice(InputDevice dev) { mInputDevice = dev; } /** * Sets the X component of the joystick heading value, defined by the * platform as being from -1.0 (left) to 1.0 (right). This function is * generally used to change the heading in response to a button-style * DPAD event. * * @param x the float x component of the joystick heading value */ public void setHeadingX(float x) { mHeadingX = x; updateHeading(); } /** * Sets the Y component of the joystick heading value, defined by the * platform as being from -1.0 (top) to 1.0 (bottom). This function is * generally used to change the heading in response to a button-style * DPAD event. * * @param y the float y component of the joystick heading value */ public void setHeadingY(float y) { mHeadingY = y; updateHeading(); } /** * Sets the heading as floating point values returned by a joystick. * These values are normalized by the Android platform to be from -1.0 * (left, top) to 1.0 (right, bottom) * * @param x the float x component of the joystick heading value * @param y the float y component of the joystick heading value */ public void setHeading(float x, float y) { mHeadingX = x; mHeadingY = y; updateHeading(); } /** * Converts the heading values from joystick devices to the polar * representation of the heading angle if the magnitude of the heading * is significant (> 0.1f). */ private void updateHeading() { mHeadingMagnitude = pythag(mHeadingX, mHeadingY); if (mHeadingMagnitude > 0.1f) { mHeadingAngle = (float) Math.atan2(mHeadingY, mHeadingX); } } /** * Bring our ship back to life, stopping the destroy animation. */ public void reincarnate() { mDestroyed = false; mDestroyAnimProgress = 0.0f; } private float polarX(float radius) { return (float) Math.cos(mHeadingAngle) * radius; } private float polarY(float radius) { return (float) Math.sin(mHeadingAngle) * radius; } /** * Gets the initial x coordinate for the bullet. * * @return the x coordinate of the bullet adjusted for the position and * direction of the ship */ public float getBulletInitialX() { return mPositionX + polarX(mSize); } /** * Gets the initial y coordinate for the bullet. * * @return the y coordinate of the bullet adjusted for the position and * direction of the ship */ public float getBulletInitialY() { return mPositionY + polarY(mSize); } /** * Returns the bullet speed Y component. * * @return adjusted Y component bullet speed for the velocity and * direction of the ship */ public float getBulletVelocityY() { return mVelocityY + polarY(mBulletSpeed); } /** * Returns the bullet speed X component * * @return adjusted X component bullet speed for the velocity and * direction of the ship */ public float getBulletVelocityX() { return mVelocityX + polarX(mBulletSpeed); } /** * Uses the heading magnitude and direction to change the acceleration * of the ship. In theory, this should be scaled according to the * elapsed time. * * @param tau the elapsed time in seconds between the last step */ public void accelerate(float tau) { final float thrust = mHeadingMagnitude * mMaxShipThrust; mVelocityX += polarX(thrust) * tau * mMaxSpeed / 4; mVelocityY += polarY(thrust) * tau * mMaxSpeed / 4; final float speed = pythag(mVelocityX, mVelocityY); if (speed > mMaxSpeed) { final float scale = mMaxSpeed / speed; mVelocityX = mVelocityX * scale * scale; mVelocityY = mVelocityY * scale * scale; } } @Override public boolean step(float tau) { if (!super.step(tau)) { return false; } wrapAtPlayfieldBoundary(); return true; } @Override public void draw(Canvas canvas) { setPaintARGBBlend(mPaint, mDestroyAnimProgress - (int) (mDestroyAnimProgress), 255, mR, mG, mB, 0, 255, 0, 0); canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.translate(mPositionX, mPositionY); canvas.rotate(mHeadingAngle * TO_DEGREES); canvas.drawPath(mPath, mPaint); canvas.restore(); } @Override public float getDestroyAnimDuration() { return 1.0f; } @Override public void destroy() { super.destroy(); vibrateController(sDestructionVibratePattern, -1); } @Override public float getDestroyAnimCycles() { return 5.0f; } public int getColor() { return mColor; } } private static final Paint mBulletPaint; static { mBulletPaint = new Paint(); mBulletPaint.setStyle(Style.FILL); } private class Bullet extends Sprite { public Bullet() { setSize(mBulletSize); } @Override public boolean step(float tau) { if (!super.step(tau)) { return false; } return !isOutsidePlayfield(); } @Override public void draw(Canvas canvas) { setPaintARGBBlend(mBulletPaint, mDestroyAnimProgress, 255, 255, 255, 0, 0, 255, 255, 255); canvas.drawCircle(mPositionX, mPositionY, mSize, mBulletPaint); } @Override public float getDestroyAnimDuration() { return 0.125f; } @Override public float getDestroyAnimCycles() { return 1.0f; } } private static final Paint mObstaclePaint; static { mObstaclePaint = new Paint(); mObstaclePaint.setARGB(255, 127, 127, 255); mObstaclePaint.setStyle(Style.FILL); } private class Obstacle extends Sprite { @Override public boolean step(float tau) { if (!super.step(tau)) { return false; } wrapAtPlayfieldBoundary(); return true; } @Override public void draw(Canvas canvas) { setPaintARGBBlend(mObstaclePaint, mDestroyAnimProgress, 255, 127, 127, 255, 0, 255, 0, 0); canvas.drawCircle(mPositionX, mPositionY, mSize * (1.0f - mDestroyAnimProgress), mObstaclePaint); } @Override public float getDestroyAnimDuration() { return 0.25f; } @Override public float getDestroyAnimCycles() { return 1.0f; } } /* * When an input device is added, we add a ship based upon the device. * @see * com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener * #onInputDeviceAdded(int) */ @Override public void onInputDeviceAdded(int deviceId) { getShipForId(deviceId); } /* * This is an unusual case. Input devices don't typically change, but they * certainly can --- for example a device may have different modes. We use * this to make sure that the ship has an up-to-date InputDevice. * @see * com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener * #onInputDeviceChanged(int) */ @Override public void onInputDeviceChanged(int deviceId) { Ship ship = getShipForId(deviceId); ship.setInputDevice(InputDevice.getDevice(deviceId)); } /* * Remove any ship associated with the ID. * @see * com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener * #onInputDeviceRemoved(int) */ @Override public void onInputDeviceRemoved(int deviceId) { removeShipForID(deviceId); } }