Home | History | Annotate | Download | only in view
      1 /*
      2  * Copyright (C) 2011 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.android.apis.view;
     18 
     19 import android.content.Context;
     20 import android.graphics.Canvas;
     21 import android.graphics.Paint;
     22 import android.graphics.Path;
     23 import android.graphics.Paint.Style;
     24 import android.os.Handler;
     25 import android.os.SystemClock;
     26 import android.os.Vibrator;
     27 import android.util.AttributeSet;
     28 import android.view.InputDevice;
     29 import android.view.KeyEvent;
     30 import android.view.MotionEvent;
     31 import android.view.View;
     32 
     33 import java.util.ArrayList;
     34 import java.util.List;
     35 import java.util.Random;
     36 
     37 /**
     38  * A trivial joystick based physics game to demonstrate joystick handling.
     39  *
     40  * If the game controller has a vibrator, then it is used to provide feedback
     41  * when a bullet is fired or the ship crashes into an obstacle.  Otherwise, the
     42  * system vibrator is used for that purpose.
     43  *
     44  * @see GameControllerInput
     45  */
     46 public class GameView extends View {
     47     private final long ANIMATION_TIME_STEP = 1000 / 60;
     48     private final int MAX_OBSTACLES = 12;
     49 
     50     private final Random mRandom;
     51     private Ship mShip;
     52     private final List<Bullet> mBullets;
     53     private final List<Obstacle> mObstacles;
     54 
     55     private long mLastStepTime;
     56     private InputDevice mLastInputDevice;
     57 
     58     private static final int DPAD_STATE_LEFT  = 1 << 0;
     59     private static final int DPAD_STATE_RIGHT = 1 << 1;
     60     private static final int DPAD_STATE_UP    = 1 << 2;
     61     private static final int DPAD_STATE_DOWN  = 1 << 3;
     62 
     63     private int mDPadState;
     64 
     65     private float mShipSize;
     66     private float mMaxShipThrust;
     67     private float mMaxShipSpeed;
     68 
     69     private float mBulletSize;
     70     private float mBulletSpeed;
     71 
     72     private float mMinObstacleSize;
     73     private float mMaxObstacleSize;
     74     private float mMinObstacleSpeed;
     75     private float mMaxObstacleSpeed;
     76 
     77     private final Runnable mAnimationRunnable = new Runnable() {
     78         public void run() {
     79             animateFrame();
     80         }
     81     };
     82 
     83     public GameView(Context context, AttributeSet attrs) {
     84         super(context, attrs);
     85 
     86         mRandom = new Random();
     87         mBullets = new ArrayList<Bullet>();
     88         mObstacles = new ArrayList<Obstacle>();
     89 
     90         setFocusable(true);
     91         setFocusableInTouchMode(true);
     92 
     93         float baseSize = getContext().getResources().getDisplayMetrics().density * 5f;
     94         float baseSpeed = baseSize * 3;
     95 
     96         mShipSize = baseSize * 3;
     97         mMaxShipThrust = baseSpeed * 0.25f;
     98         mMaxShipSpeed = baseSpeed * 12;
     99 
    100         mBulletSize = baseSize;
    101         mBulletSpeed = baseSpeed * 12;
    102 
    103         mMinObstacleSize = baseSize * 2;
    104         mMaxObstacleSize = baseSize * 12;
    105         mMinObstacleSpeed = baseSpeed;
    106         mMaxObstacleSpeed = baseSpeed * 3;
    107     }
    108 
    109     @Override
    110     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    111         super.onSizeChanged(w, h, oldw, oldh);
    112 
    113         // Reset the game when the view changes size.
    114         reset();
    115     }
    116 
    117     @Override
    118     public boolean onKeyDown(int keyCode, KeyEvent event) {
    119         ensureInitialized();
    120 
    121         // Handle DPad keys and fire button on initial down but not on auto-repeat.
    122         boolean handled = false;
    123         if (event.getRepeatCount() == 0) {
    124             switch (keyCode) {
    125                 case KeyEvent.KEYCODE_DPAD_LEFT:
    126                     mShip.setHeadingX(-1);
    127                     mDPadState |= DPAD_STATE_LEFT;
    128                     handled = true;
    129                     break;
    130                 case KeyEvent.KEYCODE_DPAD_RIGHT:
    131                     mShip.setHeadingX(1);
    132                     mDPadState |= DPAD_STATE_RIGHT;
    133                     handled = true;
    134                     break;
    135                 case KeyEvent.KEYCODE_DPAD_UP:
    136                     mShip.setHeadingY(-1);
    137                     mDPadState |= DPAD_STATE_UP;
    138                     handled = true;
    139                     break;
    140                 case KeyEvent.KEYCODE_DPAD_DOWN:
    141                     mShip.setHeadingY(1);
    142                     mDPadState |= DPAD_STATE_DOWN;
    143                     handled = true;
    144                     break;
    145                 default:
    146                     if (isFireKey(keyCode)) {
    147                         fire();
    148                         handled = true;
    149                     }
    150                     break;
    151             }
    152         }
    153         if (handled) {
    154             step(event.getEventTime());
    155             return true;
    156         }
    157         return super.onKeyDown(keyCode, event);
    158     }
    159 
    160     @Override
    161     public boolean onKeyUp(int keyCode, KeyEvent event) {
    162         ensureInitialized();
    163 
    164         // Handle keys going up.
    165         boolean handled = false;
    166         switch (keyCode) {
    167             case KeyEvent.KEYCODE_DPAD_LEFT:
    168                 mShip.setHeadingX(0);
    169                 mDPadState &= ~DPAD_STATE_LEFT;
    170                 handled = true;
    171                 break;
    172             case KeyEvent.KEYCODE_DPAD_RIGHT:
    173                 mShip.setHeadingX(0);
    174                 mDPadState &= ~DPAD_STATE_RIGHT;
    175                 handled = true;
    176                 break;
    177             case KeyEvent.KEYCODE_DPAD_UP:
    178                 mShip.setHeadingY(0);
    179                 mDPadState &= ~DPAD_STATE_UP;
    180                 handled = true;
    181                 break;
    182             case KeyEvent.KEYCODE_DPAD_DOWN:
    183                 mShip.setHeadingY(0);
    184                 mDPadState &= ~DPAD_STATE_DOWN;
    185                 handled = true;
    186                 break;
    187             default:
    188                 if (isFireKey(keyCode)) {
    189                     handled = true;
    190                 }
    191                 break;
    192         }
    193         if (handled) {
    194             step(event.getEventTime());
    195             return true;
    196         }
    197         return super.onKeyUp(keyCode, event);
    198     }
    199 
    200     private static boolean isFireKey(int keyCode) {
    201         return KeyEvent.isGamepadButton(keyCode)
    202                 || keyCode == KeyEvent.KEYCODE_DPAD_CENTER
    203                 || keyCode == KeyEvent.KEYCODE_SPACE;
    204     }
    205 
    206     @Override
    207     public boolean onGenericMotionEvent(MotionEvent event) {
    208         ensureInitialized();
    209 
    210         // Check that the event came from a joystick since a generic motion event
    211         // could be almost anything.
    212         if (event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK)
    213                 && event.getAction() == MotionEvent.ACTION_MOVE) {
    214             // Cache the most recently obtained device information.
    215             // The device information may change over time but it can be
    216             // somewhat expensive to query.
    217             if (mLastInputDevice == null || mLastInputDevice.getId() != event.getDeviceId()) {
    218                 mLastInputDevice = event.getDevice();
    219                 // It's possible for the device id to be invalid.
    220                 // In that case, getDevice() will return null.
    221                 if (mLastInputDevice == null) {
    222                     return false;
    223                 }
    224             }
    225 
    226             // Ignore joystick while the DPad is pressed to avoid conflicting motions.
    227             if (mDPadState != 0) {
    228                 return true;
    229             }
    230 
    231             // Process all historical movement samples in the batch.
    232             final int historySize = event.getHistorySize();
    233             for (int i = 0; i < historySize; i++) {
    234                 processJoystickInput(event, i);
    235             }
    236 
    237             // Process the current movement sample in the batch.
    238             processJoystickInput(event, -1);
    239             return true;
    240         }
    241         return super.onGenericMotionEvent(event);
    242     }
    243 
    244     private void processJoystickInput(MotionEvent event, int historyPos) {
    245         // Get joystick position.
    246         // Many game pads with two joysticks report the position of the second joystick
    247         // using the Z and RZ axes so we also handle those.
    248         // In a real game, we would allow the user to configure the axes manually.
    249         float x = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_X, historyPos);
    250         if (x == 0) {
    251             x = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_HAT_X, historyPos);
    252         }
    253         if (x == 0) {
    254             x = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_Z, historyPos);
    255         }
    256 
    257         float y = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_Y, historyPos);
    258         if (y == 0) {
    259             y = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_HAT_Y, historyPos);
    260         }
    261         if (y == 0) {
    262             y = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_RZ, historyPos);
    263         }
    264 
    265         // Set the ship heading.
    266         mShip.setHeading(x, y);
    267         step(historyPos < 0 ? event.getEventTime() : event.getHistoricalEventTime(historyPos));
    268     }
    269 
    270     private static float getCenteredAxis(MotionEvent event, InputDevice device,
    271             int axis, int historyPos) {
    272         final InputDevice.MotionRange range = device.getMotionRange(axis, event.getSource());
    273         if (range != null) {
    274             final float flat = range.getFlat();
    275             final float value = historyPos < 0 ? event.getAxisValue(axis)
    276                     : event.getHistoricalAxisValue(axis, historyPos);
    277 
    278             // Ignore axis values that are within the 'flat' region of the joystick axis center.
    279             // A joystick at rest does not always report an absolute position of (0,0).
    280             if (Math.abs(value) > flat) {
    281                 return value;
    282             }
    283         }
    284         return 0;
    285     }
    286 
    287     @Override
    288     public void onWindowFocusChanged(boolean hasWindowFocus) {
    289         // Turn on and off animations based on the window focus.
    290         // Alternately, we could update the game state using the Activity onResume()
    291         // and onPause() lifecycle events.
    292         if (hasWindowFocus) {
    293             getHandler().postDelayed(mAnimationRunnable, ANIMATION_TIME_STEP);
    294             mLastStepTime = SystemClock.uptimeMillis();
    295         } else {
    296             getHandler().removeCallbacks(mAnimationRunnable);
    297 
    298             mDPadState = 0;
    299             if (mShip != null) {
    300                 mShip.setHeading(0, 0);
    301                 mShip.setVelocity(0, 0);
    302             }
    303         }
    304 
    305         super.onWindowFocusChanged(hasWindowFocus);
    306     }
    307 
    308     private void fire() {
    309         if (mShip != null && !mShip.isDestroyed()) {
    310             Bullet bullet = new Bullet();
    311             bullet.setPosition(mShip.getBulletInitialX(), mShip.getBulletInitialY());
    312             bullet.setVelocity(mShip.getBulletVelocityX(mBulletSpeed),
    313                     mShip.getBulletVelocityY(mBulletSpeed));
    314             mBullets.add(bullet);
    315 
    316             getVibrator().vibrate(20);
    317         }
    318     }
    319 
    320     private void ensureInitialized() {
    321         if (mShip == null) {
    322             reset();
    323         }
    324     }
    325 
    326     private void crash() {
    327         getVibrator().vibrate(new long[] { 0, 20, 20, 40, 40, 80, 40, 300 }, -1);
    328     }
    329 
    330     private void reset() {
    331         mShip = new Ship();
    332         mBullets.clear();
    333         mObstacles.clear();
    334     }
    335 
    336     private Vibrator getVibrator() {
    337         if (mLastInputDevice != null) {
    338             Vibrator vibrator = mLastInputDevice.getVibrator();
    339             if (vibrator.hasVibrator()) {
    340                 return vibrator;
    341             }
    342         }
    343         return (Vibrator)getContext().getSystemService(Context.VIBRATOR_SERVICE);
    344     }
    345 
    346     void animateFrame() {
    347         long currentStepTime = SystemClock.uptimeMillis();
    348         step(currentStepTime);
    349 
    350         Handler handler = getHandler();
    351         if (handler != null) {
    352             handler.postAtTime(mAnimationRunnable, currentStepTime + ANIMATION_TIME_STEP);
    353             invalidate();
    354         }
    355     }
    356 
    357     private void step(long currentStepTime) {
    358         float tau = (currentStepTime - mLastStepTime) * 0.001f;
    359         mLastStepTime = currentStepTime;
    360 
    361         ensureInitialized();
    362 
    363         // Move the ship.
    364         mShip.accelerate(tau, mMaxShipThrust, mMaxShipSpeed);
    365         if (!mShip.step(tau)) {
    366             reset();
    367         }
    368 
    369         // Move the bullets.
    370         int numBullets = mBullets.size();
    371         for (int i = 0; i < numBullets; i++) {
    372             final Bullet bullet = mBullets.get(i);
    373             if (!bullet.step(tau)) {
    374                 mBullets.remove(i);
    375                 i -= 1;
    376                 numBullets -= 1;
    377             }
    378         }
    379 
    380         // Move obstacles.
    381         int numObstacles = mObstacles.size();
    382         for (int i = 0; i < numObstacles; i++) {
    383             final Obstacle obstacle = mObstacles.get(i);
    384             if (!obstacle.step(tau)) {
    385                 mObstacles.remove(i);
    386                 i -= 1;
    387                 numObstacles -= 1;
    388             }
    389         }
    390 
    391         // Check for collisions between bullets and obstacles.
    392         for (int i = 0; i < numBullets; i++) {
    393             final Bullet bullet = mBullets.get(i);
    394             for (int j = 0; j < numObstacles; j++) {
    395                 final Obstacle obstacle = mObstacles.get(j);
    396                 if (bullet.collidesWith(obstacle)) {
    397                     bullet.destroy();
    398                     obstacle.destroy();
    399                     break;
    400                 }
    401             }
    402         }
    403 
    404         // Check for collisions between the ship and obstacles.
    405         for (int i = 0; i < numObstacles; i++) {
    406             final Obstacle obstacle = mObstacles.get(i);
    407             if (mShip.collidesWith(obstacle)) {
    408                 mShip.destroy();
    409                 obstacle.destroy();
    410                 break;
    411             }
    412         }
    413 
    414         // Spawn more obstacles offscreen when needed.
    415         // Avoid putting them right on top of the ship.
    416         OuterLoop: while (mObstacles.size() < MAX_OBSTACLES) {
    417             final float minDistance = mShipSize * 4;
    418             float size = mRandom.nextFloat() * (mMaxObstacleSize - mMinObstacleSize)
    419                     + mMinObstacleSize;
    420             float positionX, positionY;
    421             int tries = 0;
    422             do {
    423                 int edge = mRandom.nextInt(4);
    424                 switch (edge) {
    425                     case 0:
    426                         positionX = -size;
    427                         positionY = mRandom.nextInt(getHeight());
    428                         break;
    429                     case 1:
    430                         positionX = getWidth() + size;
    431                         positionY = mRandom.nextInt(getHeight());
    432                         break;
    433                     case 2:
    434                         positionX = mRandom.nextInt(getWidth());
    435                         positionY = -size;
    436                         break;
    437                     default:
    438                         positionX = mRandom.nextInt(getWidth());
    439                         positionY = getHeight() + size;
    440                         break;
    441                 }
    442                 if (++tries > 10) {
    443                     break OuterLoop;
    444                 }
    445             } while (mShip.distanceTo(positionX, positionY) < minDistance);
    446 
    447             float direction = mRandom.nextFloat() * (float) Math.PI * 2;
    448             float speed = mRandom.nextFloat() * (mMaxObstacleSpeed - mMinObstacleSpeed)
    449                     + mMinObstacleSpeed;
    450             float velocityX = (float) Math.cos(direction) * speed;
    451             float velocityY = (float) Math.sin(direction) * speed;
    452 
    453             Obstacle obstacle = new Obstacle();
    454             obstacle.setPosition(positionX, positionY);
    455             obstacle.setSize(size);
    456             obstacle.setVelocity(velocityX, velocityY);
    457             mObstacles.add(obstacle);
    458         }
    459     }
    460 
    461     @Override
    462     protected void onDraw(Canvas canvas) {
    463         super.onDraw(canvas);
    464 
    465         // Draw the ship.
    466         if (mShip != null) {
    467             mShip.draw(canvas);
    468         }
    469 
    470         // Draw bullets.
    471         int numBullets = mBullets.size();
    472         for (int i = 0; i < numBullets; i++) {
    473             final Bullet bullet = mBullets.get(i);
    474             bullet.draw(canvas);
    475         }
    476 
    477         // Draw obstacles.
    478         int numObstacles = mObstacles.size();
    479         for (int i = 0; i < numObstacles; i++) {
    480             final Obstacle obstacle = mObstacles.get(i);
    481             obstacle.draw(canvas);
    482         }
    483     }
    484 
    485     static float pythag(float x, float y) {
    486         return (float) Math.hypot(x, y);
    487     }
    488 
    489     static int blend(float alpha, int from, int to) {
    490         return from + (int) ((to - from) * alpha);
    491     }
    492 
    493     static void setPaintARGBBlend(Paint paint, float alpha,
    494             int a1, int r1, int g1, int b1,
    495             int a2, int r2, int g2, int b2) {
    496         paint.setARGB(blend(alpha, a1, a2), blend(alpha, r1, r2),
    497                 blend(alpha, g1, g2), blend(alpha, b1, b2));
    498     }
    499 
    500     private abstract class Sprite {
    501         protected float mPositionX;
    502         protected float mPositionY;
    503         protected float mVelocityX;
    504         protected float mVelocityY;
    505         protected float mSize;
    506         protected boolean mDestroyed;
    507         protected float mDestroyAnimProgress;
    508 
    509         public void setPosition(float x, float y) {
    510             mPositionX = x;
    511             mPositionY = y;
    512         }
    513 
    514         public void setVelocity(float x, float y) {
    515             mVelocityX = x;
    516             mVelocityY = y;
    517         }
    518 
    519         public void setSize(float size) {
    520             mSize = size;
    521         }
    522 
    523         public float distanceTo(float x, float y) {
    524             return pythag(mPositionX - x, mPositionY - y);
    525         }
    526 
    527         public float distanceTo(Sprite other) {
    528             return distanceTo(other.mPositionX, other.mPositionY);
    529         }
    530 
    531         public boolean collidesWith(Sprite other) {
    532             // Really bad collision detection.
    533             return !mDestroyed && !other.mDestroyed
    534                     && distanceTo(other) <= Math.max(mSize, other.mSize)
    535                             + Math.min(mSize, other.mSize) * 0.5f;
    536         }
    537 
    538         public boolean isDestroyed() {
    539             return mDestroyed;
    540         }
    541 
    542         public boolean step(float tau) {
    543             mPositionX += mVelocityX * tau;
    544             mPositionY += mVelocityY * tau;
    545 
    546             if (mDestroyed) {
    547                 mDestroyAnimProgress += tau / getDestroyAnimDuration();
    548                 if (mDestroyAnimProgress >= 1.0f) {
    549                     return false;
    550                 }
    551             }
    552             return true;
    553         }
    554 
    555         public abstract void draw(Canvas canvas);
    556 
    557         public abstract float getDestroyAnimDuration();
    558 
    559         protected boolean isOutsidePlayfield() {
    560             final int width = GameView.this.getWidth();
    561             final int height = GameView.this.getHeight();
    562             return mPositionX < 0 || mPositionX >= width
    563                     || mPositionY < 0 || mPositionY >= height;
    564         }
    565 
    566         protected void wrapAtPlayfieldBoundary() {
    567             final int width = GameView.this.getWidth();
    568             final int height = GameView.this.getHeight();
    569             while (mPositionX <= -mSize) {
    570                 mPositionX += width + mSize * 2;
    571             }
    572             while (mPositionX >= width + mSize) {
    573                 mPositionX -= width + mSize * 2;
    574             }
    575             while (mPositionY <= -mSize) {
    576                 mPositionY += height + mSize * 2;
    577             }
    578             while (mPositionY >= height + mSize) {
    579                 mPositionY -= height + mSize * 2;
    580             }
    581         }
    582 
    583         public void destroy() {
    584             mDestroyed = true;
    585             step(0);
    586         }
    587     }
    588 
    589     private class Ship extends Sprite {
    590         private static final float CORNER_ANGLE = (float) Math.PI * 2 / 3;
    591         private static final float TO_DEGREES = (float) (180.0 / Math.PI);
    592 
    593         private float mHeadingX;
    594         private float mHeadingY;
    595         private float mHeadingAngle;
    596         private float mHeadingMagnitude;
    597         private final Paint mPaint;
    598         private final Path mPath;
    599 
    600 
    601         public Ship() {
    602             mPaint = new Paint();
    603             mPaint.setStyle(Style.FILL);
    604 
    605             setPosition(getWidth() * 0.5f, getHeight() * 0.5f);
    606             setVelocity(0, 0);
    607             setSize(mShipSize);
    608 
    609             mPath = new Path();
    610             mPath.moveTo(0, 0);
    611             mPath.lineTo((float)Math.cos(-CORNER_ANGLE) * mSize,
    612                     (float)Math.sin(-CORNER_ANGLE) * mSize);
    613             mPath.lineTo(mSize, 0);
    614             mPath.lineTo((float)Math.cos(CORNER_ANGLE) * mSize,
    615                     (float)Math.sin(CORNER_ANGLE) * mSize);
    616             mPath.lineTo(0, 0);
    617         }
    618 
    619         public void setHeadingX(float x) {
    620             mHeadingX = x;
    621             updateHeading();
    622         }
    623 
    624         public void setHeadingY(float y) {
    625             mHeadingY = y;
    626             updateHeading();
    627         }
    628 
    629         public void setHeading(float x, float y) {
    630             mHeadingX = x;
    631             mHeadingY = y;
    632             updateHeading();
    633         }
    634 
    635         private void updateHeading() {
    636             mHeadingMagnitude = pythag(mHeadingX, mHeadingY);
    637             if (mHeadingMagnitude > 0.1f) {
    638                 mHeadingAngle = (float) Math.atan2(mHeadingY, mHeadingX);
    639             }
    640         }
    641 
    642         private float polarX(float radius) {
    643             return (float) Math.cos(mHeadingAngle) * radius;
    644         }
    645 
    646         private float polarY(float radius) {
    647             return (float) Math.sin(mHeadingAngle) * radius;
    648         }
    649 
    650         public float getBulletInitialX() {
    651             return mPositionX + polarX(mSize);
    652         }
    653 
    654         public float getBulletInitialY() {
    655             return mPositionY + polarY(mSize);
    656         }
    657 
    658         public float getBulletVelocityX(float relativeSpeed) {
    659             return mVelocityX + polarX(relativeSpeed);
    660         }
    661 
    662         public float getBulletVelocityY(float relativeSpeed) {
    663             return mVelocityY + polarY(relativeSpeed);
    664         }
    665 
    666         public void accelerate(float tau, float maxThrust, float maxSpeed) {
    667             final float thrust = mHeadingMagnitude * maxThrust;
    668             mVelocityX += polarX(thrust);
    669             mVelocityY += polarY(thrust);
    670 
    671             final float speed = pythag(mVelocityX, mVelocityY);
    672             if (speed > maxSpeed) {
    673                 final float scale = maxSpeed / speed;
    674                 mVelocityX = mVelocityX * scale;
    675                 mVelocityY = mVelocityY * scale;
    676             }
    677         }
    678 
    679         @Override
    680         public boolean step(float tau) {
    681             if (!super.step(tau)) {
    682                 return false;
    683             }
    684             wrapAtPlayfieldBoundary();
    685             return true;
    686         }
    687 
    688         public void draw(Canvas canvas) {
    689             setPaintARGBBlend(mPaint, mDestroyAnimProgress,
    690                     255, 63, 255, 63,
    691                     0, 255, 0, 0);
    692 
    693             canvas.save();
    694             canvas.translate(mPositionX, mPositionY);
    695             canvas.rotate(mHeadingAngle * TO_DEGREES);
    696             canvas.drawPath(mPath, mPaint);
    697             canvas.restore();
    698         }
    699 
    700         @Override
    701         public float getDestroyAnimDuration() {
    702             return 1.0f;
    703         }
    704 
    705         @Override
    706         public void destroy() {
    707             super.destroy();
    708             crash();
    709         }
    710     }
    711 
    712     private class Bullet extends Sprite {
    713         private final Paint mPaint;
    714 
    715         public Bullet() {
    716             mPaint = new Paint();
    717             mPaint.setStyle(Style.FILL);
    718 
    719             setSize(mBulletSize);
    720         }
    721 
    722         @Override
    723         public boolean step(float tau) {
    724             if (!super.step(tau)) {
    725                 return false;
    726             }
    727             return !isOutsidePlayfield();
    728         }
    729 
    730         public void draw(Canvas canvas) {
    731             setPaintARGBBlend(mPaint, mDestroyAnimProgress,
    732                     255, 255, 255, 0,
    733                     0, 255, 255, 255);
    734             canvas.drawCircle(mPositionX, mPositionY, mSize, mPaint);
    735         }
    736 
    737         @Override
    738         public float getDestroyAnimDuration() {
    739             return 0.125f;
    740         }
    741     }
    742 
    743     private class Obstacle extends Sprite {
    744         private final Paint mPaint;
    745 
    746         public Obstacle() {
    747             mPaint = new Paint();
    748             mPaint.setARGB(255, 127, 127, 255);
    749             mPaint.setStyle(Style.FILL);
    750         }
    751 
    752         @Override
    753         public boolean step(float tau) {
    754             if (!super.step(tau)) {
    755                 return false;
    756             }
    757             wrapAtPlayfieldBoundary();
    758             return true;
    759         }
    760 
    761         public void draw(Canvas canvas) {
    762             setPaintARGBBlend(mPaint, mDestroyAnimProgress,
    763                     255, 127, 127, 255,
    764                     0, 255, 0, 0);
    765             canvas.drawCircle(mPositionX, mPositionY,
    766                     mSize * (1.0f - mDestroyAnimProgress), mPaint);
    767         }
    768 
    769         @Override
    770         public float getDestroyAnimDuration() {
    771             return 0.25f;
    772         }
    773     }
    774 }
    775