| /* |
| * 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; |
| |
| } |
| } |