/*
* Copyright (C) 2007 Google Inc.
*
* 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.
*/
package com.google.android.lunarlander;
import android.content.Context;
import android.content.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.view.KeyEvent;
import android.util.AttributeSet;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import java.util.Map;
/**
* 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.
*/
class LunarView extends View {
public static final int READY = 0;
public static final int RUNNING = 1;
public static final int PAUSE = 2;
public static final int LOSE = 3;
public static final int WIN = 4;
public static final int EASY = 0;
public static final int MEDIUM = 1;
public static final int HARD = 2;
// Parameters for how the physics works.
public static final int FIRE_ACCEL_SEC = 80;
public static final int DOWN_ACCEL_SEC = 35;
public static final int FUEL_SEC = 10;
public static final int SLEW_SEC = 120; // degrees/second rotate
public static final int FUEL_INIT = 60;
public static final int FUEL_MAX = 100;
public static final int SPEED_INIT = 30;
public static final int SPEED_MAX = 120;
public static final int SPEED_HYPERSPACE = 180;
// Parameters for landing successfully (MEDIUM difficulty).
public static final int TARGET_SPEED = 28;
public static final double TARGET_WIDTH = 1.6; // how much wider than lander
public static final int TARGET_ANGLE = 18;
/**
* Pixel height of the fuel/speed bar.
*/
public static final int BAR_HEIGHT = 10;
/**
* Pixel width of the fuel/speed bar.
*/
public static final int BAR = 100;
/**
* Height of the landing pad off the bottom.
*/
public static final int PAD_HEIGHT = 8;
/**
* Extra pixels below the landing gear in the images
*/
public static final int BOTTOM_PADDING = 17;
/**
* The state of the game. One of READY, RUNNING, PAUSE, LOSE, or WIN
*/
private int mMode;
/**
* Current difficulty -- amount of fuel, allowed angle, etc.
* Default is MEDIUM.
*/
private int mDifficulty;
/**
* X of lander center.
*/
private double mX;
/**
* Y of lander center.
*/
private double mY;
/**
* Velocity dx.
*/
private double mDX;
/**
* Velocity dy.
*/
private double mDY;
/**
* Lander heading in degrees, with 0 up, 90 right.
* Kept in the range 0..360.
*/
private double mHeading;
/**
* Pixel width of lander image.
*/
private int mLanderWidth;
/**
* Pixel height of lander image.
*/
private int mLanderHeight;
/**
* Currently rotating, -1 left, 0 none, 1 right.
*/
private int mRotating;
/**
* X of the landing pad.
*/
private int mGoalX;
/**
* Allowed speed.
*/
private int mGoalSpeed;
/**
* Allowed angle.
*/
private int mGoalAngle;
/**
* Width of the landing pad.
*/
private int mGoalWidth;
/**
* Number of wins in a row.
*/
private int mWinsInARow;
/**
* Fuel remaining
*/
private double mFuel;
/**
* Is the engine burning?
*/
private boolean mEngineFiring;
/**
* Used to figure out elapsed time between frames
*/
private long mLastTime;
/**
* Paint to draw the lines on screen.
*/
private Paint mLinePaint;
/**
* "Bad" speed-too-high variant of the line color.
*/
private Paint mLinePaintBad;
/**
* What to draw for the Lander in its normal state
*/
private Drawable mLanderImage;
/**
* What to draw for the Lander when the engine is firing
*/
private Drawable mFiringImage;
/**
* What to draw for the Lander when it has crashed
*/
private Drawable mCrashedImage;
/**
* Pointer to the text view to display "Paused.." etc.
*/
private TextView mStatusText;
/**
* Scratch rect object.
*/
private RectF mScratchRect;
public LunarView(Context context, 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;
}
/**
* Save game state so that the user does not lose anything
* if the game process is killed while we are in the
* background.
*
* @return Map with this view's state
*/
public Bundle saveState() {
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;
}
/**
* Restore game state if our process is being relaunched
*
* @param icicle Map containing the game state
*/
public void restoreState(Bundle icicle) {
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");
}
/**
* Installs a pointer to the text view used
* for messages.
*/
public void setTextView(TextView textView) {
mStatusText = textView;
}
/**
* 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 windowFocusChanged(boolean hasWindowFocus) {
if (!hasWindowFocus) doPause();
}
/**
* Standard override of View.draw.
* Draws the ship and fuel/speed bars.
*/
@Override
protected void onDraw(Canvas canvas) {
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();
}
/**
* 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.
*/
public void updatePhysics() {
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);
}
}
/**
* Sets if the engine is currently firing.
*/
public void setFiring(boolean firing) {
mEngineFiring = firing;
}
/**
* Sets the game mode, RUNNING, PAUSED, etc.
* @param mode RUNNING, PAUSED, ...
*/
public void setMode(int mode) {
setMode(mode, null);
}
/**
* Sets the game mode, RUNNING, PAUSED, etc.
* @param mode RUNNING, PAUSED, ...
* @param message string to add to screen or null
*/
public void setMode(int mode, CharSequence message) {
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);
}
}
/**
* Starts the game, setter parameters for the current
* difficulty.
*/
public void doStart() {
// 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);
}
/**
* Resumes from a pause.
*/
public void doResume() {
// Move the real time clock up to now
mLastTime = System.currentTimeMillis() + 100;
setMode(RUNNING);
}
/**
* Pauses from the running state.
*/
public void doPause() {
if (mMode == RUNNING) setMode(PAUSE);
}
/**
* Standard override to get key events.
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent msg) {
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;
}
/**
* Standard override for key-up. We actually care about these,
* so we can turn off the engine or stop rotating.
*/
@Override
public boolean onKeyUp(int keyCode, KeyEvent msg) {
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;
}
/**
* Sets the current difficulty.
* @param difficulty
*/
public void setDifficulty(int difficulty) {
mDifficulty = difficulty;
}
}
|