Samples
Samples
JetBoy
Language: Oxygene, Platform: Cooper, Category: Android
https://github.com/remobjects/ElementsSamples/tree/master/Oxygene/Cooper/Android/JetBoy
-
com.example.android.jetboy
-
References
- android
- Source Files
-
Other Files
- res\values\strings.android-xml
- Properties\AndroidManifest.android-xml
- JETBOY_content_README.txt
- NOTICE
- Readme.txt
- res\drawable\asteroid01.png
- res\drawable\asteroid02.png
- res\drawable\asteroid03.png
- res\drawable\asteroid04.png
- res\drawable\asteroid05.png
- res\drawable\asteroid06.png
- res\drawable\asteroid07.png
- res\drawable\asteroid08.png
- res\drawable\asteroid09.png
- res\drawable\asteroid10.png
- res\drawable\asteroid11.png
- res\drawable\asteroid12.png
- res\drawable\asteroid_explode1.png
- res\drawable\asteroid_explode2.png
- res\drawable\asteroid_explode3.png
- res\drawable\asteroid_explode4.png
- res\drawable\background_a.png
- res\drawable\background_b.png
- res\drawable\icon.png
- res\drawable\intbeam_1.png
- res\drawable\intbeam_2.png
- res\drawable\intbeam_3.png
- res\drawable\intbeam_4.png
- res\drawable\int_timer.png
- res\drawable\laser.png
- res\drawable\ship2_1.png
- res\drawable\ship2_2.png
- res\drawable\ship2_3.png
- res\drawable\ship2_4.png
- res\drawable\ship2_hit_1.png
- res\drawable\ship2_hit_2.png
- res\drawable\ship2_hit_3.png
- res\drawable\ship2_hit_4.png
- res\drawable\title_bg_hori.png
- res\drawable\title_hori.png
- res\layout\main.layout-xml
- res\raw\level1.jet
- res\values\styles.android-xml
-
References
JetBoyThread.pas
namespace com.example.android.jetboy;
{*
* Copyright (C) 2007 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.
*}
interface
// Android JET demonstration code:
// All inline comments related to the use of the JetPlayer class are preceded by "JET info:"
uses
android.content,
android.content.res,
android.graphics,
android.media,
android.os,
android.util,
android.view,
android.widget,
java.util,
java.util.concurrent;
type
// JET info: the JetBoyThread receives all the events from the JET player
// JET info: through the OnJetEventListener interface.
JetBoyThread = public class(Thread, JetPlayer.OnJetEventListener)
private
// 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.
const ANIMATION_FRAMES_PER_BEAT = 4;
// string value for timer display
var mTimerValue: String;
//NOTE: in the original code the two title bitmaps were declared in JetBoyView
// a lazy graphic fudge for the initial title splash
var mTitleBG: Bitmap;
var mTitleBG2: Bitmap;
// The drawable to use as the far background of the animation canvas
var mBackgroundImageFar: Bitmap;
// The drawable to use as the close background of the animation canvas
var mBackgroundImageNear: Bitmap;
// 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.
const NEW_ASTEROID_EVENT = 80;
const TIMER_EVENT = 82;
//NOTE: these track numbers were hardcoded in the original code
const LASER_CHANNEL = 23;
const EXPLOSION_CHANNEL = 24;
// used to track beat for synch of mute/unmute actions
var mBeatCount: Integer := 1;
// our intrepid space boy
var mShipFlying: array of Bitmap := new Bitmap[4];
// the twinkly bit
var mBeam: array of Bitmap := new Bitmap[4];
// the things you are trying to hit
var mAsteroids: array of Bitmap := new Bitmap[12];
// hit animation
var mExplosions: array of Bitmap := new Bitmap[4];
var mTimerShell: Bitmap;
var mLaserShot: Bitmap;
// used to save the beat event system time.
var mLastBeatTime: Int64;
// how much do we move the asteroids per beat?
var mPixelMoveX: Integer := 25;
// the asteroid send events are generated from the Jet File.
// but which land they start in is random.
var mRandom: Random := new Random;
// JET info: the star of our show, a reference to the JetPlayer object.
var mJet: JetPlayer := nil;
var mJetPlaying: Boolean := false;
// Message handler used by thread to interact with TextView
var mHandler: Handler;
// Handle to the surface manager object we interact with
var mSurfaceHolder: SurfaceHolder;
// Handle to the application context, used to e.g. fetch Drawables.
var mContext: Context;
// Thread management members
var mPauseLock: Object := new Object;
var mPaused: Boolean;
// updates the screen clock. Also used for tempo timing.
var mTimer: Timer := nil;
var mTimerTask: TimerTask := nil;
// one second - used to update timer
const mTaskIntervalInMillis = 1000;
// Current height of the surface/canvas.
// see setSurfaceSize
var mCanvasHeight: Integer := 1;
// Current width of the surface/canvas.
// see setSurfaceSize
var mCanvasWidth: Integer := 1;
// used to track the picture to draw for ship animation
var mShipIndex: Integer := 0;
// stores all of the asteroid objects in order
var mDangerWillRobinson: Vector<Asteroid>;
var mExplosion: Vector<Explosion>;
// right to left scroll tracker for near and far BG
var mBGFarMoveX: Integer := 0;
var mBGNearMoveX: Integer := 0;
// how far up (close to top) jet boy can fly
var mJetBoyYMin: Integer := 40;
var mJetBoyX: Integer := 0;
var mJetBoyY: Integer := 0;
//NOTE: various hardcoded numbers used to place the laser beam guide and
//work out asteroid shootability caused problems on larger displays.
//Hence some extra fields and some calculations rather than fixed values.
var mBeamImageXOffset: Integer;
const mGuideOffsetInBeamImage = 40;
const mAsteroidOffsetInImage = 8;
const mAsteroidImageWidth = 64;
const mAsteroidImageHeight = 64;
var mNumAsteroidPaths: Integer;
// this is the pixel position of the laser beam guide.
var mAsteroidMoveLimitX: Integer; //NOTE: was hardcoded at 110;
// how far up asteroid can be painted
var mAsteroidMinY: Integer := 40;
var mJetBoyView: JetBoyView;
// array to store the mute masks that are applied during game play to respond to
// the player's hit streaks
var muteMask: array of array of Boolean;
method initializeJetPlayer;
method setInitialGameState;
method doDraw(aCanvas: Canvas);
method doDrawRunning(aCanvas: Canvas);
method doDrawReady(aCanvas: Canvas);
method doDrawPlay(aCanvas: Canvas);
method doAsteroidCreation;
method doAsteroidAnimation(aCanvas: Canvas);
protected
// Queue for GameEvents
var mEventQueue: ConcurrentLinkedQueue<GameEvent> := new ConcurrentLinkedQueue<GameEvent>;
// Context for processKey to maintain state accross frames
var mKeyContext: Object := nil;
method updateGameState;
method updateLaser(inputContext: Object);
method updateAsteroids(inputContext: Object);
method updateExplosions(inputContext: Object);
method processKeyEvent(evt: KeyGameEvent; ctx: Object): Object;
method processJetEvent(player: JetPlayer; segment: SmallInt; track, channel, controller, value: SByte);
assembly
// has laser been fired and for how long?
// user for fx logic on laser fire
var mLaserOn: Boolean := false;
var mLaserFireTime: Int64 := 0;
var mRes: Resources;
public
const TAG = 'JetBoy';
// State-tracking constants.
const STATE_START = - 1;
const STATE_PLAY = 0;
const STATE_LOSE = 1;
const STATE_PAUSE = 2;
const STATE_RUNNING = 3;
var mInitialized: Boolean := false;
// the timer display in seconds
var mTimerLimit: Integer;
// used for internal timing logic.
const TIMER_LIMIT = 72;
// start, play, running, lose are the states we use
var mState: Integer;
constructor(aJetBoyView: JetBoyView; aSurfaceHolder: SurfaceHolder; aContext: Context; aHandler: Handler);
method run; override;
method onJetNumQueuedSegmentUpdate(player: JetPlayer; nbSegments: Integer);
method onJetEvent(player: JetPlayer; segment: SmallInt; track, channel, controller, value: SByte);
method onJetPauseUpdate(player: JetPlayer; paused: Integer);
method onJetUserIdUpdate(player: JetPlayer; userId, repeatCount: Integer);
method doKeyDown(keyCode: Integer; msg: KeyEvent): Boolean;
method doKeyUp(keyCode: Integer; msg: KeyEvent): Boolean;
method doCountDown;
method setSurfaceSize(width, height: Integer);
method pause;
method unPause;
method getGameState: Integer;
method setGameState(mode: Integer);
end;
CountdownTimerTask nested in JetBoyThread = private class(TimerTask)
private
_thread: JetBoyThread;
public
constructor(aThread: JetBoyThread);
method run; override;
end;
implementation
/// <summary>
/// This is the constructor for the main worker bee
/// </summary>
constructor JetBoyThread(aJetBoyView: JetBoyView; aSurfaceHolder: SurfaceHolder; aContext: Context; aHandler: Handler);
begin
mJetBoyView := aJetBoyView;
mSurfaceHolder := aSurfaceHolder;
mHandler := aHandler;
mContext := aContext;
mRes := aContext.Resources;
// JET info: this are the mute arrays associated with the music beds in the
// JET info: JET file
//Alocate all the muteMasks
muteMask := new array of Boolean[9];
for ii: Integer := 0 to pred(muteMask.length) do
muteMask[ii] := new Boolean[32];
for ii: Integer := 0 to pred(8) do
for xx: Integer := 0 to pred(32) do
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 xx: Integer := 0 to pred(32) do
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);
mBeamImageXOffset := (mShipFlying[0].Width div 2) + 40;
mAsteroidMoveLimitX := mBeamImageXOffset + mGuideOffsetInBeamImage;
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);
mTimerValue := mContext.String[R.string.timer];
end;
/// <summary>
/// Does the grunt work of setting up initial jet requirements
/// </summary>
method JetBoyThread.initializeJetPlayer();
begin
// 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.JetPlayer;
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.EventListener := self;
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.Resources.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');
mJetBoyView.mCurrentBed := 0;
var sSegmentID: SByte := 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')
end;
/// <summary>
/// the heart of the worker bee
/// </summary>
method JetBoyThread.run;
begin
while true do
begin
var c: Canvas := nil;
if mState = STATE_RUNNING then
begin
// Process any input and apply it to the game state
updateGameState;
if not mJetPlaying then
begin
mInitialized := false;
Log.d(TAG, '------> STARTING JET PLAY');
mJet.play;
mJetPlaying := true
end;
// kick off the timer task for counter update if not already
// initialized
if mTimerTask = nil then
begin
mTimerTask := new CountdownTimerTask(self);
mTimer.schedule(mTimerTask, mTaskIntervalInMillis)
end
end
else
if (mState = STATE_PLAY) and not mInitialized then
setInitialGameState
else
if mState = STATE_LOSE then
mInitialized := false;
try
c := mSurfaceHolder.lockCanvas(nil);
//NOTE: original SDK demo had this commented out
locking mSurfaceHolder do
begin
//NOTE: original SDK demo did not check for nil here
//On some devices immediately after pause (on switching away) c will be nil
if c <> nil then
doDraw(c);
end;
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 <> nil then
mSurfaceHolder.unlockCanvasAndPost(c)
end;
//If the thread has been paused, then block until we are
//notified we can proceed
locking mPauseLock do
while mPaused do
try
mPauseLock.wait
except
on e: InterruptedException do
{ whatever };
end
end;
end;
// JET info: JET event listener interface implementation:
/// <summary>
/// required OnJetEventListener method. Notifications for queue updates
/// </summary>
/// <param name="player"></param>
/// <param name="nbSegments"></param>
method JetBoyThread.onJetNumQueuedSegmentUpdate(player: JetPlayer; nbSegments: Integer);
begin
//Log.i(TAG, "onJetNumQueuedUpdate(): nbSegs =" + nbSegments);
end;
// JET info: JET event listener interface implementation:
/// <summary>
/// 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.
/// </summary>
/// <param name="player"></param>
/// <param name="segment"></param>
/// <param name="track"></param>
/// <param name="channel"></param>
/// <param name="controller"></param>
/// <param name="value"></param>
method JetBoyThread.onJetEvent(player: JetPlayer; segment: SmallInt; track, channel, controller, value: SByte);
begin
// 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));
end;
// JET info: JET event listener interface implementation:
method JetBoyThread.onJetPauseUpdate(player: JetPlayer; paused: Integer);
begin
//Log.i(TAG, "onJetPauseUpdate(): paused =" + paused);
end;
// JET info: JET event listener interface implementation:
method JetBoyThread.onJetUserIdUpdate(player: JetPlayer; userId, repeatCount: Integer);
begin
//Log.i(TAG, "onJetUserIdUpdate(): userId =" + userId + " repeatCount=" + repeatCount);
end;
/// <summary>
/// Add key press input to the GameEvent queue
/// </summary>
/// <param name="keyCode"></param>
/// <param name="msg"></param>
/// <returns></returns>
method JetBoyThread.doKeyDown(keyCode: Integer; msg: KeyEvent): Boolean;
begin
mEventQueue.add(new KeyGameEvent(keyCode, false, msg));
exit true;
end;
/// <summary>
/// Add key press input to the GameEvent queue
/// </summary>
/// <param name="keyCode"></param>
/// <param name="msg"></param>
/// <returns></returns>
method JetBoyThread.doKeyUp(keyCode: Integer; msg: KeyEvent): Boolean;
begin
mEventQueue.add(new KeyGameEvent(keyCode, true, msg));
exit true;
end;
/// <summary>
/// Does the work of updating timer
/// </summary>
method JetBoyThread.doCountDown;
begin
// Log.d(TAG,"Time left is " + mTimerLimit);
dec(mTimerLimit);
try
// subtract one minute and see what the result is.
var moreThanMinute: Integer := mTimerLimit - 60;
if moreThanMinute >= 0 then
begin
if moreThanMinute > 9 then
mTimerValue := '1:' + moreThanMinute
else
mTimerValue := '1:0' + moreThanMinute
end
else
begin
if mTimerLimit > 9 then
mTimerValue := '0:' + mTimerLimit
else
mTimerValue := '0:0' + mTimerLimit
end;
except
on e1: Exception do
Log.e(TAG, 'doCountDown threw ' + e1.toString)
end;
var msg: Message := mHandler.obtainMessage;
var b: Bundle := new Bundle;
b.putString('text', mTimerValue);
// time's up
if mTimerLimit = 0 then
begin
b.putString('STATE_LOSE', '' + STATE_LOSE);
mTimerTask := nil;
mState := STATE_LOSE
end
else
begin
mTimerTask := new CountdownTimerTask(self);
mTimer.schedule(mTimerTask, mTaskIntervalInMillis)
end;
// 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.Data := b;
mHandler.sendMessage(msg);
end;
/// <summary>
/// Callback invoked when the surface dimensions change.
/// </summary>
method JetBoyThread.setSurfaceSize(width, height: Integer);
begin
// synchronized to make sure these all change atomically
locking mSurfaceHolder do
begin
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);
//NOTE: new code to work out no. of asteroid paths based on height
//minus the non-beam section at top and bottom
mNumAsteroidPaths := (mCanvasHeight - mAsteroidMinY - mAsteroidMinY) div mAsteroidImageHeight;
end;
end;
/// <summary>
/// Pauses the physics update & animation.
/// </summary>
method JetBoyThread.pause;
begin
//Check for already-paused state
if mPaused then
exit;
locking mSurfaceHolder do
begin
//NOTE: JetBoy doesn't seem to have any support for a PAUSE state, so we'll ignore this
//if mState = STATE_RUNNING then
// setGameState(STATE_PAUSE);
if mTimerTask <> nil then
begin
mTimerTask.cancel;
//NOTE: original code did not set mTimerTask to nil
mTimerTask := nil
end;
if mJet <> nil then
begin
Log.d(TAG, ' pausing JET');
if mJetPlaying then
mJet.pause;
Log.d(TAG, ' paused JET');
//NOTE: this wasn't in the original code
mJetPlaying := false;
end;
end;
//NOTE: new thread management code
//To pause the thread we set a flag to true
locking mPauseLock do
mPaused := true;
end;
/// <summary>
/// Resumes the physics update & animation.
/// </summary>
method JetBoyThread.unPause;
begin
//NOTE: new thread management code
//To resume the thread we unset the flag and notify all waiting on the lock
locking mPauseLock do
begin
mPaused := false;
mPauseLock.notifyAll
end;
end;
/// <summary>
/// returns the current int value of game state as defined by state
/// tracking constants
/// </summary>
method JetBoyThread.getGameState: Integer;
begin
locking mSurfaceHolder do
exit mState
end;
/// <summary>
/// Sets the game mode. That is, whether we are running, paused, in the
/// failure state, in the victory state, etc.
/// see setState(int, CharSequence)
/// </summary>
/// <param name="mode">one of the STATE_* constants</param>
method JetBoyThread.setGameState(mode: Integer);
begin
locking mSurfaceHolder do
begin
// change state if needed
if mState <> mode then
mState := mode;
if mState = STATE_PLAY then
begin
var res: Resources := mContext.Resources;
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)
end
else
if mState = STATE_RUNNING then
begin
// 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 := nil
end
end;
end;
method JetBoyThread.doDraw(aCanvas: Canvas);
begin
if mState = STATE_RUNNING then
doDrawRunning(aCanvas)
else
if mState = STATE_START then
doDrawReady(aCanvas)
else
if mState in [STATE_PLAY, STATE_LOSE] then
begin
if mTitleBG2 = nil then
mTitleBG2 := BitmapFactory.decodeResource(mRes, R.drawable.title_bg_hori);
doDrawPlay(aCanvas)
end; // end state play block
end;
/// <summary>
/// Draws current state of the game Canvas.
/// </summary>
/// <param name="aCanvas"></param>
method JetBoyThread.doDrawRunning(aCanvas: Canvas);
begin
// decrement the far background
mBGFarMoveX := mBGFarMoveX - 1;
// decrement the near background
mBGNearMoveX := mBGNearMoveX - 4;
// calculate the wrap factor for matching image draw
var newFarX: Integer := mBackgroundImageFar.Width - - mBGFarMoveX;
// if we have scrolled all the way, reset to start
if newFarX <= 0 then
begin
mBGFarMoveX := 0;
// only need one draw
aCanvas.drawBitmap(mBackgroundImageFar, mBGFarMoveX, 0, nil)
end
else
begin
// need to draw original and wrap
aCanvas.drawBitmap(mBackgroundImageFar, mBGFarMoveX, 0, nil);
aCanvas.drawBitmap(mBackgroundImageFar, newFarX, 0, nil)
end;
// same story different image...
// TODO possible method call
var newNearX: Integer := mBackgroundImageNear.Width - -mBGNearMoveX;
if newNearX <= 0 then
begin
mBGNearMoveX := 0;
aCanvas.drawBitmap(mBackgroundImageNear, mBGNearMoveX, 0, nil)
end
else
begin
aCanvas.drawBitmap(mBackgroundImageNear, mBGNearMoveX, 0, nil);
aCanvas.drawBitmap(mBackgroundImageNear, newNearX, 0, nil)
end;
doAsteroidAnimation(aCanvas);
//NOTE: original code used hardcoded numbers here (X = 50 + 21), which made things
//very tight for the space ship on larger displays.
//Also the beam wasn't stretched across the full vertical space of larger displays
//TODO: remove this from the oft-called drawing code and scale the bitmap when the surface changes
var src := new Rect(0, 0, mBeam[mShipIndex].Width, mBeam[mShipIndex].Height);
var dest := new Rect(mBeamImageXOffset, 0, mBeam[mShipIndex].Width + mBeamImageXOffset, aCanvas.Height);
aCanvas.drawBitmap(mBeam[mShipIndex], src, dest, nil);
inc(mShipIndex);
if mShipIndex = 4 then
mShipIndex := 0;
// draw the space ship in the same lane as the next asteroid
aCanvas.drawBitmap(mShipFlying[mShipIndex], mJetBoyX, mJetBoyY, nil);
if mLaserOn then
aCanvas.drawBitmap(mLaserShot, mJetBoyX + mShipFlying[0].Width, mJetBoyY + (mShipFlying[0].Height div 2), nil);
// tick tock
aCanvas.drawBitmap(mTimerShell, mCanvasWidth - mTimerShell.Width, 0, nil)
end;
method JetBoyThread.doDrawReady(aCanvas: Canvas);
begin
//NOTE: original code did not resize the bitmap, so it didn't cover larger displays
//TODO: remove this from the oft-called drawing code and scale the bitmap when the surface changes
var src := new Rect(0, 0, mTitleBG.Width, mTitleBG.Height);
var dest := new Rect(0, 0, aCanvas.Width, aCanvas.Height);
aCanvas.drawBitmap(mTitleBG, src, dest, nil);
end;
method JetBoyThread.doDrawPlay(aCanvas: Canvas);
begin
//NOTE: original code did not resize the bitmap, so it didn't cover larger displays
//TODO: remove this from the oft-called drawing code and scale the bitmap when the surface changes
var src := new Rect(0, 0, mTitleBG2.Width, mTitleBG2.Height);
var dest := new Rect(0, 0, aCanvas.Width, aCanvas.Height);
aCanvas.drawBitmap(mTitleBG2, src, dest, nil);
end;
method JetBoyThread.setInitialGameState;
begin
mTimerLimit := TIMER_LIMIT;
mJetBoyY := mJetBoyYMin;
// set up jet stuff
initializeJetPlayer;
mTimer := new Timer;
mDangerWillRobinson := new Vector<Asteroid>;
mExplosion := new Vector<Explosion>;
mInitialized := true;
mJetBoyView.mHitStreak := 0;
mJetBoyView.mHitTotal := 0
end;
method JetBoyThread.doAsteroidCreation;
begin
// Log.d(TAG, 'asteroid created');
var ast: Asteroid := new Asteroid;
//NOTE: original code hardcoded 4 asteroid paths and hardcoded
//a value of 63 to multiply drawIndex by
var drawIndex: Integer := mRandom.nextInt(mNumAsteroidPaths);
ast.mDrawY := mAsteroidMinY + (drawIndex * mAsteroidImageHeight);
ast.mDrawX := mCanvasWidth - mAsteroids[0].Width;
ast.mStartTime := System.currentTimeMillis;
mDangerWillRobinson.&add(ast);
end;
method JetBoyThread.doAsteroidAnimation(aCanvas: Canvas);
begin
if ((mDangerWillRobinson = nil) or (mDangerWillRobinson.size = 0)) and
((mExplosion <> nil) and (mExplosion.size = 0)) then
exit;
// 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
var frameDelta: Int64 := System.currentTimeMillis - mLastBeatTime;
var animOffset: Integer := ANIMATION_FRAMES_PER_BEAT * frameDelta div 428;
for i: Integer := mDangerWillRobinson.size - 1 downto 0 do
begin
var ast: Asteroid := mDangerWillRobinson.elementAt(i);
if not ast.mMissed then
mJetBoyY := ast.mDrawY;
// Log.d(TAG, ' drawing asteroid ' + ii.toString + ' at ' + ast.mDrawX );
aCanvas.drawBitmap(mAsteroids[(ast.mAniIndex + animOffset) mod mAsteroids.length], ast.mDrawX, ast.mDrawY, nil);
end;
for i: Integer := mExplosion.size - 1 downto 0 do
begin
var ex: Explosion := mExplosion.elementAt(i);
aCanvas.drawBitmap(mExplosions[(ex.mAniIndex + animOffset) mod mExplosions.length], ex.mDrawX, ex.mDrawY, nil);
end;
end;
/// <summary>
/// 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.
/// </summary>
method JetBoyThread.updateGameState;
begin
// Process any game events and apply them
while true do
begin
var evt: GameEvent := mEventQueue.poll;
if evt = nil then
break;
// Log.d(TAG,'*** EVENT = ' + event);
// Process keys tracking the input context to pass in to later
// calls
if evt is KeyGameEvent then
begin
// Process the key for affects other then asteroid hits
mKeyContext := processKeyEvent(KeyGameEvent(evt), 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)
end
else
if evt is JetGameEvent then
begin
var jetEvent: JetGameEvent := JetGameEvent(evt);
// Only update state on a timer event
if jetEvent.value = TIMER_EVENT then
begin
// 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)
end;
processJetEvent(jetEvent.player, jetEvent.segment, jetEvent.track, jetEvent.channel, jetEvent.controller, jetEvent.value)
end
end;
end;
/// <summary>
/// This method updates the laser status based on user input and shot
/// duration
/// </summary>
method JetBoyThread.updateLaser(inputContext: Object);
begin
// Lookup the time of the fire event if there is one
var keyTime: Int64 := if inputContext = nil then 0 else 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 and (System.currentTimeMillis - mLaserFireTime > 400) then
mLaserOn := false
else
if System.currentTimeMillis - mLaserFireTime > 300 then
begin
// JET info: the laser sound is on track 23, we mute it (true) right away (false)
mJet.setMuteFlag(LASER_CHANNEL, true, false)
end;
// 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 not mLaserOn and (System.currentTimeMillis - keyTime <= 400) then
begin
mLaserOn := true;
mLaserFireTime := keyTime;
// JET info: unmute the laser track (false) right away (false)
mJet.setMuteFlag(LASER_CHANNEL, false, false)
end;
end;
/// <summary>
/// Update asteroid state including position and laser hit status.
/// </summary>
method JetBoyThread.updateAsteroids(inputContext: Object);
begin
if (mDangerWillRobinson = nil) or (mDangerWillRobinson.size = 0) then
exit;
for i: Integer := pred(mDangerWillRobinson.size) downto 0 do
begin
var ast: 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
//NOTE: original code hardcoded a figure of 20 instead of mAsteroidOffsetInImage
if (ast.mDrawX <= mAsteroidMoveLimitX + mAsteroidOffsetInImage) and not ast.mMissed then
begin
// If the laser was fired on the beat destroy the asteroid
if mLaserOn then
begin
// Track hit streak for adjusting music
inc(mJetBoyView.mHitStreak);
inc(mJetBoyView.mHitTotal);
// replace the asteroid with an explosion
var ex: Explosion := new Explosion;
ex.mAniIndex := 0;
ex.mDrawX := ast.mDrawX;
ex.mDrawY := ast.mDrawY;
mExplosion.add(ex);
mJet.setMuteFlag(EXPLOSION_CHANNEL, false, false);
mDangerWillRobinson.removeElementAt(i);
// This asteroid has been removed process the next one
continue;
end
else
begin
// 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
ast.mMissed := true;
dec(mJetBoyView.mHitStreak);
if mJetBoyView.mHitStreak < 0 then
mJetBoyView.mHitStreak := 0;
end;
end;
// Update the asteroids position, even missed ones keep moving
ast.mDrawX := ast.mDrawX - mPixelMoveX;
// Update asteroid animation frame
ast.mAniIndex := (ast.mAniIndex + ANIMATION_FRAMES_PER_BEAT) mod mAsteroids.length;
// if we have scrolled off the screen
if ast.mDrawX < 0 then
mDangerWillRobinson.removeElementAt(i);
end;
end;
/// <summary>
/// This method updates explosion animation and removes them once they
/// have completed.
/// </summary>
method JetBoyThread.updateExplosions(inputContext: Object);
begin
if (mExplosion = nil) or (mExplosion.size = 0) then
exit;
for i: Integer := pred(mExplosion.size) downto 0 do
begin
var ex: Explosion := mExplosion.elementAt(i);
ex.mAniIndex := ex.mAniIndex + ANIMATION_FRAMES_PER_BEAT;
// When the animation completes remove the explosion
if ex.mAniIndex > 3 then
begin
mJet.setMuteFlag(EXPLOSION_CHANNEL, true, false);
mJet.setMuteFlag(LASER_CHANNEL, true, false);
mExplosion.removeElementAt(i)
end;
end;
end;
/// <summary>
/// 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.
/// </summary>
/// <param name="evt"></param>
/// <param name="ctx"></param>
/// <returns></returns>
method JetBoyThread.processKeyEvent(evt: KeyGameEvent; ctx: Object): Object;
begin
// 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 evt.up then
begin
if evt.keyCode = KeyEvent.KEYCODE_DPAD_CENTER then
exit nil
end
else
begin
if (evt.keyCode = KeyEvent.KEYCODE_DPAD_CENTER) and (ctx = nil) then
exit evt
end;
// Return the context unchanged
exit ctx;
end;
/// <summary>
/// This method handles the state updates that can be caused by JET
/// events.
/// </summary>
method JetBoyThread.processJetEvent(player: JetPlayer; segment: SmallInt; track, channel, controller, value: SByte);
begin
// 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 then
doAsteroidCreation;
inc(mBeatCount);
if mBeatCount > 4 then
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 gone up, call a corresponding trigger clip, otherwise just
// execute the rest of the music bed change logic.
if mBeatCount = 1 then
begin
// do it back wards so you fall into the correct one
if mJetBoyView.mHitStreak > 28 then
begin
// did the bed change?
if mJetBoyView.mCurrentBed <> 7 then
begin
// did it go up?
if mJetBoyView.mCurrentBed < 7 then
mJet.triggerClip(7);
mJetBoyView.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)
end
end
else
if mJetBoyView.mHitStreak > 24 then
begin
if mJetBoyView.mCurrentBed <> 6 then
begin
if mJetBoyView.mCurrentBed < 6 then
begin
// JET info: quite a few asteroids hit, trigger the clip with the guy's
// JET info: voice that encourages the player.
mJet.triggerClip(6)
end;
mJetBoyView.mCurrentBed := 6;
mJet.setMuteArray(muteMask[6], false)
end
end
else
if mJetBoyView.mHitStreak > 20 then
begin
if mJetBoyView.mCurrentBed <> 5 then
begin
if mJetBoyView.mCurrentBed < 5 then
mJet.triggerClip(5);
mJetBoyView.mCurrentBed := 5;
mJet.setMuteArray(muteMask[5], false)
end
end
else
if mJetBoyView.mHitStreak > 16 then
begin
if mJetBoyView.mCurrentBed <> 4 then
begin
if mJetBoyView.mCurrentBed < 4 then
mJet.triggerClip(4);
mJetBoyView.mCurrentBed := 4;
mJet.setMuteArray(muteMask[4], false)
end
end
else
if mJetBoyView.mHitStreak > 12 then
begin
if mJetBoyView.mCurrentBed <> 3 then
begin
if mJetBoyView.mCurrentBed < 3 then
mJet.triggerClip(3);
mJetBoyView.mCurrentBed := 3;
mJet.setMuteArray(muteMask[3], false)
end
end
else
if mJetBoyView.mHitStreak > 8 then
begin
if mJetBoyView.mCurrentBed <> 2 then
begin
if mJetBoyView.mCurrentBed < 2 then
mJet.triggerClip(2);
mJetBoyView.mCurrentBed := 2;
mJet.setMuteArray(muteMask[2], false)
end
end
else
if mJetBoyView.mHitStreak > 4 then
begin
if mJetBoyView.mCurrentBed <> 1 then
begin
if mJetBoyView.mCurrentBed < 1 then
mJet.triggerClip(1);
mJet.setMuteArray(muteMask[1], false);
mJetBoyView.mCurrentBed := 1
end
end
end;
end;
constructor JetBoyThread.CountdownTimerTask(aThread: JetBoyThread);
begin
inherited constructor;
_thread := aThread
end;
method JetBoyThread.CountdownTimerTask.run;
begin
_thread.doCountDown
end;
end.
