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

LunarView.java

/* 
 * 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;
    }

}