| /* | |
| * Copyright (C) 2009 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| * | |
| */ | |
| // Android JET demonstration code: | |
| // All inline comments related to the use of the JetPlayer class are preceded by "JET info:" | |
| package com.example.android.jetboy; | |
| import android.content.Context; | |
| import android.content.res.Resources; | |
| import android.graphics.Bitmap; | |
| import android.graphics.BitmapFactory; | |
| import android.graphics.Canvas; | |
| import android.media.JetPlayer; | |
| import android.media.JetPlayer.OnJetEventListener; | |
| import android.os.Bundle; | |
| import android.os.Handler; | |
| import android.os.Message; | |
| import android.util.AttributeSet; | |
| import android.util.Log; | |
| import android.view.KeyEvent; | |
| import android.view.SurfaceHolder; | |
| import android.view.SurfaceView; | |
| import android.view.View; | |
| import android.widget.Button; | |
| import android.widget.TextView; | |
| import java.util.Random; | |
| import java.util.Timer; | |
| import java.util.TimerTask; | |
| import java.util.Vector; | |
| import java.util.concurrent.ConcurrentLinkedQueue; | |
| public class JetBoyView extends SurfaceView implements SurfaceHolder.Callback { | |
| // the number of asteroids that must be destroyed | |
| public static final int mSuccessThreshold = 50; | |
| // used to calculate level for mutes and trigger clip | |
| public int mHitStreak = 0; | |
| // total number asteroids you need to hit. | |
| public int mHitTotal = 0; | |
| // which music bed is currently playing? | |
| public int mCurrentBed = 0; | |
| // a lazy graphic fudge for the initial title splash | |
| private Bitmap mTitleBG; | |
| private Bitmap mTitleBG2; | |
| /** | |
| * Base class for any external event passed to the JetBoyThread. This can | |
| * include user input, system events, network input, etc. | |
| */ | |
| class GameEvent { | |
| public GameEvent() { | |
| eventTime = System.currentTimeMillis(); | |
| } | |
| long eventTime; | |
| } | |
| /** | |
| * A GameEvent subclass for key based user input. Values are those used by | |
| * the standard onKey | |
| */ | |
| class KeyGameEvent extends GameEvent { | |
| /** | |
| * Simple constructor to make populating this event easier. | |
| */ | |
| public KeyGameEvent(int keyCode, boolean up, KeyEvent msg) { | |
| this.keyCode = keyCode; | |
| this.msg = msg; | |
| this.up = up; | |
| } | |
| public int keyCode; | |
| public KeyEvent msg; | |
| public boolean up; | |
| } | |
| /** | |
| * A GameEvent subclass for events from the JetPlayer. | |
| */ | |
| class JetGameEvent extends GameEvent { | |
| /** | |
| * Simple constructor to make populating this event easier. | |
| */ | |
| public JetGameEvent(JetPlayer player, short segment, byte track, byte channel, | |
| byte controller, byte value) { | |
| this.player = player; | |
| this.segment = segment; | |
| this.track = track; | |
| this.channel = channel; | |
| this.controller = controller; | |
| this.value = value; | |
| } | |
| public JetPlayer player; | |
| public short segment; | |
| public byte track; | |
| public byte channel; | |
| public byte controller; | |
| public byte value; | |
| } | |
| // JET info: the JetBoyThread receives all the events from the JET player | |
| // JET info: through the OnJetEventListener interface. | |
| class JetBoyThread extends Thread implements OnJetEventListener { | |
| /** | |
| * State-tracking constants. | |
| */ | |
| public static final int STATE_START = -1; | |
| public static final int STATE_PLAY = 0; | |
| public static final int STATE_LOSE = 1; | |
| public static final int STATE_PAUSE = 2; | |
| public static final int STATE_RUNNING = 3; | |
| // how many frames per beat? The basic animation can be changed for | |
| // instance to 3/4 by changing this to 3. | |
| // untested is the impact on other parts of game logic for non 4/4 time. | |
| private static final int ANIMATION_FRAMES_PER_BEAT = 4; | |
| public boolean mInitialized = false; | |
| /** Queue for GameEvents */ | |
| protected ConcurrentLinkedQueue<GameEvent> mEventQueue = new ConcurrentLinkedQueue<GameEvent>(); | |
| /** Context for processKey to maintain state accross frames * */ | |
| protected Object mKeyContext = null; | |
| // the timer display in seconds | |
| public int mTimerLimit; | |
| // used for internal timing logic. | |
| public final int TIMER_LIMIT = 72; | |
| // string value for timer display | |
| private String mTimerValue = "1:12"; | |
| // start, play, running, lose are the states we use | |
| public int mState; | |
| // has laser been fired and for how long? | |
| // user for fx logic on laser fire | |
| boolean mLaserOn = false; | |
| long mLaserFireTime = 0; | |
| /** The drawable to use as the far background of the animation canvas */ | |
| private Bitmap mBackgroundImageFar; | |
| /** The drawable to use as the close background of the animation canvas */ | |
| private Bitmap mBackgroundImageNear; | |
| // JET info: event IDs within the JET file. | |
| // JET info: in this game 80 is used for sending asteroid across the screen | |
| // JET info: 82 is used as game time for 1/4 note beat. | |
| private final byte NEW_ASTEROID_EVENT = 80; | |
| private final byte TIMER_EVENT = 82; | |
| // used to track beat for synch of mute/unmute actions | |
| private int mBeatCount = 1; | |
| // our intrepid space boy | |
| private Bitmap[] mShipFlying = new Bitmap[4]; | |
| // the twinkly bit | |
| private Bitmap[] mBeam = new Bitmap[4]; | |
| // the things you are trying to hit | |
| private Bitmap[] mAsteroids = new Bitmap[12]; | |
| // hit animation | |
| private Bitmap[] mExplosions = new Bitmap[4]; | |
| private Bitmap mTimerShell; | |
| private Bitmap mLaserShot; | |
| // used to save the beat event system time. | |
| private long mLastBeatTime; | |
| private long mPassedTime; | |
| // how much do we move the asteroids per beat? | |
| private int mPixelMoveX = 25; | |
| // the asteroid send events are generated from the Jet File. | |
| // but which land they start in is random. | |
| private Random mRandom = new Random(); | |
| // JET info: the star of our show, a reference to the JetPlayer object. | |
| private JetPlayer mJet = null; | |
| private boolean mJetPlaying = false; | |
| /** Message handler used by thread to interact with TextView */ | |
| private Handler mHandler; | |
| /** Handle to the surface manager object we interact with */ | |
| private SurfaceHolder mSurfaceHolder; | |
| /** Handle to the application context, used to e.g. fetch Drawables. */ | |
| private Context mContext; | |
| /** Indicate whether the surface has been created & is ready to draw */ | |
| private boolean mRun = false; | |
| // updates the screen clock. Also used for tempo timing. | |
| private Timer mTimer = null; | |
| private TimerTask mTimerTask = null; | |
| // one second - used to update timer | |
| private int mTaskIntervalInMillis = 1000; | |
| /** | |
| * Current height of the surface/canvas. | |
| * | |
| * @see #setSurfaceSize | |
| */ | |
| private int mCanvasHeight = 1; | |
| /** | |
| * Current width of the surface/canvas. | |
| * | |
| * @see #setSurfaceSize | |
| */ | |
| private int mCanvasWidth = 1; | |
| // used to track the picture to draw for ship animation | |
| private int mShipIndex = 0; | |
| // stores all of the asteroid objects in order | |
| private Vector<Asteroid> mDangerWillRobinson; | |
| private Vector<Explosion> mExplosion; | |
| // right to left scroll tracker for near and far BG | |
| private int mBGFarMoveX = 0; | |
| private int mBGNearMoveX = 0; | |
| // how far up (close to top) jet boy can fly | |
| private int mJetBoyYMin = 40; | |
| private int mJetBoyX = 0; | |
| private int mJetBoyY = 0; | |
| // this is the pixel position of the laser beam guide. | |
| private int mAsteroidMoveLimitX = 110; | |
| // how far up asteroid can be painted | |
| private int mAsteroidMinY = 40; | |
| Resources mRes; | |
| // array to store the mute masks that are applied during game play to respond to | |
| // the player's hit streaks | |
| private boolean muteMask[][] = new boolean[9][32]; | |
| /** | |
| * This is the constructor for the main worker bee | |
| * | |
| * @param surfaceHolder | |
| * @param context | |
| * @param handler | |
| */ | |
| public JetBoyThread(SurfaceHolder surfaceHolder, Context context, Handler handler) { | |
| mSurfaceHolder = surfaceHolder; | |
| mHandler = handler; | |
| mContext = context; | |
| mRes = context.getResources(); | |
| // JET info: this are the mute arrays associated with the music beds in the | |
| // JET info: JET file | |
| for (int ii = 0; ii < 8; ii++) { | |
| for (int xx = 0; xx < 32; xx++) { | |
| muteMask[ii][xx] = true; | |
| } | |
| } | |
| muteMask[0][2] = false; | |
| muteMask[0][3] = false; | |
| muteMask[0][4] = false; | |
| muteMask[0][5] = false; | |
| muteMask[1][2] = false; | |
| muteMask[1][3] = false; | |
| muteMask[1][4] = false; | |
| muteMask[1][5] = false; | |
| muteMask[1][8] = false; | |
| muteMask[1][9] = false; | |
| muteMask[2][2] = false; | |
| muteMask[2][3] = false; | |
| muteMask[2][6] = false; | |
| muteMask[2][7] = false; | |
| muteMask[2][8] = false; | |
| muteMask[2][9] = false; | |
| muteMask[3][2] = false; | |
| muteMask[3][3] = false; | |
| muteMask[3][6] = false; | |
| muteMask[3][11] = false; | |
| muteMask[3][12] = false; | |
| muteMask[4][2] = false; | |
| muteMask[4][3] = false; | |
| muteMask[4][10] = false; | |
| muteMask[4][11] = false; | |
| muteMask[4][12] = false; | |
| muteMask[4][13] = false; | |
| muteMask[5][2] = false; | |
| muteMask[5][3] = false; | |
| muteMask[5][10] = false; | |
| muteMask[5][12] = false; | |
| muteMask[5][15] = false; | |
| muteMask[5][17] = false; | |
| muteMask[6][2] = false; | |
| muteMask[6][3] = false; | |
| muteMask[6][14] = false; | |
| muteMask[6][15] = false; | |
| muteMask[6][16] = false; | |
| muteMask[6][17] = false; | |
| muteMask[7][2] = false; | |
| muteMask[7][3] = false; | |
| muteMask[7][6] = false; | |
| muteMask[7][14] = false; | |
| muteMask[7][15] = false; | |
| muteMask[7][16] = false; | |
| muteMask[7][17] = false; | |
| muteMask[7][18] = false; | |
| // set all tracks to play | |
| for (int xx = 0; xx < 32; xx++) { | |
| muteMask[8][xx] = false; | |
| } | |
| // always set state to start, ensure we come in from front door if | |
| // app gets tucked into background | |
| mState = STATE_START; | |
| setInitialGameState(); | |
| mTitleBG = BitmapFactory.decodeResource(mRes, R.drawable.title_hori); | |
| // load background image as a Bitmap instead of a Drawable b/c | |
| // we don't need to transform it and it's faster to draw this | |
| // way...thanks lunar lander :) | |
| // two background since we want them moving at different speeds | |
| mBackgroundImageFar = BitmapFactory.decodeResource(mRes, R.drawable.background_a); | |
| mLaserShot = BitmapFactory.decodeResource(mRes, R.drawable.laser); | |
| mBackgroundImageNear = BitmapFactory.decodeResource(mRes, R.drawable.background_b); | |
| mShipFlying[0] = BitmapFactory.decodeResource(mRes, R.drawable.ship2_1); | |
| mShipFlying[1] = BitmapFactory.decodeResource(mRes, R.drawable.ship2_2); | |
| mShipFlying[2] = BitmapFactory.decodeResource(mRes, R.drawable.ship2_3); | |
| mShipFlying[3] = BitmapFactory.decodeResource(mRes, R.drawable.ship2_4); | |
| mBeam[0] = BitmapFactory.decodeResource(mRes, R.drawable.intbeam_1); | |
| mBeam[1] = BitmapFactory.decodeResource(mRes, R.drawable.intbeam_2); | |
| mBeam[2] = BitmapFactory.decodeResource(mRes, R.drawable.intbeam_3); | |
| mBeam[3] = BitmapFactory.decodeResource(mRes, R.drawable.intbeam_4); | |
| mTimerShell = BitmapFactory.decodeResource(mRes, R.drawable.int_timer); | |
| // I wanted them to rotate in a certain way | |
| // so I loaded them backwards from the way created. | |
| mAsteroids[11] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid01); | |
| mAsteroids[10] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid02); | |
| mAsteroids[9] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid03); | |
| mAsteroids[8] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid04); | |
| mAsteroids[7] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid05); | |
| mAsteroids[6] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid06); | |
| mAsteroids[5] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid07); | |
| mAsteroids[4] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid08); | |
| mAsteroids[3] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid09); | |
| mAsteroids[2] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid10); | |
| mAsteroids[1] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid11); | |
| mAsteroids[0] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid12); | |
| mExplosions[0] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid_explode1); | |
| mExplosions[1] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid_explode2); | |
| mExplosions[2] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid_explode3); | |
| mExplosions[3] = BitmapFactory.decodeResource(mRes, R.drawable.asteroid_explode4); | |
| } | |
| /** | |
| * Does the grunt work of setting up initial jet requirements | |
| */ | |
| private void initializeJetPlayer() { | |
| // JET info: let's create our JetPlayer instance using the factory. | |
| // JET info: if we already had one, the same singleton is returned. | |
| mJet = JetPlayer.getJetPlayer(); | |
| mJetPlaying = false; | |
| // JET info: make sure we flush the queue, | |
| // JET info: otherwise left over events from previous gameplay can hang around. | |
| // JET info: ok, here we don't really need that but if you ever reuse a JetPlayer | |
| // JET info: instance, clear the queue before reusing it, this will also clear any | |
| // JET info: trigger clips that have been triggered but not played yet. | |
| mJet.clearQueue(); | |
| // JET info: we are going to receive in this example all the JET callbacks | |
| // JET info: inthis animation thread object. | |
| mJet.setEventListener(this); | |
| Log.d(TAG, "opening jet file"); | |
| // JET info: load the actual JET content the game will be playing, | |
| // JET info: it's stored as a raw resource in our APK, and is labeled "level1" | |
| mJet.loadJetFile(mContext.getResources().openRawResourceFd(R.raw.level1)); | |
| // JET info: if our JET file was stored on the sdcard for instance, we would have used | |
| // JET info: mJet.loadJetFile("/sdcard/level1.jet"); | |
| Log.d(TAG, "opening jet file DONE"); | |
| mCurrentBed = 0; | |
| byte sSegmentID = 0; | |
| Log.d(TAG, " start queuing jet file"); | |
| // JET info: now we're all set to prepare queuing the JET audio segments for the game. | |
| // JET info: in this example, the game uses segment 0 for the duration of the game play, | |
| // JET info: and plays segment 1 several times as the "outro" music, so we're going to | |
| // JET info: queue everything upfront, but with more complex JET compositions, we could | |
| // JET info: also queue the segments during the game play. | |
| // JET info: this is the main game play music | |
| // JET info: it is located at segment 0 | |
| // JET info: it uses the first DLS lib in the .jet resource, which is at index 0 | |
| // JET info: index -1 means no DLS | |
| mJet.queueJetSegment(0, 0, 0, 0, 0, sSegmentID); | |
| // JET info: end game music, loop 4 times normal pitch | |
| mJet.queueJetSegment(1, 0, 4, 0, 0, sSegmentID); | |
| // JET info: end game music loop 4 times up an octave | |
| mJet.queueJetSegment(1, 0, 4, 1, 0, sSegmentID); | |
| // JET info: set the mute mask as designed for the beginning of the game, when the | |
| // JET info: the player hasn't scored yet. | |
| mJet.setMuteArray(muteMask[0], true); | |
| Log.d(TAG, " start queuing jet file DONE"); | |
| } | |
| private void doDraw(Canvas canvas) { | |
| if (mState == STATE_RUNNING) { | |
| doDrawRunning(canvas); | |
| } else if (mState == STATE_START) { | |
| doDrawReady(canvas); | |
| } else if (mState == STATE_PLAY || mState == STATE_LOSE) { | |
| if (mTitleBG2 == null) { | |
| mTitleBG2 = BitmapFactory.decodeResource(mRes, R.drawable.title_bg_hori); | |
| } | |
| doDrawPlay(canvas); | |
| }// end state play block | |
| } | |
| /** | |
| * Draws current state of the game Canvas. | |
| */ | |
| private void doDrawRunning(Canvas canvas) { | |
| // decrement the far background | |
| mBGFarMoveX = mBGFarMoveX - 1; | |
| // decrement the near background | |
| mBGNearMoveX = mBGNearMoveX - 4; | |
| // calculate the wrap factor for matching image draw | |
| int newFarX = mBackgroundImageFar.getWidth() - (-mBGFarMoveX); | |
| // if we have scrolled all the way, reset to start | |
| if (newFarX <= 0) { | |
| mBGFarMoveX = 0; | |
| // only need one draw | |
| canvas.drawBitmap(mBackgroundImageFar, mBGFarMoveX, 0, null); | |
| } else { | |
| // need to draw original and wrap | |
| canvas.drawBitmap(mBackgroundImageFar, mBGFarMoveX, 0, null); | |
| canvas.drawBitmap(mBackgroundImageFar, newFarX, 0, null); | |
| } | |
| // same story different image... | |
| // TODO possible method call | |
| int newNearX = mBackgroundImageNear.getWidth() - (-mBGNearMoveX); | |
| if (newNearX <= 0) { | |
| mBGNearMoveX = 0; | |
| canvas.drawBitmap(mBackgroundImageNear, mBGNearMoveX, 0, null); | |
| } else { | |
| canvas.drawBitmap(mBackgroundImageNear, mBGNearMoveX, 0, null); | |
| canvas.drawBitmap(mBackgroundImageNear, newNearX, 0, null); | |
| } | |
| doAsteroidAnimation(canvas); | |
| canvas.drawBitmap(mBeam[mShipIndex], 51 + 20, 0, null); | |
| mShipIndex++; | |
| if (mShipIndex == 4) | |
| mShipIndex = 0; | |
| // draw the space ship in the same lane as the next asteroid | |
| canvas.drawBitmap(mShipFlying[mShipIndex], mJetBoyX, mJetBoyY, null); | |
| if (mLaserOn) { | |
| canvas.drawBitmap(mLaserShot, mJetBoyX + mShipFlying[0].getWidth(), mJetBoyY | |
| + (mShipFlying[0].getHeight() / 2), null); | |
| } | |
| // tick tock | |
| canvas.drawBitmap(mTimerShell, mCanvasWidth - mTimerShell.getWidth(), 0, null); | |
| } | |
| private void setInitialGameState() { | |
| mTimerLimit = TIMER_LIMIT; | |
| mJetBoyY = mJetBoyYMin; | |
| // set up jet stuff | |
| initializeJetPlayer(); | |
| mTimer = new Timer(); | |
| mDangerWillRobinson = new Vector<Asteroid>(); | |
| mExplosion = new Vector<Explosion>(); | |
| mInitialized = true; | |
| mHitStreak = 0; | |
| mHitTotal = 0; | |
| } | |
| private void doAsteroidAnimation(Canvas canvas) { | |
| if ((mDangerWillRobinson == null | mDangerWillRobinson.size() == 0) | |
| && (mExplosion != null && mExplosion.size() == 0)) | |
| return; | |
| // Compute what percentage through a beat we are and adjust | |
| // animation and position based on that. This assumes 140bpm(428ms/beat). | |
| // This is just inter-beat interpolation, no game state is updated | |
| long frameDelta = System.currentTimeMillis() - mLastBeatTime; | |
| int animOffset = (int)(ANIMATION_FRAMES_PER_BEAT * frameDelta / 428); | |
| for (int i = (mDangerWillRobinson.size() - 1); i >= 0; i--) { | |
| Asteroid asteroid = mDangerWillRobinson.elementAt(i); | |
| if (!asteroid.mMissed) | |
| mJetBoyY = asteroid.mDrawY; | |
| // Log.d(TAG, " drawing asteroid " + ii + " at " + | |
| // asteroid.mDrawX ); | |
| canvas.drawBitmap( | |
| mAsteroids[(asteroid.mAniIndex + animOffset) % mAsteroids.length], | |
| asteroid.mDrawX, asteroid.mDrawY, null); | |
| } | |
| for (int i = (mExplosion.size() - 1); i >= 0; i--) { | |
| Explosion ex = mExplosion.elementAt(i); | |
| canvas.drawBitmap(mExplosions[(ex.mAniIndex + animOffset) % mExplosions.length], | |
| ex.mDrawX, ex.mDrawY, null); | |
| } | |
| } | |
| private void doDrawReady(Canvas canvas) { | |
| canvas.drawBitmap(mTitleBG, 0, 0, null); | |
| } | |
| private void doDrawPlay(Canvas canvas) { | |
| canvas.drawBitmap(mTitleBG2, 0, 0, null); | |
| } | |
| /** | |
| * the heart of the worker bee | |
| */ | |
| public void run() { | |
| // while running do stuff in this loop...bzzz! | |
| while (mRun) { | |
| Canvas c = null; | |
| if (mState == STATE_RUNNING) { | |
| // Process any input and apply it to the game state | |
| updateGameState(); | |
| if (!mJetPlaying) { | |
| mInitialized = false; | |
| Log.d(TAG, "------> STARTING JET PLAY"); | |
| mJet.play(); | |
| mJetPlaying = true; | |
| } | |
| mPassedTime = System.currentTimeMillis(); | |
| // kick off the timer task for counter update if not already | |
| // initialized | |
| if (mTimerTask == null) { | |
| mTimerTask = new TimerTask() { | |
| public void run() { | |
| doCountDown(); | |
| } | |
| }; | |
| mTimer.schedule(mTimerTask, mTaskIntervalInMillis); | |
| }// end of TimerTask init block | |
| }// end of STATE_RUNNING block | |
| else if (mState == STATE_PLAY && !mInitialized) | |
| { | |
| setInitialGameState(); | |
| } else if (mState == STATE_LOSE) { | |
| mInitialized = false; | |
| } | |
| try { | |
| c = mSurfaceHolder.lockCanvas(null); | |
| // synchronized (mSurfaceHolder) { | |
| doDraw(c); | |
| // } | |
| } finally { | |
| // do this in a finally so that if an exception is thrown | |
| // during the above, we don't leave the Surface in an | |
| // inconsistent state | |
| if (c != null) { | |
| mSurfaceHolder.unlockCanvasAndPost(c); | |
| } | |
| }// end finally block | |
| }// end while mrun block | |
| } | |
| /** | |
| * This method handles updating the model of the game state. No | |
| * rendering is done here only processing of inputs and update of state. | |
| * This includes positons of all game objects (asteroids, player, | |
| * explosions), their state (animation frame, hit), creation of new | |
| * objects, etc. | |
| */ | |
| protected void updateGameState() { | |
| // Process any game events and apply them | |
| while (true) { | |
| GameEvent event = mEventQueue.poll(); | |
| if (event == null) | |
| break; | |
| // Log.d(TAG,"*** EVENT = " + event); | |
| // Process keys tracking the input context to pass in to later | |
| // calls | |
| if (event instanceof KeyGameEvent) { | |
| // Process the key for affects other then asteroid hits | |
| mKeyContext = processKeyEvent((KeyGameEvent)event, mKeyContext); | |
| // Update laser state. Having this here allows the laser to | |
| // be triggered right when the key is | |
| // pressed. If we comment this out the laser will only be | |
| // turned on when updateLaser is called | |
| // when processing a timer event below. | |
| updateLaser(mKeyContext); | |
| } | |
| // JET events trigger a state update | |
| else if (event instanceof JetGameEvent) { | |
| JetGameEvent jetEvent = (JetGameEvent)event; | |
| // Only update state on a timer event | |
| if (jetEvent.value == TIMER_EVENT) { | |
| // Note the time of the last beat | |
| mLastBeatTime = System.currentTimeMillis(); | |
| // Update laser state, turning it on if a key has been | |
| // pressed or off if it has been | |
| // on for too long. | |
| updateLaser(mKeyContext); | |
| // Update explosions before we update asteroids because | |
| // updateAsteroids may add | |
| // new explosions that we do not want updated until next | |
| // frame | |
| updateExplosions(mKeyContext); | |
| // Update asteroid positions, hit status and animations | |
| updateAsteroids(mKeyContext); | |
| } | |
| processJetEvent(jetEvent.player, jetEvent.segment, jetEvent.track, | |
| jetEvent.channel, jetEvent.controller, jetEvent.value); | |
| } | |
| } | |
| } | |
| /** | |
| * This method handles the state updates that can be caused by key press | |
| * events. Key events may mean different things depending on what has | |
| * come before, to support this concept this method takes an opaque | |
| * context object as a parameter and returns an updated version. This | |
| * context should be set to null for the first event then should be set | |
| * to the last value returned for subsequent events. | |
| */ | |
| protected Object processKeyEvent(KeyGameEvent event, Object context) { | |
| // Log.d(TAG, "key code is " + event.keyCode + " " + (event.up ? | |
| // "up":"down")); | |
| // If it is a key up on the fire key make sure we mute the | |
| // associated sound | |
| if (event.up) { | |
| if (event.keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { | |
| return null; | |
| } | |
| } | |
| // If it is a key down on the fire key start playing the sound and | |
| // update the context | |
| // to indicate that a key has been pressed and to ignore further | |
| // presses | |
| else { | |
| if (event.keyCode == KeyEvent.KEYCODE_DPAD_CENTER && (context == null)) { | |
| return event; | |
| } | |
| } | |
| // Return the context unchanged | |
| return context; | |
| } | |
| /** | |
| * This method updates the laser status based on user input and shot | |
| * duration | |
| */ | |
| protected void updateLaser(Object inputContext) { | |
| // Lookup the time of the fire event if there is one | |
| long keyTime = inputContext == null ? 0 : ((GameEvent)inputContext).eventTime; | |
| // Log.d(TAG,"keyTime delta = " + | |
| // (System.currentTimeMillis()-keyTime) + ": obj = " + | |
| // inputContext); | |
| // If the laser has been on too long shut it down | |
| if (mLaserOn && System.currentTimeMillis() - mLaserFireTime > 400) { | |
| mLaserOn = false; | |
| } | |
| // trying to tune the laser hit timing | |
| else if (System.currentTimeMillis() - mLaserFireTime > 300) { | |
| // JET info: the laser sound is on track 23, we mute it (true) right away (false) | |
| mJet.setMuteFlag(23, true, false); | |
| } | |
| // Now check to see if we should turn the laser on. We do this after | |
| // the above shutdown | |
| // logic so it can be turned back on in the same frame it was turned | |
| // off in. If we want | |
| // to add a cooldown period this may change. | |
| if (!mLaserOn && System.currentTimeMillis() - keyTime <= 400) { | |
| mLaserOn = true; | |
| mLaserFireTime = keyTime; | |
| // JET info: unmute the laser track (false) right away (false) | |
| mJet.setMuteFlag(23, false, false); | |
| } | |
| } | |
| /** | |
| * Update asteroid state including position and laser hit status. | |
| */ | |
| protected void updateAsteroids(Object inputContext) { | |
| if (mDangerWillRobinson == null | mDangerWillRobinson.size() == 0) | |
| return; | |
| for (int i = (mDangerWillRobinson.size() - 1); i >= 0; i--) { | |
| Asteroid asteroid = mDangerWillRobinson.elementAt(i); | |
| // If the asteroid is within laser range but not already missed | |
| // check if the key was pressed close enough to the beat to make a hit | |
| if (asteroid.mDrawX <= mAsteroidMoveLimitX + 20 && !asteroid.mMissed) | |
| { | |
| // If the laser was fired on the beat destroy the asteroid | |
| if (mLaserOn) { | |
| // Track hit streak for adjusting music | |
| mHitStreak++; | |
| mHitTotal++; | |
| // replace the asteroid with an explosion | |
| Explosion ex = new Explosion(); | |
| ex.mAniIndex = 0; | |
| ex.mDrawX = asteroid.mDrawX; | |
| ex.mDrawY = asteroid.mDrawY; | |
| mExplosion.add(ex); | |
| mJet.setMuteFlag(24, false, false); | |
| mDangerWillRobinson.removeElementAt(i); | |
| // This asteroid has been removed process the next one | |
| continue; | |
| } else { | |
| // Sorry, timing was not good enough, mark the asteroid | |
| // as missed so on next frame it cannot be hit even if it is still | |
| // within range | |
| asteroid.mMissed = true; | |
| mHitStreak = mHitStreak - 1; | |
| if (mHitStreak < 0) | |
| mHitStreak = 0; | |
| } | |
| } | |
| // Update the asteroids position, even missed ones keep moving | |
| asteroid.mDrawX -= mPixelMoveX; | |
| // Update asteroid animation frame | |
| asteroid.mAniIndex = (asteroid.mAniIndex + ANIMATION_FRAMES_PER_BEAT) | |
| % mAsteroids.length; | |
| // if we have scrolled off the screen | |
| if (asteroid.mDrawX < 0) { | |
| mDangerWillRobinson.removeElementAt(i); | |
| } | |
| } | |
| } | |
| /** | |
| * This method updates explosion animation and removes them once they | |
| * have completed. | |
| */ | |
| protected void updateExplosions(Object inputContext) { | |
| if (mExplosion == null | mExplosion.size() == 0) | |
| return; | |
| for (int i = mExplosion.size() - 1; i >= 0; i--) { | |
| Explosion ex = mExplosion.elementAt(i); | |
| ex.mAniIndex += ANIMATION_FRAMES_PER_BEAT; | |
| // When the animation completes remove the explosion | |
| if (ex.mAniIndex > 3) { | |
| mJet.setMuteFlag(24, true, false); | |
| mJet.setMuteFlag(23, true, false); | |
| mExplosion.removeElementAt(i); | |
| } | |
| } | |
| } | |
| /** | |
| * This method handles the state updates that can be caused by JET | |
| * events. | |
| */ | |
| protected void processJetEvent(JetPlayer player, short segment, byte track, byte channel, | |
| byte controller, byte value) { | |
| //Log.d(TAG, "onJetEvent(): seg=" + segment + " track=" + track + " chan=" + channel | |
| // + " cntrlr=" + controller + " val=" + value); | |
| // Check for an event that triggers a new asteroid | |
| if (value == NEW_ASTEROID_EVENT) { | |
| doAsteroidCreation(); | |
| } | |
| mBeatCount++; | |
| if (mBeatCount > 4) { | |
| mBeatCount = 1; | |
| } | |
| // Scale the music based on progress | |
| // it was a game requirement to change the mute array on 1st beat of | |
| // the next measure when needed | |
| // and so we track beat count, after that we track hitStreak to | |
| // determine the music "intensity" | |
| // if the intensity has go gone up, call a corresponding trigger clip, otherwise just | |
| // execute the rest of the music bed change logic. | |
| if (mBeatCount == 1) { | |
| // do it back wards so you fall into the correct one | |
| if (mHitStreak > 28) { | |
| // did the bed change? | |
| if (mCurrentBed != 7) { | |
| // did it go up? | |
| if (mCurrentBed < 7) { | |
| mJet.triggerClip(7); | |
| } | |
| mCurrentBed = 7; | |
| // JET info: change the mute mask to update the way the music plays based | |
| // JET info: on the player's skills. | |
| mJet.setMuteArray(muteMask[7], false); | |
| } | |
| } else if (mHitStreak > 24) { | |
| if (mCurrentBed != 6) { | |
| if (mCurrentBed < 6) { | |
| // JET info: quite a few asteroids hit, trigger the clip with the guy's | |
| // JET info: voice that encourages the player. | |
| mJet.triggerClip(6); | |
| } | |
| mCurrentBed = 6; | |
| mJet.setMuteArray(muteMask[6], false); | |
| } | |
| } else if (mHitStreak > 20) { | |
| if (mCurrentBed != 5) { | |
| if (mCurrentBed < 5) { | |
| mJet.triggerClip(5); | |
| } | |
| mCurrentBed = 5; | |
| mJet.setMuteArray(muteMask[5], false); | |
| } | |
| } else if (mHitStreak > 16) { | |
| if (mCurrentBed != 4) { | |
| if (mCurrentBed < 4) { | |
| mJet.triggerClip(4); | |
| } | |
| mCurrentBed = 4; | |
| mJet.setMuteArray(muteMask[4], false); | |
| } | |
| } else if (mHitStreak > 12) { | |
| if (mCurrentBed != 3) { | |
| if (mCurrentBed < 3) { | |
| mJet.triggerClip(3); | |
| } | |
| mCurrentBed = 3; | |
| mJet.setMuteArray(muteMask[3], false); | |
| } | |
| } else if (mHitStreak > 8) { | |
| if (mCurrentBed != 2) { | |
| if (mCurrentBed < 2) { | |
| mJet.triggerClip(2); | |
| } | |
| mCurrentBed = 2; | |
| mJet.setMuteArray(muteMask[2], false); | |
| } | |
| } else if (mHitStreak > 4) { | |
| if (mCurrentBed != 1) { | |
| if (mCurrentBed < 1) { | |
| mJet.triggerClip(1); | |
| } | |
| mJet.setMuteArray(muteMask[1], false); | |
| mCurrentBed = 1; | |
| } | |
| } | |
| } | |
| } | |
| private void doAsteroidCreation() { | |
| // Log.d(TAG, "asteroid created"); | |
| Asteroid _as = new Asteroid(); | |
| int drawIndex = mRandom.nextInt(4); | |
| // TODO Remove hard coded value | |
| _as.mDrawY = mAsteroidMinY + (drawIndex * 63); | |
| _as.mDrawX = (mCanvasWidth - mAsteroids[0].getWidth()); | |
| _as.mStartTime = System.currentTimeMillis(); | |
| mDangerWillRobinson.add(_as); | |
| } | |
| /** | |
| * Used to signal the thread whether it should be running or not. | |
| * Passing true allows the thread to run; passing false will shut it | |
| * down if it's already running. Calling start() after this was most | |
| * recently called with false will result in an immediate shutdown. | |
| * | |
| * @param b true to run, false to shut down | |
| */ | |
| public void setRunning(boolean b) { | |
| mRun = b; | |
| if (mRun == false) { | |
| if (mTimerTask != null) | |
| mTimerTask.cancel(); | |
| } | |
| } | |
| /** | |
| * returns the current int value of game state as defined by state | |
| * tracking constants | |
| * | |
| * @return | |
| */ | |
| public int getGameState() { | |
| synchronized (mSurfaceHolder) { | |
| return mState; | |
| } | |
| } | |
| /** | |
| * Sets the game mode. That is, whether we are running, paused, in the | |
| * failure state, in the victory state, etc. | |
| * | |
| * @see #setState(int, CharSequence) | |
| * @param mode one of the STATE_* constants | |
| */ | |
| public void setGameState(int mode) { | |
| synchronized (mSurfaceHolder) { | |
| setGameState(mode, null); | |
| } | |
| } | |
| /** | |
| * Sets state based on input, optionally also passing in a text message. | |
| * | |
| * @param state | |
| * @param message | |
| */ | |
| public void setGameState(int state, CharSequence message) { | |
| synchronized (mSurfaceHolder) { | |
| // change state if needed | |
| if (mState != state) { | |
| mState = state; | |
| } | |
| if (mState == STATE_PLAY) { | |
| Resources res = mContext.getResources(); | |
| mBackgroundImageFar = BitmapFactory | |
| .decodeResource(res, R.drawable.background_a); | |
| // don't forget to resize the background image | |
| mBackgroundImageFar = Bitmap.createScaledBitmap(mBackgroundImageFar, | |
| mCanvasWidth * 2, mCanvasHeight, true); | |
| mBackgroundImageNear = BitmapFactory.decodeResource(res, | |
| R.drawable.background_b); | |
| // don't forget to resize the background image | |
| mBackgroundImageNear = Bitmap.createScaledBitmap(mBackgroundImageNear, | |
| mCanvasWidth * 2, mCanvasHeight, true); | |
| } else if (mState == STATE_RUNNING) { | |
| // When we enter the running state we should clear any old | |
| // events in the queue | |
| mEventQueue.clear(); | |
| // And reset the key state so we don't think a button is pressed when it isn't | |
| mKeyContext = null; | |
| } | |
| } | |
| } | |
| /** | |
| * Add key press input to the GameEvent queue | |
| */ | |
| public boolean doKeyDown(int keyCode, KeyEvent msg) { | |
| mEventQueue.add(new KeyGameEvent(keyCode, false, msg)); | |
| return true; | |
| } | |
| /** | |
| * Add key press input to the GameEvent queue | |
| */ | |
| public boolean doKeyUp(int keyCode, KeyEvent msg) { | |
| mEventQueue.add(new KeyGameEvent(keyCode, true, msg)); | |
| return true; | |
| } | |
| /* Callback invoked when the surface dimensions change. */ | |
| public void setSurfaceSize(int width, int height) { | |
| // synchronized to make sure these all change atomically | |
| synchronized (mSurfaceHolder) { | |
| mCanvasWidth = width; | |
| mCanvasHeight = height; | |
| // don't forget to resize the background image | |
| mBackgroundImageFar = Bitmap.createScaledBitmap(mBackgroundImageFar, width * 2, | |
| height, true); | |
| // don't forget to resize the background image | |
| mBackgroundImageNear = Bitmap.createScaledBitmap(mBackgroundImageNear, width * 2, | |
| height, true); | |
| } | |
| } | |
| /** | |
| * Pauses the physics update & animation. | |
| */ | |
| public void pause() { | |
| synchronized (mSurfaceHolder) { | |
| if (mState == STATE_RUNNING) | |
| setGameState(STATE_PAUSE); | |
| if (mTimerTask != null) { | |
| mTimerTask.cancel(); | |
| } | |
| if (mJet != null) { | |
| mJet.pause(); | |
| } | |
| } | |
| } | |
| /** | |
| * Does the work of updating timer | |
| * | |
| */ | |
| private void doCountDown() { | |
| //Log.d(TAG,"Time left is " + mTimerLimit); | |
| mTimerLimit = mTimerLimit - 1; | |
| try { | |
| //subtract one minute and see what the result is. | |
| int moreThanMinute = mTimerLimit - 60; | |
| if (moreThanMinute >= 0) { | |
| if (moreThanMinute > 9) { | |
| mTimerValue = "1:" + moreThanMinute; | |
| } | |
| //need an extra '0' for formatting | |
| else { | |
| mTimerValue = "1:0" + moreThanMinute; | |
| } | |
| } else { | |
| if (mTimerLimit > 9) { | |
| mTimerValue = "0:" + mTimerLimit; | |
| } else { | |
| mTimerValue = "0:0" + mTimerLimit; | |
| } | |
| } | |
| } catch (Exception e1) { | |
| Log.e(TAG, "doCountDown threw " + e1.toString()); | |
| } | |
| Message msg = mHandler.obtainMessage(); | |
| Bundle b = new Bundle(); | |
| b.putString("text", mTimerValue); | |
| //time's up | |
| if (mTimerLimit == 0) { | |
| b.putString("STATE_LOSE", "" + STATE_LOSE); | |
| mTimerTask = null; | |
| mState = STATE_LOSE; | |
| } else { | |
| mTimerTask = new TimerTask() { | |
| public void run() { | |
| doCountDown(); | |
| } | |
| }; | |
| mTimer.schedule(mTimerTask, mTaskIntervalInMillis); | |
| } | |
| //this is how we send data back up to the main JetBoyView thread. | |
| //if you look in constructor of JetBoyView you will see code for | |
| //Handling of messages. This is borrowed directly from lunar lander. | |
| //Thanks again! | |
| msg.setData(b); | |
| mHandler.sendMessage(msg); | |
| } | |
| // JET info: JET event listener interface implementation: | |
| /** | |
| * required OnJetEventListener method. Notifications for queue updates | |
| * | |
| * @param player | |
| * @param nbSegments | |
| */ | |
| public void onJetNumQueuedSegmentUpdate(JetPlayer player, int nbSegments) { | |
| //Log.i(TAG, "onJetNumQueuedUpdate(): nbSegs =" + nbSegments); | |
| } | |
| // JET info: JET event listener interface implementation: | |
| /** | |
| * The method which receives notification from event listener. | |
| * This is where we queue up events 80 and 82. | |
| * | |
| * Most of this data passed is unneeded for JetBoy logic but shown | |
| * for code sample completeness. | |
| * | |
| * @param player | |
| * @param segment | |
| * @param track | |
| * @param channel | |
| * @param controller | |
| * @param value | |
| */ | |
| public void onJetEvent(JetPlayer player, short segment, byte track, byte channel, | |
| byte controller, byte value) { | |
| //Log.d(TAG, "jet got event " + value); | |
| //events fire outside the animation thread. This can cause timing issues. | |
| //put in queue for processing by animation thread. | |
| mEventQueue.add(new JetGameEvent(player, segment, track, channel, controller, value)); | |
| } | |
| // JET info: JET event listener interface implementation: | |
| public void onJetPauseUpdate(JetPlayer player, int paused) { | |
| //Log.i(TAG, "onJetPauseUpdate(): paused =" + paused); | |
| } | |
| // JET info: JET event listener interface implementation: | |
| public void onJetUserIdUpdate(JetPlayer player, int userId, int repeatCount) { | |
| //Log.i(TAG, "onJetUserIdUpdate(): userId =" + userId + " repeatCount=" + repeatCount); | |
| } | |
| }//end thread class | |
| public static final String TAG = "JetBoy"; | |
| /** The thread that actually draws the animation */ | |
| private JetBoyThread thread; | |
| private TextView mTimerView; | |
| private Button mButtonRetry; | |
| // private Button mButtonRestart; | |
| private TextView mTextView; | |
| /** | |
| * The constructor called from the main JetBoy activity | |
| * | |
| * @param context | |
| * @param attrs | |
| */ | |
| public JetBoyView(Context context, AttributeSet attrs) { | |
| super(context, attrs); | |
| // register our interest in hearing about changes to our surface | |
| SurfaceHolder holder = getHolder(); | |
| holder.addCallback(this); | |
| // create thread only; it's started in surfaceCreated() | |
| // except if used in the layout editor. | |
| if (isInEditMode() == false) { | |
| thread = new JetBoyThread(holder, context, new Handler() { | |
| public void handleMessage(Message m) { | |
| mTimerView.setText(m.getData().getString("text")); | |
| if (m.getData().getString("STATE_LOSE") != null) { | |
| //mButtonRestart.setVisibility(View.VISIBLE); | |
| mButtonRetry.setVisibility(View.VISIBLE); | |
| mTimerView.setVisibility(View.INVISIBLE); | |
| mTextView.setVisibility(View.VISIBLE); | |
| Log.d(TAG, "the total was " + mHitTotal); | |
| if (mHitTotal >= mSuccessThreshold) { | |
| mTextView.setText(R.string.winText); | |
| } else { | |
| mTextView.setText("Sorry, You Lose! You got " + mHitTotal | |
| + ". You need 50 to win."); | |
| } | |
| mTimerView.setText("1:12"); | |
| mTextView.setHeight(20); | |
| } | |
| }//end handle msg | |
| }); | |
| } | |
| setFocusable(true); // make sure we get key events | |
| Log.d(TAG, "@@@ done creating view!"); | |
| } | |
| /** | |
| * Pass in a reference to the timer view widget so we can update it from here. | |
| * | |
| * @param tv | |
| */ | |
| public void setTimerView(TextView tv) { | |
| mTimerView = tv; | |
| } | |
| /** | |
| * Standard window-focus override. Notice focus lost so we can pause on | |
| * focus lost. e.g. user switches to take a call. | |
| */ | |
| @Override | |
| public void onWindowFocusChanged(boolean hasWindowFocus) { | |
| if (!hasWindowFocus) { | |
| if (thread != null) | |
| thread.pause(); | |
| } | |
| } | |
| /** | |
| * Fetches the animation thread corresponding to this LunarView. | |
| * | |
| * @return the animation thread | |
| */ | |
| public JetBoyThread getThread() { | |
| return thread; | |
| } | |
| /* Callback invoked when the surface dimensions change. */ | |
| public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { | |
| thread.setSurfaceSize(width, height); | |
| } | |
| public void surfaceCreated(SurfaceHolder arg0) { | |
| // start the thread here so that we don't busy-wait in run() | |
| // waiting for the surface to be created | |
| thread.setRunning(true); | |
| thread.start(); | |
| } | |
| public void surfaceDestroyed(SurfaceHolder arg0) { | |
| boolean retry = true; | |
| thread.setRunning(false); | |
| while (retry) { | |
| try { | |
| thread.join(); | |
| retry = false; | |
| } catch (InterruptedException e) { | |
| } | |
| } | |
| } | |
| /** | |
| * A reference to the button to start game over. | |
| * | |
| * @param _buttonRetry | |
| * | |
| */ | |
| public void SetButtonView(Button _buttonRetry) { | |
| mButtonRetry = _buttonRetry; | |
| // mButtonRestart = _buttonRestart; | |
| } | |
| //we reuse the help screen from the end game screen. | |
| public void SetTextView(TextView textView) { | |
| mTextView = textView; | |
| } | |
| } |