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