| /* |
| * 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. |
| * |
| */ |
| |
| // FIXME: review and cleanup |
| |
| package com.example.android.jetboy; |
| |
| import java.util.Random; |
| import java.util.Timer; |
| import java.util.TimerTask; |
| import java.util.Vector; |
| import java.util.concurrent.ConcurrentLinkedQueue; |
| |
| 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.*; |
| |
| 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; |
| |
| |
| 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; |
| } |
| |
| class JetBoyThread extends Thread implements OnJetEventListener{ |
| |
| /* |
| * State-tracking constants |
| * |
| * we don't actually use all of these in JetBoy, borrowed wholesale from lunar lander. |
| */ |
| 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_READY = 3; |
| public static final int STATE_RUNNING = 4; |
| public static final int STATE_WIN = 5; |
| |
| //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; |
| |
| //event ID within JET file. 80,81, 82 are tested to use. |
| //in this game 80 is used for sending asteroid |
| //82 is used as game time for 1/4 note beat. |
| private final String mSendEvent = "80"; |
| private final String mTimerEvent = "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. |
| //right now we use System.currentMillis |
| //should it use android stuff?? |
| 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(); |
| |
| |
| //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; |
| |
| |
| //this is has "high" (close to top) that 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; |
| |
| //Jet does not pick up volume and pan changes? |
| private boolean mMuteArrayHack = false; |
| |
| Resources mRes; |
| |
| |
| //eight music beds, added a 9th to fix some kind of bug in JET around pans and volume controls |
| //ask Jenn about this |
| 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) { |
| |
| |
| // get handles to some important objects |
| mSurfaceHolder = surfaceHolder; |
| mHandler = handler; |
| mContext = context; |
| mRes = context.getResources(); |
| |
| //this are the mute arrays associated with the music beds in the 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, do it for one beat and then switch to mute array zero |
| //hack for jet bug on pan and mutes |
| 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() |
| { |
| |
| |
| // if (mJet!=null) |
| // { |
| // mJet.release(); |
| //mJet = null; |
| // } |
| |
| mJet = JetPlayer.getJetPlayer(); |
| |
| mJetPlaying = false; |
| |
| //make sure we flush the queue |
| //otherwise left over events from previous gameplay |
| // can hang around. |
| mJet.clearQueue(); |
| |
| // mJet.setStatusUpdateListener(this); |
| mJet.setEventListener(this); |
| |
| Log.d(TAG,"opening jet file"); |
| |
| |
| //mJet.loadJetFile(m_PathToJetFile); |
| |
| mJet.loadJetFile(mContext.getResources().openRawResourceFd(R.raw.level1)); |
| |
| Log.d(TAG,"opening jet file DONE"); |
| |
| |
| mCurrentBed = 0; |
| byte sSegmentID = 0; |
| |
| Log.d(TAG," start queuing jet file"); |
| |
| |
| //this is the main game play music |
| //it is located at segment 0 |
| //it uses lib #0 this allows DLS to play |
| //-1 would mean no DLS |
| //higher numbers untested |
| mJet.queueJetSegment(0, 0, 0, 0,0,sSegmentID); |
| |
| |
| //end game music, loop 4 times normal pitch |
| mJet.queueJetSegment(1, 0, 4, 0,0,sSegmentID); |
| |
| //end game music loop 4 times up an octave |
| mJet.queueJetSegment(1, 0, 4, 1,0,sSegmentID); |
| |
| |
| |
| mJet.setMuteArray(muteMask[8],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. |
| //This will have code to match asteroid lane |
| 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; |
| |
| mMuteArrayHack = false; |
| |
| //set up jet stuff |
| initializeJetPlayer(); |
| |
| mTimer = new Timer(); |
| |
| mDangerWillRobinson = new Vector<Asteroid>(); |
| |
| mExplosion = new Vector<Explosion>(); |
| |
| mInitialized = true; |
| |
| mHitStreak = 0; |
| mHitTotal = 0; |
| // mTimerTotal="1:20"; |
| |
| |
| } |
| |
| 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 postion |
| // based on that. This assumes 140bpm(428ms/beat), we really should compute this |
| // based on properties of the music file under ideal circumstances. This is just |
| // interbeat interpolation, no game state is updated |
| long frameDelta = System.currentTimeMillis() - mLastBeatTime; |
| // mPixelMoveX per beat |
| // This hid the feeling of the asteroids moving to the beat and caused some issues with |
| // explosions not aligned with asteroids last position, so fix that if we use this again. |
| //int asteroidDrawOffset = (int)(mPixelMoveX * frameDelta/428L); |
| // animation frames per beat |
| 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 trigered 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 == 82 ) |
| { |
| // 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 updated 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 subsiquent 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 ) |
| { |
| // if (mJet!=null) |
| // { |
| 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; |
| |
| //Log.d(TAG, "*** LASER ON ***"); |
| 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 ranged but not already missed check if the |
| // key was pressed close enough to the beat to make a hit |
| |
| //there isnt any real logic here. just played with it until the game "felt right" |
| 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, marke 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); |
| |
| String eventID = ""+value; |
| |
| |
| // Check for an event that triggers a new asteroid |
| if (eventID.equalsIgnoreCase(mSendEvent)) |
| { |
| //Log.d(TAG, "~~~~ setting create to true"); |
| |
| doAsteroidCreation(); |
| } |
| |
| mBeatCount++; |
| |
| if (!mMuteArrayHack) |
| { |
| mMuteArrayHack = true; |
| |
| mJet.setMuteArray(muteMask[0], false); |
| |
| } |
| |
| 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 than we track hitStreak to determine music level |
| //if the level has go gone up, call trigger clip otherwise just execute rest of change music bed logic. |
| //could probably be a method call here. |
| 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; |
| mJet.setMuteArray(muteMask[7], false); |
| |
| |
| } |
| } |
| else if (mHitStreak>24) |
| { |
| if (mCurrentBed !=6) |
| { |
| if (mCurrentBed <6) |
| { |
| mJet.triggerClip(6); |
| } |
| |
| mCurrentBed = 6; |
| mJet.setMuteArray(muteMask[6], false); |
| //mJet.triggerClip(6); |
| } |
| } |
| else if (mHitStreak>20 ) |
| { |
| if (mCurrentBed !=5) |
| { |
| if (mCurrentBed <5) |
| { |
| mJet.triggerClip(5); |
| } |
| |
| mCurrentBed = 5; |
| mJet.setMuteArray(muteMask[5], false); |
| //mJet.triggerClip(5); |
| } |
| } |
| else if (mHitStreak>16 ) |
| { |
| if (mCurrentBed !=4) |
| { |
| |
| if (mCurrentBed <4) |
| { |
| mJet.triggerClip(4); |
| } |
| mCurrentBed = 4; |
| mJet.setMuteArray(muteMask[4], false); |
| //mJet.triggerClip(4); |
| } |
| } |
| else if (mHitStreak >12 ) |
| { |
| if (mCurrentBed !=3) |
| { |
| if (mCurrentBed <3) |
| { |
| mJet.triggerClip(3); |
| } |
| mCurrentBed = 3; |
| mJet.setMuteArray(muteMask[3], false); |
| //mJet.triggerClip(3); |
| } |
| } |
| else if (mHitStreak >8 ) |
| { |
| if (mCurrentBed !=2) |
| { |
| if (mCurrentBed <2) |
| { |
| mJet.triggerClip(2); |
| } |
| |
| mCurrentBed = 2; |
| mJet.setMuteArray(muteMask[2], false); |
| //mJet.triggerClip(2); |
| } |
| } |
| else if (mHitStreak>4) |
| { |
| if (mCurrentBed !=1) |
| { |
| |
| if (mCurrentBed <1) |
| { |
| mJet.triggerClip(1); |
| } |
| |
| mJet.setMuteArray(muteMask[1], false); |
| // mJet.triggerClip(1); |
| |
| mCurrentBed = 1; |
| } |
| } |
| } |
| |
| |
| |
| /* |
| try |
| { |
| Log.d(TAG,"onJetEvent: segment =" + segment); |
| Log.d(TAG,"onJetEvent(): track =" + track); |
| Log.d(TAG,"onJetEvent(): channel =" + channel); |
| Log.d(TAG,"onJetEvent(): controller =" + controller); |
| Log.d(TAG,"onJetEvent(): value =" + value); |
| |
| } |
| catch(Exception e1) |
| { |
| Log.e(TAG,"on Jet Event caught " + e1.toString()); |
| } |
| |
| */ |
| } |
| |
| |
| 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 |
| */ |
| //TODO - Modeled from lunar lander Determine if best way to do this. |
| 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 ket state so we dont think a button is pressed that 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. |
| */ |
| //TODO should probably add a pause to the menu button |
| public void pause() { |
| synchronized (mSurfaceHolder) { |
| if (mState == STATE_RUNNING) setGameState(STATE_PAUSE); |
| if (mTimerTask!=null){ |
| mTimerTask.cancel(); |
| } |
| |
| if (mJet!=null) |
| { |
| mJet.pause(); |
| mJet.release(); |
| } |
| } |
| } |
| |
| |
| /** |
| * 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); |
| |
| |
| |
| |
| |
| } |
| |
| |
| /** |
| * required JetPlayer method. Informs listener of when a queue segement |
| * event has been generated. |
| * |
| * @param nbSegments |
| */ |
| public void onJetNumQueuedSegmentUpdate(int nbSegments) { |
| Log.i(TAG,"onJetNumQueuedSegmentUpdate(): nbSegments =" + nbSegments); |
| } |
| |
| |
| |
| /** |
| * required JetPlayer method. A more specific handler. |
| * |
| * @param player |
| * @param nbSegments |
| */ |
| public void onJetNumQueuedSegmentUpdate(JetPlayer player, int nbSegments) { |
| Log.i(TAG,"onJetNumQueuedUpdate(): nbSegs =" + nbSegments); |
| |
| } |
| |
| /** |
| * 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) ); |
| } |
| |
| public void onJetPauseUpdate(JetPlayer player, int paused) { |
| Log.i(TAG,"onJetPauseUpdate(): paused =" + paused); |
| |
| } |
| |
| 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); |
| |
| mContext= context; |
| |
| // create thread only; it's started in surfaceCreated() |
| thread = new JetBoyThread(holder, context, new Handler() { |
| |
| public void handleMessage(Message m) { |
| |
| mTimerView.setText(m.getData().getString("text")); |
| |
| //ok so maybe it isn't really a "lose" |
| //this bit was borrowed from lunar lander and then evolved. |
| //too close to deadline to mess with now. |
| 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) { |
| |
| Log.d(TAG,"@@@FOCUS CHANGED!"); |
| |
| 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; |
| |
| } |
| |
| |
| |
| |
| |
| |
| } |