blob: 2bd6a05862721c454bb22064f32124fba1f9c4d3 [file] [log] [blame]
/*
* 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;
}
}