1 |
torben |
337 |
/* |
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 |
|
|
} |