1 /* 2 * Copyright (C) 2009 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 18 // Android JET demonstration code: 19 // All inline comments related to the use of the JetPlayer class are preceded by "JET info:" 20 21 package com.example.android.jetboy; 22 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.graphics.Bitmap; 26 import android.graphics.BitmapFactory; 27 import android.graphics.Canvas; 28 import android.media.JetPlayer; 29 import android.media.JetPlayer.OnJetEventListener; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.os.Message; 33 import android.util.AttributeSet; 34 import android.util.Log; 35 import android.view.KeyEvent; 36 import android.view.SurfaceHolder; 37 import android.view.SurfaceView; 38 import android.view.View; 39 import android.widget.Button; 40 import android.widget.TextView; 41 42 import java.util.Random; 43 import java.util.Timer; 44 import java.util.TimerTask; 45 import java.util.Vector; 46 import java.util.concurrent.ConcurrentLinkedQueue; 47 48 public class JetBoyView extends SurfaceView implements SurfaceHolder.Callback { 49 50 // the number of asteroids that must be destroyed 51 public static final int mSuccessThreshold = 50; 52 53 // used to calculate level for mutes and trigger clip 54 public int mHitStreak = 0; 55 56 // total number asteroids you need to hit. 57 public int mHitTotal = 0; 58 59 // which music bed is currently playing? 60 public int mCurrentBed = 0; 61 62 // a lazy graphic fudge for the initial title splash 63 private Bitmap mTitleBG; 64 65 private Bitmap mTitleBG2; 66 67 /** 68 * Base class for any external event passed to the JetBoyThread. This can 69 * include user input, system events, network input, etc. 70 */ 71 class GameEvent { 72 public GameEvent() { 73 eventTime = System.currentTimeMillis(); 74 } 75 76 long eventTime; 77 } 78 79 /** 80 * A GameEvent subclass for key based user input. Values are those used by 81 * the standard onKey 82 */ 83 class KeyGameEvent extends GameEvent { 84 /** 85 * Simple constructor to make populating this event easier. 86 */ 87 public KeyGameEvent(int keyCode, boolean up, KeyEvent msg) { 88 this.keyCode = keyCode; 89 this.msg = msg; 90 this.up = up; 91 } 92 93 public int keyCode; 94 public KeyEvent msg; 95 public boolean up; 96 } 97 98 /** 99 * A GameEvent subclass for events from the JetPlayer. 100 */ 101 class JetGameEvent extends GameEvent { 102 /** 103 * Simple constructor to make populating this event easier. 104 */ 105 public JetGameEvent(JetPlayer player, short segment, byte track, byte channel, 106 byte controller, byte value) { 107 this.player = player; 108 this.segment = segment; 109 this.track = track; 110 this.channel = channel; 111 this.controller = controller; 112 this.value = value; 113 } 114 115 public JetPlayer player; 116 public short segment; 117 public byte track; 118 public byte channel; 119 public byte controller; 120 public byte value; 121 } 122 123 // JET info: the JetBoyThread receives all the events from the JET player 124 // JET info: through the OnJetEventListener interface. 125 class JetBoyThread extends Thread implements OnJetEventListener { 126 127 /** 128 * State-tracking constants. 129 */ 130 public static final int STATE_START = -1; 131 public static final int STATE_PLAY = 0; 132 public static final int STATE_LOSE = 1; 133 public static final int STATE_PAUSE = 2; 134 public static final int STATE_RUNNING = 3; 135 136 // how many frames per beat? The basic animation can be changed for 137 // instance to 3/4 by changing this to 3. 138 // untested is the impact on other parts of game logic for non 4/4 time. 139 private static final int ANIMATION_FRAMES_PER_BEAT = 4; 140 141 public boolean mInitialized = false; 142 143 /** Queue for GameEvents */ 144 protected ConcurrentLinkedQueue<GameEvent> mEventQueue = new ConcurrentLinkedQueue<GameEvent>(); 145 146 /** Context for processKey to maintain state accross frames * */ 147 protected Object mKeyContext = null; 148 149 // the timer display in seconds 150 public int mTimerLimit; 151 152 // used for internal timing logic. 153 public final int TIMER_LIMIT = 72; 154 155 // string value for timer display 156 private String mTimerValue = "1:12"; 157 158 // start, play, running, lose are the states we use 159 public int mState; 160 161 // has laser been fired and for how long? 162 // user for fx logic on laser fire 163 boolean mLaserOn = false; 164 165 long mLaserFireTime = 0; 166 167 /** The drawable to use as the far background of the animation canvas */ 168 private Bitmap mBackgroundImageFar; 169 170 /** The drawable to use as the close background of the animation canvas */ 171 private Bitmap mBackgroundImageNear; 172 173 // JET info: event IDs within the JET file. 174 // JET info: in this game 80 is used for sending asteroid across the screen 175 // JET info: 82 is used as game time for 1/4 note beat. 176 private final byte NEW_ASTEROID_EVENT = 80; 177 private final byte TIMER_EVENT = 82; 178 179 // used to track beat for synch of mute/unmute actions 180 private int mBeatCount = 1; 181 182 // our intrepid space boy 183 private Bitmap[] mShipFlying = new Bitmap[4]; 184 185 // the twinkly bit 186 private Bitmap[] mBeam = new Bitmap[4]; 187 188 // the things you are trying to hit 189 private Bitmap[] mAsteroids = new Bitmap[12]; 190 191 // hit animation 192 private Bitmap[] mExplosions = new Bitmap[4]; 193 194 private Bitmap mTimerShell; 195 196 private Bitmap mLaserShot; 197 198 // used to save the beat event system time. 199 private long mLastBeatTime; 200 201 private long mPassedTime; 202 203 // how much do we move the asteroids per beat? 204 private int mPixelMoveX = 25; 205 206 // the asteroid send events are generated from the Jet File. 207 // but which land they start in is random. 208 private Random mRandom = new Random(); 209 210 // JET info: the star of our show, a reference to the JetPlayer object. 211 private JetPlayer mJet = null; 212 213 private boolean mJetPlaying = false; 214 215 /** Message handler used by thread to interact with TextView */ 216 private Handler mHandler; 217 218 /** Handle to the surface manager object we interact with */ 219 private SurfaceHolder mSurfaceHolder; 220 221 /** Handle to the application context, used to e.g. fetch Drawables. */ 222 private Context mContext; 223 224 /** Indicate whether the surface has been created & is ready to draw */ 225 private boolean mRun = false; 226 227 // updates the screen clock. Also used for tempo timing. 228 private Timer mTimer = null; 229 230 private TimerTask mTimerTask = null; 231 232 // one second - used to update timer 233 private int mTaskIntervalInMillis = 1000; 234 235 /** 236 * Current height of the surface/canvas. 237 * 238 * @see #setSurfaceSize 239 */ 240 private int mCanvasHeight = 1; 241 242 /** 243 * Current width of the surface/canvas. 244 * 245 * @see #setSurfaceSize 246 */ 247 private int mCanvasWidth = 1; 248 249 // used to track the picture to draw for ship animation 250 private int mShipIndex = 0; 251 252 // stores all of the asteroid objects in order 253 private Vector<Asteroid> mDangerWillRobinson; 254 255 private Vector<Explosion> mExplosion; 256 257 // right to left scroll tracker for near and far BG 258 private int mBGFarMoveX = 0; 259 private int mBGNearMoveX = 0; 260 261 // how far up (close to top) jet boy can fly 262 private int mJetBoyYMin = 40; 263 private int mJetBoyX = 0; 264 private int mJetBoyY = 0; 265 266 // this is the pixel position of the laser beam guide. 267 private int mAsteroidMoveLimitX = 110; 268 269 // how far up asteroid can be painted 270 private int mAsteroidMinY = 40; 271 272 273 Resources mRes; 274 275 // array to store the mute masks that are applied during game play to respond to 276 // the player's hit streaks 277 private boolean muteMask[][] = new boolean[9][32]; 278 279 /** 280 * This is the constructor for the main worker bee 281 * 282 * @param surfaceHolder 283 * @param context 284 * @param handler 285 */ 286 public JetBoyThread(SurfaceHolder surfaceHolder, Context context, Handler handler) { 287 288 mSurfaceHolder = surfaceHolder; 289 mHandler = handler; 290 mContext = context; 291 mRes = context.getResources(); 292 293 // JET info: this are the mute arrays associated with the music beds in the 294 // JET info: JET file 295 for (int ii = 0; ii < 8; ii++) { 296 for (int xx = 0; xx < 32; xx++) { 297 muteMask[ii][xx] = true; 298 } 299 } 300 301 muteMask[0][2] = false; 302 muteMask[0][3] = false; 303 muteMask[0][4] = false; 304 muteMask[0][5] = false; 305 306 muteMask[1][2] = false; 307 muteMask[1][3] = false; 308 muteMask[1][4] = false; 309 muteMask[1][5] = false; 310 muteMask[1][8] = false; 311 muteMask[1][9] = false; 312 313 muteMask[2][2] = false; 314 muteMask[2][3] = false; 315 muteMask[2][6] = false; 316 muteMask[2][7] = false; 317 muteMask[2][8] = false; 318 muteMask[2][9] = false; 319 320 muteMask[3][2] = false; 321 muteMask[3][3] = false; 322 muteMask[3][6] = false; 323 muteMask[3][11] = false; 324 muteMask[3][12] = false; 325 326 muteMask[4][2] = false; 327 muteMask[4][3] = false; 328 muteMask[4][10] = false; 329 muteMask[4][11] = false; 330 muteMask[4][12] = false; 331 muteMask[4][13] = false; 332 333 muteMask[5][2] = false; 334 muteMask[5][3] = false; 335 muteMask[5][10] = false; 336 muteMask[5][12] = false; 337 muteMask[5][15] = false; 338 muteMask[5][17] = false; 339 340 muteMask[6][2] = false; 341 muteMask[6][3] = false; 342 muteMask[6][14] = false; 343 muteMask[6][15] = false; 344 muteMask[6][16] = false; 345 muteMask[6][17] = false; 346 347 muteMask[7][2] = false; 348 muteMask[7][3] = false; 349 muteMask[7][6] = false; 350 muteMask[7][14] = false; 351 muteMask[7][15] = false; 352 muteMask[7][16] = false; 353 muteMask[7][17] = false; 354 muteMask[7][18] = false; 355 356 // set all tracks to play 357 for (int xx = 0; xx < 32; xx++) { 358 muteMask[8][xx] = false; 359 } 360 361 // always set state to start, ensure we come in from front door if 362 // app gets tucked into background 363 mState = STATE_START; 364 365 setInitialGameState(); 366 367 mTitleBG = BitmapFactory.decodeResource(mRes, R.drawable.title_hori); 368 369 // load background image as a Bitmap instead of a Drawable b/c 370 // we don't need to transform it and it's faster to draw this 371 // way...thanks lunar lander :) 372 373 // two background since we want them moving at different speeds 374 mBackgroundImageFar = BitmapFactory.decodeResource(mRes, R.drawable.background_a); 375 376 mLaserShot = BitmapFactory.decodeResource(mRes, R.drawable.laser); 377 378 mBackgroundImageNear = BitmapFactory.decodeResource(mRes, R.drawable.background_b); 379 380 mShipFlying[0] = BitmapFactory.decodeResource(mRes, R.drawable.ship2_1); 381 mShipFlying[1] = BitmapFactory.decodeResource(mRes, R.drawable.ship2_2); 382 mShipFlying[2] = BitmapFactory.decodeResource(mRes, R.drawable.ship2_3); 383 mShipFlying[3] = BitmapFactory.decodeResource(mRes, R.drawable.ship2_4); 384 385 mBeam[0] = BitmapFactory.decodeResource(mRes, R.drawable.intbeam_1); 386 mBeam[1] = BitmapFactory.decodeResource(mRes, R.drawable.intbeam_2); 387 mBeam[2] = BitmapFactory.decodeResource(mRes, R.drawable.intbeam_3); 388 mBeam[3] = BitmapFactory.decodeResource(mRes, R.drawable.intbeam_4); 389 390 mTimerShell = BitmapFactory.decodeResource(mRes, R.drawable.int_timer); 391 392 // I wanted them to rotate in a certain way 393 // so I loaded them backwards from the way created. 394 mAsteroids[11] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid01); 395 mAsteroids[10] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid02); 396 mAsteroids[9] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid03); 397 mAsteroids[8] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid04); 398 mAsteroids[7] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid05); 399 mAsteroids[6] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid06); 400 mAsteroids[5] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid07); 401 mAsteroids[4] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid08); 402 mAsteroids[3] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid09); 403 mAsteroids[2] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid10); 404 mAsteroids[1] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid11); 405 mAsteroids[0] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid12); 406 407 mExplosions[0] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid_explode1); 408 mExplosions[1] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid_explode2); 409 mExplosions[2] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid_explode3); 410 mExplosions[3] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid_explode4); 411 412 } 413 414 /** 415 * Does the grunt work of setting up initial jet requirements 416 */ 417 private void initializeJetPlayer() { 418 419 // JET info: let's create our JetPlayer instance using the factory. 420 // JET info: if we already had one, the same singleton is returned. 421 mJet = JetPlayer.getJetPlayer(); 422 423 mJetPlaying = false; 424 425 // JET info: make sure we flush the queue, 426 // JET info: otherwise left over events from previous gameplay can hang around. 427 // JET info: ok, here we don't really need that but if you ever reuse a JetPlayer 428 // JET info: instance, clear the queue before reusing it, this will also clear any 429 // JET info: trigger clips that have been triggered but not played yet. 430 mJet.clearQueue(); 431 432 // JET info: we are going to receive in this example all the JET callbacks 433 // JET info: inthis animation thread object. 434 mJet.setEventListener(this); 435 436 Log.d(TAG, "opening jet file"); 437 438 // JET info: load the actual JET content the game will be playing, 439 // JET info: it's stored as a raw resource in our APK, and is labeled "level1" 440 mJet.loadJetFile(mContext.getResources().openRawResourceFd(R.raw.level1)); 441 // JET info: if our JET file was stored on the sdcard for instance, we would have used 442 // JET info: mJet.loadJetFile("/sdcard/level1.jet"); 443 444 Log.d(TAG, "opening jet file DONE"); 445 446 mCurrentBed = 0; 447 byte sSegmentID = 0; 448 449 Log.d(TAG, " start queuing jet file"); 450 451 // JET info: now we're all set to prepare queuing the JET audio segments for the game. 452 // JET info: in this example, the game uses segment 0 for the duration of the game play, 453 // JET info: and plays segment 1 several times as the "outro" music, so we're going to 454 // JET info: queue everything upfront, but with more complex JET compositions, we could 455 // JET info: also queue the segments during the game play. 456 457 // JET info: this is the main game play music 458 // JET info: it is located at segment 0 459 // JET info: it uses the first DLS lib in the .jet resource, which is at index 0 460 // JET info: index -1 means no DLS 461 mJet.queueJetSegment(0, 0, 0, 0, 0, sSegmentID); 462 463 // JET info: end game music, loop 4 times normal pitch 464 mJet.queueJetSegment(1, 0, 4, 0, 0, sSegmentID); 465 466 // JET info: end game music loop 4 times up an octave 467 mJet.queueJetSegment(1, 0, 4, 1, 0, sSegmentID); 468 469 // JET info: set the mute mask as designed for the beginning of the game, when the 470 // JET info: the player hasn't scored yet. 471 mJet.setMuteArray(muteMask[0], true); 472 473 Log.d(TAG, " start queuing jet file DONE"); 474 475 } 476 477 478 private void doDraw(Canvas canvas) { 479 480 if (mState == STATE_RUNNING) { 481 doDrawRunning(canvas); 482 } else if (mState == STATE_START) { 483 doDrawReady(canvas); 484 } else if (mState == STATE_PLAY || mState == STATE_LOSE) { 485 if (mTitleBG2 == null) { 486 mTitleBG2 = BitmapFactory.decodeResource(mRes, R.drawable.title_bg_hori); 487 } 488 doDrawPlay(canvas); 489 }// end state play block 490 } 491 492 493 /** 494 * Draws current state of the game Canvas. 495 */ 496 private void doDrawRunning(Canvas canvas) { 497 498 // decrement the far background 499 mBGFarMoveX = mBGFarMoveX - 1; 500 501 // decrement the near background 502 mBGNearMoveX = mBGNearMoveX - 4; 503 504 // calculate the wrap factor for matching image draw 505 int newFarX = mBackgroundImageFar.getWidth() - (-mBGFarMoveX); 506 507 // if we have scrolled all the way, reset to start 508 if (newFarX <= 0) { 509 mBGFarMoveX = 0; 510 // only need one draw 511 canvas.drawBitmap(mBackgroundImageFar, mBGFarMoveX, 0, null); 512 513 } else { 514 // need to draw original and wrap 515 canvas.drawBitmap(mBackgroundImageFar, mBGFarMoveX, 0, null); 516 canvas.drawBitmap(mBackgroundImageFar, newFarX, 0, null); 517 } 518 519 // same story different image... 520 // TODO possible method call 521 int newNearX = mBackgroundImageNear.getWidth() - (-mBGNearMoveX); 522 523 if (newNearX <= 0) { 524 mBGNearMoveX = 0; 525 canvas.drawBitmap(mBackgroundImageNear, mBGNearMoveX, 0, null); 526 527 } else { 528 canvas.drawBitmap(mBackgroundImageNear, mBGNearMoveX, 0, null); 529 canvas.drawBitmap(mBackgroundImageNear, newNearX, 0, null); 530 } 531 532 doAsteroidAnimation(canvas); 533 534 canvas.drawBitmap(mBeam[mShipIndex], 51 + 20, 0, null); 535 536 mShipIndex++; 537 538 if (mShipIndex == 4) 539 mShipIndex = 0; 540 541 // draw the space ship in the same lane as the next asteroid 542 canvas.drawBitmap(mShipFlying[mShipIndex], mJetBoyX, mJetBoyY, null); 543 544 if (mLaserOn) { 545 canvas.drawBitmap(mLaserShot, mJetBoyX + mShipFlying[0].getWidth(), mJetBoyY 546 + (mShipFlying[0].getHeight() / 2), null); 547 } 548 549 // tick tock 550 canvas.drawBitmap(mTimerShell, mCanvasWidth - mTimerShell.getWidth(), 0, null); 551 552 } 553 554 private void setInitialGameState() { 555 mTimerLimit = TIMER_LIMIT; 556 557 mJetBoyY = mJetBoyYMin; 558 559 // set up jet stuff 560 initializeJetPlayer(); 561 562 mTimer = new Timer(); 563 564 mDangerWillRobinson = new Vector<Asteroid>(); 565 566 mExplosion = new Vector<Explosion>(); 567 568 mInitialized = true; 569 570 mHitStreak = 0; 571 mHitTotal = 0; 572 } 573 574 private void doAsteroidAnimation(Canvas canvas) { 575 if ((mDangerWillRobinson == null | mDangerWillRobinson.size() == 0) 576 && (mExplosion != null && mExplosion.size() == 0)) 577 return; 578 579 // Compute what percentage through a beat we are and adjust 580 // animation and position based on that. This assumes 140bpm(428ms/beat). 581 // This is just inter-beat interpolation, no game state is updated 582 long frameDelta = System.currentTimeMillis() - mLastBeatTime; 583 584 int animOffset = (int)(ANIMATION_FRAMES_PER_BEAT * frameDelta / 428); 585 586 for (int i = (mDangerWillRobinson.size() - 1); i >= 0; i--) { 587 Asteroid asteroid = mDangerWillRobinson.elementAt(i); 588 589 if (!asteroid.mMissed) 590 mJetBoyY = asteroid.mDrawY; 591 592 // Log.d(TAG, " drawing asteroid " + ii + " at " + 593 // asteroid.mDrawX ); 594 595 canvas.drawBitmap( 596 mAsteroids[(asteroid.mAniIndex + animOffset) % mAsteroids.length], 597 asteroid.mDrawX, asteroid.mDrawY, null); 598 } 599 600 for (int i = (mExplosion.size() - 1); i >= 0; i--) { 601 Explosion ex = mExplosion.elementAt(i); 602 603 canvas.drawBitmap(mExplosions[(ex.mAniIndex + animOffset) % mExplosions.length], 604 ex.mDrawX, ex.mDrawY, null); 605 } 606 } 607 608 private void doDrawReady(Canvas canvas) { 609 canvas.drawBitmap(mTitleBG, 0, 0, null); 610 } 611 612 private void doDrawPlay(Canvas canvas) { 613 canvas.drawBitmap(mTitleBG2, 0, 0, null); 614 } 615 616 617 /** 618 * the heart of the worker bee 619 */ 620 public void run() { 621 // while running do stuff in this loop...bzzz! 622 while (mRun) { 623 Canvas c = null; 624 625 if (mState == STATE_RUNNING) { 626 // Process any input and apply it to the game state 627 updateGameState(); 628 629 if (!mJetPlaying) { 630 631 mInitialized = false; 632 Log.d(TAG, "------> STARTING JET PLAY"); 633 mJet.play(); 634 635 mJetPlaying = true; 636 637 } 638 639 mPassedTime = System.currentTimeMillis(); 640 641 // kick off the timer task for counter update if not already 642 // initialized 643 if (mTimerTask == null) { 644 mTimerTask = new TimerTask() { 645 public void run() { 646 doCountDown(); 647 } 648 }; 649 650 mTimer.schedule(mTimerTask, mTaskIntervalInMillis); 651 652 }// end of TimerTask init block 653 654 }// end of STATE_RUNNING block 655 else if (mState == STATE_PLAY && !mInitialized) 656 { 657 setInitialGameState(); 658 } else if (mState == STATE_LOSE) { 659 mInitialized = false; 660 } 661 662 try { 663 c = mSurfaceHolder.lockCanvas(null); 664 // synchronized (mSurfaceHolder) { 665 doDraw(c); 666 // } 667 } finally { 668 // do this in a finally so that if an exception is thrown 669 // during the above, we don't leave the Surface in an 670 // inconsistent state 671 if (c != null) { 672 mSurfaceHolder.unlockCanvasAndPost(c); 673 } 674 }// end finally block 675 }// end while mrun block 676 } 677 678 679 /** 680 * This method handles updating the model of the game state. No 681 * rendering is done here only processing of inputs and update of state. 682 * This includes positons of all game objects (asteroids, player, 683 * explosions), their state (animation frame, hit), creation of new 684 * objects, etc. 685 */ 686 protected void updateGameState() { 687 // Process any game events and apply them 688 while (true) { 689 GameEvent event = mEventQueue.poll(); 690 if (event == null) 691 break; 692 693 // Log.d(TAG,"*** EVENT = " + event); 694 695 // Process keys tracking the input context to pass in to later 696 // calls 697 if (event instanceof KeyGameEvent) { 698 // Process the key for affects other then asteroid hits 699 mKeyContext = processKeyEvent((KeyGameEvent)event, mKeyContext); 700 701 // Update laser state. Having this here allows the laser to 702 // be triggered right when the key is 703 // pressed. If we comment this out the laser will only be 704 // turned on when updateLaser is called 705 // when processing a timer event below. 706 updateLaser(mKeyContext); 707 708 } 709 // JET events trigger a state update 710 else if (event instanceof JetGameEvent) { 711 JetGameEvent jetEvent = (JetGameEvent)event; 712 713 // Only update state on a timer event 714 if (jetEvent.value == TIMER_EVENT) { 715 // Note the time of the last beat 716 mLastBeatTime = System.currentTimeMillis(); 717 718 // Update laser state, turning it on if a key has been 719 // pressed or off if it has been 720 // on for too long. 721 updateLaser(mKeyContext); 722 723 // Update explosions before we update asteroids because 724 // updateAsteroids may add 725 // new explosions that we do not want updated until next 726 // frame 727 updateExplosions(mKeyContext); 728 729 // Update asteroid positions, hit status and animations 730 updateAsteroids(mKeyContext); 731 } 732 733 processJetEvent(jetEvent.player, jetEvent.segment, jetEvent.track, 734 jetEvent.channel, jetEvent.controller, jetEvent.value); 735 } 736 } 737 } 738 739 740 /** 741 * This method handles the state updates that can be caused by key press 742 * events. Key events may mean different things depending on what has 743 * come before, to support this concept this method takes an opaque 744 * context object as a parameter and returns an updated version. This 745 * context should be set to null for the first event then should be set 746 * to the last value returned for subsequent events. 747 */ 748 protected Object processKeyEvent(KeyGameEvent event, Object context) { 749 // Log.d(TAG, "key code is " + event.keyCode + " " + (event.up ? 750 // "up":"down")); 751 752 // If it is a key up on the fire key make sure we mute the 753 // associated sound 754 if (event.up) { 755 if (event.keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { 756 return null; 757 } 758 } 759 // If it is a key down on the fire key start playing the sound and 760 // update the context 761 // to indicate that a key has been pressed and to ignore further 762 // presses 763 else { 764 if (event.keyCode == KeyEvent.KEYCODE_DPAD_CENTER && (context == null)) { 765 return event; 766 } 767 } 768 769 // Return the context unchanged 770 return context; 771 } 772 773 774 /** 775 * This method updates the laser status based on user input and shot 776 * duration 777 */ 778 protected void updateLaser(Object inputContext) { 779 // Lookup the time of the fire event if there is one 780 long keyTime = inputContext == null ? 0 : ((GameEvent)inputContext).eventTime; 781 782 // Log.d(TAG,"keyTime delta = " + 783 // (System.currentTimeMillis()-keyTime) + ": obj = " + 784 // inputContext); 785 786 // If the laser has been on too long shut it down 787 if (mLaserOn && System.currentTimeMillis() - mLaserFireTime > 400) { 788 mLaserOn = false; 789 } 790 791 // trying to tune the laser hit timing 792 else if (System.currentTimeMillis() - mLaserFireTime > 300) { 793 // JET info: the laser sound is on track 23, we mute it (true) right away (false) 794 mJet.setMuteFlag(23, true, false); 795 796 } 797 798 // Now check to see if we should turn the laser on. We do this after 799 // the above shutdown 800 // logic so it can be turned back on in the same frame it was turned 801 // off in. If we want 802 // to add a cooldown period this may change. 803 if (!mLaserOn && System.currentTimeMillis() - keyTime <= 400) { 804 805 mLaserOn = true; 806 mLaserFireTime = keyTime; 807 808 // JET info: unmute the laser track (false) right away (false) 809 mJet.setMuteFlag(23, false, false); 810 } 811 } 812 813 /** 814 * Update asteroid state including position and laser hit status. 815 */ 816 protected void updateAsteroids(Object inputContext) { 817 if (mDangerWillRobinson == null | mDangerWillRobinson.size() == 0) 818 return; 819 820 for (int i = (mDangerWillRobinson.size() - 1); i >= 0; i--) { 821 Asteroid asteroid = mDangerWillRobinson.elementAt(i); 822 823 // If the asteroid is within laser range but not already missed 824 // check if the key was pressed close enough to the beat to make a hit 825 if (asteroid.mDrawX <= mAsteroidMoveLimitX + 20 && !asteroid.mMissed) 826 { 827 // If the laser was fired on the beat destroy the asteroid 828 if (mLaserOn) { 829 // Track hit streak for adjusting music 830 mHitStreak++; 831 mHitTotal++; 832 833 // replace the asteroid with an explosion 834 Explosion ex = new Explosion(); 835 ex.mAniIndex = 0; 836 ex.mDrawX = asteroid.mDrawX; 837 ex.mDrawY = asteroid.mDrawY; 838 mExplosion.add(ex); 839 840 mJet.setMuteFlag(24, false, false); 841 842 mDangerWillRobinson.removeElementAt(i); 843 844 // This asteroid has been removed process the next one 845 continue; 846 } else { 847 // Sorry, timing was not good enough, mark the asteroid 848 // as missed so on next frame it cannot be hit even if it is still 849 // within range 850 asteroid.mMissed = true; 851 852 mHitStreak = mHitStreak - 1; 853 854 if (mHitStreak < 0) 855 mHitStreak = 0; 856 857 } 858 } 859 860 // Update the asteroids position, even missed ones keep moving 861 asteroid.mDrawX -= mPixelMoveX; 862 863 // Update asteroid animation frame 864 asteroid.mAniIndex = (asteroid.mAniIndex + ANIMATION_FRAMES_PER_BEAT) 865 % mAsteroids.length; 866 867 // if we have scrolled off the screen 868 if (asteroid.mDrawX < 0) { 869 mDangerWillRobinson.removeElementAt(i); 870 } 871 } 872 } 873 874 /** 875 * This method updates explosion animation and removes them once they 876 * have completed. 877 */ 878 protected void updateExplosions(Object inputContext) { 879 if (mExplosion == null | mExplosion.size() == 0) 880 return; 881 882 for (int i = mExplosion.size() - 1; i >= 0; i--) { 883 Explosion ex = mExplosion.elementAt(i); 884 885 ex.mAniIndex += ANIMATION_FRAMES_PER_BEAT; 886 887 // When the animation completes remove the explosion 888 if (ex.mAniIndex > 3) { 889 mJet.setMuteFlag(24, true, false); 890 mJet.setMuteFlag(23, true, false); 891 892 mExplosion.removeElementAt(i); 893 } 894 } 895 } 896 897 /** 898 * This method handles the state updates that can be caused by JET 899 * events. 900 */ 901 protected void processJetEvent(JetPlayer player, short segment, byte track, byte channel, 902 byte controller, byte value) { 903 904 //Log.d(TAG, "onJetEvent(): seg=" + segment + " track=" + track + " chan=" + channel 905 // + " cntrlr=" + controller + " val=" + value); 906 907 908 // Check for an event that triggers a new asteroid 909 if (value == NEW_ASTEROID_EVENT) { 910 doAsteroidCreation(); 911 } 912 913 mBeatCount++; 914 915 if (mBeatCount > 4) { 916 mBeatCount = 1; 917 918 } 919 920 // Scale the music based on progress 921 922 // it was a game requirement to change the mute array on 1st beat of 923 // the next measure when needed 924 // and so we track beat count, after that we track hitStreak to 925 // determine the music "intensity" 926 // if the intensity has go gone up, call a corresponding trigger clip, otherwise just 927 // execute the rest of the music bed change logic. 928 if (mBeatCount == 1) { 929 930 // do it back wards so you fall into the correct one 931 if (mHitStreak > 28) { 932 933 // did the bed change? 934 if (mCurrentBed != 7) { 935 // did it go up? 936 if (mCurrentBed < 7) { 937 mJet.triggerClip(7); 938 } 939 940 mCurrentBed = 7; 941 // JET info: change the mute mask to update the way the music plays based 942 // JET info: on the player's skills. 943 mJet.setMuteArray(muteMask[7], false); 944 945 } 946 } else if (mHitStreak > 24) { 947 if (mCurrentBed != 6) { 948 if (mCurrentBed < 6) { 949 // JET info: quite a few asteroids hit, trigger the clip with the guy's 950 // JET info: voice that encourages the player. 951 mJet.triggerClip(6); 952 } 953 954 mCurrentBed = 6; 955 mJet.setMuteArray(muteMask[6], false); 956 } 957 } else if (mHitStreak > 20) { 958 if (mCurrentBed != 5) { 959 if (mCurrentBed < 5) { 960 mJet.triggerClip(5); 961 } 962 963 mCurrentBed = 5; 964 mJet.setMuteArray(muteMask[5], false); 965 } 966 } else if (mHitStreak > 16) { 967 if (mCurrentBed != 4) { 968 969 if (mCurrentBed < 4) { 970 mJet.triggerClip(4); 971 } 972 mCurrentBed = 4; 973 mJet.setMuteArray(muteMask[4], false); 974 } 975 } else if (mHitStreak > 12) { 976 if (mCurrentBed != 3) { 977 if (mCurrentBed < 3) { 978 mJet.triggerClip(3); 979 } 980 mCurrentBed = 3; 981 mJet.setMuteArray(muteMask[3], false); 982 } 983 } else if (mHitStreak > 8) { 984 if (mCurrentBed != 2) { 985 if (mCurrentBed < 2) { 986 mJet.triggerClip(2); 987 } 988 989 mCurrentBed = 2; 990 mJet.setMuteArray(muteMask[2], false); 991 } 992 } else if (mHitStreak > 4) { 993 if (mCurrentBed != 1) { 994 995 if (mCurrentBed < 1) { 996 mJet.triggerClip(1); 997 } 998 999 mJet.setMuteArray(muteMask[1], false); 1000 1001 mCurrentBed = 1; 1002 } 1003 } 1004 } 1005 } 1006 1007 1008 private void doAsteroidCreation() { 1009 // Log.d(TAG, "asteroid created"); 1010 1011 Asteroid _as = new Asteroid(); 1012 1013 int drawIndex = mRandom.nextInt(4); 1014 1015 // TODO Remove hard coded value 1016 _as.mDrawY = mAsteroidMinY + (drawIndex * 63); 1017 1018 _as.mDrawX = (mCanvasWidth - mAsteroids[0].getWidth()); 1019 1020 _as.mStartTime = System.currentTimeMillis(); 1021 1022 mDangerWillRobinson.add(_as); 1023 } 1024 1025 1026 /** 1027 * Used to signal the thread whether it should be running or not. 1028 * Passing true allows the thread to run; passing false will shut it 1029 * down if it's already running. Calling start() after this was most 1030 * recently called with false will result in an immediate shutdown. 1031 * 1032 * @param b true to run, false to shut down 1033 */ 1034 public void setRunning(boolean b) { 1035 mRun = b; 1036 1037 if (mRun == false) { 1038 if (mTimerTask != null) 1039 mTimerTask.cancel(); 1040 } 1041 } 1042 1043 1044 /** 1045 * returns the current int value of game state as defined by state 1046 * tracking constants 1047 * 1048 * @return 1049 */ 1050 public int getGameState() { 1051 synchronized (mSurfaceHolder) { 1052 return mState; 1053 } 1054 } 1055 1056 1057 /** 1058 * Sets the game mode. That is, whether we are running, paused, in the 1059 * failure state, in the victory state, etc. 1060 * 1061 * @see #setState(int, CharSequence) 1062 * @param mode one of the STATE_* constants 1063 */ 1064 public void setGameState(int mode) { 1065 synchronized (mSurfaceHolder) { 1066 setGameState(mode, null); 1067 } 1068 } 1069 1070 1071 /** 1072 * Sets state based on input, optionally also passing in a text message. 1073 * 1074 * @param state 1075 * @param message 1076 */ 1077 public void setGameState(int state, CharSequence message) { 1078 1079 synchronized (mSurfaceHolder) { 1080 1081 // change state if needed 1082 if (mState != state) { 1083 mState = state; 1084 } 1085 1086 if (mState == STATE_PLAY) { 1087 Resources res = mContext.getResources(); 1088 mBackgroundImageFar = BitmapFactory 1089 .decodeResource(res, R.drawable.background_a); 1090 1091 // don't forget to resize the background image 1092 mBackgroundImageFar = Bitmap.createScaledBitmap(mBackgroundImageFar, 1093 mCanvasWidth * 2, mCanvasHeight, true); 1094 1095 mBackgroundImageNear = BitmapFactory.decodeResource(res, 1096 R.drawable.background_b); 1097 1098 // don't forget to resize the background image 1099 mBackgroundImageNear = Bitmap.createScaledBitmap(mBackgroundImageNear, 1100 mCanvasWidth * 2, mCanvasHeight, true); 1101 1102 } else if (mState == STATE_RUNNING) { 1103 // When we enter the running state we should clear any old 1104 // events in the queue 1105 mEventQueue.clear(); 1106 1107 // And reset the key state so we don't think a button is pressed when it isn't 1108 mKeyContext = null; 1109 } 1110 1111 } 1112 } 1113 1114 1115 /** 1116 * Add key press input to the GameEvent queue 1117 */ 1118 public boolean doKeyDown(int keyCode, KeyEvent msg) { 1119 mEventQueue.add(new KeyGameEvent(keyCode, false, msg)); 1120 1121 return true; 1122 } 1123 1124 1125 /** 1126 * Add key press input to the GameEvent queue 1127 */ 1128 public boolean doKeyUp(int keyCode, KeyEvent msg) { 1129 mEventQueue.add(new KeyGameEvent(keyCode, true, msg)); 1130 1131 return true; 1132 } 1133 1134 1135 /* Callback invoked when the surface dimensions change. */ 1136 public void setSurfaceSize(int width, int height) { 1137 // synchronized to make sure these all change atomically 1138 synchronized (mSurfaceHolder) { 1139 mCanvasWidth = width; 1140 mCanvasHeight = height; 1141 1142 // don't forget to resize the background image 1143 mBackgroundImageFar = Bitmap.createScaledBitmap(mBackgroundImageFar, width * 2, 1144 height, true); 1145 1146 // don't forget to resize the background image 1147 mBackgroundImageNear = Bitmap.createScaledBitmap(mBackgroundImageNear, width * 2, 1148 height, true); 1149 } 1150 } 1151 1152 1153 /** 1154 * Pauses the physics update & animation. 1155 */ 1156 public void pause() { 1157 synchronized (mSurfaceHolder) { 1158 if (mState == STATE_RUNNING) 1159 setGameState(STATE_PAUSE); 1160 if (mTimerTask != null) { 1161 mTimerTask.cancel(); 1162 } 1163 1164 if (mJet != null) { 1165 mJet.pause(); 1166 } 1167 } 1168 } 1169 1170 1171 /** 1172 * Does the work of updating timer 1173 * 1174 */ 1175 private void doCountDown() { 1176 //Log.d(TAG,"Time left is " + mTimerLimit); 1177 1178 mTimerLimit = mTimerLimit - 1; 1179 try { 1180 //subtract one minute and see what the result is. 1181 int moreThanMinute = mTimerLimit - 60; 1182 1183 if (moreThanMinute >= 0) { 1184 1185 if (moreThanMinute > 9) { 1186 mTimerValue = "1:" + moreThanMinute; 1187 1188 } 1189 //need an extra '0' for formatting 1190 else { 1191 mTimerValue = "1:0" + moreThanMinute; 1192 } 1193 } else { 1194 if (mTimerLimit > 9) { 1195 mTimerValue = "0:" + mTimerLimit; 1196 } else { 1197 mTimerValue = "0:0" + mTimerLimit; 1198 } 1199 } 1200 } catch (Exception e1) { 1201 Log.e(TAG, "doCountDown threw " + e1.toString()); 1202 } 1203 1204 Message msg = mHandler.obtainMessage(); 1205 1206 Bundle b = new Bundle(); 1207 b.putString("text", mTimerValue); 1208 1209 //time's up 1210 if (mTimerLimit == 0) { 1211 b.putString("STATE_LOSE", "" + STATE_LOSE); 1212 mTimerTask = null; 1213 1214 mState = STATE_LOSE; 1215 1216 } else { 1217 1218 mTimerTask = new TimerTask() { 1219 public void run() { 1220 doCountDown(); 1221 } 1222 }; 1223 1224 mTimer.schedule(mTimerTask, mTaskIntervalInMillis); 1225 } 1226 1227 //this is how we send data back up to the main JetBoyView thread. 1228 //if you look in constructor of JetBoyView you will see code for 1229 //Handling of messages. This is borrowed directly from lunar lander. 1230 //Thanks again! 1231 msg.setData(b); 1232 mHandler.sendMessage(msg); 1233 1234 } 1235 1236 1237 // JET info: JET event listener interface implementation: 1238 /** 1239 * required OnJetEventListener method. Notifications for queue updates 1240 * 1241 * @param player 1242 * @param nbSegments 1243 */ 1244 public void onJetNumQueuedSegmentUpdate(JetPlayer player, int nbSegments) { 1245 //Log.i(TAG, "onJetNumQueuedUpdate(): nbSegs =" + nbSegments); 1246 1247 } 1248 1249 1250 // JET info: JET event listener interface implementation: 1251 /** 1252 * The method which receives notification from event listener. 1253 * This is where we queue up events 80 and 82. 1254 * 1255 * Most of this data passed is unneeded for JetBoy logic but shown 1256 * for code sample completeness. 1257 * 1258 * @param player 1259 * @param segment 1260 * @param track 1261 * @param channel 1262 * @param controller 1263 * @param value 1264 */ 1265 public void onJetEvent(JetPlayer player, short segment, byte track, byte channel, 1266 byte controller, byte value) { 1267 1268 //Log.d(TAG, "jet got event " + value); 1269 1270 //events fire outside the animation thread. This can cause timing issues. 1271 //put in queue for processing by animation thread. 1272 mEventQueue.add(new JetGameEvent(player, segment, track, channel, controller, value)); 1273 } 1274 1275 1276 // JET info: JET event listener interface implementation: 1277 public void onJetPauseUpdate(JetPlayer player, int paused) { 1278 //Log.i(TAG, "onJetPauseUpdate(): paused =" + paused); 1279 1280 } 1281 1282 // JET info: JET event listener interface implementation: 1283 public void onJetUserIdUpdate(JetPlayer player, int userId, int repeatCount) { 1284 //Log.i(TAG, "onJetUserIdUpdate(): userId =" + userId + " repeatCount=" + repeatCount); 1285 1286 } 1287 1288 }//end thread class 1289 1290 public static final String TAG = "JetBoy"; 1291 1292 /** The thread that actually draws the animation */ 1293 private JetBoyThread thread; 1294 1295 private TextView mTimerView; 1296 1297 private Button mButtonRetry; 1298 1299 // private Button mButtonRestart; 1300 private TextView mTextView; 1301 1302 /** 1303 * The constructor called from the main JetBoy activity 1304 * 1305 * @param context 1306 * @param attrs 1307 */ 1308 public JetBoyView(Context context, AttributeSet attrs) { 1309 super(context, attrs); 1310 1311 // register our interest in hearing about changes to our surface 1312 SurfaceHolder holder = getHolder(); 1313 holder.addCallback(this); 1314 1315 // create thread only; it's started in surfaceCreated() 1316 // except if used in the layout editor. 1317 if (isInEditMode() == false) { 1318 thread = new JetBoyThread(holder, context, new Handler() { 1319 1320 public void handleMessage(Message m) { 1321 1322 mTimerView.setText(m.getData().getString("text")); 1323 1324 if (m.getData().getString("STATE_LOSE") != null) { 1325 //mButtonRestart.setVisibility(View.VISIBLE); 1326 mButtonRetry.setVisibility(View.VISIBLE); 1327 1328 mTimerView.setVisibility(View.INVISIBLE); 1329 1330 mTextView.setVisibility(View.VISIBLE); 1331 1332 Log.d(TAG, "the total was " + mHitTotal); 1333 1334 if (mHitTotal >= mSuccessThreshold) { 1335 mTextView.setText(R.string.winText); 1336 } else { 1337 mTextView.setText("Sorry, You Lose! You got " + mHitTotal 1338 + ". You need 50 to win."); 1339 } 1340 1341 mTimerView.setText("1:12"); 1342 mTextView.setHeight(20); 1343 1344 } 1345 }//end handle msg 1346 }); 1347 } 1348 1349 setFocusable(true); // make sure we get key events 1350 1351 Log.d(TAG, "@@@ done creating view!"); 1352 } 1353 1354 1355 /** 1356 * Pass in a reference to the timer view widget so we can update it from here. 1357 * 1358 * @param tv 1359 */ 1360 public void setTimerView(TextView tv) { 1361 mTimerView = tv; 1362 } 1363 1364 1365 /** 1366 * Standard window-focus override. Notice focus lost so we can pause on 1367 * focus lost. e.g. user switches to take a call. 1368 */ 1369 @Override 1370 public void onWindowFocusChanged(boolean hasWindowFocus) { 1371 if (!hasWindowFocus) { 1372 if (thread != null) 1373 thread.pause(); 1374 1375 } 1376 } 1377 1378 1379 /** 1380 * Fetches the animation thread corresponding to this LunarView. 1381 * 1382 * @return the animation thread 1383 */ 1384 public JetBoyThread getThread() { 1385 return thread; 1386 } 1387 1388 1389 /* Callback invoked when the surface dimensions change. */ 1390 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 1391 thread.setSurfaceSize(width, height); 1392 } 1393 1394 1395 public void surfaceCreated(SurfaceHolder arg0) { 1396 // start the thread here so that we don't busy-wait in run() 1397 // waiting for the surface to be created 1398 thread.setRunning(true); 1399 thread.start(); 1400 } 1401 1402 1403 public void surfaceDestroyed(SurfaceHolder arg0) { 1404 boolean retry = true; 1405 thread.setRunning(false); 1406 while (retry) { 1407 try { 1408 thread.join(); 1409 retry = false; 1410 1411 } catch (InterruptedException e) { 1412 } 1413 } 1414 } 1415 1416 1417 /** 1418 * A reference to the button to start game over. 1419 * 1420 * @param _buttonRetry 1421 * 1422 */ 1423 public void SetButtonView(Button _buttonRetry) { 1424 mButtonRetry = _buttonRetry; 1425 // mButtonRestart = _buttonRestart; 1426 } 1427 1428 1429 //we reuse the help screen from the end game screen. 1430 public void SetTextView(TextView textView) { 1431 mTextView = textView; 1432 1433 } 1434 } 1435