| /* |
| * 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); |
| } |
| } |