Home | History | Annotate | Download | only in controllersample
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.example.controllersample;
     18 
     19 import com.example.inputmanagercompat.InputManagerCompat;
     20 import com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener;
     21 
     22 import android.annotation.SuppressLint;
     23 import android.annotation.TargetApi;
     24 import android.content.Context;
     25 import android.graphics.Canvas;
     26 import android.graphics.Paint;
     27 import android.graphics.Paint.Style;
     28 import android.graphics.Path;
     29 import android.os.Build;
     30 import android.os.SystemClock;
     31 import android.os.Vibrator;
     32 import android.util.AttributeSet;
     33 import android.util.SparseArray;
     34 import android.view.InputDevice;
     35 import android.view.KeyEvent;
     36 import android.view.MotionEvent;
     37 import android.view.View;
     38 
     39 import java.util.ArrayList;
     40 import java.util.HashMap;
     41 import java.util.List;
     42 import java.util.Map;
     43 import java.util.Random;
     44 
     45 /*
     46  * A trivial joystick based physics game to demonstrate joystick handling. If
     47  * the game controller has a vibrator, then it is used to provide feedback when
     48  * a bullet is fired or the ship crashes into an obstacle. Otherwise, the system
     49  * vibrator is used for that purpose.
     50  */
     51 @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
     52 public class GameView extends View implements InputDeviceListener {
     53     private static final int MAX_OBSTACLES = 12;
     54 
     55     private static final int DPAD_STATE_LEFT = 1 << 0;
     56     private static final int DPAD_STATE_RIGHT = 1 << 1;
     57     private static final int DPAD_STATE_UP = 1 << 2;
     58     private static final int DPAD_STATE_DOWN = 1 << 3;
     59 
     60     private final Random mRandom;
     61     /*
     62      * Each ship is created as an event comes in from a new Joystick device
     63      */
     64     private final SparseArray<Ship> mShips;
     65     private final Map<String, Integer> mDescriptorMap;
     66     private final List<Bullet> mBullets;
     67     private final List<Obstacle> mObstacles;
     68 
     69     private long mLastStepTime;
     70     private final InputManagerCompat mInputManager;
     71 
     72     private final float mBaseSpeed;
     73 
     74     private final float mShipSize;
     75 
     76     private final float mBulletSize;
     77 
     78     private final float mMinObstacleSize;
     79     private final float mMaxObstacleSize;
     80     private final float mMinObstacleSpeed;
     81     private final float mMaxObstacleSpeed;
     82 
     83     public GameView(Context context, AttributeSet attrs) {
     84         super(context, attrs);
     85 
     86         mRandom = new Random();
     87         mShips = new SparseArray<Ship>();
     88         mDescriptorMap = new HashMap<String, Integer>();
     89         mBullets = new ArrayList<Bullet>();
     90         mObstacles = new ArrayList<Obstacle>();
     91 
     92         setFocusable(true);
     93         setFocusableInTouchMode(true);
     94 
     95         float baseSize = getContext().getResources().getDisplayMetrics().density * 5f;
     96         mBaseSpeed = baseSize * 3;
     97 
     98         mShipSize = baseSize * 3;
     99 
    100         mBulletSize = baseSize;
    101 
    102         mMinObstacleSize = baseSize * 2;
    103         mMaxObstacleSize = baseSize * 12;
    104         mMinObstacleSpeed = mBaseSpeed;
    105         mMaxObstacleSpeed = mBaseSpeed * 3;
    106 
    107         mInputManager = InputManagerCompat.Factory.getInputManager(this.getContext());
    108         mInputManager.registerInputDeviceListener(this, null);
    109     }
    110 
    111     // Iterate through the input devices, looking for controllers. Create a ship
    112     // for every device that reports itself as a gamepad or joystick.
    113     void findControllersAndAttachShips() {
    114         int[] deviceIds = mInputManager.getInputDeviceIds();
    115         for (int deviceId : deviceIds) {
    116             InputDevice dev = mInputManager.getInputDevice(deviceId);
    117             int sources = dev.getSources();
    118             // if the device is a gamepad/joystick, create a ship to represent it
    119             if (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
    120                     ((sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK)) {
    121                 // if the device has a gamepad or joystick
    122                 getShipForId(deviceId);
    123             }
    124         }
    125     }
    126 
    127     @Override
    128     public boolean onKeyDown(int keyCode, KeyEvent event) {
    129         int deviceId = event.getDeviceId();
    130         if (deviceId != -1) {
    131             Ship currentShip = getShipForId(deviceId);
    132             if (currentShip.onKeyDown(keyCode, event)) {
    133                 step(event.getEventTime());
    134                 return true;
    135             }
    136         }
    137 
    138         return super.onKeyDown(keyCode, event);
    139     }
    140 
    141     @Override
    142     public boolean onKeyUp(int keyCode, KeyEvent event) {
    143         int deviceId = event.getDeviceId();
    144         if (deviceId != -1) {
    145             Ship currentShip = getShipForId(deviceId);
    146             if (currentShip.onKeyUp(keyCode, event)) {
    147                 step(event.getEventTime());
    148                 return true;
    149             }
    150         }
    151 
    152         return super.onKeyUp(keyCode, event);
    153     }
    154 
    155     @Override
    156     public boolean onGenericMotionEvent(MotionEvent event) {
    157         mInputManager.onGenericMotionEvent(event);
    158 
    159         // Check that the event came from a joystick or gamepad since a generic
    160         // motion event could be almost anything. API level 18 adds the useful
    161         // event.isFromSource() helper function.
    162         int eventSource = event.getSource();
    163         if ((((eventSource & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
    164                 ((eventSource & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK))
    165                 && event.getAction() == MotionEvent.ACTION_MOVE) {
    166             int id = event.getDeviceId();
    167             if (-1 != id) {
    168                 Ship curShip = getShipForId(id);
    169                 if (curShip.onGenericMotionEvent(event)) {
    170                     return true;
    171                 }
    172             }
    173         }
    174         return super.onGenericMotionEvent(event);
    175     }
    176 
    177     @Override
    178     public void onWindowFocusChanged(boolean hasWindowFocus) {
    179         // Turn on and off animations based on the window focus.
    180         // Alternately, we could update the game state using the Activity
    181         // onResume()
    182         // and onPause() lifecycle events.
    183         if (hasWindowFocus) {
    184             mLastStepTime = SystemClock.uptimeMillis();
    185             mInputManager.onResume();
    186         } else {
    187             int numShips = mShips.size();
    188             for (int i = 0; i < numShips; i++) {
    189                 Ship currentShip = mShips.valueAt(i);
    190                 if (currentShip != null) {
    191                     currentShip.setHeading(0, 0);
    192                     currentShip.setVelocity(0, 0);
    193                     currentShip.mDPadState = 0;
    194                 }
    195             }
    196             mInputManager.onPause();
    197         }
    198 
    199         super.onWindowFocusChanged(hasWindowFocus);
    200     }
    201 
    202     @Override
    203     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    204         super.onSizeChanged(w, h, oldw, oldh);
    205 
    206         // Reset the game when the view changes size.
    207         reset();
    208     }
    209 
    210     @Override
    211     protected void onDraw(Canvas canvas) {
    212         super.onDraw(canvas);
    213         // Update the animation
    214         animateFrame();
    215 
    216         // Draw the ships.
    217         int numShips = mShips.size();
    218         for (int i = 0; i < numShips; i++) {
    219             Ship currentShip = mShips.valueAt(i);
    220             if (currentShip != null) {
    221                 currentShip.draw(canvas);
    222             }
    223         }
    224 
    225         // Draw bullets.
    226         int numBullets = mBullets.size();
    227         for (int i = 0; i < numBullets; i++) {
    228             final Bullet bullet = mBullets.get(i);
    229             bullet.draw(canvas);
    230         }
    231 
    232         // Draw obstacles.
    233         int numObstacles = mObstacles.size();
    234         for (int i = 0; i < numObstacles; i++) {
    235             final Obstacle obstacle = mObstacles.get(i);
    236             obstacle.draw(canvas);
    237         }
    238     }
    239 
    240     /**
    241      * Uses the device descriptor to try to assign the same color to the same
    242      * joystick. If there are two joysticks of the same type connected over USB,
    243      * or the API is < API level 16, it will be unable to distinguish the two
    244      * devices.
    245      *
    246      * @param shipID
    247      * @return
    248      */
    249     @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    250     private Ship getShipForId(int shipID) {
    251         Ship currentShip = mShips.get(shipID);
    252         if (null == currentShip) {
    253 
    254             // do we know something about this ship already?
    255             InputDevice dev = InputDevice.getDevice(shipID);
    256             String deviceString = null;
    257             Integer shipColor = null;
    258             if (null != dev) {
    259                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
    260                     deviceString = dev.getDescriptor();
    261                 } else {
    262                     deviceString = dev.getName();
    263                 }
    264                 shipColor = mDescriptorMap.get(deviceString);
    265             }
    266 
    267             if (null != shipColor) {
    268                 int color = shipColor;
    269                 int numShips = mShips.size();
    270                 // do we already have a ship with this color?
    271                 for (int i = 0; i < numShips; i++) {
    272                     if (mShips.valueAt(i).getColor() == color) {
    273                         shipColor = null;
    274                         // we won't store this value either --- if the first
    275                         // controller gets disconnected/connected, it will get
    276                         // the same color.
    277                         deviceString = null;
    278                     }
    279                 }
    280             }
    281             if (null != shipColor) {
    282                 currentShip = new Ship(shipColor);
    283                 if (null != deviceString) {
    284                     mDescriptorMap.remove(deviceString);
    285                 }
    286             } else {
    287                 currentShip = new Ship(getNextShipColor());
    288             }
    289             mShips.append(shipID, currentShip);
    290             currentShip.setInputDevice(dev);
    291 
    292             if (null != deviceString) {
    293                 mDescriptorMap.put(deviceString, currentShip.getColor());
    294             }
    295         }
    296         return currentShip;
    297     }
    298 
    299     /**
    300      * Remove the ship from the array of active ships by ID.
    301      *
    302      * @param shipID
    303      */
    304     private void removeShipForID(int shipID) {
    305         mShips.remove(shipID);
    306     }
    307 
    308     private void reset() {
    309         mShips.clear();
    310         mBullets.clear();
    311         mObstacles.clear();
    312         findControllersAndAttachShips();
    313     }
    314 
    315     private void animateFrame() {
    316         long currentStepTime = SystemClock.uptimeMillis();
    317         step(currentStepTime);
    318         invalidate();
    319     }
    320 
    321     private void step(long currentStepTime) {
    322         float tau = (currentStepTime - mLastStepTime) * 0.001f;
    323         mLastStepTime = currentStepTime;
    324 
    325         // Move the ships
    326         int numShips = mShips.size();
    327         for (int i = 0; i < numShips; i++) {
    328             Ship currentShip = mShips.valueAt(i);
    329             if (currentShip != null) {
    330                 currentShip.accelerate(tau);
    331                 if (!currentShip.step(tau)) {
    332                     currentShip.reincarnate();
    333                 }
    334             }
    335         }
    336 
    337         // Move the bullets.
    338         int numBullets = mBullets.size();
    339         for (int i = 0; i < numBullets; i++) {
    340             final Bullet bullet = mBullets.get(i);
    341             if (!bullet.step(tau)) {
    342                 mBullets.remove(i);
    343                 i -= 1;
    344                 numBullets -= 1;
    345             }
    346         }
    347 
    348         // Move obstacles.
    349         int numObstacles = mObstacles.size();
    350         for (int i = 0; i < numObstacles; i++) {
    351             final Obstacle obstacle = mObstacles.get(i);
    352             if (!obstacle.step(tau)) {
    353                 mObstacles.remove(i);
    354                 i -= 1;
    355                 numObstacles -= 1;
    356             }
    357         }
    358 
    359         // Check for collisions between bullets and obstacles.
    360         for (int i = 0; i < numBullets; i++) {
    361             final Bullet bullet = mBullets.get(i);
    362             for (int j = 0; j < numObstacles; j++) {
    363                 final Obstacle obstacle = mObstacles.get(j);
    364                 if (bullet.collidesWith(obstacle)) {
    365                     bullet.destroy();
    366                     obstacle.destroy();
    367                     break;
    368                 }
    369             }
    370         }
    371 
    372         // Check for collisions between the ship and obstacles --- this could
    373         // get slow
    374         for (int i = 0; i < numObstacles; i++) {
    375             final Obstacle obstacle = mObstacles.get(i);
    376             for (int j = 0; j < numShips; j++) {
    377                 Ship currentShip = mShips.valueAt(j);
    378                 if (currentShip != null) {
    379                     if (currentShip.collidesWith(obstacle)) {
    380                         currentShip.destroy();
    381                         obstacle.destroy();
    382                         break;
    383                     }
    384                 }
    385             }
    386         }
    387 
    388         // Spawn more obstacles offscreen when needed.
    389         // Avoid putting them right on top of the ship.
    390         int tries = MAX_OBSTACLES - mObstacles.size() + 10;
    391         final float minDistance = mShipSize * 4;
    392         while (mObstacles.size() < MAX_OBSTACLES && tries-- > 0) {
    393             float size = mRandom.nextFloat() * (mMaxObstacleSize - mMinObstacleSize)
    394                     + mMinObstacleSize;
    395             float positionX, positionY;
    396             int edge = mRandom.nextInt(4);
    397             switch (edge) {
    398                 case 0:
    399                     positionX = -size;
    400                     positionY = mRandom.nextInt(getHeight());
    401                     break;
    402                 case 1:
    403                     positionX = getWidth() + size;
    404                     positionY = mRandom.nextInt(getHeight());
    405                     break;
    406                 case 2:
    407                     positionX = mRandom.nextInt(getWidth());
    408                     positionY = -size;
    409                     break;
    410                 default:
    411                     positionX = mRandom.nextInt(getWidth());
    412                     positionY = getHeight() + size;
    413                     break;
    414             }
    415             boolean positionSafe = true;
    416 
    417             // If the obstacle is too close to any ships, we don't want to
    418             // spawn it.
    419             for (int i = 0; i < numShips; i++) {
    420                 Ship currentShip = mShips.valueAt(i);
    421                 if (currentShip != null) {
    422                     if (currentShip.distanceTo(positionX, positionY) < minDistance) {
    423                         // try to spawn again
    424                         positionSafe = false;
    425                         break;
    426                     }
    427                 }
    428             }
    429 
    430             // if the position is safe, add the obstacle and reset the retry
    431             // counter
    432             if (positionSafe) {
    433                 tries = MAX_OBSTACLES - mObstacles.size() + 10;
    434                 // we can add the obstacle now since it isn't close to any ships
    435                 float direction = mRandom.nextFloat() * (float) Math.PI * 2;
    436                 float speed = mRandom.nextFloat() * (mMaxObstacleSpeed - mMinObstacleSpeed)
    437                         + mMinObstacleSpeed;
    438                 float velocityX = (float) Math.cos(direction) * speed;
    439                 float velocityY = (float) Math.sin(direction) * speed;
    440 
    441                 Obstacle obstacle = new Obstacle();
    442                 obstacle.setPosition(positionX, positionY);
    443                 obstacle.setSize(size);
    444                 obstacle.setVelocity(velocityX, velocityY);
    445                 mObstacles.add(obstacle);
    446             }
    447         }
    448     }
    449 
    450     private static float pythag(float x, float y) {
    451         return (float) Math.sqrt(x * x + y * y);
    452     }
    453 
    454     private static int blend(float alpha, int from, int to) {
    455         return from + (int) ((to - from) * alpha);
    456     }
    457 
    458     private static void setPaintARGBBlend(Paint paint, float alpha,
    459             int a1, int r1, int g1, int b1,
    460             int a2, int r2, int g2, int b2) {
    461         paint.setARGB(blend(alpha, a1, a2), blend(alpha, r1, r2),
    462                 blend(alpha, g1, g2), blend(alpha, b1, b2));
    463     }
    464 
    465     private static float getCenteredAxis(MotionEvent event, InputDevice device,
    466             int axis, int historyPos) {
    467         final InputDevice.MotionRange range = device.getMotionRange(axis, event.getSource());
    468         if (range != null) {
    469             final float flat = range.getFlat();
    470             final float value = historyPos < 0 ? event.getAxisValue(axis)
    471                     : event.getHistoricalAxisValue(axis, historyPos);
    472 
    473             // Ignore axis values that are within the 'flat' region of the
    474             // joystick axis center.
    475             // A joystick at rest does not always report an absolute position of
    476             // (0,0).
    477             if (Math.abs(value) > flat) {
    478                 return value;
    479             }
    480         }
    481         return 0;
    482     }
    483 
    484     /**
    485      * Any gamepad button + the spacebar or DPAD_CENTER will be used as the fire
    486      * key.
    487      *
    488      * @param keyCode
    489      * @return true of it's a fire key.
    490      */
    491     private static boolean isFireKey(int keyCode) {
    492         return KeyEvent.isGamepadButton(keyCode)
    493                 || keyCode == KeyEvent.KEYCODE_DPAD_CENTER
    494                 || keyCode == KeyEvent.KEYCODE_SPACE;
    495     }
    496 
    497     private abstract class Sprite {
    498         protected float mPositionX;
    499         protected float mPositionY;
    500         protected float mVelocityX;
    501         protected float mVelocityY;
    502         protected float mSize;
    503         protected boolean mDestroyed;
    504         protected float mDestroyAnimProgress;
    505 
    506         public void setPosition(float x, float y) {
    507             mPositionX = x;
    508             mPositionY = y;
    509         }
    510 
    511         public void setVelocity(float x, float y) {
    512             mVelocityX = x;
    513             mVelocityY = y;
    514         }
    515 
    516         public void setSize(float size) {
    517             mSize = size;
    518         }
    519 
    520         public float distanceTo(float x, float y) {
    521             return pythag(mPositionX - x, mPositionY - y);
    522         }
    523 
    524         public float distanceTo(Sprite other) {
    525             return distanceTo(other.mPositionX, other.mPositionY);
    526         }
    527 
    528         public boolean collidesWith(Sprite other) {
    529             // Really bad collision detection.
    530             return !mDestroyed && !other.mDestroyed
    531                     && distanceTo(other) <= Math.max(mSize, other.mSize)
    532                             + Math.min(mSize, other.mSize) * 0.5f;
    533         }
    534 
    535         public boolean isDestroyed() {
    536             return mDestroyed;
    537         }
    538 
    539         /**
    540          * Moves the sprite based on the elapsed time defined by tau.
    541          *
    542          * @param tau the elapsed time in seconds since the last step
    543          * @return false if the sprite is to be removed from the display
    544          */
    545         public boolean step(float tau) {
    546             mPositionX += mVelocityX * tau;
    547             mPositionY += mVelocityY * tau;
    548 
    549             if (mDestroyed) {
    550                 mDestroyAnimProgress += tau / getDestroyAnimDuration();
    551                 if (mDestroyAnimProgress >= getDestroyAnimCycles()) {
    552                     return false;
    553                 }
    554             }
    555             return true;
    556         }
    557 
    558         /**
    559          * Draws the sprite.
    560          *
    561          * @param canvas the Canvas upon which to draw the sprite.
    562          */
    563         public abstract void draw(Canvas canvas);
    564 
    565         /**
    566          * Returns the duration of the destruction animation of the sprite in
    567          * seconds.
    568          *
    569          * @return the float duration in seconds of the destruction animation
    570          */
    571         public abstract float getDestroyAnimDuration();
    572 
    573         /**
    574          * Returns the number of cycles to play the destruction animation. A
    575          * destruction animation has a duration and a number of cycles to play
    576          * it for, so we can have an extended death sequence when a ship or
    577          * object is destroyed.
    578          *
    579          * @return the float number of cycles to play the destruction animation
    580          */
    581         public abstract float getDestroyAnimCycles();
    582 
    583         protected boolean isOutsidePlayfield() {
    584             final int width = GameView.this.getWidth();
    585             final int height = GameView.this.getHeight();
    586             return mPositionX < 0 || mPositionX >= width
    587                     || mPositionY < 0 || mPositionY >= height;
    588         }
    589 
    590         protected void wrapAtPlayfieldBoundary() {
    591             final int width = GameView.this.getWidth();
    592             final int height = GameView.this.getHeight();
    593             while (mPositionX <= -mSize) {
    594                 mPositionX += width + mSize * 2;
    595             }
    596             while (mPositionX >= width + mSize) {
    597                 mPositionX -= width + mSize * 2;
    598             }
    599             while (mPositionY <= -mSize) {
    600                 mPositionY += height + mSize * 2;
    601             }
    602             while (mPositionY >= height + mSize) {
    603                 mPositionY -= height + mSize * 2;
    604             }
    605         }
    606 
    607         public void destroy() {
    608             mDestroyed = true;
    609             step(0);
    610         }
    611     }
    612 
    613     private static int sShipColor = 0;
    614 
    615     /**
    616      * Returns the next ship color in the sequence. Very simple. Does not in any
    617      * way guarantee that there are not multiple ships with the same color on
    618      * the screen.
    619      *
    620      * @return an int containing the index of the next ship color
    621      */
    622     private static int getNextShipColor() {
    623         int color = sShipColor & 0x07;
    624         if (0 == color) {
    625             color++;
    626             sShipColor++;
    627         }
    628         sShipColor++;
    629         return color;
    630     }
    631 
    632     /*
    633      * Static constants associated with Ship inner class
    634      */
    635     private static final long[] sDestructionVibratePattern = new long[] {
    636             0, 20, 20, 40, 40, 80, 40, 300
    637     };
    638 
    639     private class Ship extends Sprite {
    640         private static final float CORNER_ANGLE = (float) Math.PI * 2 / 3;
    641         private static final float TO_DEGREES = (float) (180.0 / Math.PI);
    642 
    643         private final float mMaxShipThrust = mBaseSpeed * 0.25f;
    644         private final float mMaxSpeed = mBaseSpeed * 12;
    645 
    646         // The ship actually determines the speed of the bullet, not the bullet
    647         // itself
    648         private final float mBulletSpeed = mBaseSpeed * 12;
    649 
    650         private final Paint mPaint;
    651         private final Path mPath;
    652         private final int mR, mG, mB;
    653         private final int mColor;
    654 
    655         // The current device that is controlling the ship
    656         private InputDevice mInputDevice;
    657 
    658         private float mHeadingX;
    659         private float mHeadingY;
    660         private float mHeadingAngle;
    661         private float mHeadingMagnitude;
    662 
    663         private int mDPadState;
    664 
    665         /**
    666          * The colorIndex is used to create the color based on the lower three
    667          * bits of the value in the current implementation.
    668          *
    669          * @param colorIndex
    670          */
    671         public Ship(int colorIndex) {
    672             mPaint = new Paint();
    673             mPaint.setStyle(Style.FILL);
    674 
    675             setPosition(getWidth() * 0.5f, getHeight() * 0.5f);
    676             setVelocity(0, 0);
    677             setSize(mShipSize);
    678 
    679             mPath = new Path();
    680             mPath.moveTo(0, 0);
    681             mPath.lineTo((float) Math.cos(-CORNER_ANGLE) * mSize,
    682                     (float) Math.sin(-CORNER_ANGLE) * mSize);
    683             mPath.lineTo(mSize, 0);
    684             mPath.lineTo((float) Math.cos(CORNER_ANGLE) * mSize,
    685                     (float) Math.sin(CORNER_ANGLE) * mSize);
    686             mPath.lineTo(0, 0);
    687 
    688             mR = (colorIndex & 0x01) == 0 ? 63 : 255;
    689             mG = (colorIndex & 0x02) == 0 ? 63 : 255;
    690             mB = (colorIndex & 0x04) == 0 ? 63 : 255;
    691 
    692             mColor = colorIndex;
    693         }
    694 
    695         public boolean onKeyUp(int keyCode, KeyEvent event) {
    696 
    697             // Handle keys going up.
    698             boolean handled = false;
    699             switch (keyCode) {
    700                 case KeyEvent.KEYCODE_DPAD_LEFT:
    701                     setHeadingX(0);
    702                     mDPadState &= ~DPAD_STATE_LEFT;
    703                     handled = true;
    704                     break;
    705                 case KeyEvent.KEYCODE_DPAD_RIGHT:
    706                     setHeadingX(0);
    707                     mDPadState &= ~DPAD_STATE_RIGHT;
    708                     handled = true;
    709                     break;
    710                 case KeyEvent.KEYCODE_DPAD_UP:
    711                     setHeadingY(0);
    712                     mDPadState &= ~DPAD_STATE_UP;
    713                     handled = true;
    714                     break;
    715                 case KeyEvent.KEYCODE_DPAD_DOWN:
    716                     setHeadingY(0);
    717                     mDPadState &= ~DPAD_STATE_DOWN;
    718                     handled = true;
    719                     break;
    720                 default:
    721                     if (isFireKey(keyCode)) {
    722                         handled = true;
    723                     }
    724                     break;
    725             }
    726             return handled;
    727         }
    728 
    729         /*
    730          * Firing is a unique case where a ship creates a bullet. A bullet needs
    731          * to be created with a position near the ship that is firing with a
    732          * velocity that is based upon the speed of the ship.
    733          */
    734         private void fire() {
    735             if (!isDestroyed()) {
    736                 Bullet bullet = new Bullet();
    737                 bullet.setPosition(getBulletInitialX(), getBulletInitialY());
    738                 bullet.setVelocity(getBulletVelocityX(),
    739                         getBulletVelocityY());
    740                 mBullets.add(bullet);
    741                 vibrateController(20);
    742             }
    743         }
    744 
    745         public boolean onKeyDown(int keyCode, KeyEvent event) {
    746             // Handle DPad keys and fire button on initial down but not on
    747             // auto-repeat.
    748             boolean handled = false;
    749             if (event.getRepeatCount() == 0) {
    750                 switch (keyCode) {
    751                     case KeyEvent.KEYCODE_DPAD_LEFT:
    752                         setHeadingX(-1);
    753                         mDPadState |= DPAD_STATE_LEFT;
    754                         handled = true;
    755                         break;
    756                     case KeyEvent.KEYCODE_DPAD_RIGHT:
    757                         setHeadingX(1);
    758                         mDPadState |= DPAD_STATE_RIGHT;
    759                         handled = true;
    760                         break;
    761                     case KeyEvent.KEYCODE_DPAD_UP:
    762                         setHeadingY(-1);
    763                         mDPadState |= DPAD_STATE_UP;
    764                         handled = true;
    765                         break;
    766                     case KeyEvent.KEYCODE_DPAD_DOWN:
    767                         setHeadingY(1);
    768                         mDPadState |= DPAD_STATE_DOWN;
    769                         handled = true;
    770                         break;
    771                     default:
    772                         if (isFireKey(keyCode)) {
    773                             fire();
    774                             handled = true;
    775                         }
    776                         break;
    777                 }
    778             }
    779             return handled;
    780         }
    781 
    782         /**
    783          * Gets the vibrator from the controller if it is present. Note that it
    784          * would be easy to get the system vibrator here if the controller one
    785          * is not present, but we don't choose to do it in this case.
    786          *
    787          * @return the Vibrator for the controller, or null if it is not
    788          *         present. or the API level cannot support it
    789          */
    790         @SuppressLint("NewApi")
    791         private final Vibrator getVibrator() {
    792             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
    793                     null != mInputDevice) {
    794                 return mInputDevice.getVibrator();
    795             }
    796             return null;
    797         }
    798 
    799         private void vibrateController(int time) {
    800             Vibrator vibrator = getVibrator();
    801             if (null != vibrator) {
    802                 vibrator.vibrate(time);
    803             }
    804         }
    805 
    806         private void vibrateController(long[] pattern, int repeat) {
    807             Vibrator vibrator = getVibrator();
    808             if (null != vibrator) {
    809                 vibrator.vibrate(pattern, repeat);
    810             }
    811         }
    812 
    813         /**
    814          * The ship directly handles joystick input.
    815          *
    816          * @param event
    817          * @param historyPos
    818          */
    819         private void processJoystickInput(MotionEvent event, int historyPos) {
    820             // Get joystick position.
    821             // Many game pads with two joysticks report the position of the
    822             // second
    823             // joystick
    824             // using the Z and RZ axes so we also handle those.
    825             // In a real game, we would allow the user to configure the axes
    826             // manually.
    827             if (null == mInputDevice) {
    828                 mInputDevice = event.getDevice();
    829             }
    830             float x = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_X, historyPos);
    831             if (x == 0) {
    832                 x = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_HAT_X, historyPos);
    833             }
    834             if (x == 0) {
    835                 x = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_Z, historyPos);
    836             }
    837 
    838             float y = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_Y, historyPos);
    839             if (y == 0) {
    840                 y = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_HAT_Y, historyPos);
    841             }
    842             if (y == 0) {
    843                 y = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_RZ, historyPos);
    844             }
    845 
    846             // Set the ship heading.
    847             setHeading(x, y);
    848             GameView.this.step(historyPos < 0 ? event.getEventTime() : event
    849                     .getHistoricalEventTime(historyPos));
    850         }
    851 
    852         public boolean onGenericMotionEvent(MotionEvent event) {
    853             if (0 == mDPadState) {
    854                 // Process all historical movement samples in the batch.
    855                 final int historySize = event.getHistorySize();
    856                 for (int i = 0; i < historySize; i++) {
    857                     processJoystickInput(event, i);
    858                 }
    859 
    860                 // Process the current movement sample in the batch.
    861                 processJoystickInput(event, -1);
    862             }
    863             return true;
    864         }
    865 
    866         /**
    867          * Set the game controller to be used to control the ship.
    868          *
    869          * @param dev the input device that will be controlling the ship
    870          */
    871         public void setInputDevice(InputDevice dev) {
    872             mInputDevice = dev;
    873         }
    874 
    875         /**
    876          * Sets the X component of the joystick heading value, defined by the
    877          * platform as being from -1.0 (left) to 1.0 (right). This function is
    878          * generally used to change the heading in response to a button-style
    879          * DPAD event.
    880          *
    881          * @param x the float x component of the joystick heading value
    882          */
    883         public void setHeadingX(float x) {
    884             mHeadingX = x;
    885             updateHeading();
    886         }
    887 
    888         /**
    889          * Sets the Y component of the joystick heading value, defined by the
    890          * platform as being from -1.0 (top) to 1.0 (bottom). This function is
    891          * generally used to change the heading in response to a button-style
    892          * DPAD event.
    893          *
    894          * @param y the float y component of the joystick heading value
    895          */
    896         public void setHeadingY(float y) {
    897             mHeadingY = y;
    898             updateHeading();
    899         }
    900 
    901         /**
    902          * Sets the heading as floating point values returned by a joystick.
    903          * These values are normalized by the Android platform to be from -1.0
    904          * (left, top) to 1.0 (right, bottom)
    905          *
    906          * @param x the float x component of the joystick heading value
    907          * @param y the float y component of the joystick heading value
    908          */
    909         public void setHeading(float x, float y) {
    910             mHeadingX = x;
    911             mHeadingY = y;
    912             updateHeading();
    913         }
    914 
    915         /**
    916          * Converts the heading values from joystick devices to the polar
    917          * representation of the heading angle if the magnitude of the heading
    918          * is significant (> 0.1f).
    919          */
    920         private void updateHeading() {
    921             mHeadingMagnitude = pythag(mHeadingX, mHeadingY);
    922             if (mHeadingMagnitude > 0.1f) {
    923                 mHeadingAngle = (float) Math.atan2(mHeadingY, mHeadingX);
    924             }
    925         }
    926 
    927         /**
    928          * Bring our ship back to life, stopping the destroy animation.
    929          */
    930         public void reincarnate() {
    931             mDestroyed = false;
    932             mDestroyAnimProgress = 0.0f;
    933         }
    934 
    935         private float polarX(float radius) {
    936             return (float) Math.cos(mHeadingAngle) * radius;
    937         }
    938 
    939         private float polarY(float radius) {
    940             return (float) Math.sin(mHeadingAngle) * radius;
    941         }
    942 
    943         /**
    944          * Gets the initial x coordinate for the bullet.
    945          *
    946          * @return the x coordinate of the bullet adjusted for the position and
    947          *         direction of the ship
    948          */
    949         public float getBulletInitialX() {
    950             return mPositionX + polarX(mSize);
    951         }
    952 
    953         /**
    954          * Gets the initial y coordinate for the bullet.
    955          *
    956          * @return the y coordinate of the bullet adjusted for the position and
    957          *         direction of the ship
    958          */
    959         public float getBulletInitialY() {
    960             return mPositionY + polarY(mSize);
    961         }
    962 
    963         /**
    964          * Returns the bullet speed Y component.
    965          *
    966          * @return adjusted Y component bullet speed for the velocity and
    967          *         direction of the ship
    968          */
    969         public float getBulletVelocityY() {
    970             return mVelocityY + polarY(mBulletSpeed);
    971         }
    972 
    973         /**
    974          * Returns the bullet speed X component
    975          *
    976          * @return adjusted X component bullet speed for the velocity and
    977          *         direction of the ship
    978          */
    979         public float getBulletVelocityX() {
    980             return mVelocityX + polarX(mBulletSpeed);
    981         }
    982 
    983         /**
    984          * Uses the heading magnitude and direction to change the acceleration
    985          * of the ship. In theory, this should be scaled according to the
    986          * elapsed time.
    987          *
    988          * @param tau the elapsed time in seconds between the last step
    989          */
    990         public void accelerate(float tau) {
    991             final float thrust = mHeadingMagnitude * mMaxShipThrust;
    992             mVelocityX += polarX(thrust) * tau * mMaxSpeed / 4;
    993             mVelocityY += polarY(thrust) * tau * mMaxSpeed / 4;
    994 
    995             final float speed = pythag(mVelocityX, mVelocityY);
    996             if (speed > mMaxSpeed) {
    997                 final float scale = mMaxSpeed / speed;
    998                 mVelocityX = mVelocityX * scale * scale;
    999                 mVelocityY = mVelocityY * scale * scale;
   1000             }
   1001         }
   1002 
   1003         @Override
   1004         public boolean step(float tau) {
   1005             if (!super.step(tau)) {
   1006                 return false;
   1007             }
   1008             wrapAtPlayfieldBoundary();
   1009             return true;
   1010         }
   1011 
   1012         @Override
   1013         public void draw(Canvas canvas) {
   1014             setPaintARGBBlend(mPaint, mDestroyAnimProgress - (int) (mDestroyAnimProgress),
   1015                     255, mR, mG, mB,
   1016                     0, 255, 0, 0);
   1017 
   1018             canvas.save(Canvas.MATRIX_SAVE_FLAG);
   1019             canvas.translate(mPositionX, mPositionY);
   1020             canvas.rotate(mHeadingAngle * TO_DEGREES);
   1021             canvas.drawPath(mPath, mPaint);
   1022             canvas.restore();
   1023         }
   1024 
   1025         @Override
   1026         public float getDestroyAnimDuration() {
   1027             return 1.0f;
   1028         }
   1029 
   1030         @Override
   1031         public void destroy() {
   1032             super.destroy();
   1033             vibrateController(sDestructionVibratePattern, -1);
   1034         }
   1035 
   1036         @Override
   1037         public float getDestroyAnimCycles() {
   1038             return 5.0f;
   1039         }
   1040 
   1041         public int getColor() {
   1042             return mColor;
   1043         }
   1044     }
   1045 
   1046     private static final Paint mBulletPaint;
   1047     static {
   1048         mBulletPaint = new Paint();
   1049         mBulletPaint.setStyle(Style.FILL);
   1050     }
   1051 
   1052     private class Bullet extends Sprite {
   1053 
   1054         public Bullet() {
   1055             setSize(mBulletSize);
   1056         }
   1057 
   1058         @Override
   1059         public boolean step(float tau) {
   1060             if (!super.step(tau)) {
   1061                 return false;
   1062             }
   1063             return !isOutsidePlayfield();
   1064         }
   1065 
   1066         @Override
   1067         public void draw(Canvas canvas) {
   1068             setPaintARGBBlend(mBulletPaint, mDestroyAnimProgress,
   1069                     255, 255, 255, 0,
   1070                     0, 255, 255, 255);
   1071             canvas.drawCircle(mPositionX, mPositionY, mSize, mBulletPaint);
   1072         }
   1073 
   1074         @Override
   1075         public float getDestroyAnimDuration() {
   1076             return 0.125f;
   1077         }
   1078 
   1079         @Override
   1080         public float getDestroyAnimCycles() {
   1081             return 1.0f;
   1082         }
   1083 
   1084     }
   1085 
   1086     private static final Paint mObstaclePaint;
   1087     static {
   1088         mObstaclePaint = new Paint();
   1089         mObstaclePaint.setARGB(255, 127, 127, 255);
   1090         mObstaclePaint.setStyle(Style.FILL);
   1091     }
   1092 
   1093     private class Obstacle extends Sprite {
   1094 
   1095         @Override
   1096         public boolean step(float tau) {
   1097             if (!super.step(tau)) {
   1098                 return false;
   1099             }
   1100             wrapAtPlayfieldBoundary();
   1101             return true;
   1102         }
   1103 
   1104         @Override
   1105         public void draw(Canvas canvas) {
   1106             setPaintARGBBlend(mObstaclePaint, mDestroyAnimProgress,
   1107                     255, 127, 127, 255,
   1108                     0, 255, 0, 0);
   1109             canvas.drawCircle(mPositionX, mPositionY,
   1110                     mSize * (1.0f - mDestroyAnimProgress), mObstaclePaint);
   1111         }
   1112 
   1113         @Override
   1114         public float getDestroyAnimDuration() {
   1115             return 0.25f;
   1116         }
   1117 
   1118         @Override
   1119         public float getDestroyAnimCycles() {
   1120             return 1.0f;
   1121         }
   1122     }
   1123 
   1124     /*
   1125      * When an input device is added, we add a ship based upon the device.
   1126      * @see
   1127      * com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener
   1128      * #onInputDeviceAdded(int)
   1129      */
   1130     @Override
   1131     public void onInputDeviceAdded(int deviceId) {
   1132         getShipForId(deviceId);
   1133     }
   1134 
   1135     /*
   1136      * This is an unusual case. Input devices don't typically change, but they
   1137      * certainly can --- for example a device may have different modes. We use
   1138      * this to make sure that the ship has an up-to-date InputDevice.
   1139      * @see
   1140      * com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener
   1141      * #onInputDeviceChanged(int)
   1142      */
   1143     @Override
   1144     public void onInputDeviceChanged(int deviceId) {
   1145         Ship ship = getShipForId(deviceId);
   1146         ship.setInputDevice(InputDevice.getDevice(deviceId));
   1147     }
   1148 
   1149     /*
   1150      * Remove any ship associated with the ID.
   1151      * @see
   1152      * com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener
   1153      * #onInputDeviceRemoved(int)
   1154      */
   1155     @Override
   1156     public void onInputDeviceRemoved(int deviceId) {
   1157         removeShipForID(deviceId);
   1158     }
   1159 }
   1160