1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.systemui.egg; 18 19import android.animation.TimeAnimator; 20import android.content.Context; 21import android.content.res.Resources; 22import android.graphics.Canvas; 23import android.graphics.Color; 24import android.graphics.Matrix; 25import android.graphics.Outline; 26import android.graphics.Paint; 27import android.graphics.Path; 28import android.graphics.PorterDuff; 29import android.graphics.Rect; 30import android.graphics.drawable.Drawable; 31import android.graphics.drawable.GradientDrawable; 32import android.media.AudioAttributes; 33import android.media.AudioManager; 34import android.os.Vibrator; 35import android.util.AttributeSet; 36import android.util.Log; 37import android.view.Gravity; 38import android.view.KeyEvent; 39import android.view.MotionEvent; 40import android.util.Slog; 41import android.view.View; 42import android.view.ViewOutlineProvider; 43import android.view.animation.DecelerateInterpolator; 44import android.widget.FrameLayout; 45import android.widget.ImageView; 46import android.widget.TextView; 47 48import com.android.systemui.R; 49 50import java.util.ArrayList; 51 52public class LLand extends FrameLayout { 53 public static final String TAG = "LLand"; 54 55 public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 56 public static final boolean DEBUG_DRAW = false; // DEBUG 57 58 public static void L(String s, Object ... objects) { 59 if (DEBUG) { 60 Slog.d(TAG, objects.length == 0 ? s : String.format(s, objects)); 61 } 62 } 63 64 public static final boolean AUTOSTART = true; 65 public static final boolean HAVE_STARS = true; 66 67 public static final float DEBUG_SPEED_MULTIPLIER = 1f; // 0.1f; 68 public static final boolean DEBUG_IDDQD = Log.isLoggable(TAG + ".iddqd", Log.DEBUG); 69 70 final static int[] POPS = { 71 // resid // spinny! // alpha 72 R.drawable.pop_belt, 0, 255, 73 R.drawable.pop_droid, 0, 255, 74 R.drawable.pop_pizza, 1, 255, 75 R.drawable.pop_stripes, 0, 255, 76 R.drawable.pop_swirl, 1, 255, 77 R.drawable.pop_vortex, 1, 255, 78 R.drawable.pop_vortex2, 1, 255, 79 R.drawable.pop_ball, 0, 190, 80 }; 81 82 private static class Params { 83 public float TRANSLATION_PER_SEC; 84 public int OBSTACLE_SPACING, OBSTACLE_PERIOD; 85 public int BOOST_DV; 86 public int PLAYER_HIT_SIZE; 87 public int PLAYER_SIZE; 88 public int OBSTACLE_WIDTH, OBSTACLE_STEM_WIDTH; 89 public int OBSTACLE_GAP; 90 public int OBSTACLE_MIN; 91 public int BUILDING_WIDTH_MIN, BUILDING_WIDTH_MAX; 92 public int BUILDING_HEIGHT_MIN; 93 public int CLOUD_SIZE_MIN, CLOUD_SIZE_MAX; 94 public int STAR_SIZE_MIN, STAR_SIZE_MAX; 95 public int G; 96 public int MAX_V; 97 public float SCENERY_Z, OBSTACLE_Z, PLAYER_Z, PLAYER_Z_BOOST, HUD_Z; 98 public Params(Resources res) { 99 TRANSLATION_PER_SEC = res.getDimension(R.dimen.translation_per_sec); 100 OBSTACLE_SPACING = res.getDimensionPixelSize(R.dimen.obstacle_spacing); 101 OBSTACLE_PERIOD = (int) (OBSTACLE_SPACING / TRANSLATION_PER_SEC); 102 BOOST_DV = res.getDimensionPixelSize(R.dimen.boost_dv); 103 PLAYER_HIT_SIZE = res.getDimensionPixelSize(R.dimen.player_hit_size); 104 PLAYER_SIZE = res.getDimensionPixelSize(R.dimen.player_size); 105 OBSTACLE_WIDTH = res.getDimensionPixelSize(R.dimen.obstacle_width); 106 OBSTACLE_STEM_WIDTH = res.getDimensionPixelSize(R.dimen.obstacle_stem_width); 107 OBSTACLE_GAP = res.getDimensionPixelSize(R.dimen.obstacle_gap); 108 OBSTACLE_MIN = res.getDimensionPixelSize(R.dimen.obstacle_height_min); 109 BUILDING_HEIGHT_MIN = res.getDimensionPixelSize(R.dimen.building_height_min); 110 BUILDING_WIDTH_MIN = res.getDimensionPixelSize(R.dimen.building_width_min); 111 BUILDING_WIDTH_MAX = res.getDimensionPixelSize(R.dimen.building_width_max); 112 CLOUD_SIZE_MIN = res.getDimensionPixelSize(R.dimen.cloud_size_min); 113 CLOUD_SIZE_MAX = res.getDimensionPixelSize(R.dimen.cloud_size_max); 114 STAR_SIZE_MIN = res.getDimensionPixelSize(R.dimen.star_size_min); 115 STAR_SIZE_MAX = res.getDimensionPixelSize(R.dimen.star_size_max); 116 117 G = res.getDimensionPixelSize(R.dimen.G); 118 MAX_V = res.getDimensionPixelSize(R.dimen.max_v); 119 120 SCENERY_Z = res.getDimensionPixelSize(R.dimen.scenery_z); 121 OBSTACLE_Z = res.getDimensionPixelSize(R.dimen.obstacle_z); 122 PLAYER_Z = res.getDimensionPixelSize(R.dimen.player_z); 123 PLAYER_Z_BOOST = res.getDimensionPixelSize(R.dimen.player_z_boost); 124 HUD_Z = res.getDimensionPixelSize(R.dimen.hud_z); 125 126 // Sanity checking 127 if (OBSTACLE_MIN <= OBSTACLE_WIDTH / 2) { 128 Slog.e(TAG, "error: obstacles might be too short, adjusting"); 129 OBSTACLE_MIN = OBSTACLE_WIDTH / 2 + 1; 130 } 131 } 132 } 133 134 private TimeAnimator mAnim; 135 private Vibrator mVibrator; 136 private AudioManager mAudioManager; 137 private final AudioAttributes mAudioAttrs = new AudioAttributes.Builder() 138 .setUsage(AudioAttributes.USAGE_GAME).build(); 139 140 private TextView mScoreField; 141 private View mSplash; 142 143 private Player mDroid; 144 private ArrayList<Obstacle> mObstaclesInPlay = new ArrayList<Obstacle>(); 145 146 private float t, dt; 147 148 private int mScore; 149 private float mLastPipeTime; // in sec 150 private int mWidth, mHeight; 151 private boolean mAnimating, mPlaying; 152 private boolean mFrozen; // after death, a short backoff 153 private boolean mFlipped; 154 155 private int mTimeOfDay; 156 private static final int DAY = 0, NIGHT = 1, TWILIGHT = 2, SUNSET = 3; 157 private static final int[][] SKIES = { 158 { 0xFFc0c0FF, 0xFFa0a0FF }, // DAY 159 { 0xFF000010, 0xFF000000 }, // NIGHT 160 { 0xFF000040, 0xFF000010 }, // TWILIGHT 161 { 0xFFa08020, 0xFF204080 }, // SUNSET 162 }; 163 164 private static Params PARAMS; 165 166 public LLand(Context context) { 167 this(context, null); 168 } 169 170 public LLand(Context context, AttributeSet attrs) { 171 this(context, attrs, 0); 172 } 173 174 public LLand(Context context, AttributeSet attrs, int defStyle) { 175 super(context, attrs, defStyle); 176 mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); 177 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 178 setFocusable(true); 179 PARAMS = new Params(getResources()); 180 mTimeOfDay = irand(0, SKIES.length); 181 182 // we assume everything will be laid out left|top 183 setLayoutDirection(LAYOUT_DIRECTION_LTR); 184 } 185 186 @Override 187 public boolean willNotDraw() { 188 return !DEBUG; 189 } 190 191 public int getGameWidth() { return mWidth; } 192 public int getGameHeight() { return mHeight; } 193 public float getGameTime() { return t; } 194 public float getLastTimeStep() { return dt; } 195 196 public void setScoreField(TextView tv) { 197 mScoreField = tv; 198 if (tv != null) { 199 tv.setTranslationZ(PARAMS.HUD_Z); 200 if (!(mAnimating && mPlaying)) { 201 tv.setTranslationY(-500); 202 } 203 } 204 } 205 206 public void setSplash(View v) { 207 mSplash = v; 208 } 209 210 @Override 211 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 212 stop(); 213 reset(); 214 if (AUTOSTART) { 215 start(false); 216 } 217 } 218 219 final float hsv[] = {0, 0, 0}; 220 221 private void thump() { 222 if (mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_SILENT) { 223 // No interruptions. Not even game haptics. 224 return; 225 } 226 mVibrator.vibrate(80, mAudioAttrs); 227 } 228 229 public void reset() { 230 L("reset"); 231 final Drawable sky = new GradientDrawable( 232 GradientDrawable.Orientation.BOTTOM_TOP, 233 SKIES[mTimeOfDay] 234 ); 235 sky.setDither(true); 236 setBackground(sky); 237 238 mFlipped = frand() > 0.5f; 239 setScaleX(mFlipped ? -1 : 1); 240 241 setScore(0); 242 243 int i = getChildCount(); 244 while (i-->0) { 245 final View v = getChildAt(i); 246 if (v instanceof GameView) { 247 removeViewAt(i); 248 } 249 } 250 251 mObstaclesInPlay.clear(); 252 253 mWidth = getWidth(); 254 mHeight = getHeight(); 255 256 boolean showingSun = (mTimeOfDay == DAY || mTimeOfDay == SUNSET) && frand() > 0.25; 257 if (showingSun) { 258 final Star sun = new Star(getContext()); 259 sun.setBackgroundResource(R.drawable.sun); 260 final int w = getResources().getDimensionPixelSize(R.dimen.sun_size); 261 sun.setTranslationX(frand(w, mWidth-w)); 262 if (mTimeOfDay == DAY) { 263 sun.setTranslationY(frand(w, (mHeight * 0.66f))); 264 sun.getBackground().setTint(0); 265 } else { 266 sun.setTranslationY(frand(mHeight * 0.66f, mHeight - w)); 267 sun.getBackground().setTintMode(PorterDuff.Mode.SRC_ATOP); 268 sun.getBackground().setTint(0xC0FF8000); 269 270 } 271 addView(sun, new LayoutParams(w, w)); 272 } 273 if (!showingSun) { 274 final boolean dark = mTimeOfDay == NIGHT || mTimeOfDay == TWILIGHT; 275 final float ff = frand(); 276 if ((dark && ff < 0.75f) || ff < 0.5f) { 277 final Star moon = new Star(getContext()); 278 moon.setBackgroundResource(R.drawable.moon); 279 moon.getBackground().setAlpha(dark ? 255 : 128); 280 moon.setScaleX(frand() > 0.5 ? -1 : 1); 281 moon.setRotation(moon.getScaleX() * frand(5, 30)); 282 final int w = getResources().getDimensionPixelSize(R.dimen.sun_size); 283 moon.setTranslationX(frand(w, mWidth - w)); 284 moon.setTranslationY(frand(w, mHeight - w)); 285 addView(moon, new LayoutParams(w, w)); 286 } 287 } 288 289 final int mh = mHeight / 6; 290 final boolean cloudless = frand() < 0.25; 291 final int N = 20; 292 for (i=0; i<N; i++) { 293 final float r1 = frand(); 294 final Scenery s; 295 if (HAVE_STARS && r1 < 0.3 && mTimeOfDay != DAY) { 296 s = new Star(getContext()); 297 } else if (r1 < 0.6 && !cloudless) { 298 s = new Cloud(getContext()); 299 } else { 300 s = new Building(getContext()); 301 302 s.z = (float)i/N; 303 s.setTranslationZ(PARAMS.SCENERY_Z * (1+s.z)); 304 s.v = 0.85f * s.z; // buildings move proportional to their distance 305 hsv[0] = 175; 306 hsv[1] = 0.25f; 307 hsv[2] = 1 * s.z; 308 s.setBackgroundColor(Color.HSVToColor(hsv)); 309 s.h = irand(PARAMS.BUILDING_HEIGHT_MIN, mh); 310 } 311 final LayoutParams lp = new LayoutParams(s.w, s.h); 312 if (s instanceof Building) { 313 lp.gravity = Gravity.BOTTOM; 314 } else { 315 lp.gravity = Gravity.TOP; 316 final float r = frand(); 317 if (s instanceof Star) { 318 lp.topMargin = (int) (r * r * mHeight); 319 } else { 320 lp.topMargin = (int) (1 - r*r * mHeight/2) + mHeight/2; 321 } 322 } 323 324 addView(s, lp); 325 s.setTranslationX(frand(-lp.width, mWidth + lp.width)); 326 } 327 328 mDroid = new Player(getContext()); 329 mDroid.setX(mWidth / 2); 330 mDroid.setY(mHeight / 2); 331 addView(mDroid, new LayoutParams(PARAMS.PLAYER_SIZE, PARAMS.PLAYER_SIZE)); 332 333 mAnim = new TimeAnimator(); 334 mAnim.setTimeListener(new TimeAnimator.TimeListener() { 335 @Override 336 public void onTimeUpdate(TimeAnimator timeAnimator, long t, long dt) { 337 step(t, dt); 338 } 339 }); 340 } 341 342 private void setScore(int score) { 343 mScore = score; 344 if (mScoreField != null) { 345 mScoreField.setText(DEBUG_IDDQD ? "??" : String.valueOf(score)); 346 } 347 } 348 349 private void addScore(int incr) { 350 setScore(mScore + incr); 351 } 352 353 public void start(boolean startPlaying) { 354 L("start(startPlaying=%s)", startPlaying?"true":"false"); 355 if (startPlaying) { 356 mPlaying = true; 357 358 t = 0; 359 // there's a sucker born every OBSTACLE_PERIOD 360 mLastPipeTime = getGameTime() - PARAMS.OBSTACLE_PERIOD; 361 362 if (mSplash != null && mSplash.getAlpha() > 0f) { 363 mSplash.setTranslationZ(PARAMS.HUD_Z); 364 mSplash.animate().alpha(0).translationZ(0).setDuration(400); 365 366 mScoreField.animate().translationY(0) 367 .setInterpolator(new DecelerateInterpolator()) 368 .setDuration(1500); 369 } 370 371 mScoreField.setTextColor(0xFFAAAAAA); 372 mScoreField.setBackgroundResource(R.drawable.scorecard); 373 mDroid.setVisibility(View.VISIBLE); 374 mDroid.setX(mWidth / 2); 375 mDroid.setY(mHeight / 2); 376 } else { 377 mDroid.setVisibility(View.GONE); 378 } 379 if (!mAnimating) { 380 mAnim.start(); 381 mAnimating = true; 382 } 383 } 384 385 public void stop() { 386 if (mAnimating) { 387 mAnim.cancel(); 388 mAnim = null; 389 mAnimating = false; 390 mScoreField.setTextColor(0xFFFFFFFF); 391 mScoreField.setBackgroundResource(R.drawable.scorecard_gameover); 392 mTimeOfDay = irand(0, SKIES.length); // for next reset 393 mFrozen = true; 394 postDelayed(new Runnable() { 395 @Override 396 public void run() { 397 mFrozen = false; 398 } 399 }, 250); 400 } 401 } 402 403 public static final float lerp(float x, float a, float b) { 404 return (b - a) * x + a; 405 } 406 407 public static final float rlerp(float v, float a, float b) { 408 return (v - a) / (b - a); 409 } 410 411 public static final float clamp(float f) { 412 return f < 0f ? 0f : f > 1f ? 1f : f; 413 } 414 415 public static final float frand() { 416 return (float) Math.random(); 417 } 418 419 public static final float frand(float a, float b) { 420 return lerp(frand(), a, b); 421 } 422 423 public static final int irand(int a, int b) { 424 return (int) lerp(frand(), (float) a, (float) b); 425 } 426 427 private void step(long t_ms, long dt_ms) { 428 t = t_ms / 1000f; // seconds 429 dt = dt_ms / 1000f; 430 431 if (DEBUG) { 432 t *= DEBUG_SPEED_MULTIPLIER; 433 dt *= DEBUG_SPEED_MULTIPLIER; 434 } 435 436 // 1. Move all objects and update bounds 437 final int N = getChildCount(); 438 int i = 0; 439 for (; i<N; i++) { 440 final View v = getChildAt(i); 441 if (v instanceof GameView) { 442 ((GameView) v).step(t_ms, dt_ms, t, dt); 443 } 444 } 445 446 // 2. Check for altitude 447 if (mPlaying && mDroid.below(mHeight)) { 448 if (DEBUG_IDDQD) { 449 poke(); 450 unpoke(); 451 } else { 452 L("player hit the floor"); 453 thump(); 454 stop(); 455 } 456 } 457 458 // 3. Check for obstacles 459 boolean passedBarrier = false; 460 for (int j = mObstaclesInPlay.size(); j-->0;) { 461 final Obstacle ob = mObstaclesInPlay.get(j); 462 if (mPlaying && ob.intersects(mDroid) && !DEBUG_IDDQD) { 463 L("player hit an obstacle"); 464 thump(); 465 stop(); 466 } else if (ob.cleared(mDroid)) { 467 if (ob instanceof Stem) passedBarrier = true; 468 mObstaclesInPlay.remove(j); 469 } 470 } 471 472 if (mPlaying && passedBarrier) { 473 addScore(1); 474 } 475 476 // 4. Handle edge of screen 477 // Walk backwards to make sure removal is safe 478 while (i-->0) { 479 final View v = getChildAt(i); 480 if (v instanceof Obstacle) { 481 if (v.getTranslationX() + v.getWidth() < 0) { 482 removeViewAt(i); 483 } 484 } else if (v instanceof Scenery) { 485 final Scenery s = (Scenery) v; 486 if (v.getTranslationX() + s.w < 0) { 487 v.setTranslationX(getWidth()); 488 } 489 } 490 } 491 492 // 3. Time for more obstacles! 493 if (mPlaying && (t - mLastPipeTime) > PARAMS.OBSTACLE_PERIOD) { 494 mLastPipeTime = t; 495 final int obstacley = 496 (int)(frand() * (mHeight - 2*PARAMS.OBSTACLE_MIN - PARAMS.OBSTACLE_GAP)) + 497 PARAMS.OBSTACLE_MIN; 498 499 final int inset = (PARAMS.OBSTACLE_WIDTH - PARAMS.OBSTACLE_STEM_WIDTH) / 2; 500 final int yinset = PARAMS.OBSTACLE_WIDTH/2; 501 502 final int d1 = irand(0,250); 503 final Obstacle s1 = new Stem(getContext(), obstacley - yinset, false); 504 addView(s1, new LayoutParams( 505 PARAMS.OBSTACLE_STEM_WIDTH, 506 (int) s1.h, 507 Gravity.TOP|Gravity.LEFT)); 508 s1.setTranslationX(mWidth+inset); 509 s1.setTranslationY(-s1.h-yinset); 510 s1.setTranslationZ(PARAMS.OBSTACLE_Z*0.75f); 511 s1.animate() 512 .translationY(0) 513 .setStartDelay(d1) 514 .setDuration(250); 515 mObstaclesInPlay.add(s1); 516 517 final Obstacle p1 = new Pop(getContext(), PARAMS.OBSTACLE_WIDTH); 518 addView(p1, new LayoutParams( 519 PARAMS.OBSTACLE_WIDTH, 520 PARAMS.OBSTACLE_WIDTH, 521 Gravity.TOP|Gravity.LEFT)); 522 p1.setTranslationX(mWidth); 523 p1.setTranslationY(-PARAMS.OBSTACLE_WIDTH); 524 p1.setTranslationZ(PARAMS.OBSTACLE_Z); 525 p1.setScaleX(0.25f); 526 p1.setScaleY(0.25f); 527 p1.animate() 528 .translationY(s1.h-inset) 529 .scaleX(1f) 530 .scaleY(1f) 531 .setStartDelay(d1) 532 .setDuration(250); 533 mObstaclesInPlay.add(p1); 534 535 final int d2 = irand(0,250); 536 final Obstacle s2 = new Stem(getContext(), 537 mHeight - obstacley - PARAMS.OBSTACLE_GAP - yinset, 538 true); 539 addView(s2, new LayoutParams( 540 PARAMS.OBSTACLE_STEM_WIDTH, 541 (int) s2.h, 542 Gravity.TOP|Gravity.LEFT)); 543 s2.setTranslationX(mWidth+inset); 544 s2.setTranslationY(mHeight+yinset); 545 s2.setTranslationZ(PARAMS.OBSTACLE_Z*0.75f); 546 s2.animate() 547 .translationY(mHeight-s2.h) 548 .setStartDelay(d2) 549 .setDuration(400); 550 mObstaclesInPlay.add(s2); 551 552 final Obstacle p2 = new Pop(getContext(), PARAMS.OBSTACLE_WIDTH); 553 addView(p2, new LayoutParams( 554 PARAMS.OBSTACLE_WIDTH, 555 PARAMS.OBSTACLE_WIDTH, 556 Gravity.TOP|Gravity.LEFT)); 557 p2.setTranslationX(mWidth); 558 p2.setTranslationY(mHeight); 559 p2.setTranslationZ(PARAMS.OBSTACLE_Z); 560 p2.setScaleX(0.25f); 561 p2.setScaleY(0.25f); 562 p2.animate() 563 .translationY(mHeight-s2.h-yinset) 564 .scaleX(1f) 565 .scaleY(1f) 566 .setStartDelay(d2) 567 .setDuration(400); 568 mObstaclesInPlay.add(p2); 569 } 570 571 if (DEBUG_DRAW) invalidate(); 572 } 573 574 @Override 575 public boolean onTouchEvent(MotionEvent ev) { 576 L("touch: %s", ev); 577 switch (ev.getAction()) { 578 case MotionEvent.ACTION_DOWN: 579 poke(); 580 return true; 581 case MotionEvent.ACTION_UP: 582 unpoke(); 583 return true; 584 } 585 return false; 586 } 587 588 @Override 589 public boolean onTrackballEvent(MotionEvent ev) { 590 L("trackball: %s", ev); 591 switch (ev.getAction()) { 592 case MotionEvent.ACTION_DOWN: 593 poke(); 594 return true; 595 case MotionEvent.ACTION_UP: 596 unpoke(); 597 return true; 598 } 599 return false; 600 } 601 602 @Override 603 public boolean onKeyDown(int keyCode, KeyEvent ev) { 604 L("keyDown: %d", keyCode); 605 switch (keyCode) { 606 case KeyEvent.KEYCODE_DPAD_CENTER: 607 case KeyEvent.KEYCODE_DPAD_UP: 608 case KeyEvent.KEYCODE_SPACE: 609 case KeyEvent.KEYCODE_ENTER: 610 case KeyEvent.KEYCODE_BUTTON_A: 611 poke(); 612 return true; 613 } 614 return false; 615 } 616 617 @Override 618 public boolean onKeyUp(int keyCode, KeyEvent ev) { 619 L("keyDown: %d", keyCode); 620 switch (keyCode) { 621 case KeyEvent.KEYCODE_DPAD_CENTER: 622 case KeyEvent.KEYCODE_DPAD_UP: 623 case KeyEvent.KEYCODE_SPACE: 624 case KeyEvent.KEYCODE_ENTER: 625 case KeyEvent.KEYCODE_BUTTON_A: 626 unpoke(); 627 return true; 628 } 629 return false; 630 } 631 632 @Override 633 public boolean onGenericMotionEvent (MotionEvent ev) { 634 L("generic: %s", ev); 635 return false; 636 } 637 638 private void poke() { 639 L("poke"); 640 if (mFrozen) return; 641 if (!mAnimating) { 642 reset(); 643 start(true); 644 } else if (!mPlaying) { 645 start(true); 646 } 647 mDroid.boost(); 648 if (DEBUG) { 649 mDroid.dv *= DEBUG_SPEED_MULTIPLIER; 650 mDroid.animate().setDuration((long) (200/DEBUG_SPEED_MULTIPLIER)); 651 } 652 } 653 654 private void unpoke() { 655 L("unboost"); 656 if (mFrozen) return; 657 if (!mAnimating) return; 658 mDroid.unboost(); 659 } 660 661 @Override 662 public void onDraw(Canvas c) { 663 super.onDraw(c); 664 665 if (!DEBUG_DRAW) return; 666 667 final Paint pt = new Paint(); 668 pt.setColor(0xFFFFFFFF); 669 final int L = mDroid.corners.length; 670 final int N = L/2; 671 for (int i=0; i<N; i++) { 672 final int x = (int) mDroid.corners[i*2]; 673 final int y = (int) mDroid.corners[i*2+1]; 674 c.drawCircle(x, y, 4, pt); 675 c.drawLine(x, y, 676 mDroid.corners[(i*2+2)%L], 677 mDroid.corners[(i*2+3)%L], 678 pt); 679 } 680 681 pt.setStyle(Paint.Style.STROKE); 682 pt.setStrokeWidth(getResources().getDisplayMetrics().density); 683 684 final int M = getChildCount(); 685 pt.setColor(0x8000FF00); 686 for (int i=0; i<M; i++) { 687 final View v = getChildAt(i); 688 if (v == mDroid) continue; 689 if (!(v instanceof GameView)) continue; 690 if (v instanceof Pop) { 691 final Pop p = (Pop) v; 692 c.drawCircle(p.cx, p.cy, p.r, pt); 693 } else { 694 final Rect r = new Rect(); 695 v.getHitRect(r); 696 c.drawRect(r, pt); 697 } 698 } 699 700 pt.setColor(Color.BLACK); 701 final StringBuilder sb = new StringBuilder("obstacles: "); 702 for (Obstacle ob : mObstaclesInPlay) { 703 sb.append(ob.hitRect.toShortString()); 704 sb.append(" "); 705 } 706 pt.setTextSize(20f); 707 c.drawText(sb.toString(), 20, 100, pt); 708 } 709 710 static final Rect sTmpRect = new Rect(); 711 712 private interface GameView { 713 public void step(long t_ms, long dt_ms, float t, float dt); 714 } 715 716 private class Player extends ImageView implements GameView { 717 public float dv; 718 719 private boolean mBoosting; 720 721 private final int[] sColors = new int[] { 722 0xFF78C557, 723 }; 724 725 private final float[] sHull = new float[] { 726 0.3f, 0f, // left antenna 727 0.7f, 0f, // right antenna 728 0.92f, 0.33f, // off the right shoulder of Orion 729 0.92f, 0.75f, // right hand (our right, not his right) 730 0.6f, 1f, // right foot 731 0.4f, 1f, // left foot BLUE! 732 0.08f, 0.75f, // sinistram 733 0.08f, 0.33f, // cold shoulder 734 }; 735 public final float[] corners = new float[sHull.length]; 736 737 public Player(Context context) { 738 super(context); 739 740 setBackgroundResource(R.drawable.android); 741 getBackground().setTintMode(PorterDuff.Mode.SRC_ATOP); 742 getBackground().setTint(sColors[0]); 743 setOutlineProvider(new ViewOutlineProvider() { 744 @Override 745 public void getOutline(View view, Outline outline) { 746 final int w = view.getWidth(); 747 final int h = view.getHeight(); 748 final int ix = (int) (w * 0.3f); 749 final int iy = (int) (h * 0.2f); 750 outline.setRect(ix, iy, w - ix, h - iy); 751 } 752 }); 753 } 754 755 public void prepareCheckIntersections() { 756 final int inset = (PARAMS.PLAYER_SIZE - PARAMS.PLAYER_HIT_SIZE)/2; 757 final int scale = PARAMS.PLAYER_HIT_SIZE; 758 final int N = sHull.length/2; 759 for (int i=0; i<N; i++) { 760 corners[i*2] = scale * sHull[i*2] + inset; 761 corners[i*2+1] = scale * sHull[i*2+1] + inset; 762 } 763 final Matrix m = getMatrix(); 764 m.mapPoints(corners); 765 } 766 767 public boolean below(int h) { 768 final int N = corners.length/2; 769 for (int i=0; i<N; i++) { 770 final int y = (int) corners[i*2+1]; 771 if (y >= h) return true; 772 } 773 return false; 774 } 775 776 public void step(long t_ms, long dt_ms, float t, float dt) { 777 if (getVisibility() != View.VISIBLE) return; // not playing yet 778 779 if (mBoosting) { 780 dv = -PARAMS.BOOST_DV; 781 } else { 782 dv += PARAMS.G; 783 } 784 if (dv < -PARAMS.MAX_V) dv = -PARAMS.MAX_V; 785 else if (dv > PARAMS.MAX_V) dv = PARAMS.MAX_V; 786 787 final float y = getTranslationY() + dv * dt; 788 setTranslationY(y < 0 ? 0 : y); 789 setRotation( 790 90 + lerp(clamp(rlerp(dv, PARAMS.MAX_V, -1 * PARAMS.MAX_V)), 90, -90)); 791 792 prepareCheckIntersections(); 793 } 794 795 public void boost() { 796 mBoosting = true; 797 dv = -PARAMS.BOOST_DV; 798 799 animate().cancel(); 800 animate() 801 .scaleX(1.25f) 802 .scaleY(1.25f) 803 .translationZ(PARAMS.PLAYER_Z_BOOST) 804 .setDuration(100); 805 setScaleX(1.25f); 806 setScaleY(1.25f); 807 } 808 809 public void unboost() { 810 mBoosting = false; 811 812 animate().cancel(); 813 animate() 814 .scaleX(1f) 815 .scaleY(1f) 816 .translationZ(PARAMS.PLAYER_Z) 817 .setDuration(200); 818 } 819 } 820 821 private class Obstacle extends View implements GameView { 822 public float h; 823 824 public final Rect hitRect = new Rect(); 825 826 public Obstacle(Context context, float h) { 827 super(context); 828 setBackgroundColor(0xFFFF0000); 829 this.h = h; 830 } 831 832 public boolean intersects(Player p) { 833 final int N = p.corners.length/2; 834 for (int i=0; i<N; i++) { 835 final int x = (int) p.corners[i*2]; 836 final int y = (int) p.corners[i*2+1]; 837 if (hitRect.contains(x, y)) return true; 838 } 839 return false; 840 } 841 842 public boolean cleared(Player p) { 843 final int N = p.corners.length/2; 844 for (int i=0; i<N; i++) { 845 final int x = (int) p.corners[i*2]; 846 if (hitRect.right >= x) return false; 847 } 848 return true; 849 } 850 851 @Override 852 public void step(long t_ms, long dt_ms, float t, float dt) { 853 setTranslationX(getTranslationX()-PARAMS.TRANSLATION_PER_SEC*dt); 854 getHitRect(hitRect); 855 } 856 } 857 858 private class Pop extends Obstacle { 859 int mRotate; 860 int cx, cy, r; 861 public Pop(Context context, float h) { 862 super(context, h); 863 int idx = 3*irand(0, POPS.length/3); 864 setBackgroundResource(POPS[idx]); 865 setAlpha((float)(POPS[idx+2])/255); 866 setScaleX(frand() < 0.5f ? -1 : 1); 867 mRotate = POPS[idx+1] == 0 ? 0 : (frand() < 0.5f ? -1 : 1); 868 setOutlineProvider(new ViewOutlineProvider() { 869 @Override 870 public void getOutline(View view, Outline outline) { 871 final int pad = (int) (getWidth() * 0.02f); 872 outline.setOval(pad, pad, getWidth()-pad, getHeight()-pad); 873 } 874 }); 875 } 876 877 public boolean intersects(Player p) { 878 final int N = p.corners.length/2; 879 for (int i=0; i<N; i++) { 880 final int x = (int) p.corners[i*2]; 881 final int y = (int) p.corners[i*2+1]; 882 if (Math.hypot(x-cx, y-cy) <= r) return true; 883 } 884 return false; 885 } 886 887 @Override 888 public void step(long t_ms, long dt_ms, float t, float dt) { 889 super.step(t_ms, dt_ms, t, dt); 890 if (mRotate != 0) { 891 setRotation(getRotation() + dt * 45 * mRotate); 892 } 893 894 cx = (hitRect.left + hitRect.right)/2; 895 cy = (hitRect.top + hitRect.bottom)/2; 896 r = getWidth()/2; 897 } 898 } 899 900 private class Stem extends Obstacle { 901 Paint mPaint = new Paint(); 902 Path mShadow = new Path(); 903 boolean mDrawShadow; 904 905 public Stem(Context context, float h, boolean drawShadow) { 906 super(context, h); 907 mDrawShadow = drawShadow; 908 mPaint.setColor(0xFFAAAAAA); 909 setBackground(null); 910 } 911 912 @Override 913 public void onAttachedToWindow() { 914 super.onAttachedToWindow(); 915 setWillNotDraw(false); 916 setOutlineProvider(new ViewOutlineProvider() { 917 @Override 918 public void getOutline(View view, Outline outline) { 919 outline.setRect(0, 0, getWidth(), getHeight()); 920 } 921 }); 922 } 923 @Override 924 public void onDraw(Canvas c) { 925 final int w = c.getWidth(); 926 final int h = c.getHeight(); 927 final GradientDrawable g = new GradientDrawable(); 928 g.setOrientation(GradientDrawable.Orientation.LEFT_RIGHT); 929 g.setGradientCenter(w * 0.75f, 0); 930 g.setColors(new int[] { 0xFFFFFFFF, 0xFFAAAAAA }); 931 g.setBounds(0, 0, w, h); 932 g.draw(c); 933 if (!mDrawShadow) return; 934 mShadow.reset(); 935 mShadow.moveTo(0,0); 936 mShadow.lineTo(w, 0); 937 mShadow.lineTo(w, PARAMS.OBSTACLE_WIDTH/2+w*1.5f); 938 mShadow.lineTo(0, PARAMS.OBSTACLE_WIDTH/2); 939 mShadow.close(); 940 c.drawPath(mShadow, mPaint); 941 } 942 } 943 944 private class Scenery extends FrameLayout implements GameView { 945 public float z; 946 public float v; 947 public int h, w; 948 public Scenery(Context context) { 949 super(context); 950 } 951 952 @Override 953 public void step(long t_ms, long dt_ms, float t, float dt) { 954 setTranslationX(getTranslationX() - PARAMS.TRANSLATION_PER_SEC * dt * v); 955 } 956 } 957 958 private class Building extends Scenery { 959 public Building(Context context) { 960 super(context); 961 962 w = irand(PARAMS.BUILDING_WIDTH_MIN, PARAMS.BUILDING_WIDTH_MAX); 963 h = 0; // will be setup later, along with z 964 965 setTranslationZ(PARAMS.SCENERY_Z); 966 } 967 } 968 969 private class Cloud extends Scenery { 970 public Cloud(Context context) { 971 super(context); 972 setBackgroundResource(frand() < 0.01f ? R.drawable.cloud_off : R.drawable.cloud); 973 getBackground().setAlpha(0x40); 974 w = h = irand(PARAMS.CLOUD_SIZE_MIN, PARAMS.CLOUD_SIZE_MAX); 975 z = 0; 976 v = frand(0.15f,0.5f); 977 } 978 } 979 980 private class Star extends Scenery { 981 public Star(Context context) { 982 super(context); 983 setBackgroundResource(R.drawable.star); 984 w = h = irand(PARAMS.STAR_SIZE_MIN, PARAMS.STAR_SIZE_MAX); 985 v = z = 0; 986 } 987 } 988} 989