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