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