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 
LunarThread(SurfaceHolder surfaceHolder, Context context, Handler handler)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          */
doStart()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          */
pause()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          */
restoreState(Bundle savedState)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
run()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          */
saveState(Bundle map)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          */
setDifficulty(int difficulty)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          */
setFiring(boolean firing)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          */
setRunning(boolean b)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          */
setState(int mode)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          */
setState(int mode, CharSequence message)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. */
setSurfaceSize(int width, int height)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          */
unpause()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          */
doKeyDown(int keyCode, KeyEvent msg)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          */
doKeyUp(int keyCode, KeyEvent msg)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          */
doDraw(Canvas canvas)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.hypot(mDX, 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          */
updatePhysics()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.hypot(mDX, 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 
LunarView(Context context, AttributeSet attrs)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      */
getThread()823     public LunarThread getThread() {
824         return thread;
825     }
826 
827     /**
828      * Standard override to get key-press events.
829      */
830     @Override
onKeyDown(int keyCode, KeyEvent msg)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
onKeyUp(int keyCode, KeyEvent msg)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
onWindowFocusChanged(boolean hasWindowFocus)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      */
setTextView(TextView textView)856     public void setTextView(TextView textView) {
857         mStatusText = textView;
858     }
859 
860     /* Callback invoked when the surface dimensions change. */
surfaceChanged(SurfaceHolder holder, int format, int width, int height)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      */
surfaceCreated(SurfaceHolder holder)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      */
surfaceDestroyed(SurfaceHolder holder)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