1 |
/* |
2 |
* Copyright (C) 2007 Google Inc. |
3 |
* |
4 |
* Licensed under the Apache License, Version 2.0 (the "License"); you may not |
5 |
* use this file except in compliance with the License. You may obtain a copy of |
6 |
* the License at |
7 |
* |
8 |
* http://www.apache.org/licenses/LICENSE-2.0 |
9 |
* |
10 |
* Unless required by applicable law or agreed to in writing, software |
11 |
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
12 |
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
13 |
* License for the specific language governing permissions and limitations under |
14 |
* the License. |
15 |
*/ |
16 |
|
17 |
package com.example.admob.lunarlander; |
18 |
|
19 |
import com.admob.android.ads.*; |
20 |
import android.content.Context; |
21 |
import android.content.res.Resources; |
22 |
import android.graphics.Bitmap; |
23 |
import android.graphics.BitmapFactory; |
24 |
import android.graphics.Canvas; |
25 |
import android.graphics.Paint; |
26 |
import android.graphics.RectF; |
27 |
import android.graphics.drawable.Drawable; |
28 |
import android.hardware.*; |
29 |
import android.os.Bundle; |
30 |
import android.os.Handler; |
31 |
import android.os.Message; |
32 |
import android.util.AttributeSet; |
33 |
import android.view.*; |
34 |
import android.widget.TextView; |
35 |
|
36 |
|
37 |
/** |
38 |
* View that draws, takes keystrokes, etc. for a simple LunarLander game. |
39 |
* |
40 |
* Has a mode which RUNNING, PAUSED, etc. Has a x, y, dx, dy, ... capturing the |
41 |
* current ship physics. All x/y etc. are measured with (0,0) at the lower left. |
42 |
* updatePhysics() advances the physics based on realtime. draw() renders the |
43 |
* ship, and does an invalidate() to prompt another draw() as soon as possible |
44 |
* by the system. |
45 |
*/ |
46 |
class LunarView extends SurfaceView implements SurfaceHolder.Callback, SensorEventListener { |
47 |
class LunarThread extends Thread { |
48 |
/* |
49 |
* Difficulty setting constants |
50 |
*/ |
51 |
public static final int DIFFICULTY_EASY = 0; |
52 |
public static final int DIFFICULTY_HARD = 1; |
53 |
public static final int DIFFICULTY_MEDIUM = 2; |
54 |
/* |
55 |
* Physics constants |
56 |
*/ |
57 |
public static final int PHYS_DOWN_ACCEL_SEC = 35; |
58 |
public static final int PHYS_FIRE_ACCEL_SEC = 80; |
59 |
public static final int PHYS_FUEL_INIT = 60; |
60 |
public static final int PHYS_FUEL_MAX = 100; |
61 |
public static final int PHYS_FUEL_SEC = 10; |
62 |
public static final int PHYS_SLEW_SEC = 120; // degrees/second rotate |
63 |
public static final int PHYS_SPEED_HYPERSPACE = 180; |
64 |
public static final int PHYS_SPEED_INIT = 30; |
65 |
public static final int PHYS_SPEED_MAX = 120; |
66 |
/* |
67 |
* State-tracking constants |
68 |
*/ |
69 |
public static final int STATE_LOSE = 1; |
70 |
public static final int STATE_PAUSE = 2; |
71 |
public static final int STATE_READY = 3; |
72 |
public static final int STATE_RUNNING = 4; |
73 |
public static final int STATE_WIN = 5; |
74 |
|
75 |
/* |
76 |
* Goal condition constants |
77 |
*/ |
78 |
public static final int TARGET_ANGLE = 18; // > this angle means crash |
79 |
public static final int TARGET_BOTTOM_PADDING = 17; // px below gear |
80 |
public static final int TARGET_PAD_HEIGHT = 8; // how high above ground |
81 |
public static final int TARGET_SPEED = 28; // > this speed means crash |
82 |
public static final double TARGET_WIDTH = 1.6; // width of target |
83 |
/* |
84 |
* UI constants (i.e. the speed & fuel bars) |
85 |
*/ |
86 |
public static final int UI_BAR = 100; // width of the bar(s) |
87 |
public static final int UI_BAR_HEIGHT = 10; // height of the bar(s) |
88 |
private static final String KEY_DIFFICULTY = "mDifficulty"; |
89 |
private static final String KEY_DX = "mDX"; |
90 |
|
91 |
private static final String KEY_DY = "mDY"; |
92 |
private static final String KEY_FUEL = "mFuel"; |
93 |
private static final String KEY_GOAL_ANGLE = "mGoalAngle"; |
94 |
private static final String KEY_GOAL_SPEED = "mGoalSpeed"; |
95 |
private static final String KEY_GOAL_WIDTH = "mGoalWidth"; |
96 |
|
97 |
private static final String KEY_GOAL_X = "mGoalX"; |
98 |
private static final String KEY_HEADING = "mHeading"; |
99 |
private static final String KEY_LANDER_HEIGHT = "mLanderHeight"; |
100 |
private static final String KEY_LANDER_WIDTH = "mLanderWidth"; |
101 |
private static final String KEY_WINS = "mWinsInARow"; |
102 |
|
103 |
private static final String KEY_X = "mX"; |
104 |
private static final String KEY_Y = "mY"; |
105 |
|
106 |
/* |
107 |
* Member (state) fields |
108 |
*/ |
109 |
/** The drawable to use as the background of the animation canvas */ |
110 |
private Bitmap mBackgroundImage; |
111 |
|
112 |
/** |
113 |
* Current height of the surface/canvas. |
114 |
* |
115 |
* @see #setSurfaceSize |
116 |
*/ |
117 |
private int mCanvasHeight = 1; |
118 |
|
119 |
/** |
120 |
* Current width of the surface/canvas. |
121 |
* |
122 |
* @see #setSurfaceSize |
123 |
*/ |
124 |
private int mCanvasWidth = 1; |
125 |
|
126 |
/** What to draw for the Lander when it has crashed */ |
127 |
private Drawable mCrashedImage; |
128 |
|
129 |
/** |
130 |
* Current difficulty -- amount of fuel, allowed angle, etc. Default is |
131 |
* MEDIUM. |
132 |
*/ |
133 |
private int mDifficulty; |
134 |
|
135 |
/** Velocity dx. */ |
136 |
private double mDX; |
137 |
|
138 |
/** Velocity dy. */ |
139 |
private double mDY; |
140 |
|
141 |
/** Is the engine burning? */ |
142 |
private boolean mEngineFiring; |
143 |
|
144 |
/** What to draw for the Lander when the engine is firing */ |
145 |
private Drawable mFiringImage; |
146 |
|
147 |
/** Fuel remaining */ |
148 |
private double mFuel; |
149 |
|
150 |
/** Allowed angle. */ |
151 |
private int mGoalAngle; |
152 |
|
153 |
/** Allowed speed. */ |
154 |
private int mGoalSpeed; |
155 |
|
156 |
/** Width of the landing pad. */ |
157 |
private int mGoalWidth; |
158 |
|
159 |
/** X of the landing pad. */ |
160 |
private int mGoalX; |
161 |
|
162 |
/** Message handler used by thread to interact with TextView */ |
163 |
private Handler mHandler; |
164 |
|
165 |
/** |
166 |
* Lander heading in degrees, with 0 up, 90 right. Kept in the range |
167 |
* 0..360. |
168 |
*/ |
169 |
private double mHeading; |
170 |
|
171 |
/** Pixel height of lander image. */ |
172 |
private int mLanderHeight; |
173 |
|
174 |
/** What to draw for the Lander in its normal state */ |
175 |
private Drawable mLanderImage; |
176 |
|
177 |
/** Pixel width of lander image. */ |
178 |
private int mLanderWidth; |
179 |
|
180 |
/** Used to figure out elapsed time between frames */ |
181 |
private long mLastTime; |
182 |
|
183 |
/** Paint to draw the lines on screen. */ |
184 |
private Paint mLinePaint; |
185 |
|
186 |
/** "Bad" speed-too-high variant of the line color. */ |
187 |
private Paint mLinePaintBad; |
188 |
|
189 |
/** The state of the game. One of READY, RUNNING, PAUSE, LOSE, or WIN */ |
190 |
private int mMode; |
191 |
|
192 |
/** Currently rotating, -1 left, 0 none, 1 right. */ |
193 |
private int mRotating; |
194 |
|
195 |
/** Indicate whether the surface has been created & is ready to draw */ |
196 |
private boolean mRun = false; |
197 |
|
198 |
/** Scratch rect object. */ |
199 |
private RectF mScratchRect; |
200 |
|
201 |
/** Handle to the surface manager object we interact with */ |
202 |
private SurfaceHolder mSurfaceHolder; |
203 |
|
204 |
/** Number of wins in a row. */ |
205 |
private int mWinsInARow; |
206 |
|
207 |
/** X of lander center. */ |
208 |
private double mX; |
209 |
|
210 |
/** Y of lander center. */ |
211 |
private double mY; |
212 |
|
213 |
public LunarThread(SurfaceHolder surfaceHolder, Context context, |
214 |
Handler handler) { |
215 |
// get handles to some important objects |
216 |
mSurfaceHolder = surfaceHolder; |
217 |
mHandler = handler; |
218 |
mContext = context; |
219 |
|
220 |
Resources res = context.getResources(); |
221 |
// cache handles to our key sprites & other drawables |
222 |
mLanderImage = context.getResources().getDrawable( |
223 |
R.drawable.lander_plain); |
224 |
mFiringImage = context.getResources().getDrawable( |
225 |
R.drawable.lander_firing); |
226 |
mCrashedImage = context.getResources().getDrawable( |
227 |
R.drawable.lander_crashed); |
228 |
|
229 |
// load background image as a Bitmap instead of a Drawable b/c |
230 |
// we don't need to transform it and it's faster to draw this way |
231 |
mBackgroundImage = BitmapFactory.decodeResource(res, |
232 |
R.drawable.earthrise); |
233 |
|
234 |
// Use the regular lander image as the model size for all sprites |
235 |
mLanderWidth = mLanderImage.getIntrinsicWidth(); |
236 |
mLanderHeight = mLanderImage.getIntrinsicHeight(); |
237 |
|
238 |
// Initialize paints for speedometer |
239 |
mLinePaint = new Paint(); |
240 |
mLinePaint.setAntiAlias(true); |
241 |
mLinePaint.setARGB(255, 0, 255, 0); |
242 |
|
243 |
mLinePaintBad = new Paint(); |
244 |
mLinePaintBad.setAntiAlias(true); |
245 |
mLinePaintBad.setARGB(255, 120, 180, 0); |
246 |
|
247 |
mScratchRect = new RectF(0, 0, 0, 0); |
248 |
|
249 |
mWinsInARow = 0; |
250 |
mDifficulty = DIFFICULTY_MEDIUM; |
251 |
|
252 |
// initial show-up of lander (not yet playing) |
253 |
mX = mLanderWidth; |
254 |
mY = mLanderHeight * 2; |
255 |
mFuel = PHYS_FUEL_INIT; |
256 |
mDX = 0; |
257 |
mDY = 0; |
258 |
mHeading = 0; |
259 |
mEngineFiring = true; |
260 |
} |
261 |
|
262 |
/** |
263 |
* Starts the game, setting parameters for the current difficulty. |
264 |
*/ |
265 |
public void doStart() { |
266 |
synchronized (mSurfaceHolder) { |
267 |
// First set the game for Medium difficulty |
268 |
mFuel = PHYS_FUEL_INIT; |
269 |
mEngineFiring = false; |
270 |
mGoalWidth = (int) (mLanderWidth * TARGET_WIDTH); |
271 |
mGoalSpeed = TARGET_SPEED; |
272 |
mGoalAngle = TARGET_ANGLE; |
273 |
int speedInit = PHYS_SPEED_INIT; |
274 |
|
275 |
// Adjust difficulty params for EASY/HARD |
276 |
if (mDifficulty == DIFFICULTY_EASY) { |
277 |
mFuel = mFuel * 3 / 2; |
278 |
mGoalWidth = mGoalWidth * 4 / 3; |
279 |
mGoalSpeed = mGoalSpeed * 3 / 2; |
280 |
mGoalAngle = mGoalAngle * 4 / 3; |
281 |
speedInit = speedInit * 3 / 4; |
282 |
} else if (mDifficulty == DIFFICULTY_HARD) { |
283 |
mFuel = mFuel * 7 / 8; |
284 |
mGoalWidth = mGoalWidth * 3 / 4; |
285 |
mGoalSpeed = mGoalSpeed * 7 / 8; |
286 |
speedInit = speedInit * 4 / 3; |
287 |
} |
288 |
|
289 |
// pick a convenient initial location for the lander sprite |
290 |
mX = mCanvasWidth / 2; |
291 |
mY = mCanvasHeight - mLanderHeight / 2; |
292 |
|
293 |
// start with a little random motion |
294 |
mDY = Math.random() * -speedInit; |
295 |
mDX = Math.random() * 2 * speedInit - speedInit; |
296 |
mHeading = 0; |
297 |
|
298 |
// Figure initial spot for landing, not too near center |
299 |
while (true) { |
300 |
mGoalX = (int) (Math.random() * (mCanvasWidth - mGoalWidth)); |
301 |
if (Math.abs(mGoalX - (mX - mLanderWidth / 2)) > mCanvasHeight / 6) |
302 |
break; |
303 |
} |
304 |
|
305 |
mLastTime = System.currentTimeMillis() + 100; |
306 |
setState(STATE_RUNNING); |
307 |
} |
308 |
} |
309 |
|
310 |
/** |
311 |
* Pauses the physics update & animation. |
312 |
*/ |
313 |
public void pause() { |
314 |
synchronized (mSurfaceHolder) { |
315 |
if (mMode == STATE_RUNNING) setState(STATE_PAUSE); |
316 |
} |
317 |
} |
318 |
|
319 |
/** |
320 |
* Restores game state from the indicated Bundle. Typically called when |
321 |
* the Activity is being restored after having been previously |
322 |
* destroyed. |
323 |
* |
324 |
* @param savedState Bundle containing the game state |
325 |
*/ |
326 |
public synchronized void restoreState(Bundle savedState) { |
327 |
synchronized (mSurfaceHolder) { |
328 |
setState(STATE_PAUSE); |
329 |
mRotating = 0; |
330 |
mEngineFiring = false; |
331 |
|
332 |
mDifficulty = savedState.getInt(KEY_DIFFICULTY); |
333 |
mX = savedState.getDouble(KEY_X); |
334 |
mY = savedState.getDouble(KEY_Y); |
335 |
mDX = savedState.getDouble(KEY_DX); |
336 |
mDY = savedState.getDouble(KEY_DY); |
337 |
mHeading = savedState.getDouble(KEY_HEADING); |
338 |
|
339 |
mLanderWidth = savedState.getInt(KEY_LANDER_WIDTH); |
340 |
mLanderHeight = savedState.getInt(KEY_LANDER_HEIGHT); |
341 |
mGoalX = savedState.getInt(KEY_GOAL_X); |
342 |
mGoalSpeed = savedState.getInt(KEY_GOAL_SPEED); |
343 |
mGoalAngle = savedState.getInt(KEY_GOAL_ANGLE); |
344 |
mGoalWidth = savedState.getInt(KEY_GOAL_WIDTH); |
345 |
mWinsInARow = savedState.getInt(KEY_WINS); |
346 |
mFuel = savedState.getDouble(KEY_FUEL); |
347 |
} |
348 |
} |
349 |
|
350 |
@Override |
351 |
public void run() { |
352 |
while (mRun) { |
353 |
Canvas c = null; |
354 |
try { |
355 |
c = mSurfaceHolder.lockCanvas(null); |
356 |
synchronized (mSurfaceHolder) { |
357 |
if (mMode == STATE_RUNNING) updatePhysics(); |
358 |
doDraw(c); |
359 |
} |
360 |
} finally { |
361 |
// do this in a finally so that if an exception is thrown |
362 |
// during the above, we don't leave the Surface in an |
363 |
// inconsistent state |
364 |
if (c != null) { |
365 |
mSurfaceHolder.unlockCanvasAndPost(c); |
366 |
} |
367 |
} |
368 |
} |
369 |
} |
370 |
|
371 |
/** |
372 |
* Dump game state to the provided Bundle. Typically called when the |
373 |
* Activity is being suspended. |
374 |
* |
375 |
* @return Bundle with this view's state |
376 |
*/ |
377 |
public Bundle saveState(Bundle map) { |
378 |
synchronized (mSurfaceHolder) { |
379 |
if (map != null) { |
380 |
map.putInt(KEY_DIFFICULTY, Integer.valueOf(mDifficulty)); |
381 |
map.putDouble(KEY_X, Double.valueOf(mX)); |
382 |
map.putDouble(KEY_Y, Double.valueOf(mY)); |
383 |
map.putDouble(KEY_DX, Double.valueOf(mDX)); |
384 |
map.putDouble(KEY_DY, Double.valueOf(mDY)); |
385 |
map.putDouble(KEY_HEADING, Double.valueOf(mHeading)); |
386 |
map.putInt(KEY_LANDER_WIDTH, Integer.valueOf(mLanderWidth)); |
387 |
map.putInt(KEY_LANDER_HEIGHT, Integer |
388 |
.valueOf(mLanderHeight)); |
389 |
map.putInt(KEY_GOAL_X, Integer.valueOf(mGoalX)); |
390 |
map.putInt(KEY_GOAL_SPEED, Integer.valueOf(mGoalSpeed)); |
391 |
map.putInt(KEY_GOAL_ANGLE, Integer.valueOf(mGoalAngle)); |
392 |
map.putInt(KEY_GOAL_WIDTH, Integer.valueOf(mGoalWidth)); |
393 |
map.putInt(KEY_WINS, Integer.valueOf(mWinsInARow)); |
394 |
map.putDouble(KEY_FUEL, Double.valueOf(mFuel)); |
395 |
} |
396 |
} |
397 |
return map; |
398 |
} |
399 |
|
400 |
/** |
401 |
* Sets the current difficulty. |
402 |
* |
403 |
* @param difficulty |
404 |
*/ |
405 |
public void setDifficulty(int difficulty) { |
406 |
synchronized (mSurfaceHolder) { |
407 |
mDifficulty = difficulty; |
408 |
} |
409 |
} |
410 |
|
411 |
/** |
412 |
* Sets if the engine is currently firing. |
413 |
*/ |
414 |
public void setFiring(boolean firing) { |
415 |
synchronized (mSurfaceHolder) { |
416 |
mEngineFiring = firing; |
417 |
} |
418 |
} |
419 |
|
420 |
/** |
421 |
* Used to signal the thread whether it should be running or not. |
422 |
* Passing true allows the thread to run; passing false will shut it |
423 |
* down if it's already running. Calling start() after this was most |
424 |
* recently called with false will result in an immediate shutdown. |
425 |
* |
426 |
* @param b true to run, false to shut down |
427 |
*/ |
428 |
public void setRunning(boolean b) { |
429 |
mRun = b; |
430 |
} |
431 |
|
432 |
/** |
433 |
* Sets the game mode. That is, whether we are running, paused, in the |
434 |
* failure state, in the victory state, etc. |
435 |
* |
436 |
* @see #setState(int, CharSequence) |
437 |
* @param mode one of the STATE_* constants |
438 |
*/ |
439 |
public void setState(int mode) { |
440 |
synchronized (mSurfaceHolder) { |
441 |
setState(mode, null); |
442 |
} |
443 |
} |
444 |
|
445 |
/** |
446 |
* Sets the game mode. That is, whether we are running, paused, in the |
447 |
* failure state, in the victory state, etc. |
448 |
* |
449 |
* @param mode one of the STATE_* constants |
450 |
* @param message string to add to screen or null |
451 |
*/ |
452 |
public void setState(int mode, CharSequence message) { |
453 |
/* |
454 |
* This method optionally can cause a text message to be displayed |
455 |
* to the user when the mode changes. Since the View that actually |
456 |
* renders that text is part of the main View hierarchy and not |
457 |
* owned by this thread, we can't touch the state of that View. |
458 |
* Instead we use a Message + Handler to relay commands to the main |
459 |
* thread, which updates the user-text View. |
460 |
*/ |
461 |
synchronized (mSurfaceHolder) { |
462 |
mMode = mode; |
463 |
|
464 |
if (mMode == STATE_RUNNING) { |
465 |
Message msg = mHandler.obtainMessage(); |
466 |
Bundle b = new Bundle(); |
467 |
b.putString("text", ""); |
468 |
b.putInt("viz", View.INVISIBLE); |
469 |
msg.setData(b); |
470 |
mHandler.sendMessage(msg); |
471 |
} else { |
472 |
mRotating = 0; |
473 |
mEngineFiring = false; |
474 |
Resources res = mContext.getResources(); |
475 |
CharSequence str = ""; |
476 |
if (mMode == STATE_READY) |
477 |
str = res.getText(R.string.mode_ready); |
478 |
else if (mMode == STATE_PAUSE) |
479 |
str = res.getText(R.string.mode_pause); |
480 |
else if (mMode == STATE_LOSE) |
481 |
str = res.getText(R.string.mode_lose); |
482 |
else if (mMode == STATE_WIN) |
483 |
str = res.getString(R.string.mode_win_prefix) |
484 |
+ mWinsInARow + " " |
485 |
+ res.getString(R.string.mode_win_suffix); |
486 |
|
487 |
if (message != null) { |
488 |
str = message + "\n" + str; |
489 |
} |
490 |
|
491 |
if (mMode == STATE_LOSE) mWinsInARow = 0; |
492 |
|
493 |
Message msg = mHandler.obtainMessage(); |
494 |
Bundle b = new Bundle(); |
495 |
b.putString("text", str.toString()); |
496 |
b.putInt("viz", View.VISIBLE); |
497 |
msg.setData(b); |
498 |
mHandler.sendMessage(msg); |
499 |
} |
500 |
} |
501 |
} |
502 |
|
503 |
/* Callback invoked when the surface dimensions change. */ |
504 |
public void setSurfaceSize(int width, int height) { |
505 |
// synchronized to make sure these all change atomically |
506 |
synchronized (mSurfaceHolder) { |
507 |
mCanvasWidth = width; |
508 |
mCanvasHeight = height; |
509 |
|
510 |
// don't forget to resize the background image |
511 |
mBackgroundImage = Bitmap.createScaledBitmap( |
512 |
mBackgroundImage, width, height, true); |
513 |
} |
514 |
} |
515 |
|
516 |
/** |
517 |
* Resumes from a pause. |
518 |
*/ |
519 |
public void unpause() { |
520 |
// Move the real time clock up to now |
521 |
synchronized (mSurfaceHolder) { |
522 |
mLastTime = System.currentTimeMillis() + 100; |
523 |
} |
524 |
setState(STATE_RUNNING); |
525 |
} |
526 |
|
527 |
/** |
528 |
* Handles a key-down event. |
529 |
* |
530 |
* @param keyCode the key that was pressed |
531 |
* @param msg the original event object |
532 |
* @return true |
533 |
*/ |
534 |
boolean doKeyDown(int keyCode, KeyEvent msg) { |
535 |
synchronized (mSurfaceHolder) { |
536 |
boolean okStart = false; |
537 |
if (keyCode == KeyEvent.KEYCODE_DPAD_UP) okStart = true; |
538 |
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) okStart = true; |
539 |
if (keyCode == KeyEvent.KEYCODE_S) okStart = true; |
540 |
|
541 |
//boolean center = (keyCode == KeyEvent.KEYCODE_DPAD_UP); |
542 |
|
543 |
if (okStart |
544 |
&& (mMode == STATE_READY || mMode == STATE_LOSE || mMode == STATE_WIN)) { |
545 |
// ready-to-start -> start |
546 |
doStart(); |
547 |
return true; |
548 |
} else if (mMode == STATE_PAUSE && okStart) { |
549 |
// paused -> running |
550 |
unpause(); |
551 |
return true; |
552 |
} else if (mMode == STATE_RUNNING) { |
553 |
// center/space -> fire |
554 |
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER |
555 |
|| keyCode == KeyEvent.KEYCODE_SPACE) { |
556 |
setFiring(true); |
557 |
return true; |
558 |
// left/q -> left |
559 |
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT |
560 |
|| keyCode == KeyEvent.KEYCODE_Q) { |
561 |
mRotating = -1; |
562 |
return true; |
563 |
// right/w -> right |
564 |
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT |
565 |
|| keyCode == KeyEvent.KEYCODE_W) { |
566 |
mRotating = 1; |
567 |
return true; |
568 |
// up -> pause |
569 |
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { |
570 |
pause(); |
571 |
return true; |
572 |
} |
573 |
} |
574 |
|
575 |
return false; |
576 |
} |
577 |
} |
578 |
|
579 |
/** |
580 |
* Handles a key-up event. |
581 |
* |
582 |
* @param keyCode the key that was pressed |
583 |
* @param msg the original event object |
584 |
* @return true if the key was handled and consumed, or else false |
585 |
*/ |
586 |
boolean doKeyUp(int keyCode, KeyEvent msg) { |
587 |
boolean handled = false; |
588 |
|
589 |
synchronized (mSurfaceHolder) { |
590 |
if (mMode == STATE_RUNNING) { |
591 |
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER |
592 |
|| keyCode == KeyEvent.KEYCODE_SPACE) { |
593 |
setFiring(false); |
594 |
handled = true; |
595 |
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT |
596 |
|| keyCode == KeyEvent.KEYCODE_Q |
597 |
|| keyCode == KeyEvent.KEYCODE_DPAD_RIGHT |
598 |
|| keyCode == KeyEvent.KEYCODE_W) { |
599 |
mRotating = 0; |
600 |
handled = true; |
601 |
} |
602 |
} |
603 |
} |
604 |
|
605 |
return handled; |
606 |
} |
607 |
|
608 |
/** |
609 |
* Handles turning the ship left and right by tilting the phone. |
610 |
*/ |
611 |
void doOrientationChange( float tiltLeftRight ) |
612 |
{ |
613 |
mRotating = 0; |
614 |
mHeading = Math.round( tiltLeftRight ); |
615 |
invalidate(); |
616 |
} |
617 |
|
618 |
/** |
619 |
* Draws the ship, fuel/speed bars, and background to the provided |
620 |
* Canvas. |
621 |
*/ |
622 |
private void doDraw(Canvas canvas) { |
623 |
// Draw the background image. Operations on the Canvas accumulate |
624 |
// so this is like clearing the screen. |
625 |
canvas.drawBitmap(mBackgroundImage, 0, 0, null); |
626 |
|
627 |
int yTop = mCanvasHeight - ((int) mY + mLanderHeight / 2); |
628 |
int xLeft = (int) mX - mLanderWidth / 2; |
629 |
|
630 |
// Draw the fuel gauge |
631 |
int fuelWidth = (int) (UI_BAR * mFuel / PHYS_FUEL_MAX); |
632 |
mScratchRect.set(4, 4, 4 + fuelWidth, 4 + UI_BAR_HEIGHT); |
633 |
canvas.drawRect(mScratchRect, mLinePaint); |
634 |
|
635 |
// Draw the speed gauge, with a two-tone effect |
636 |
double speed = Math.sqrt(mDX * mDX + mDY * mDY); |
637 |
int speedWidth = (int) (UI_BAR * speed / PHYS_SPEED_MAX); |
638 |
|
639 |
if (speed <= mGoalSpeed) { |
640 |
mScratchRect.set(4 + UI_BAR + 4, 4, |
641 |
4 + UI_BAR + 4 + speedWidth, 4 + UI_BAR_HEIGHT); |
642 |
canvas.drawRect(mScratchRect, mLinePaint); |
643 |
} else { |
644 |
// Draw the bad color in back, with the good color in front of |
645 |
// it |
646 |
mScratchRect.set(4 + UI_BAR + 4, 4, |
647 |
4 + UI_BAR + 4 + speedWidth, 4 + UI_BAR_HEIGHT); |
648 |
canvas.drawRect(mScratchRect, mLinePaintBad); |
649 |
int goalWidth = (UI_BAR * mGoalSpeed / PHYS_SPEED_MAX); |
650 |
mScratchRect.set(4 + UI_BAR + 4, 4, 4 + UI_BAR + 4 + goalWidth, |
651 |
4 + UI_BAR_HEIGHT); |
652 |
canvas.drawRect(mScratchRect, mLinePaint); |
653 |
} |
654 |
|
655 |
// Draw the landing pad |
656 |
canvas.drawLine(mGoalX, 1 + mCanvasHeight - TARGET_PAD_HEIGHT, |
657 |
mGoalX + mGoalWidth, 1 + mCanvasHeight - TARGET_PAD_HEIGHT, |
658 |
mLinePaint); |
659 |
|
660 |
|
661 |
// Draw the ship with its current rotation |
662 |
canvas.save(); |
663 |
canvas.rotate((float) mHeading, (float) mX, mCanvasHeight |
664 |
- (float) mY); |
665 |
if (mMode == STATE_LOSE) { |
666 |
mCrashedImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop |
667 |
+ mLanderHeight); |
668 |
mCrashedImage.draw(canvas); |
669 |
} else if (mEngineFiring) { |
670 |
mFiringImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop |
671 |
+ mLanderHeight); |
672 |
mFiringImage.draw(canvas); |
673 |
} else { |
674 |
mLanderImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop |
675 |
+ mLanderHeight); |
676 |
mLanderImage.draw(canvas); |
677 |
} |
678 |
canvas.restore(); |
679 |
} |
680 |
|
681 |
/** |
682 |
* Figures the lander state (x, y, fuel, ...) based on the passage of |
683 |
* realtime. Does not invalidate(). Called at the start of draw(). |
684 |
* Detects the end-of-game and sets the UI to the next state. |
685 |
*/ |
686 |
private void updatePhysics() { |
687 |
long now = System.currentTimeMillis(); |
688 |
|
689 |
// Do nothing if mLastTime is in the future. |
690 |
// This allows the game-start to delay the start of the physics |
691 |
// by 100ms or whatever. |
692 |
if (mLastTime > now) return; |
693 |
|
694 |
double elapsed = (now - mLastTime) / 1000.0; |
695 |
|
696 |
// mRotating -- update heading |
697 |
if (mRotating != 0) { |
698 |
mHeading += mRotating * (PHYS_SLEW_SEC * elapsed); |
699 |
|
700 |
// Bring things back into the range 0..360 |
701 |
if (mHeading < 0) |
702 |
mHeading += 360; |
703 |
else if (mHeading >= 360) mHeading -= 360; |
704 |
} |
705 |
|
706 |
// Base accelerations -- 0 for x, gravity for y |
707 |
double ddx = 0.0; |
708 |
double ddy = -PHYS_DOWN_ACCEL_SEC * elapsed; |
709 |
|
710 |
if (mEngineFiring) { |
711 |
// taking 0 as up, 90 as to the right |
712 |
// cos(deg) is ddy component, sin(deg) is ddx component |
713 |
double elapsedFiring = elapsed; |
714 |
double fuelUsed = elapsedFiring * PHYS_FUEL_SEC; |
715 |
|
716 |
// tricky case where we run out of fuel partway through the |
717 |
// elapsed |
718 |
if (fuelUsed > mFuel) { |
719 |
elapsedFiring = mFuel / fuelUsed * elapsed; |
720 |
fuelUsed = mFuel; |
721 |
|
722 |
// Oddball case where we adjust the "control" from here |
723 |
mEngineFiring = false; |
724 |
} |
725 |
|
726 |
mFuel -= fuelUsed; |
727 |
|
728 |
// have this much acceleration from the engine |
729 |
double accel = PHYS_FIRE_ACCEL_SEC * elapsedFiring; |
730 |
|
731 |
double radians = 2 * Math.PI * mHeading / 360; |
732 |
ddx = Math.sin(radians) * accel; |
733 |
ddy += Math.cos(radians) * accel; |
734 |
} |
735 |
|
736 |
double dxOld = mDX; |
737 |
double dyOld = mDY; |
738 |
|
739 |
// figure speeds for the end of the period |
740 |
mDX += ddx; |
741 |
mDY += ddy; |
742 |
|
743 |
// figure position based on average speed during the period |
744 |
mX += elapsed * (mDX + dxOld) / 2; |
745 |
mY += elapsed * (mDY + dyOld) / 2; |
746 |
|
747 |
mLastTime = now; |
748 |
|
749 |
// Evaluate if we have landed ... stop the game |
750 |
double yLowerBound = TARGET_PAD_HEIGHT + mLanderHeight / 2 |
751 |
- TARGET_BOTTOM_PADDING; |
752 |
if (mY <= yLowerBound) { |
753 |
mY = yLowerBound; |
754 |
|
755 |
int result = STATE_LOSE; |
756 |
CharSequence message = ""; |
757 |
Resources res = mContext.getResources(); |
758 |
double speed = Math.sqrt(mDX * mDX + mDY * mDY); |
759 |
boolean onGoal = (mGoalX <= mX - mLanderWidth / 2 && mX |
760 |
+ mLanderWidth / 2 <= mGoalX + mGoalWidth); |
761 |
|
762 |
// "Hyperspace" win -- upside down, going fast, |
763 |
// puts you back at the top. |
764 |
if (onGoal && Math.abs(mHeading - 180) < mGoalAngle |
765 |
&& speed > PHYS_SPEED_HYPERSPACE) { |
766 |
result = STATE_WIN; |
767 |
mWinsInARow++; |
768 |
doStart(); |
769 |
|
770 |
return; |
771 |
// Oddball case: this case does a return, all other cases |
772 |
// fall through to setMode() below. |
773 |
} else if (!onGoal) { |
774 |
message = res.getText(R.string.message_off_pad); |
775 |
} else if (!(mHeading <= mGoalAngle || mHeading >= 360 - mGoalAngle)) { |
776 |
message = res.getText(R.string.message_bad_angle); |
777 |
} else if (speed > mGoalSpeed) { |
778 |
message = res.getText(R.string.message_too_fast); |
779 |
} else { |
780 |
result = STATE_WIN; |
781 |
mWinsInARow++; |
782 |
} |
783 |
|
784 |
setState(result, message); |
785 |
} |
786 |
} |
787 |
} |
788 |
|
789 |
/** Handle to the application context, used to e.g. fetch Drawables. */ |
790 |
private Context mContext; |
791 |
|
792 |
/** Pointer to the text view to display "Paused.." etc. */ |
793 |
private TextView mStatusText; |
794 |
|
795 |
/** Pointer to the ad view shown below <code>mStatusText</code>. */ |
796 |
private AdView mAd; // TODO You will need an AdView (this one is defined in res/layout/lunar_layout.xml). |
797 |
|
798 |
/** The thread that actually draws the animation */ |
799 |
private LunarThread thread; |
800 |
|
801 |
public LunarView(Context context, AttributeSet attrs) { |
802 |
super(context, attrs); |
803 |
|
804 |
// register our interest in hearing about changes to our surface |
805 |
SurfaceHolder holder = getHolder(); |
806 |
holder.addCallback(this); |
807 |
|
808 |
// create thread only; it's started in surfaceCreated() |
809 |
thread = new LunarThread(holder, context, new Handler() { |
810 |
private boolean firstTime = true; |
811 |
|
812 |
@Override |
813 |
public void handleMessage(Message m) { |
814 |
int vis = m.getData().getInt("viz"); |
815 |
mStatusText.setVisibility( vis ); |
816 |
mStatusText.setText(m.getData().getString("text")); |
817 |
|
818 |
// TODO Here we show the ad when paused or the game is over and |
819 |
// hide the ad when the game is being played. We choose not to |
820 |
// show and ad when the user starts the game (the first time the |
821 |
// screen is displayed). |
822 |
if ( !firstTime ) |
823 |
{ |
824 |
// Make the ad disappear with the text or a new ad appear. |
825 |
mAd.setVisibility( vis ); |
826 |
} |
827 |
|
828 |
firstTime = false; |
829 |
} |
830 |
}); |
831 |
|
832 |
setFocusable(true); // make sure we get key events |
833 |
} |
834 |
|
835 |
/** |
836 |
* Fetches the animation thread corresponding to this LunarView. |
837 |
* |
838 |
* @return the animation thread |
839 |
*/ |
840 |
public LunarThread getThread() { |
841 |
return thread; |
842 |
} |
843 |
|
844 |
/** |
845 |
* Standard override to get key-press events. |
846 |
*/ |
847 |
@Override |
848 |
public boolean onKeyDown(int keyCode, KeyEvent msg) { |
849 |
return thread.doKeyDown(keyCode, msg); |
850 |
} |
851 |
|
852 |
/** |
853 |
* Standard override for key-up. We actually care about these, so we can |
854 |
* turn off the engine or stop rotating. |
855 |
*/ |
856 |
@Override |
857 |
public boolean onKeyUp(int keyCode, KeyEvent msg) { |
858 |
return thread.doKeyUp(keyCode, msg); |
859 |
} |
860 |
|
861 |
/** |
862 |
* Called when the accuracy of one of the sensors changes. |
863 |
*/ |
864 |
public void onAccuracyChanged( Sensor sensor, int accuracy ) |
865 |
{ |
866 |
// We don't care. |
867 |
} |
868 |
|
869 |
/** |
870 |
* Called when the the value of a sensor changes. |
871 |
*/ |
872 |
public void onSensorChanged( SensorEvent sensor ) |
873 |
{ |
874 |
// We only use the sensor for the orientation of the phone. |
875 |
if ( sensor.sensor.getType() == Sensor.TYPE_ORIENTATION ) |
876 |
{ |
877 |
// How much is the phone rotating to the left or right? |
878 |
float tiltLeftRight = sensor.values[2]; |
879 |
|
880 |
// Simulate it as a left or right key press. |
881 |
thread.doOrientationChange( tiltLeftRight ); |
882 |
} |
883 |
} |
884 |
|
885 |
/** |
886 |
* Standard window-focus override. Notice focus lost so we can pause on |
887 |
* focus lost. e.g. user switches to take a call. |
888 |
*/ |
889 |
@Override |
890 |
public void onWindowFocusChanged(boolean hasWindowFocus) { |
891 |
if (!hasWindowFocus) thread.pause(); |
892 |
} |
893 |
|
894 |
/** |
895 |
* Installs a pointer to the text view used for messages. |
896 |
*/ |
897 |
public void setTextView(TextView textView, AdView adView) { |
898 |
mStatusText = textView; |
899 |
mAd = adView; |
900 |
} |
901 |
|
902 |
/* Callback invoked when the surface dimensions change. */ |
903 |
public void surfaceChanged(SurfaceHolder holder, int format, int width, |
904 |
int height) { |
905 |
thread.setSurfaceSize(width, height); |
906 |
} |
907 |
|
908 |
/* |
909 |
* Callback invoked when the Surface has been created and is ready to be |
910 |
* used. |
911 |
*/ |
912 |
public void surfaceCreated(SurfaceHolder holder) { |
913 |
// start the thread here so that we don't busy-wait in run() |
914 |
// waiting for the surface to be created |
915 |
thread.setRunning(true); |
916 |
thread.start(); |
917 |
} |
918 |
|
919 |
/* |
920 |
* Callback invoked when the Surface has been destroyed and must no longer |
921 |
* be touched. WARNING: after this method returns, the Surface/Canvas must |
922 |
* never be touched again! |
923 |
*/ |
924 |
public void surfaceDestroyed(SurfaceHolder holder) { |
925 |
// we have to tell thread to shut down & wait for it to finish, or else |
926 |
// it might touch the Surface after we return and explode |
927 |
boolean retry = true; |
928 |
thread.setRunning(false); |
929 |
while (retry) { |
930 |
try { |
931 |
thread.join(); |
932 |
retry = false; |
933 |
} catch (InterruptedException e) { |
934 |
} |
935 |
} |
936 |
} |
937 |
} |