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 /** 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 = mBackgroundImage.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 boolean center = (keyCode == KeyEvent.KEYCODE_DPAD_UP); 543 544 if (okStart 545 && (mMode == STATE_READY || mMode == STATE_LOSE || mMode == STATE_WIN)) { 546 // ready-to-start -> start 547 doStart(); 548 return true; 549 } else if (mMode == STATE_PAUSE && okStart) { 550 // paused -> running 551 unpause(); 552 return true; 553 } else if (mMode == STATE_RUNNING) { 554 // center/space -> fire 555 if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER 556 || keyCode == KeyEvent.KEYCODE_SPACE) { 557 setFiring(true); 558 return true; 559 // left/q -> left 560 } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT 561 || keyCode == KeyEvent.KEYCODE_Q) { 562 mRotating = -1; 563 return true; 564 // right/w -> right 565 } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT 566 || keyCode == KeyEvent.KEYCODE_W) { 567 mRotating = 1; 568 return true; 569 // up -> pause 570 } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { 571 pause(); 572 return true; 573 } 574 } 575 576 return false; 577 } 578 } 579 580 /** 581 * Handles a key-up event. 582 * 583 * @param keyCode the key that was pressed 584 * @param msg the original event object 585 * @return true if the key was handled and consumed, or else false 586 */ 587 boolean doKeyUp(int keyCode, KeyEvent msg) { 588 boolean handled = false; 589 590 synchronized (mSurfaceHolder) { 591 if (mMode == STATE_RUNNING) { 592 if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER 593 || keyCode == KeyEvent.KEYCODE_SPACE) { 594 setFiring(false); 595 handled = true; 596 } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT 597 || keyCode == KeyEvent.KEYCODE_Q 598 || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT 599 || keyCode == KeyEvent.KEYCODE_W) { 600 mRotating = 0; 601 handled = true; 602 } 603 } 604 } 605 606 return handled; 607 } 608 609 /** 610 * Draws the ship, fuel/speed bars, and background to the provided 611 * Canvas. 612 */ 613 private void doDraw(Canvas canvas) { 614 // Draw the background image. Operations on the Canvas accumulate 615 // so this is like clearing the screen. 616 canvas.drawBitmap(mBackgroundImage, 0, 0, null); 617 618 int yTop = mCanvasHeight - ((int) mY + mLanderHeight / 2); 619 int xLeft = (int) mX - mLanderWidth / 2; 620 621 // Draw the fuel gauge 622 int fuelWidth = (int) (UI_BAR * mFuel / PHYS_FUEL_MAX); 623 mScratchRect.set(4, 4, 4 + fuelWidth, 4 + UI_BAR_HEIGHT); 624 canvas.drawRect(mScratchRect, mLinePaint); 625 626 // Draw the speed gauge, with a two-tone effect 627 double speed = Math.sqrt(mDX * mDX + mDY * mDY); 628 int speedWidth = (int) (UI_BAR * speed / PHYS_SPEED_MAX); 629 630 if (speed <= mGoalSpeed) { 631 mScratchRect.set(4 + UI_BAR + 4, 4, 632 4 + UI_BAR + 4 + speedWidth, 4 + UI_BAR_HEIGHT); 633 canvas.drawRect(mScratchRect, mLinePaint); 634 } else { 635 // Draw the bad color in back, with the good color in front of 636 // it 637 mScratchRect.set(4 + UI_BAR + 4, 4, 638 4 + UI_BAR + 4 + speedWidth, 4 + UI_BAR_HEIGHT); 639 canvas.drawRect(mScratchRect, mLinePaintBad); 640 int goalWidth = (UI_BAR * mGoalSpeed / PHYS_SPEED_MAX); 641 mScratchRect.set(4 + UI_BAR + 4, 4, 4 + UI_BAR + 4 + goalWidth, 642 4 + UI_BAR_HEIGHT); 643 canvas.drawRect(mScratchRect, mLinePaint); 644 } 645 646 // Draw the landing pad 647 canvas.drawLine(mGoalX, 1 + mCanvasHeight - TARGET_PAD_HEIGHT, 648 mGoalX + mGoalWidth, 1 + mCanvasHeight - TARGET_PAD_HEIGHT, 649 mLinePaint); 650 651 652 // Draw the ship with its current rotation 653 canvas.save(); 654 canvas.rotate((float) mHeading, (float) mX, mCanvasHeight 655 - (float) mY); 656 if (mMode == STATE_LOSE) { 657 mCrashedImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop 658 + mLanderHeight); 659 mCrashedImage.draw(canvas); 660 } else if (mEngineFiring) { 661 mFiringImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop 662 + mLanderHeight); 663 mFiringImage.draw(canvas); 664 } else { 665 mLanderImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop 666 + mLanderHeight); 667 mLanderImage.draw(canvas); 668 } 669 canvas.restore(); 670 } 671 672 /** 673 * Figures the lander state (x, y, fuel, ...) based on the passage of 674 * realtime. Does not invalidate(). Called at the start of draw(). 675 * Detects the end-of-game and sets the UI to the next state. 676 */ 677 private void updatePhysics() { 678 long now = System.currentTimeMillis(); 679 680 // Do nothing if mLastTime is in the future. 681 // This allows the game-start to delay the start of the physics 682 // by 100ms or whatever. 683 if (mLastTime > now) return; 684 685 double elapsed = (now - mLastTime) / 1000.0; 686 687 // mRotating -- update heading 688 if (mRotating != 0) { 689 mHeading += mRotating * (PHYS_SLEW_SEC * elapsed); 690 691 // Bring things back into the range 0..360 692 if (mHeading < 0) 693 mHeading += 360; 694 else if (mHeading >= 360) mHeading -= 360; 695 } 696 697 // Base accelerations -- 0 for x, gravity for y 698 double ddx = 0.0; 699 double ddy = -PHYS_DOWN_ACCEL_SEC * elapsed; 700 701 if (mEngineFiring) { 702 // taking 0 as up, 90 as to the right 703 // cos(deg) is ddy component, sin(deg) is ddx component 704 double elapsedFiring = elapsed; 705 double fuelUsed = elapsedFiring * PHYS_FUEL_SEC; 706 707 // tricky case where we run out of fuel partway through the 708 // elapsed 709 if (fuelUsed > mFuel) { 710 elapsedFiring = mFuel / fuelUsed * elapsed; 711 fuelUsed = mFuel; 712 713 // Oddball case where we adjust the "control" from here 714 mEngineFiring = false; 715 } 716 717 mFuel -= fuelUsed; 718 719 // have this much acceleration from the engine 720 double accel = PHYS_FIRE_ACCEL_SEC * elapsedFiring; 721 722 double radians = 2 * Math.PI * mHeading / 360; 723 ddx = Math.sin(radians) * accel; 724 ddy += Math.cos(radians) * accel; 725 } 726 727 double dxOld = mDX; 728 double dyOld = mDY; 729 730 // figure speeds for the end of the period 731 mDX += ddx; 732 mDY += ddy; 733 734 // figure position based on average speed during the period 735 mX += elapsed * (mDX + dxOld) / 2; 736 mY += elapsed * (mDY + dyOld) / 2; 737 738 mLastTime = now; 739 740 // Evaluate if we have landed ... stop the game 741 double yLowerBound = TARGET_PAD_HEIGHT + mLanderHeight / 2 742 - TARGET_BOTTOM_PADDING; 743 if (mY <= yLowerBound) { 744 mY = yLowerBound; 745 746 int result = STATE_LOSE; 747 CharSequence message = ""; 748 Resources res = mContext.getResources(); 749 double speed = Math.sqrt(mDX * mDX + mDY * mDY); 750 boolean onGoal = (mGoalX <= mX - mLanderWidth / 2 && mX 751 + mLanderWidth / 2 <= mGoalX + mGoalWidth); 752 753 // "Hyperspace" win -- upside down, going fast, 754 // puts you back at the top. 755 if (onGoal && Math.abs(mHeading - 180) < mGoalAngle 756 && speed > PHYS_SPEED_HYPERSPACE) { 757 result = STATE_WIN; 758 mWinsInARow++; 759 doStart(); 760 761 return; 762 // Oddball case: this case does a return, all other cases 763 // fall through to setMode() below. 764 } else if (!onGoal) { 765 message = res.getText(R.string.message_off_pad); 766 } else if (!(mHeading <= mGoalAngle || mHeading >= 360 - mGoalAngle)) { 767 message = res.getText(R.string.message_bad_angle); 768 } else if (speed > mGoalSpeed) { 769 message = res.getText(R.string.message_too_fast); 770 } else { 771 result = STATE_WIN; 772 mWinsInARow++; 773 } 774 775 setState(result, message); 776 } 777 } 778 } 779 780 /** Handle to the application context, used to e.g. fetch Drawables. */ 781 private Context mContext; 782 783 /** Pointer to the text view to display "Paused.." etc. */ 784 private TextView mStatusText; 785 786 /** The thread that actually draws the animation */ 787 private LunarThread thread; 788 789 public LunarView(Context context, AttributeSet attrs) { 790 super(context, attrs); 791 792 // register our interest in hearing about changes to our surface 793 SurfaceHolder holder = getHolder(); 794 holder.addCallback(this); 795 796 // create thread only; it's started in surfaceCreated() 797 thread = new LunarThread(holder, context, new Handler() { 798 @Override 799 public void handleMessage(Message m) { 800 mStatusText.setVisibility(m.getData().getInt("viz")); 801 mStatusText.setText(m.getData().getString("text")); 802 } 803 }); 804 805 setFocusable(true); // make sure we get key events 806 } 807 808 /** 809 * Fetches the animation thread corresponding to this LunarView. 810 * 811 * @return the animation thread 812 */ 813 public LunarThread getThread() { 814 return thread; 815 } 816 817 /** 818 * Standard override to get key-press events. 819 */ 820 @Override 821 public boolean onKeyDown(int keyCode, KeyEvent msg) { 822 return thread.doKeyDown(keyCode, msg); 823 } 824 825 /** 826 * Standard override for key-up. We actually care about these, so we can 827 * turn off the engine or stop rotating. 828 */ 829 @Override 830 public boolean onKeyUp(int keyCode, KeyEvent msg) { 831 return thread.doKeyUp(keyCode, msg); 832 } 833 834 /** 835 * Standard window-focus override. Notice focus lost so we can pause on 836 * focus lost. e.g. user switches to take a call. 837 */ 838 @Override 839 public void onWindowFocusChanged(boolean hasWindowFocus) { 840 if (!hasWindowFocus) thread.pause(); 841 } 842 843 /** 844 * Installs a pointer to the text view used for messages. 845 */ 846 public void setTextView(TextView textView) { 847 mStatusText = textView; 848 } 849 850 /* Callback invoked when the surface dimensions change. */ 851 public void surfaceChanged(SurfaceHolder holder, int format, int width, 852 int height) { 853 thread.setSurfaceSize(width, height); 854 } 855 856 /* 857 * Callback invoked when the Surface has been created and is ready to be 858 * used. 859 */ 860 public void surfaceCreated(SurfaceHolder holder) { 861 // start the thread here so that we don't busy-wait in run() 862 // waiting for the surface to be created 863 thread.setRunning(true); 864 thread.start(); 865 } 866 867 /* 868 * Callback invoked when the Surface has been destroyed and must no longer 869 * be touched. WARNING: after this method returns, the Surface/Canvas must 870 * never be touched again! 871 */ 872 public void surfaceDestroyed(SurfaceHolder holder) { 873 // we have to tell thread to shut down & wait for it to finish, or else 874 // it might touch the Surface after we return and explode 875 boolean retry = true; 876 thread.setRunning(false); 877 while (retry) { 878 try { 879 thread.join(); 880 retry = false; 881 } catch (InterruptedException e) { 882 } 883 } 884 } 885 } 886