diff options
Diffstat (limited to 'samples/ControllerSample/src/com/example/controllersample/GameView.java')
-rw-r--r-- | samples/ControllerSample/src/com/example/controllersample/GameView.java | 1159 |
1 files changed, 1159 insertions, 0 deletions
diff --git a/samples/ControllerSample/src/com/example/controllersample/GameView.java b/samples/ControllerSample/src/com/example/controllersample/GameView.java new file mode 100644 index 000000000..6481a2a39 --- /dev/null +++ b/samples/ControllerSample/src/com/example/controllersample/GameView.java @@ -0,0 +1,1159 @@ +/* + * 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.sqrt(x * x + y * 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); + } +} |