Home | History | Annotate | Download | only in lunarlander
      1 /*
      2  * Copyright (C) 2007 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.lunarlander;
     18 
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.graphics.Bitmap;
     22 import android.graphics.BitmapFactory;
     23 import android.graphics.Canvas;
     24 import android.graphics.Paint;
     25 import android.graphics.RectF;
     26 import android.graphics.drawable.Drawable;
     27 import android.os.Bundle;
     28 import android.os.Handler;
     29 import android.os.Message;
     30 import android.util.AttributeSet;
     31 import android.view.KeyEvent;
     32 import android.view.SurfaceHolder;
     33 import android.view.SurfaceView;
     34 import android.view.View;
     35 import android.widget.TextView;
     36 
     37 
     38 /**
     39  * View that draws, takes keystrokes, etc. for a simple LunarLander game.
     40  *
     41  * Has a mode which RUNNING, PAUSED, etc. Has a x, y, dx, dy, ... capturing the
     42  * current ship physics. All x/y etc. are measured with (0,0) at the lower left.
     43  * updatePhysics() advances the physics based on realtime. draw() renders the
     44  * ship, and does an invalidate() to prompt another draw() as soon as possible
     45  * by the system.
     46  */
     47 class LunarView extends SurfaceView implements SurfaceHolder.Callback {
     48     class LunarThread extends Thread {
     49         /*
     50          * Difficulty setting constants
     51          */
     52         public static final int DIFFICULTY_EASY = 0;
     53         public static final int DIFFICULTY_HARD = 1;
     54         public static final int DIFFICULTY_MEDIUM = 2;
     55         /*
     56          * Physics constants
     57          */
     58         public static final int PHYS_DOWN_ACCEL_SEC = 35;
     59         public static final int PHYS_FIRE_ACCEL_SEC = 80;
     60         public static final int PHYS_FUEL_INIT = 60;
     61         public static final int PHYS_FUEL_MAX = 100;
     62         public static final int PHYS_FUEL_SEC = 10;
     63         public static final int PHYS_SLEW_SEC = 120; // degrees/second rotate
     64         public static final int PHYS_SPEED_HYPERSPACE = 180;
     65         public static final int PHYS_SPEED_INIT = 30;
     66         public static final int PHYS_SPEED_MAX = 120;
     67         /*
     68          * State-tracking constants
     69          */
     70         public static final int STATE_LOSE = 1;
     71         public static final int STATE_PAUSE = 2;
     72         public static final int STATE_READY = 3;
     73         public static final int STATE_RUNNING = 4;
     74         public static final int STATE_WIN = 5;
     75 
     76         /*
     77          * Goal condition constants
     78          */
     79         public static final int TARGET_ANGLE = 18; // > this angle means crash
     80         public static final int TARGET_BOTTOM_PADDING = 17; // px below gear
     81         public static final int TARGET_PAD_HEIGHT = 8; // how high above ground
     82         public static final int TARGET_SPEED = 28; // > this speed means crash
     83         public static final double TARGET_WIDTH = 1.6; // width of target
     84         /*
     85          * UI constants (i.e. the speed & fuel bars)
     86          */
     87         public static final int UI_BAR = 100; // width of the bar(s)
     88         public static final int UI_BAR_HEIGHT = 10; // height of the bar(s)
     89         private static final String KEY_DIFFICULTY = "mDifficulty";
     90         private static final String KEY_DX = "mDX";
     91 
     92         private static final String KEY_DY = "mDY";
     93         private static final String KEY_FUEL = "mFuel";
     94         private static final String KEY_GOAL_ANGLE = "mGoalAngle";
     95         private static final String KEY_GOAL_SPEED = "mGoalSpeed";
     96         private static final String KEY_GOAL_WIDTH = "mGoalWidth";
     97 
     98         private static final String KEY_GOAL_X = "mGoalX";
     99         private static final String KEY_HEADING = "mHeading";
    100         private static final String KEY_LANDER_HEIGHT = "mLanderHeight";
    101         private static final String KEY_LANDER_WIDTH = "mLanderWidth";
    102         private static final String KEY_WINS = "mWinsInARow";
    103 
    104         private static final String KEY_X = "mX";
    105         private static final String KEY_Y = "mY";
    106 
    107         /*
    108          * Member (state) fields
    109          */
    110         /** The drawable to use as the background of the animation canvas */
    111         private Bitmap mBackgroundImage;
    112 
    113         /**
    114          * Current height of the surface/canvas.
    115          *
    116          * @see #setSurfaceSize
    117          */
    118         private int mCanvasHeight = 1;
    119 
    120         /**
    121          * Current width of the surface/canvas.
    122          *
    123          * @see #setSurfaceSize
    124          */
    125         private int mCanvasWidth = 1;
    126 
    127         /** What to draw for the Lander when it has crashed */
    128         private Drawable mCrashedImage;
    129 
    130         /**
    131          * Current difficulty -- amount of fuel, allowed angle, etc. Default is
    132          * MEDIUM.
    133          */
    134         private int mDifficulty;
    135 
    136         /** Velocity dx. */
    137         private double mDX;
    138 
    139         /** Velocity dy. */
    140         private double mDY;
    141 
    142         /** Is the engine burning? */
    143         private boolean mEngineFiring;
    144 
    145         /** What to draw for the Lander when the engine is firing */
    146         private Drawable mFiringImage;
    147 
    148         /** Fuel remaining */
    149         private double mFuel;
    150 
    151         /** Allowed angle. */
    152         private int mGoalAngle;
    153 
    154         /** Allowed speed. */
    155         private int mGoalSpeed;
    156 
    157         /** Width of the landing pad. */
    158         private int mGoalWidth;
    159 
    160         /** X of the landing pad. */
    161         private int mGoalX;
    162 
    163         /** Message handler used by thread to interact with TextView */
    164         private Handler mHandler;
    165 
    166         /**
    167          * Lander heading in degrees, with 0 up, 90 right. Kept in the range
    168          * 0..360.
    169          */
    170         private double mHeading;
    171 
    172         /** Pixel height of lander image. */
    173         private int mLanderHeight;
    174 
    175         /** What to draw for the Lander in its normal state */
    176         private Drawable mLanderImage;
    177 
    178         /** Pixel width of lander image. */
    179         private int mLanderWidth;
    180 
    181         /** Used to figure out elapsed time between frames */
    182         private long mLastTime;
    183 
    184         /** Paint to draw the lines on screen. */
    185         private Paint mLinePaint;
    186 
    187         /** "Bad" speed-too-high variant of the line color. */
    188         private Paint mLinePaintBad;
    189 
    190         /** The state of the game. One of READY, RUNNING, PAUSE, LOSE, or WIN */
    191         private int mMode;
    192 
    193         /** Currently rotating, -1 left, 0 none, 1 right. */
    194         private int mRotating;
    195 
    196         /** Indicate whether the surface has been created & is ready to draw */
    197         private boolean mRun = false;
    198 
    199         private final Object mRunLock = new Object();
    200 
    201         /** Scratch rect object. */
    202         private RectF mScratchRect;
    203 
    204         /** Handle to the surface manager object we interact with */
    205         private SurfaceHolder mSurfaceHolder;
    206 
    207         /** Number of wins in a row. */
    208         private int mWinsInARow;
    209 
    210         /** X of lander center. */
    211         private double mX;
    212 
    213         /** Y of lander center. */
    214         private double mY;
    215 
    216         public LunarThread(SurfaceHolder surfaceHolder, Context context,
    217                 Handler handler) {
    218             // get handles to some important objects
    219             mSurfaceHolder = surfaceHolder;
    220             mHandler = handler;
    221             mContext = context;
    222 
    223             Resources res = context.getResources();
    224             // cache handles to our key sprites & other drawables
    225             mLanderImage = context.getResources().getDrawable(
    226                     R.drawable.lander_plain);
    227             mFiringImage = context.getResources().getDrawable(
    228                     R.drawable.lander_firing);
    229             mCrashedImage = context.getResources().getDrawable(
    230                     R.drawable.lander_crashed);
    231 
    232             // load background image as a Bitmap instead of a Drawable b/c
    233             // we don't need to transform it and it's faster to draw this way
    234             mBackgroundImage = BitmapFactory.decodeResource(res,
    235                     R.drawable.earthrise);
    236 
    237             // Use the regular lander image as the model size for all sprites
    238             mLanderWidth = mLanderImage.getIntrinsicWidth();
    239             mLanderHeight = mLanderImage.getIntrinsicHeight();
    240 
    241             // Initialize paints for speedometer
    242             mLinePaint = new Paint();
    243             mLinePaint.setAntiAlias(true);
    244             mLinePaint.setARGB(255, 0, 255, 0);
    245 
    246             mLinePaintBad = new Paint();
    247             mLinePaintBad.setAntiAlias(true);
    248             mLinePaintBad.setARGB(255, 120, 180, 0);
    249 
    250             mScratchRect = new RectF(0, 0, 0, 0);
    251 
    252             mWinsInARow = 0;
    253             mDifficulty = DIFFICULTY_MEDIUM;
    254 
    255             // initial show-up of lander (not yet playing)
    256             mX = mLanderWidth;
    257             mY = mLanderHeight * 2;
    258             mFuel = PHYS_FUEL_INIT;
    259             mDX = 0;
    260             mDY = 0;
    261             mHeading = 0;
    262             mEngineFiring = true;
    263         }
    264 
    265         /**
    266          * Starts the game, setting parameters for the current difficulty.
    267          */
    268         public void doStart() {
    269             synchronized (mSurfaceHolder) {
    270                 // First set the game for Medium difficulty
    271                 mFuel = PHYS_FUEL_INIT;
    272                 mEngineFiring = false;
    273                 mGoalWidth = (int) (mLanderWidth * TARGET_WIDTH);
    274                 mGoalSpeed = TARGET_SPEED;
    275                 mGoalAngle = TARGET_ANGLE;
    276                 int speedInit = PHYS_SPEED_INIT;
    277 
    278                 // Adjust difficulty params for EASY/HARD
    279                 if (mDifficulty == DIFFICULTY_EASY) {
    280                     mFuel = mFuel * 3 / 2;
    281                     mGoalWidth = mGoalWidth * 4 / 3;
    282                     mGoalSpeed = mGoalSpeed * 3 / 2;
    283                     mGoalAngle = mGoalAngle * 4 / 3;
    284                     speedInit = speedInit * 3 / 4;
    285                 } else if (mDifficulty == DIFFICULTY_HARD) {
    286                     mFuel = mFuel * 7 / 8;
    287                     mGoalWidth = mGoalWidth * 3 / 4;
    288                     mGoalSpeed = mGoalSpeed * 7 / 8;
    289                     speedInit = speedInit * 4 / 3;
    290                 }
    291 
    292                 // pick a convenient initial location for the lander sprite
    293                 mX = mCanvasWidth / 2;
    294                 mY = mCanvasHeight - mLanderHeight / 2;
    295 
    296                 // start with a little random motion
    297                 mDY = Math.random() * -speedInit;
    298                 mDX = Math.random() * 2 * speedInit - speedInit;
    299                 mHeading = 0;
    300 
    301                 // Figure initial spot for landing, not too near center
    302                 while (true) {
    303                     mGoalX = (int) (Math.random() * (mCanvasWidth - mGoalWidth));
    304                     if (Math.abs(mGoalX - (mX - mLanderWidth / 2)) > mCanvasHeight / 6)
    305                         break;
    306                 }
    307 
    308                 mLastTime = System.currentTimeMillis() + 100;
    309                 setState(STATE_RUNNING);
    310             }
    311         }
    312 
    313         /**
    314          * Pauses the physics update & animation.
    315          */
    316         public void pause() {
    317             synchronized (mSurfaceHolder) {
    318                 if (mMode == STATE_RUNNING) setState(STATE_PAUSE);
    319             }
    320         }
    321 
    322         /**
    323          * Restores game state from the indicated Bundle. Typically called when
    324          * the Activity is being restored after having been previously
    325          * destroyed.
    326          *
    327          * @param savedState Bundle containing the game state
    328          */
    329         public synchronized void restoreState(Bundle savedState) {
    330             synchronized (mSurfaceHolder) {
    331                 setState(STATE_PAUSE);
    332                 mRotating = 0;
    333                 mEngineFiring = false;
    334 
    335                 mDifficulty = savedState.getInt(KEY_DIFFICULTY);
    336                 mX = savedState.getDouble(KEY_X);
    337                 mY = savedState.getDouble(KEY_Y);
    338                 mDX = savedState.getDouble(KEY_DX);
    339                 mDY = savedState.getDouble(KEY_DY);
    340                 mHeading = savedState.getDouble(KEY_HEADING);
    341 
    342                 mLanderWidth = savedState.getInt(KEY_LANDER_WIDTH);
    343                 mLanderHeight = savedState.getInt(KEY_LANDER_HEIGHT);
    344                 mGoalX = savedState.getInt(KEY_GOAL_X);
    345                 mGoalSpeed = savedState.getInt(KEY_GOAL_SPEED);
    346                 mGoalAngle = savedState.getInt(KEY_GOAL_ANGLE);
    347                 mGoalWidth = savedState.getInt(KEY_GOAL_WIDTH);
    348                 mWinsInARow = savedState.getInt(KEY_WINS);
    349                 mFuel = savedState.getDouble(KEY_FUEL);
    350             }
    351         }
    352 
    353         @Override
    354         public void run() {
    355             while (mRun) {
    356                 Canvas c = null;
    357                 try {
    358                     c = mSurfaceHolder.lockCanvas(null);
    359                     synchronized (mSurfaceHolder) {
    360                         if (mMode == STATE_RUNNING) updatePhysics();
    361                         // Critical section. Do not allow mRun to be set false until
    362                         // we are sure all canvas draw operations are complete.
    363                         //
    364                         // If mRun has been toggled false, inhibit canvas operations.
    365                         synchronized (mRunLock) {
    366                             if (mRun) doDraw(c);
    367                         }
    368                     }
    369                 } finally {
    370                     // do this in a finally so that if an exception is thrown
    371                     // during the above, we don't leave the Surface in an
    372                     // inconsistent state
    373                     if (c != null) {
    374                         mSurfaceHolder.unlockCanvasAndPost(c);
    375                     }
    376                 }
    377             }
    378         }
    379 
    380         /**
    381          * Dump game state to the provided Bundle. Typically called when the
    382          * Activity is being suspended.
    383          *
    384          * @return Bundle with this view's state
    385          */
    386         public Bundle saveState(Bundle map) {
    387             synchronized (mSurfaceHolder) {
    388                 if (map != null) {
    389                     map.putInt(KEY_DIFFICULTY, Integer.valueOf(mDifficulty));
    390                     map.putDouble(KEY_X, Double.valueOf(mX));
    391                     map.putDouble(KEY_Y, Double.valueOf(mY));
    392                     map.putDouble(KEY_DX, Double.valueOf(mDX));
    393                     map.putDouble(KEY_DY, Double.valueOf(mDY));
    394                     map.putDouble(KEY_HEADING, Double.valueOf(mHeading));
    395                     map.putInt(KEY_LANDER_WIDTH, Integer.valueOf(mLanderWidth));
    396                     map.putInt(KEY_LANDER_HEIGHT, Integer
    397                             .valueOf(mLanderHeight));
    398                     map.putInt(KEY_GOAL_X, Integer.valueOf(mGoalX));
    399                     map.putInt(KEY_GOAL_SPEED, Integer.valueOf(mGoalSpeed));
    400                     map.putInt(KEY_GOAL_ANGLE, Integer.valueOf(mGoalAngle));
    401                     map.putInt(KEY_GOAL_WIDTH, Integer.valueOf(mGoalWidth));
    402                     map.putInt(KEY_WINS, Integer.valueOf(mWinsInARow));
    403                     map.putDouble(KEY_FUEL, Double.valueOf(mFuel));
    404                 }
    405             }
    406             return map;
    407         }
    408 
    409         /**
    410          * Sets the current difficulty.
    411          *
    412          * @param difficulty
    413          */
    414         public void setDifficulty(int difficulty) {
    415             synchronized (mSurfaceHolder) {
    416                 mDifficulty = difficulty;
    417             }
    418         }
    419 
    420         /**
    421          * Sets if the engine is currently firing.
    422          */
    423         public void setFiring(boolean firing) {
    424             synchronized (mSurfaceHolder) {
    425                 mEngineFiring = firing;
    426             }
    427         }
    428 
    429         /**
    430          * Used to signal the thread whether it should be running or not.
    431          * Passing true allows the thread to run; passing false will shut it
    432          * down if it's already running. Calling start() after this was most
    433          * recently called with false will result in an immediate shutdown.
    434          *
    435          * @param b true to run, false to shut down
    436          */
    437         public void setRunning(boolean b) {
    438             // Do not allow mRun to be modified while any canvas operations
    439             // are potentially in-flight. See doDraw().
    440             synchronized (mRunLock) {
    441                 mRun = b;
    442             }
    443         }
    444 
    445         /**
    446          * Sets the game mode. That is, whether we are running, paused, in the
    447          * failure state, in the victory state, etc.
    448          *
    449          * @see #setState(int, CharSequence)
    450          * @param mode one of the STATE_* constants
    451          */
    452         public void setState(int mode) {
    453             synchronized (mSurfaceHolder) {
    454                 setState(mode, null);
    455             }
    456         }
    457 
    458         /**
    459          * Sets the game mode. That is, whether we are running, paused, in the
    460          * failure state, in the victory state, etc.
    461          *
    462          * @param mode one of the STATE_* constants
    463          * @param message string to add to screen or null
    464          */
    465         public void setState(int mode, CharSequence message) {
    466             /*
    467              * This method optionally can cause a text message to be displayed
    468              * to the user when the mode changes. Since the View that actually
    469              * renders that text is part of the main View hierarchy and not
    470              * owned by this thread, we can't touch the state of that View.
    471              * Instead we use a Message + Handler to relay commands to the main
    472              * thread, which updates the user-text View.
    473              */
    474             synchronized (mSurfaceHolder) {
    475                 mMode = mode;
    476 
    477                 if (mMode == STATE_RUNNING) {
    478                     Message msg = mHandler.obtainMessage();
    479                     Bundle b = new Bundle();
    480                     b.putString("text", "");
    481                     b.putInt("viz", View.INVISIBLE);
    482                     msg.setData(b);
    483                     mHandler.sendMessage(msg);
    484                 } else {
    485                     mRotating = 0;
    486                     mEngineFiring = false;
    487                     Resources res = mContext.getResources();
    488                     CharSequence str = "";
    489                     if (mMode == STATE_READY)
    490                         str = res.getText(R.string.mode_ready);
    491                     else if (mMode == STATE_PAUSE)
    492                         str = res.getText(R.string.mode_pause);
    493                     else if (mMode == STATE_LOSE)
    494                         str = res.getText(R.string.mode_lose);
    495                     else if (mMode == STATE_WIN)
    496                         str = res.getString(R.string.mode_win_prefix)
    497                                 + mWinsInARow + " "
    498                                 + res.getString(R.string.mode_win_suffix);
    499 
    500                     if (message != null) {
    501                         str = message + "\n" + str;
    502                     }
    503 
    504                     if (mMode == STATE_LOSE) mWinsInARow = 0;
    505 
    506                     Message msg = mHandler.obtainMessage();
    507                     Bundle b = new Bundle();
    508                     b.putString("text", str.toString());
    509                     b.putInt("viz", View.VISIBLE);
    510                     msg.setData(b);
    511                     mHandler.sendMessage(msg);
    512                 }
    513             }
    514         }
    515 
    516         /* Callback invoked when the surface dimensions change. */
    517         public void setSurfaceSize(int width, int height) {
    518             // synchronized to make sure these all change atomically
    519             synchronized (mSurfaceHolder) {
    520                 mCanvasWidth = width;
    521                 mCanvasHeight = height;
    522 
    523                 // don't forget to resize the background image
    524                 mBackgroundImage = Bitmap.createScaledBitmap(
    525                         mBackgroundImage, width, height, true);
    526             }
    527         }
    528 
    529         /**
    530          * Resumes from a pause.
    531          */
    532         public void unpause() {
    533             // Move the real time clock up to now
    534             synchronized (mSurfaceHolder) {
    535                 mLastTime = System.currentTimeMillis() + 100;
    536             }
    537             setState(STATE_RUNNING);
    538         }
    539 
    540         /**
    541          * Handles a key-down event.
    542          *
    543          * @param keyCode the key that was pressed
    544          * @param msg the original event object
    545          * @return true
    546          */
    547         boolean doKeyDown(int keyCode, KeyEvent msg) {
    548             synchronized (mSurfaceHolder) {
    549                 boolean okStart = false;
    550                 if (keyCode == KeyEvent.KEYCODE_DPAD_UP) okStart = true;
    551                 if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) okStart = true;
    552                 if (keyCode == KeyEvent.KEYCODE_S) okStart = true;
    553 
    554                 if (okStart
    555                         && (mMode == STATE_READY || mMode == STATE_LOSE || mMode == STATE_WIN)) {
    556                     // ready-to-start -> start
    557                     doStart();
    558                     return true;
    559                 } else if (mMode == STATE_PAUSE && okStart) {
    560                     // paused -> running
    561                     unpause();
    562                     return true;
    563                 } else if (mMode == STATE_RUNNING) {
    564                     // center/space -> fire
    565                     if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER
    566                             || keyCode == KeyEvent.KEYCODE_SPACE) {
    567                         setFiring(true);
    568                         return true;
    569                         // left/q -> left
    570                     } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
    571                             || keyCode == KeyEvent.KEYCODE_Q) {
    572                         mRotating = -1;
    573                         return true;
    574                         // right/w -> right
    575                     } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
    576                             || keyCode == KeyEvent.KEYCODE_W) {
    577                         mRotating = 1;
    578                         return true;
    579                         // up -> pause
    580                     } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
    581                         pause();
    582                         return true;
    583                     }
    584                 }
    585 
    586                 return false;
    587             }
    588         }
    589 
    590         /**
    591          * Handles a key-up event.
    592          *
    593          * @param keyCode the key that was pressed
    594          * @param msg the original event object
    595          * @return true if the key was handled and consumed, or else false
    596          */
    597         boolean doKeyUp(int keyCode, KeyEvent msg) {
    598             boolean handled = false;
    599 
    600             synchronized (mSurfaceHolder) {
    601                 if (mMode == STATE_RUNNING) {
    602                     if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER
    603                             || keyCode == KeyEvent.KEYCODE_SPACE) {
    604                         setFiring(false);
    605                         handled = true;
    606                     } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
    607                             || keyCode == KeyEvent.KEYCODE_Q
    608                             || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
    609                             || keyCode == KeyEvent.KEYCODE_W) {
    610                         mRotating = 0;
    611                         handled = true;
    612                     }
    613                 }
    614             }
    615 
    616             return handled;
    617         }
    618 
    619         /**
    620          * Draws the ship, fuel/speed bars, and background to the provided
    621          * Canvas.
    622          */
    623         private void doDraw(Canvas canvas) {
    624             // Draw the background image. Operations on the Canvas accumulate
    625             // so this is like clearing the screen.
    626             canvas.drawBitmap(mBackgroundImage, 0, 0, null);
    627 
    628             int yTop = mCanvasHeight - ((int) mY + mLanderHeight / 2);
    629             int xLeft = (int) mX - mLanderWidth / 2;
    630 
    631             // Draw the fuel gauge
    632             int fuelWidth = (int) (UI_BAR * mFuel / PHYS_FUEL_MAX);
    633             mScratchRect.set(4, 4, 4 + fuelWidth, 4 + UI_BAR_HEIGHT);
    634             canvas.drawRect(mScratchRect, mLinePaint);
    635 
    636             // Draw the speed gauge, with a two-tone effect
    637             double speed = Math.sqrt(mDX * mDX + mDY * mDY);
    638             int speedWidth = (int) (UI_BAR * speed / PHYS_SPEED_MAX);
    639 
    640             if (speed <= mGoalSpeed) {
    641                 mScratchRect.set(4 + UI_BAR + 4, 4,
    642                         4 + UI_BAR + 4 + speedWidth, 4 + UI_BAR_HEIGHT);
    643                 canvas.drawRect(mScratchRect, mLinePaint);
    644             } else {
    645                 // Draw the bad color in back, with the good color in front of
    646                 // it
    647                 mScratchRect.set(4 + UI_BAR + 4, 4,
    648                         4 + UI_BAR + 4 + speedWidth, 4 + UI_BAR_HEIGHT);
    649                 canvas.drawRect(mScratchRect, mLinePaintBad);
    650                 int goalWidth = (UI_BAR * mGoalSpeed / PHYS_SPEED_MAX);
    651                 mScratchRect.set(4 + UI_BAR + 4, 4, 4 + UI_BAR + 4 + goalWidth,
    652                         4 + UI_BAR_HEIGHT);
    653                 canvas.drawRect(mScratchRect, mLinePaint);
    654             }
    655 
    656             // Draw the landing pad
    657             canvas.drawLine(mGoalX, 1 + mCanvasHeight - TARGET_PAD_HEIGHT,
    658                     mGoalX + mGoalWidth, 1 + mCanvasHeight - TARGET_PAD_HEIGHT,
    659                     mLinePaint);
    660 
    661 
    662             // Draw the ship with its current rotation
    663             canvas.save();
    664             canvas.rotate((float) mHeading, (float) mX, mCanvasHeight
    665                     - (float) mY);
    666             if (mMode == STATE_LOSE) {
    667                 mCrashedImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop
    668                         + mLanderHeight);
    669                 mCrashedImage.draw(canvas);
    670             } else if (mEngineFiring) {
    671                 mFiringImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop
    672                         + mLanderHeight);
    673                 mFiringImage.draw(canvas);
    674             } else {
    675                 mLanderImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop
    676                         + mLanderHeight);
    677                 mLanderImage.draw(canvas);
    678             }
    679             canvas.restore();
    680         }
    681 
    682         /**
    683          * Figures the lander state (x, y, fuel, ...) based on the passage of
    684          * realtime. Does not invalidate(). Called at the start of draw().
    685          * Detects the end-of-game and sets the UI to the next state.
    686          */
    687         private void updatePhysics() {
    688             long now = System.currentTimeMillis();
    689 
    690             // Do nothing if mLastTime is in the future.
    691             // This allows the game-start to delay the start of the physics
    692             // by 100ms or whatever.
    693             if (mLastTime > now) return;
    694 
    695             double elapsed = (now - mLastTime) / 1000.0;
    696 
    697             // mRotating -- update heading
    698             if (mRotating != 0) {
    699                 mHeading += mRotating * (PHYS_SLEW_SEC * elapsed);
    700 
    701                 // Bring things back into the range 0..360
    702                 if (mHeading < 0)
    703                     mHeading += 360;
    704                 else if (mHeading >= 360) mHeading -= 360;
    705             }
    706 
    707             // Base accelerations -- 0 for x, gravity for y
    708             double ddx = 0.0;
    709             double ddy = -PHYS_DOWN_ACCEL_SEC * elapsed;
    710 
    711             if (mEngineFiring) {
    712                 // taking 0 as up, 90 as to the right
    713                 // cos(deg) is ddy component, sin(deg) is ddx component
    714                 double elapsedFiring = elapsed;
    715                 double fuelUsed = elapsedFiring * PHYS_FUEL_SEC;
    716 
    717                 // tricky case where we run out of fuel partway through the
    718                 // elapsed
    719                 if (fuelUsed > mFuel) {
    720                     elapsedFiring = mFuel / fuelUsed * elapsed;
    721                     fuelUsed = mFuel;
    722 
    723                     // Oddball case where we adjust the "control" from here
    724                     mEngineFiring = false;
    725                 }
    726 
    727                 mFuel -= fuelUsed;
    728 
    729                 // have this much acceleration from the engine
    730                 double accel = PHYS_FIRE_ACCEL_SEC * elapsedFiring;
    731 
    732                 double radians = 2 * Math.PI * mHeading / 360;
    733                 ddx = Math.sin(radians) * accel;
    734                 ddy += Math.cos(radians) * accel;
    735             }
    736 
    737             double dxOld = mDX;
    738             double dyOld = mDY;
    739 
    740             // figure speeds for the end of the period
    741             mDX += ddx;
    742             mDY += ddy;
    743 
    744             // figure position based on average speed during the period
    745             mX += elapsed * (mDX + dxOld) / 2;
    746             mY += elapsed * (mDY + dyOld) / 2;
    747 
    748             mLastTime = now;
    749 
    750             // Evaluate if we have landed ... stop the game
    751             double yLowerBound = TARGET_PAD_HEIGHT + mLanderHeight / 2
    752                     - TARGET_BOTTOM_PADDING;
    753             if (mY <= yLowerBound) {
    754                 mY = yLowerBound;
    755 
    756                 int result = STATE_LOSE;
    757                 CharSequence message = "";
    758                 Resources res = mContext.getResources();
    759                 double speed = Math.sqrt(mDX * mDX + mDY * mDY);
    760                 boolean onGoal = (mGoalX <= mX - mLanderWidth / 2 && mX
    761                         + mLanderWidth / 2 <= mGoalX + mGoalWidth);
    762 
    763                 // "Hyperspace" win -- upside down, going fast,
    764                 // puts you back at the top.
    765                 if (onGoal && Math.abs(mHeading - 180) < mGoalAngle
    766                         && speed > PHYS_SPEED_HYPERSPACE) {
    767                     result = STATE_WIN;
    768                     mWinsInARow++;
    769                     doStart();
    770 
    771                     return;
    772                     // Oddball case: this case does a return, all other cases
    773                     // fall through to setMode() below.
    774                 } else if (!onGoal) {
    775                     message = res.getText(R.string.message_off_pad);
    776                 } else if (!(mHeading <= mGoalAngle || mHeading >= 360 - mGoalAngle)) {
    777                     message = res.getText(R.string.message_bad_angle);
    778                 } else if (speed > mGoalSpeed) {
    779                     message = res.getText(R.string.message_too_fast);
    780                 } else {
    781                     result = STATE_WIN;
    782                     mWinsInARow++;
    783                 }
    784 
    785                 setState(result, message);
    786             }
    787         }
    788     }
    789 
    790     /** Handle to the application context, used to e.g. fetch Drawables. */
    791     private Context mContext;
    792 
    793     /** Pointer to the text view to display "Paused.." etc. */
    794     private TextView mStatusText;
    795 
    796     /** The thread that actually draws the animation */
    797     private LunarThread thread;
    798 
    799     public LunarView(Context context, AttributeSet attrs) {
    800         super(context, attrs);
    801 
    802         // register our interest in hearing about changes to our surface
    803         SurfaceHolder holder = getHolder();
    804         holder.addCallback(this);
    805 
    806         // create thread only; it's started in surfaceCreated()
    807         thread = new LunarThread(holder, context, new Handler() {
    808             @Override
    809             public void handleMessage(Message m) {
    810                 mStatusText.setVisibility(m.getData().getInt("viz"));
    811                 mStatusText.setText(m.getData().getString("text"));
    812             }
    813         });
    814 
    815         setFocusable(true); // make sure we get key events
    816     }
    817 
    818     /**
    819      * Fetches the animation thread corresponding to this LunarView.
    820      *
    821      * @return the animation thread
    822      */
    823     public LunarThread getThread() {
    824         return thread;
    825     }
    826 
    827     /**
    828      * Standard override to get key-press events.
    829      */
    830     @Override
    831     public boolean onKeyDown(int keyCode, KeyEvent msg) {
    832         return thread.doKeyDown(keyCode, msg);
    833     }
    834 
    835     /**
    836      * Standard override for key-up. We actually care about these, so we can
    837      * turn off the engine or stop rotating.
    838      */
    839     @Override
    840     public boolean onKeyUp(int keyCode, KeyEvent msg) {
    841         return thread.doKeyUp(keyCode, msg);
    842     }
    843 
    844     /**
    845      * Standard window-focus override. Notice focus lost so we can pause on
    846      * focus lost. e.g. user switches to take a call.
    847      */
    848     @Override
    849     public void onWindowFocusChanged(boolean hasWindowFocus) {
    850         if (!hasWindowFocus) thread.pause();
    851     }
    852 
    853     /**
    854      * Installs a pointer to the text view used for messages.
    855      */
    856     public void setTextView(TextView textView) {
    857         mStatusText = textView;
    858     }
    859 
    860     /* Callback invoked when the surface dimensions change. */
    861     public void surfaceChanged(SurfaceHolder holder, int format, int width,
    862             int height) {
    863         thread.setSurfaceSize(width, height);
    864     }
    865 
    866     /*
    867      * Callback invoked when the Surface has been created and is ready to be
    868      * used.
    869      */
    870     public void surfaceCreated(SurfaceHolder holder) {
    871         // start the thread here so that we don't busy-wait in run()
    872         // waiting for the surface to be created
    873         thread.setRunning(true);
    874         thread.start();
    875     }
    876 
    877     /*
    878      * Callback invoked when the Surface has been destroyed and must no longer
    879      * be touched. WARNING: after this method returns, the Surface/Canvas must
    880      * never be touched again!
    881      */
    882     public void surfaceDestroyed(SurfaceHolder holder) {
    883         // we have to tell thread to shut down & wait for it to finish, or else
    884         // it might touch the Surface after we return and explode
    885         boolean retry = true;
    886         thread.setRunning(false);
    887         while (retry) {
    888             try {
    889                 thread.join();
    890                 retry = false;
    891             } catch (InterruptedException e) {
    892             }
    893         }
    894     }
    895 }
    896