LunarViewpublic class LunarView extends android.view.View View that draws, takes keystrokes, etc. for a simple LunarLander game.
Has a mode which RUNNING, PAUSED, etc. Has a x, y, dx, dy, ... capturing the
current ship physics. All x/y etc. are measured with (0,0) at the lower left.
updatePhysics() advances the physics based on realtime. draw() renders the
ship, and does an invalidate() to prompt another draw() as soon as possible
by the system. |
Fields Summary |
---|
public static final int | READY | public static final int | RUNNING | public static final int | PAUSE | public static final int | LOSE | public static final int | WIN | public static final int | EASY | public static final int | MEDIUM | public static final int | HARD | public static final int | FIRE_ACCEL_SEC | public static final int | DOWN_ACCEL_SEC | public static final int | FUEL_SEC | public static final int | SLEW_SEC | public static final int | FUEL_INIT | public static final int | FUEL_MAX | public static final int | SPEED_INIT | public static final int | SPEED_MAX | public static final int | SPEED_HYPERSPACE | public static final int | TARGET_SPEED | public static final double | TARGET_WIDTH | public static final int | TARGET_ANGLE | public static final int | BAR_HEIGHTPixel height of the fuel/speed bar. | public static final int | BARPixel width of the fuel/speed bar. | public static final int | PAD_HEIGHTHeight of the landing pad off the bottom. | public static final int | BOTTOM_PADDINGExtra pixels below the landing gear in the images | private int | mModeThe state of the game. One of READY, RUNNING, PAUSE, LOSE, or WIN | private int | mDifficultyCurrent difficulty -- amount of fuel, allowed angle, etc.
Default is MEDIUM. | private double | mXX of lander center. | private double | mYY of lander center. | private double | mDXVelocity dx. | private double | mDYVelocity dy. | private double | mHeadingLander heading in degrees, with 0 up, 90 right.
Kept in the range 0..360. | private int | mLanderWidthPixel width of lander image. | private int | mLanderHeightPixel height of lander image. | private int | mRotatingCurrently rotating, -1 left, 0 none, 1 right. | private int | mGoalXX of the landing pad. | private int | mGoalSpeedAllowed speed. | private int | mGoalAngleAllowed angle. | private int | mGoalWidthWidth of the landing pad. | private int | mWinsInARowNumber of wins in a row. | private double | mFuelFuel remaining | private boolean | mEngineFiringIs the engine burning? | private long | mLastTimeUsed to figure out elapsed time between frames | private android.graphics.Paint | mLinePaintPaint to draw the lines on screen. | private android.graphics.Paint | mLinePaintBad"Bad" speed-too-high variant of the line color. | private android.graphics.drawable.Drawable | mLanderImageWhat to draw for the Lander in its normal state | private android.graphics.drawable.Drawable | mFiringImageWhat to draw for the Lander when the engine is firing | private android.graphics.drawable.Drawable | mCrashedImageWhat to draw for the Lander when it has crashed | private android.widget.TextView | mStatusTextPointer to the text view to display "Paused.." etc. | private android.graphics.RectF | mScratchRectScratch rect object. |
Constructors Summary |
---|
public LunarView(android.content.Context context, android.util.AttributeSet attrs, Map inflateParams)
super(context, attrs, inflateParams);
mLanderImage = context.getResources().getDrawable(
R.drawable.lander_plain);
mFiringImage = context.getResources().getDrawable(
R.drawable.lander_firing);
mCrashedImage = context.getResources().getDrawable(
R.drawable.lander_crashed);
// Use the regular lander image as the model size
// for all the images.
mLanderWidth = mLanderImage.getIntrinsicWidth();
mLanderHeight = mLanderImage.getIntrinsicHeight();
setBackground(R.drawable.earthrise);
// Make sure we get keys
setFocusable(true);
// Initialize paints for speedometer
mLinePaint = new Paint();
mLinePaint.setAntiAlias(true);
mLinePaint.setARGB(255, 0, 255, 0);
mLinePaintBad = new Paint();
mLinePaintBad.setAntiAlias(true);
mLinePaintBad.setARGB(255, 120, 180, 0);
mScratchRect= new RectF(0,0,0,0);
mWinsInARow = 0;
mDifficulty = MEDIUM;
// initial show-up of lander (not yet playing)
mX = mLanderWidth;
mY = mLanderHeight * 2;
mFuel = FUEL_INIT;
mDX = 0;
mDY = 0;
mHeading = 0;
mEngineFiring = true;
|
Methods Summary |
---|
public void | doPause()Pauses from the running state.
if (mMode == RUNNING) setMode(PAUSE);
| public void | doResume()Resumes from a pause.
// Move the real time clock up to now
mLastTime = System.currentTimeMillis() + 100;
setMode(RUNNING);
| public void | doStart()Starts the game, setter parameters for the current
difficulty.
// First set the game for Medium difficulty
mFuel = FUEL_INIT;
mEngineFiring = false;
mGoalWidth = (int) (mLanderWidth * TARGET_WIDTH);
mGoalSpeed = TARGET_SPEED;
mGoalAngle = TARGET_ANGLE;
//mGoalAngle = TARGET_ANGLE;
int speedInit = SPEED_INIT;
// Adjust difficulty params for EASY/HARD
if (mDifficulty == EASY) {
mFuel = mFuel * 3 / 2;
mGoalWidth = mGoalWidth * 4 / 3;
mGoalSpeed = mGoalSpeed * 3 / 2;
mGoalAngle = mGoalAngle * 4 / 3;
speedInit = speedInit * 3 / 4;
}
else if (mDifficulty == HARD) {
mFuel = mFuel * 7 / 8;
mGoalWidth = mGoalWidth * 3 / 4;
mGoalSpeed = mGoalSpeed * 7 / 8;
speedInit = speedInit * 4 / 3;
}
mX = getWidth()/2;
mY = getHeight() - mLanderHeight/2;
// start with a little random motion
mDY = Math.random() * -speedInit;
mDX = Math.random() * 2*speedInit - speedInit;
mHeading = 0;
// Figure initial spot for landing, not too near center
while (true) {
mGoalX = (int)(Math.random() * (getWidth() - mGoalWidth));
if (Math.abs(mGoalX - (mX - mLanderWidth/2)) > getWidth()/6) break;
}
mLastTime = System.currentTimeMillis() + 100;
setMode(RUNNING);
| protected void | onDraw(android.graphics.Canvas canvas)Standard override of View.draw.
Draws the ship and fuel/speed bars.
super.onDraw(canvas);
if (mMode == RUNNING) updatePhysics();
int screenWidth = getWidth();
int screenHeight = getHeight();
int yTop = screenHeight - ((int)mY + mLanderHeight/2);
int xLeft = (int)mX - mLanderWidth/2;
// Draw fuel rect
int fuelWidth = (int)(BAR * mFuel / FUEL_MAX);
mScratchRect.set(4, 4, 4 + fuelWidth, 4 + BAR_HEIGHT);
canvas.drawRect(mScratchRect, mLinePaint);
double speed = Math.sqrt(mDX*mDX + mDY*mDY);
int speedWidth = (int)(BAR * speed / SPEED_MAX);
if (speed <= mGoalSpeed) {
mScratchRect.set(4 + BAR + 4, 4, 4 + BAR + 4 + speedWidth, 4 + BAR_HEIGHT);
canvas.drawRect(mScratchRect, mLinePaint);
} else {
// Draw the bad color in back, with the good color in front of it
mScratchRect.set(4 + BAR + 4, 4, 4 + BAR + 4 + speedWidth, 4 + BAR_HEIGHT);
canvas.drawRect(mScratchRect, mLinePaintBad);
int goalWidth = (BAR * mGoalSpeed / SPEED_MAX);
mScratchRect.set(4 + BAR + 4, 4, 4 + BAR + 4 + goalWidth, 4 + BAR_HEIGHT);
canvas.drawRect(mScratchRect, mLinePaint);
}
// Draw the landing pad
canvas.drawLine(mGoalX, 1 + screenHeight - PAD_HEIGHT,
mGoalX + mGoalWidth, 1 + screenHeight - PAD_HEIGHT, mLinePaint);
// Draw the ship with its current rotation
canvas.save();
canvas.rotate((float)mHeading, (float)mX, screenHeight - (float)mY);
if (mMode == LOSE) {
mCrashedImage.setBounds(xLeft, yTop, xLeft+mLanderWidth, yTop+mLanderHeight);
mCrashedImage.draw(canvas);
}
else if (mEngineFiring) {
mFiringImage.setBounds(xLeft, yTop, xLeft+mLanderWidth, yTop+mLanderHeight);
mFiringImage.draw(canvas);
}
else {
mLanderImage.setBounds(xLeft, yTop, xLeft+mLanderWidth, yTop+mLanderHeight);
mLanderImage.draw(canvas);
}
/*
* Our animation strategy is that each draw() does an invalidate(),
* so we get a series of draws. This is a known animation strategy
* within Android, and the system throttles the draws down to match
* the refresh rate.
*/
if (mMode == RUNNING) {
// Invalidate a space around the current lander + the bars at the top.
// Note: invalidating a relatively small part of the screen to draw
// is a good optimization. In this case, the bars and the ship
// may be far apart, limiting the value of the optimization.
invalidate(xLeft-20, yTop-20, xLeft+mLanderWidth+20, yTop+mLanderHeight+20);
invalidate(0, 0, screenWidth, 4 + BAR_HEIGHT);
}
canvas.restore();
| public boolean | onKeyDown(int keyCode, android.view.KeyEvent msg)Standard override to get key events.
boolean handled = false;
boolean okStart = keyCode == KeyEvent.KEYCODE_DPAD_UP ||
keyCode == KeyEvent.KEYCODE_DPAD_DOWN ||
keyCode == KeyEvent.KEYCODE_S;
boolean center = keyCode == KeyEvent.KEYCODE_DPAD_UP;
// ready-to-start -> start
if (okStart && (mMode == READY || mMode == LOSE || mMode == WIN)) {
doStart();
handled = true;
}
// paused -> running
else if (mMode == PAUSE && okStart) {
doResume();
handled = true;
} else if (mMode == RUNNING) {
// center/space -> fire
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER ||
keyCode == KeyEvent.KEYCODE_SPACE) {
setFiring(true);
handled = true;
// left/q -> left
} else if (keyCode==KeyEvent.KEYCODE_DPAD_LEFT ||
keyCode == KeyEvent.KEYCODE_Q) {
mRotating = -1;
handled = true;
// right/w -> right
} else if (keyCode==KeyEvent.KEYCODE_DPAD_RIGHT ||
keyCode == KeyEvent.KEYCODE_W) {
mRotating = 1;
handled = true;
// up -> pause
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
doPause();
handled = true;
}
}
return handled;
| public boolean | onKeyUp(int keyCode, android.view.KeyEvent msg)Standard override for key-up. We actually care about these,
so we can turn off the engine or stop rotating.
boolean handled = false;
if (mMode == RUNNING) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER ||
keyCode == KeyEvent.KEYCODE_SPACE) {
setFiring(false);
handled = true;
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT ||
keyCode == KeyEvent.KEYCODE_Q ||
keyCode == KeyEvent.KEYCODE_DPAD_RIGHT ||
keyCode == KeyEvent.KEYCODE_W) {
mRotating = 0;
handled = true;
}
}
return handled;
| public void | restoreState(android.os.Bundle icicle)Restore game state if our process is being relaunched
setMode(PAUSE);
mRotating = 0;
mEngineFiring = false;
mDifficulty = icicle.getInteger("mDifficulty");
mX = icicle.getDouble("mX");
mY = icicle.getDouble("mY");
mDX = icicle.getDouble("mDX");
mDY = icicle.getDouble("mDY");
mHeading = icicle.getDouble("mHeading");
mLanderWidth = icicle.getInteger("mLanderWidth");
mLanderHeight = icicle.getInteger("mLanderHeight");
mGoalX = icicle.getInteger("mGoalX");
mGoalSpeed = icicle.getInteger("mGoalSpeed");
mGoalAngle = icicle.getInteger("mGoalAngle");
mGoalWidth = icicle.getInteger("mGoalWidth");
mWinsInARow = icicle.getInteger("mWinsInARow");
mFuel = icicle.getDouble("mFuel");
| public android.os.Bundle | saveState()Save game state so that the user does not lose anything
if the game process is killed while we are in the
background.
Bundle map = new Bundle();
map.putInteger("mDifficulty", Integer.valueOf(mDifficulty));
map.putDouble("mX", Double.valueOf(mX));
map.putDouble("mY", Double.valueOf(mY));
map.putDouble("mDX", Double.valueOf(mDX));
map.putDouble("mDY", Double.valueOf(mDY));
map.putDouble("mHeading", Double.valueOf(mHeading));
map.putInteger("mLanderWidth", Integer.valueOf(mLanderWidth));
map.putInteger("mLanderHeight", Integer.valueOf(mLanderHeight));
map.putInteger("mGoalX", Integer.valueOf(mGoalX));
map.putInteger("mGoalSpeed", Integer.valueOf(mGoalSpeed));
map.putInteger("mGoalAngle", Integer.valueOf(mGoalAngle));
map.putInteger("mGoalWidth", Integer.valueOf(mGoalWidth));
map.putInteger("mWinsInARow", Integer.valueOf(mWinsInARow));
map.putDouble("mFuel", Double.valueOf(mFuel));
return map;
| public void | setDifficulty(int difficulty)Sets the current difficulty.
mDifficulty = difficulty;
| public void | setFiring(boolean firing)Sets if the engine is currently firing.
mEngineFiring = firing;
| public void | setMode(int mode, java.lang.CharSequence message)Sets the game mode, RUNNING, PAUSED, etc.
mMode = mode;
invalidate();
if (mMode == RUNNING) {
mStatusText.setVisibility(View.INVISIBLE);
} else {
mRotating = 0;
mEngineFiring = false;
Resources res = getContext().getResources();
CharSequence str = "";
if (mMode == READY)
str = res.getText(R.string.mode_ready);
else if (mMode == PAUSE)
str = res.getText(R.string.mode_pause);
else if (mMode == LOSE)
str = res.getText(R.string.mode_lose);
else if (mMode == WIN)
str = res.getString(R.string.mode_win_prefix)
+ mWinsInARow + " "
+ res.getString(R.string.mode_win_suffix);
if (message != null) {
str = message + "\n" + str;
}
if (mMode == LOSE) mWinsInARow = 0;
mStatusText.setText(str);
mStatusText.setVisibility(View.VISIBLE);
}
| public void | setMode(int mode)Sets the game mode, RUNNING, PAUSED, etc.
setMode(mode, null);
| public void | setTextView(android.widget.TextView textView)Installs a pointer to the text view used
for messages.
mStatusText = textView;
| public void | updatePhysics()Figures the lander state (x, y, fuel, ...) based on the passage of
realtime. Does not invalidate(). Called at the start
of draw(). Detects the end-of-game and sets the UI to the next state.
long now = System.currentTimeMillis();
// Do nothing if mLastTime is in the future.
// This allows the game-start to delay the start of the physics
// by 100ms or whatever.
if (mLastTime > now) return;
double elapsed = (now - mLastTime) / 1000.0;
// mRotating -- update heading
if (mRotating != 0) {
mHeading += mRotating * (SLEW_SEC * elapsed);
// Bring things back into the range 0..360
if (mHeading < 0) mHeading += 360;
else if (mHeading >= 360) mHeading -= 360;
}
// Base accelerations -- 0 for x, gravity for y
double ddx = 0.0;
double ddy = -DOWN_ACCEL_SEC * elapsed;
if (mEngineFiring) {
// taking 0 as up, 90 as to the right
// cos(deg) is ddy component, sin(deg) is ddx component
double elapsedFiring = elapsed;
double fuelUsed = elapsedFiring * FUEL_SEC;
// tricky case where we run out of fuel partway through the elapsed
if (fuelUsed > mFuel) {
elapsedFiring = mFuel / fuelUsed * elapsed;
fuelUsed = mFuel;
// Oddball case where we adjust the "control" from here
mEngineFiring = false;
}
mFuel -= fuelUsed;
// have this much acceleration from the engine
double accel = FIRE_ACCEL_SEC * elapsedFiring;
double radians = 2*Math.PI*mHeading/360;
ddx = Math.sin(radians) * accel;
ddy += Math.cos(radians) * accel;
}
double dxOld = mDX;
double dyOld = mDY;
// figure speeds for the end of the period
mDX += ddx;
mDY += ddy;
// figure position based on average speed during the period
mX += elapsed * (mDX + dxOld)/2;
mY += elapsed * (mDY + dyOld)/2;
mLastTime = now;
// Evaluate if we have landed ... stop the game
double yLowerBound = PAD_HEIGHT + mLanderHeight/2 - BOTTOM_PADDING;
if (mY <= yLowerBound) {
mY = yLowerBound;
int result = LOSE;
CharSequence message = "";
Resources res = getContext().getResources();
double speed = Math.sqrt(mDX*mDX + mDY*mDY);
boolean onGoal = (mGoalX <= mX - mLanderWidth/2 &&
mX + mLanderWidth/2 <= mGoalX + mGoalWidth);
// "Hyperspace" win -- upside down, going fast,
// puts you back at the top.
if (onGoal && Math.abs(mHeading - 180) < mGoalAngle &&
speed > SPEED_HYPERSPACE) {
result = WIN;
mWinsInARow++;
doStart();
return;
// Oddball case: this case does a return, all other cases
// fall through to setMode() below.
} else if (!onGoal) {
message = res.getText(R.string.message_off_pad);
} else if (!(mHeading <= mGoalAngle ||
mHeading >= 360 - mGoalAngle)) {
message = res.getText(R.string.message_bad_angle);
} else if (speed > mGoalSpeed) {
message = res.getText(R.string.message_too_fast);
} else {
result = WIN;
mWinsInARow++;
}
setMode(result, message);
}
| public void | windowFocusChanged(boolean hasWindowFocus)Standard window-focus override.
Notice focus lost so we can pause on focus lost.
e.g. user switches to take a call.
if (!hasWindowFocus) doPause();
|
|