FileDocCategorySizeDatePackage
LunarView.javaAPI DocGoogle Android v1.5 Example22211Sun Nov 11 13:01:04 GMT 2007com.google.android.lunarlander

LunarView

public 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_HEIGHT
Pixel height of the fuel/speed bar.
public static final int
BAR
Pixel width of the fuel/speed bar.
public static final int
PAD_HEIGHT
Height of the landing pad off the bottom.
public static final int
BOTTOM_PADDING
Extra pixels below the landing gear in the images
private int
mMode
The state of the game. One of READY, RUNNING, PAUSE, LOSE, or WIN
private int
mDifficulty
Current difficulty -- amount of fuel, allowed angle, etc. Default is MEDIUM.
private double
mX
X of lander center.
private double
mY
Y of lander center.
private double
mDX
Velocity dx.
private double
mDY
Velocity dy.
private double
mHeading
Lander heading in degrees, with 0 up, 90 right. Kept in the range 0..360.
private int
mLanderWidth
Pixel width of lander image.
private int
mLanderHeight
Pixel height of lander image.
private int
mRotating
Currently rotating, -1 left, 0 none, 1 right.
private int
mGoalX
X of the landing pad.
private int
mGoalSpeed
Allowed speed.
private int
mGoalAngle
Allowed angle.
private int
mGoalWidth
Width of the landing pad.
private int
mWinsInARow
Number of wins in a row.
private double
mFuel
Fuel remaining
private boolean
mEngineFiring
Is the engine burning?
private long
mLastTime
Used to figure out elapsed time between frames
private android.graphics.Paint
mLinePaint
Paint 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
mLanderImage
What to draw for the Lander in its normal state
private android.graphics.drawable.Drawable
mFiringImage
What to draw for the Lander when the engine is firing
private android.graphics.drawable.Drawable
mCrashedImage
What to draw for the Lander when it has crashed
private android.widget.TextView
mStatusText
Pointer to the text view to display "Paused.." etc.
private android.graphics.RectF
mScratchRect
Scratch 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 voiddoPause()
Pauses from the running state.

        if (mMode == RUNNING) setMode(PAUSE);
    
public voiddoResume()
Resumes from a pause.

        // Move the real time clock up to now
        mLastTime = System.currentTimeMillis() + 100;
        setMode(RUNNING);
    
public voiddoStart()
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 voidonDraw(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 booleanonKeyDown(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 booleanonKeyUp(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 voidrestoreState(android.os.Bundle icicle)
Restore game state if our process is being relaunched

param
icicle Map containing the game state

        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.BundlesaveState()
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

        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 voidsetDifficulty(int difficulty)
Sets the current difficulty.

param
difficulty

        mDifficulty = difficulty;
    
public voidsetFiring(boolean firing)
Sets if the engine is currently firing.

        mEngineFiring = firing;
    
public voidsetMode(int mode, java.lang.CharSequence message)
Sets the game mode, RUNNING, PAUSED, etc.

param
mode RUNNING, PAUSED, ...
param
message string to add to screen or null

        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 voidsetMode(int mode)
Sets the game mode, RUNNING, PAUSED, etc.

param
mode RUNNING, PAUSED, ...

        setMode(mode, null);
    
public voidsetTextView(android.widget.TextView textView)
Installs a pointer to the text view used for messages.

        mStatusText = textView;
    
public voidupdatePhysics()
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 voidwindowFocusChanged(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();