package com.example.helloandroid; import java.util.ArrayList; import java.util.Iterator; import java.util.Random; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.Paint.FontMetrics; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.widget.TextView; /** * 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 GameView extends SurfaceView implements SurfaceHolder.Callback { class DrawingThread extends Thread { /* * State-tracking constants */ public static final int STATE_LOSE = 1; public static final int STATE_PAUSE = 2; public static final int STATE_READY = 3; public static final int STATE_RUNNING = 4; public static final int STATE_WIN = 5; /* * UI constants (i.e. the speed & fuel bars) */ public static final int UI_BAR = 100; // width of the bar(s) public static final int UI_BAR_HEIGHT = 10; // height of the bar(s) /* * Member (state) fields */ private int mCanvasHeight = 1; private int mCanvasWidth = 1; /** Message handler used by thread to interact with TextView */ private Handler mHandler; /** Used to figure out elapsed time between frames */ private long mLastTime; /** Paint to draw the lines on screen. */ private Paint mLinePaint, mTextPaint; /** The state of the game. One of READY, RUNNING, PAUSE, LOSE, or WIN */ private int mMode; /** Indicate whether the surface has been created & is ready to draw */ private boolean mRun = false; /** Handle to the surface manager object we interact with */ private SurfaceHolder mSurfaceHolder; public Object planetsLock, fleetsLock; public ArrayList planets; public ArrayList fleets; public Planet planetSelected; int mFleetSize; public boolean connectionStarted; public String connMessage; public DrawingThread(SurfaceHolder surfaceHolder, Context context, Handler handler) { // get handles to some important objects mSurfaceHolder = surfaceHolder; mHandler = handler; mContext = context; // Initialize paints for speedometer mLinePaint = new Paint(); mLinePaint.setAntiAlias(true); mLinePaint.setARGB(255, 0, 255, 0); mTextPaint = new Paint(); mTextPaint.setAntiAlias(true); mTextPaint.setARGB(255, 255, 255, 255); planetsLock = new Object(); fleetsLock = new Object(); planets = new ArrayList(); planetSelected = null; fleets = new ArrayList(); mFleetSize = 50; connectionStarted = false; connMessage = ""; } /** * Starts the game, setting parameters for the current difficulty. */ public void doStart() { synchronized (mSurfaceHolder) { mLastTime = System.currentTimeMillis() + 100; setState(STATE_RUNNING); } } /** * Pauses the physics update & animation. */ public void pause() { synchronized (mSurfaceHolder) { if (mMode == STATE_RUNNING) setState(STATE_PAUSE); } } @Override public void run() { while (mRun) { Canvas c = null; try { c = mSurfaceHolder.lockCanvas(null); synchronized(mSurfaceHolder) { if(mMode == STATE_RUNNING) updatePhysics(); doDraw(c); } } 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 != null) { mSurfaceHolder.unlockCanvasAndPost(c); } } } } /** * Used to signal the thread whether it should be running or not. * Passing true allows the thread to run; passing false will shut it * down if it's already running. Calling start() after this was most * recently called with false will result in an immediate shutdown. * * @param b true to run, false to shut down */ public void setRunning(boolean b) { mRun = b; } /** * Sets the game mode. That is, whether we are running, paused, in the * failure state, in the victory state, etc. * * @see #setState(int, CharSequence) * @param mode one of the STATE_* constants */ public void setState(int mode) { synchronized (mSurfaceHolder) { setState(mode, null); } } /** * Sets the game mode. That is, whether we are running, paused, in the * failure state, in the victory state, etc. * * @param mode one of the STATE_* constants * @param message string to add to screen or null */ public void setState(int mode, CharSequence message) { /* * This method optionally can cause a text message to be displayed * to the user when the mode changes. Since the View that actually * renders that text is part of the main View hierarchy and not * owned by this thread, we can't touch the state of that View. * Instead we use a Message + Handler to relay commands to the main * thread, which updates the user-text View. */ synchronized (mSurfaceHolder) { mMode = mode; if (mMode == STATE_RUNNING) { Message msg = mHandler.obtainMessage(); Bundle b = new Bundle(); b.putString("text", ""); b.putInt("viz", GameView.INVISIBLE); msg.setData(b); mHandler.sendMessage(msg); } else { Resources res = mContext.getResources(); CharSequence str = ""; if (mMode == STATE_READY) str = res.getText(R.string.mode_ready); else if (mMode == STATE_PAUSE) str = res.getText(R.string.mode_pause); else if (mMode == STATE_LOSE) str = res.getText(R.string.mode_lose); if (message != null) { str = message + "\n" + str; } Message msg = mHandler.obtainMessage(); Bundle b = new Bundle(); b.putString("text", str.toString()); b.putInt("viz", GameView.VISIBLE); msg.setData(b); mHandler.sendMessage(msg); } } } /* Callback invoked when the surface dimensions change. */ public void setSurfaceSize(int width, int height) { // synchronized to make sure these all change atomically synchronized (mSurfaceHolder) { mCanvasWidth = width; mCanvasHeight = height; Log.i("Gencon", "width: "+mCanvasWidth+", height: "+mCanvasHeight); Random rand = new Random(); synchronized(planetsLock) { for(int x=0; x<15; x++) { Planet p = new Planet(rand.nextInt(45)+5, rand.nextInt(mCanvasWidth), rand.nextInt(mCanvasHeight)); if(Planet.collisionDetected(p, planets)) { x--; }else if(p.getX()-p.getRadius() < 0 || mCanvasWidth-20<=p.getX()+p.getRadius() || p.getY()-p.getRadius() < 0 || mCanvasHeight-20<=p.getY()+p.getRadius()) { x--; }else { p.setNumShips(rand.nextInt(150)); p.setFaction(rand.nextInt(5)); planets.add(p); } } } } } /** * Resumes from a pause. */ public void unpause() { // Move the real time clock up to now synchronized (mSurfaceHolder) { mLastTime = System.currentTimeMillis() + 100; } setState(STATE_RUNNING); } /** * Handles a key-down event. * * @param keyCode the key that was pressed * @param msg the original event object * @return true */ boolean doKeyDown(int keyCode, KeyEvent msg) { synchronized (mSurfaceHolder) { boolean okStart = false; if (keyCode == KeyEvent.KEYCODE_DPAD_UP) okStart = true; if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) okStart = true; if (keyCode == KeyEvent.KEYCODE_S) okStart = true; if (okStart && (mMode == STATE_READY || mMode == STATE_LOSE || mMode == STATE_WIN)) { // ready-to-start -> start doStart(); return true; } else if (mMode == STATE_PAUSE && okStart) { // paused -> running unpause(); return true; } else if (mMode == STATE_RUNNING) { return true; } return false; } } /** * Handles a key-up event. * * @param keyCode the key that was pressed * @param msg the original event object * @return true if the key was handled and consumed, or else false */ boolean doKeyUp(int keyCode, KeyEvent msg) { boolean handled = false; synchronized (mSurfaceHolder) { if (mMode == STATE_RUNNING) { if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_SPACE) { handled = true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_Q || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || keyCode == KeyEvent.KEYCODE_W) { handled = true; } } } return handled; } /** * Draws the ship, fuel/speed bars, and background to the provided * Canvas. */ private void doDraw(Canvas canvas) { canvas.drawColor(Color.BLACK); synchronized(planetsLock) { for(Planet p : planets) { p.draw(canvas, mLinePaint, mTextPaint); } } if(planetSelected != null) { planetSelected.drawSelectionCircle(canvas); } synchronized(fleetsLock) { for(Fleet f : fleets) { f.draw(canvas, mLinePaint); } } //draw the fleet size bar on the bottom of the screen float textSize = mTextPaint.getTextSize(); mTextPaint.setTextSize(24); FontMetrics metrics = mTextPaint.getFontMetrics(); mTextPaint.setColor(Color.WHITE); canvas.drawText(mFleetSize+"%", mCanvasWidth-mTextPaint.measureText(mFleetSize+"%"), mCanvasHeight-20-(metrics.ascent+metrics.descent), mTextPaint); mTextPaint.setTextSize(textSize); mLinePaint.setColor(Color.YELLOW); canvas.drawRoundRect(new RectF(70, mCanvasHeight-15, mCanvasWidth-70, mCanvasHeight-5), 5, 5, mLinePaint); mLinePaint.setColor(Color.GREEN); canvas.drawRoundRect(new RectF(70, mCanvasHeight-15, 70+(mCanvasWidth-140)*mFleetSize/100, mCanvasHeight-5), 5, 5, mLinePaint); //draw connection info if(connectionStarted) { canvas.drawText(connMessage, (mCanvasWidth-mTextPaint.measureText(connMessage))/2, mCanvasHeight-200-(metrics.ascent+metrics.descent), mTextPaint); } } /** * 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. */ private 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; synchronized(planetsLock) { for(Planet p : planets) { p.update(); } } synchronized(fleetsLock) { Iterator i = fleets.iterator(); Fleet f = null; while(i.hasNext()){ f = i.next(); if(f.getNumShips() == 0) { i.remove(); }else f.update(planets); } } mLastTime = now+50; } } /** Handle to the application context, used to e.g. fetch Drawables. */ private Context mContext; /** Pointer to the text view to display "Paused.." etc. */ private TextView mStatusText; /** The thread that actually draws the animation */ private DrawingThread thread; public GameView(Context context, AttributeSet attrs) { super(context, attrs); // register our interest in hearing about changes to our surface SurfaceHolder holder = getHolder(); holder.addCallback(this); // create thread only; it's started in surfaceCreated() thread = new DrawingThread(holder, context, new Handler() { @Override public void handleMessage(Message m) { mStatusText.setVisibility(m.getData().getInt("viz")); mStatusText.setText(m.getData().getString("text")); } }); setFocusable(true); // make sure we get key events } @Override public boolean onTouchEvent(MotionEvent event) { Log.i("Gencon", "Detected touch event"); if(event.getAction() == MotionEvent.ACTION_UP) { if(70 <= event.getX() && event.getX() <= thread.mCanvasWidth-70 && thread.mCanvasHeight-15 <= event.getY() && event.getY() <= thread.mCanvasHeight-5) { thread.mFleetSize = ((int)event.getX()-70)*100/(thread.mCanvasWidth-140); } }else if(event.getAction() == MotionEvent.ACTION_DOWN) { synchronized(thread.planetsLock) { if(thread.planetSelected != null) { Planet target = null; for(Planet p : thread.planets) { if(p.contains((int)event.getX(), (int)event.getY())) { target = p; break; } } if(target != null && target != thread.planetSelected && thread.planetSelected.getFaction() != 0) { synchronized(thread.fleetsLock) { Fleet f = new Fleet(thread.planetSelected, target, thread.planetSelected.getNumShips()*thread.mFleetSize/100, thread.planetSelected.getFaction()); f.setFaction(thread.planetSelected.getFaction()); thread.fleets.add(f); } } thread.planetSelected.unselect(); thread.planetSelected = null; }else { for(Planet p : thread.planets) { if(p.contains((int)event.getX(), (int)event.getY())) { p.select(); thread.planetSelected = p; break; } } } } } return true; } /** * Fetches the animation thread corresponding to this LunarView. * * @return the animation thread */ public DrawingThread getThread() { return thread; } /** * Standard override to get key-press events. */ @Override public boolean onKeyDown(int keyCode, KeyEvent msg) { return thread.doKeyDown(keyCode, msg); } /** * 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) { return thread.doKeyUp(keyCode, msg); } /** * 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 onWindowFocusChanged(boolean hasWindowFocus) { if (!hasWindowFocus) thread.pause(); } /** * Installs a pointer to the text view used for messages. */ public void setTextView(TextView textView) { mStatusText = textView; } /* Callback invoked when the surface dimensions change. */ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { thread.setSurfaceSize(width, height); } /* * Callback invoked when the Surface has been created and is ready to be * used. */ public void surfaceCreated(SurfaceHolder holder) { // start the thread here so that we don't busy-wait in run() // waiting for the surface to be created thread.setRunning(true); thread.start(); } /* * Callback invoked when the Surface has been destroyed and must no longer * be touched. WARNING: after this method returns, the Surface/Canvas must * never be touched again! */ public void surfaceDestroyed(SurfaceHolder holder) { // we have to tell thread to shut down & wait for it to finish, or else // it might touch the Surface after we return and explode boolean retry = true; thread.setRunning(false); while (retry) { try { thread.join(); retry = false; } catch (InterruptedException e) { } } } }