1 /* 2 * Copyright (C) 2011 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 17 package com.example.android.apis.view; 18 19 import android.content.Context; 20 import android.graphics.Canvas; 21 import android.graphics.Paint; 22 import android.graphics.Path; 23 import android.graphics.Paint.Style; 24 import android.os.Handler; 25 import android.os.SystemClock; 26 import android.util.AttributeSet; 27 import android.view.InputDevice; 28 import android.view.KeyEvent; 29 import android.view.MotionEvent; 30 import android.view.View; 31 32 import java.util.ArrayList; 33 import java.util.List; 34 import java.util.Random; 35 36 /** 37 * A trivial joystick based physics game to demonstrate joystick handling. 38 * 39 * @see GameControllerInput 40 */ 41 public class GameView extends View { 42 private final long ANIMATION_TIME_STEP = 1000 / 60; 43 private final int MAX_OBSTACLES = 12; 44 45 private final Random mRandom; 46 private Ship mShip; 47 private final List<Bullet> mBullets; 48 private final List<Obstacle> mObstacles; 49 50 private long mLastStepTime; 51 private InputDevice mLastInputDevice; 52 53 private static final int DPAD_STATE_LEFT = 1 << 0; 54 private static final int DPAD_STATE_RIGHT = 1 << 1; 55 private static final int DPAD_STATE_UP = 1 << 2; 56 private static final int DPAD_STATE_DOWN = 1 << 3; 57 58 private int mDPadState; 59 60 private float mShipSize; 61 private float mMaxShipThrust; 62 private float mMaxShipSpeed; 63 64 private float mBulletSize; 65 private float mBulletSpeed; 66 67 private float mMinObstacleSize; 68 private float mMaxObstacleSize; 69 private float mMinObstacleSpeed; 70 private float mMaxObstacleSpeed; 71 72 private final Runnable mAnimationRunnable = new Runnable() { 73 public void run() { 74 animateFrame(); 75 } 76 }; 77 GameView(Context context, AttributeSet attrs)78 public GameView(Context context, AttributeSet attrs) { 79 super(context, attrs); 80 81 mRandom = new Random(); 82 mBullets = new ArrayList<Bullet>(); 83 mObstacles = new ArrayList<Obstacle>(); 84 85 setFocusable(true); 86 setFocusableInTouchMode(true); 87 88 float baseSize = getContext().getResources().getDisplayMetrics().density * 5f; 89 float baseSpeed = baseSize * 3; 90 91 mShipSize = baseSize * 3; 92 mMaxShipThrust = baseSpeed * 0.25f; 93 mMaxShipSpeed = baseSpeed * 12; 94 95 mBulletSize = baseSize; 96 mBulletSpeed = baseSpeed * 12; 97 98 mMinObstacleSize = baseSize * 2; 99 mMaxObstacleSize = baseSize * 12; 100 mMinObstacleSpeed = baseSpeed; 101 mMaxObstacleSpeed = baseSpeed * 3; 102 } 103 104 @Override onSizeChanged(int w, int h, int oldw, int oldh)105 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 106 super.onSizeChanged(w, h, oldw, oldh); 107 108 // Reset the game when the view changes size. 109 reset(); 110 } 111 112 @Override onKeyDown(int keyCode, KeyEvent event)113 public boolean onKeyDown(int keyCode, KeyEvent event) { 114 ensureInitialized(); 115 116 // Handle DPad keys and fire button on initial down but not on auto-repeat. 117 boolean handled = false; 118 if (event.getRepeatCount() == 0) { 119 switch (keyCode) { 120 case KeyEvent.KEYCODE_DPAD_LEFT: 121 mShip.setHeadingX(-1); 122 mDPadState |= DPAD_STATE_LEFT; 123 handled = true; 124 break; 125 case KeyEvent.KEYCODE_DPAD_RIGHT: 126 mShip.setHeadingX(1); 127 mDPadState |= DPAD_STATE_RIGHT; 128 handled = true; 129 break; 130 case KeyEvent.KEYCODE_DPAD_UP: 131 mShip.setHeadingY(-1); 132 mDPadState |= DPAD_STATE_UP; 133 handled = true; 134 break; 135 case KeyEvent.KEYCODE_DPAD_DOWN: 136 mShip.setHeadingY(1); 137 mDPadState |= DPAD_STATE_DOWN; 138 handled = true; 139 break; 140 default: 141 if (isFireKey(keyCode)) { 142 fire(); 143 handled = true; 144 } 145 break; 146 } 147 } 148 if (handled) { 149 step(event.getEventTime()); 150 return true; 151 } 152 return super.onKeyDown(keyCode, event); 153 } 154 155 @Override onKeyUp(int keyCode, KeyEvent event)156 public boolean onKeyUp(int keyCode, KeyEvent event) { 157 ensureInitialized(); 158 159 // Handle keys going up. 160 boolean handled = false; 161 switch (keyCode) { 162 case KeyEvent.KEYCODE_DPAD_LEFT: 163 mShip.setHeadingX(0); 164 mDPadState &= ~DPAD_STATE_LEFT; 165 handled = true; 166 break; 167 case KeyEvent.KEYCODE_DPAD_RIGHT: 168 mShip.setHeadingX(0); 169 mDPadState &= ~DPAD_STATE_RIGHT; 170 handled = true; 171 break; 172 case KeyEvent.KEYCODE_DPAD_UP: 173 mShip.setHeadingY(0); 174 mDPadState &= ~DPAD_STATE_UP; 175 handled = true; 176 break; 177 case KeyEvent.KEYCODE_DPAD_DOWN: 178 mShip.setHeadingY(0); 179 mDPadState &= ~DPAD_STATE_DOWN; 180 handled = true; 181 break; 182 default: 183 if (isFireKey(keyCode)) { 184 handled = true; 185 } 186 break; 187 } 188 if (handled) { 189 step(event.getEventTime()); 190 return true; 191 } 192 return super.onKeyUp(keyCode, event); 193 } 194 isFireKey(int keyCode)195 private static boolean isFireKey(int keyCode) { 196 return KeyEvent.isGamepadButton(keyCode) 197 || keyCode == KeyEvent.KEYCODE_DPAD_CENTER 198 || keyCode == KeyEvent.KEYCODE_SPACE; 199 } 200 201 @Override onGenericMotionEvent(MotionEvent event)202 public boolean onGenericMotionEvent(MotionEvent event) { 203 ensureInitialized(); 204 205 // Check that the event came from a joystick since a generic motion event 206 // could be almost anything. 207 if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 208 && event.getAction() == MotionEvent.ACTION_MOVE) { 209 // Cache the most recently obtained device information. 210 // The device information may change over time but it can be 211 // somewhat expensive to query. 212 if (mLastInputDevice == null || mLastInputDevice.getId() != event.getDeviceId()) { 213 mLastInputDevice = event.getDevice(); 214 // It's possible for the device id to be invalid. 215 // In that case, getDevice() will return null. 216 if (mLastInputDevice == null) { 217 return false; 218 } 219 } 220 221 // Ignore joystick while the DPad is pressed to avoid conflicting motions. 222 if (mDPadState != 0) { 223 return true; 224 } 225 226 // Process all historical movement samples in the batch. 227 final int historySize = event.getHistorySize(); 228 for (int i = 0; i < historySize; i++) { 229 processJoystickInput(event, i); 230 } 231 232 // Process the current movement sample in the batch. 233 processJoystickInput(event, -1); 234 return true; 235 } 236 return super.onGenericMotionEvent(event); 237 } 238 processJoystickInput(MotionEvent event, int historyPos)239 private void processJoystickInput(MotionEvent event, int historyPos) { 240 // Get joystick position. 241 // Many game pads with two joysticks report the position of the second joystick 242 // using the Z and RZ axes so we also handle those. 243 // In a real game, we would allow the user to configure the axes manually. 244 float x = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_X, historyPos); 245 if (x == 0) { 246 x = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_HAT_X, historyPos); 247 } 248 if (x == 0) { 249 x = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_Z, historyPos); 250 } 251 252 float y = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_Y, historyPos); 253 if (y == 0) { 254 y = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_HAT_Y, historyPos); 255 } 256 if (y == 0) { 257 y = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_RZ, historyPos); 258 } 259 260 // Set the ship heading. 261 mShip.setHeading(x, y); 262 step(historyPos < 0 ? event.getEventTime() : event.getHistoricalEventTime(historyPos)); 263 } 264 265 private static float getCenteredAxis(MotionEvent event, InputDevice device, 266 int axis, int historyPos) { 267 final InputDevice.MotionRange range = device.getMotionRange(axis, event.getSource()); 268 if (range != null) { 269 final float flat = range.getFlat(); 270 final float value = historyPos < 0 ? event.getAxisValue(axis) 271 : event.getHistoricalAxisValue(axis, historyPos); 272 273 // Ignore axis values that are within the 'flat' region of the joystick axis center. 274 // A joystick at rest does not always report an absolute position of (0,0). 275 if (Math.abs(value) > flat) { 276 return value; 277 } 278 } 279 return 0; 280 } 281 282 @Override 283 public void onWindowFocusChanged(boolean hasWindowFocus) { 284 // Turn on and off animations based on the window focus. 285 // Alternately, we could update the game state using the Activity onResume() 286 // and onPause() lifecycle events. 287 if (hasWindowFocus) { 288 getHandler().postDelayed(mAnimationRunnable, ANIMATION_TIME_STEP); 289 mLastStepTime = SystemClock.uptimeMillis(); 290 } else { 291 getHandler().removeCallbacks(mAnimationRunnable); 292 293 mDPadState = 0; 294 if (mShip != null) { 295 mShip.setHeading(0, 0); 296 mShip.setVelocity(0, 0); 297 } 298 } 299 300 super.onWindowFocusChanged(hasWindowFocus); 301 } 302 303 private void fire() { 304 if (mShip != null && !mShip.isDestroyed()) { 305 Bullet bullet = new Bullet(); 306 bullet.setPosition(mShip.getBulletInitialX(), mShip.getBulletInitialY()); 307 bullet.setVelocity(mShip.getBulletVelocityX(mBulletSpeed), 308 mShip.getBulletVelocityY(mBulletSpeed)); 309 mBullets.add(bullet); 310 } 311 } 312 313 private void ensureInitialized() { 314 if (mShip == null) { 315 reset(); 316 } 317 } 318 319 private void reset() { 320 mShip = new Ship(); 321 mBullets.clear(); 322 mObstacles.clear(); 323 } 324 325 void animateFrame() { 326 long currentStepTime = SystemClock.uptimeMillis(); 327 step(currentStepTime); 328 329 Handler handler = getHandler(); 330 if (handler != null) { 331 handler.postAtTime(mAnimationRunnable, currentStepTime + ANIMATION_TIME_STEP); 332 invalidate(); 333 } 334 } 335 336 private void step(long currentStepTime) { 337 float tau = (currentStepTime - mLastStepTime) * 0.001f; 338 mLastStepTime = currentStepTime; 339 340 ensureInitialized(); 341 342 // Move the ship. 343 mShip.accelerate(tau, mMaxShipThrust, mMaxShipSpeed); 344 if (!mShip.step(tau)) { 345 reset(); 346 } 347 348 // Move the bullets. 349 int numBullets = mBullets.size(); 350 for (int i = 0; i < numBullets; i++) { 351 final Bullet bullet = mBullets.get(i); 352 if (!bullet.step(tau)) { 353 mBullets.remove(i); 354 i -= 1; 355 numBullets -= 1; 356 } 357 } 358 359 // Move obstacles. 360 int numObstacles = mObstacles.size(); 361 for (int i = 0; i < numObstacles; i++) { 362 final Obstacle obstacle = mObstacles.get(i); 363 if (!obstacle.step(tau)) { 364 mObstacles.remove(i); 365 i -= 1; 366 numObstacles -= 1; 367 } 368 } 369 370 // Check for collisions between bullets and obstacles. 371 for (int i = 0; i < numBullets; i++) { 372 final Bullet bullet = mBullets.get(i); 373 for (int j = 0; j < numObstacles; j++) { 374 final Obstacle obstacle = mObstacles.get(j); 375 if (bullet.collidesWith(obstacle)) { 376 bullet.destroy(); 377 obstacle.destroy(); 378 break; 379 } 380 } 381 } 382 383 // Check for collisions between the ship and obstacles. 384 for (int i = 0; i < numObstacles; i++) { 385 final Obstacle obstacle = mObstacles.get(i); 386 if (mShip.collidesWith(obstacle)) { 387 mShip.destroy(); 388 obstacle.destroy(); 389 break; 390 } 391 } 392 393 // Spawn more obstacles offscreen when needed. 394 // Avoid putting them right on top of the ship. 395 OuterLoop: while (mObstacles.size() < MAX_OBSTACLES) { 396 final float minDistance = mShipSize * 4; 397 float size = mRandom.nextFloat() * (mMaxObstacleSize - mMinObstacleSize) 398 + mMinObstacleSize; 399 float positionX, positionY; 400 int tries = 0; 401 do { 402 int edge = mRandom.nextInt(4); 403 switch (edge) { 404 case 0: 405 positionX = -size; 406 positionY = mRandom.nextInt(getHeight()); 407 break; 408 case 1: 409 positionX = getWidth() + size; 410 positionY = mRandom.nextInt(getHeight()); 411 break; 412 case 2: 413 positionX = mRandom.nextInt(getWidth()); 414 positionY = -size; 415 break; 416 default: 417 positionX = mRandom.nextInt(getWidth()); 418 positionY = getHeight() + size; 419 break; 420 } 421 if (++tries > 10) { 422 break OuterLoop; 423 } 424 } while (mShip.distanceTo(positionX, positionY) < minDistance); 425 426 float direction = mRandom.nextFloat() * (float) Math.PI * 2; 427 float speed = mRandom.nextFloat() * (mMaxObstacleSpeed - mMinObstacleSpeed) 428 + mMinObstacleSpeed; 429 float velocityX = (float) Math.cos(direction) * speed; 430 float velocityY = (float) Math.sin(direction) * speed; 431 432 Obstacle obstacle = new Obstacle(); 433 obstacle.setPosition(positionX, positionY); 434 obstacle.setSize(size); 435 obstacle.setVelocity(velocityX, velocityY); 436 mObstacles.add(obstacle); 437 } 438 } 439 440 @Override 441 protected void onDraw(Canvas canvas) { 442 super.onDraw(canvas); 443 444 // Draw the ship. 445 if (mShip != null) { 446 mShip.draw(canvas); 447 } 448 449 // Draw bullets. 450 int numBullets = mBullets.size(); 451 for (int i = 0; i < numBullets; i++) { 452 final Bullet bullet = mBullets.get(i); 453 bullet.draw(canvas); 454 } 455 456 // Draw obstacles. 457 int numObstacles = mObstacles.size(); 458 for (int i = 0; i < numObstacles; i++) { 459 final Obstacle obstacle = mObstacles.get(i); 460 obstacle.draw(canvas); 461 } 462 } 463 464 static float pythag(float x, float y) { 465 return (float) Math.sqrt(x * x + y * y); 466 } 467 468 static int blend(float alpha, int from, int to) { 469 return from + (int) ((to - from) * alpha); 470 } 471 472 static void setPaintARGBBlend(Paint paint, float alpha, 473 int a1, int r1, int g1, int b1, 474 int a2, int r2, int g2, int b2) { 475 paint.setARGB(blend(alpha, a1, a2), blend(alpha, r1, r2), 476 blend(alpha, g1, g2), blend(alpha, b1, b2)); 477 } 478 479 private abstract class Sprite { 480 protected float mPositionX; 481 protected float mPositionY; 482 protected float mVelocityX; 483 protected float mVelocityY; 484 protected float mSize; 485 protected boolean mDestroyed; 486 protected float mDestroyAnimProgress; 487 488 public void setPosition(float x, float y) { 489 mPositionX = x; 490 mPositionY = y; 491 } 492 493 public void setVelocity(float x, float y) { 494 mVelocityX = x; 495 mVelocityY = y; 496 } 497 498 public void setSize(float size) { 499 mSize = size; 500 } 501 502 public float distanceTo(float x, float y) { 503 return pythag(mPositionX - x, mPositionY - y); 504 } 505 506 public float distanceTo(Sprite other) { 507 return distanceTo(other.mPositionX, other.mPositionY); 508 } 509 510 public boolean collidesWith(Sprite other) { 511 // Really bad collision detection. 512 return !mDestroyed && !other.mDestroyed 513 && distanceTo(other) <= Math.max(mSize, other.mSize) 514 + Math.min(mSize, other.mSize) * 0.5f; 515 } 516 517 public boolean isDestroyed() { 518 return mDestroyed; 519 } 520 521 public boolean step(float tau) { 522 mPositionX += mVelocityX * tau; 523 mPositionY += mVelocityY * tau; 524 525 if (mDestroyed) { 526 mDestroyAnimProgress += tau / getDestroyAnimDuration(); 527 if (mDestroyAnimProgress >= 1.0f) { 528 return false; 529 } 530 } 531 return true; 532 } 533 534 public abstract void draw(Canvas canvas); 535 536 public abstract float getDestroyAnimDuration(); 537 538 protected boolean isOutsidePlayfield() { 539 final int width = GameView.this.getWidth(); 540 final int height = GameView.this.getHeight(); 541 return mPositionX < 0 || mPositionX >= width 542 || mPositionY < 0 || mPositionY >= height; 543 } 544 545 protected void wrapAtPlayfieldBoundary() { 546 final int width = GameView.this.getWidth(); 547 final int height = GameView.this.getHeight(); 548 while (mPositionX <= -mSize) { 549 mPositionX += width + mSize * 2; 550 } 551 while (mPositionX >= width + mSize) { 552 mPositionX -= width + mSize * 2; 553 } 554 while (mPositionY <= -mSize) { 555 mPositionY += height + mSize * 2; 556 } 557 while (mPositionY >= height + mSize) { 558 mPositionY -= height + mSize * 2; 559 } 560 } 561 562 public void destroy() { 563 mDestroyed = true; 564 step(0); 565 } 566 } 567 568 private class Ship extends Sprite { 569 private static final float CORNER_ANGLE = (float) Math.PI * 2 / 3; 570 private static final float TO_DEGREES = (float) (180.0 / Math.PI); 571 572 private float mHeadingX; 573 private float mHeadingY; 574 private float mHeadingAngle; 575 private float mHeadingMagnitude; 576 private final Paint mPaint; 577 private final Path mPath; 578 579 580 public Ship() { 581 mPaint = new Paint(); 582 mPaint.setStyle(Style.FILL); 583 584 setPosition(getWidth() * 0.5f, getHeight() * 0.5f); 585 setVelocity(0, 0); 586 setSize(mShipSize); 587 588 mPath = new Path(); 589 mPath.moveTo(0, 0); 590 mPath.lineTo((float)Math.cos(-CORNER_ANGLE) * mSize, 591 (float)Math.sin(-CORNER_ANGLE) * mSize); 592 mPath.lineTo(mSize, 0); 593 mPath.lineTo((float)Math.cos(CORNER_ANGLE) * mSize, 594 (float)Math.sin(CORNER_ANGLE) * mSize); 595 mPath.lineTo(0, 0); 596 } 597 598 public void setHeadingX(float x) { 599 mHeadingX = x; 600 updateHeading(); 601 } 602 603 public void setHeadingY(float y) { 604 mHeadingY = y; 605 updateHeading(); 606 } 607 608 public void setHeading(float x, float y) { 609 mHeadingX = x; 610 mHeadingY = y; 611 updateHeading(); 612 } 613 614 private void updateHeading() { 615 mHeadingMagnitude = pythag(mHeadingX, mHeadingY); 616 if (mHeadingMagnitude > 0.1f) { 617 mHeadingAngle = (float) Math.atan2(mHeadingY, mHeadingX); 618 } 619 } 620 621 private float polarX(float radius) { 622 return (float) Math.cos(mHeadingAngle) * radius; 623 } 624 625 private float polarY(float radius) { 626 return (float) Math.sin(mHeadingAngle) * radius; 627 } 628 629 public float getBulletInitialX() { 630 return mPositionX + polarX(mSize); 631 } 632 633 public float getBulletInitialY() { 634 return mPositionY + polarY(mSize); 635 } 636 637 public float getBulletVelocityX(float relativeSpeed) { 638 return mVelocityX + polarX(relativeSpeed); 639 } 640 641 public float getBulletVelocityY(float relativeSpeed) { 642 return mVelocityY + polarY(relativeSpeed); 643 } 644 645 public void accelerate(float tau, float maxThrust, float maxSpeed) { 646 final float thrust = mHeadingMagnitude * maxThrust; 647 mVelocityX += polarX(thrust); 648 mVelocityY += polarY(thrust); 649 650 final float speed = pythag(mVelocityX, mVelocityY); 651 if (speed > maxSpeed) { 652 final float scale = maxSpeed / speed; 653 mVelocityX = mVelocityX * scale; 654 mVelocityY = mVelocityY * scale; 655 } 656 } 657 658 @Override 659 public boolean step(float tau) { 660 if (!super.step(tau)) { 661 return false; 662 } 663 wrapAtPlayfieldBoundary(); 664 return true; 665 } 666 667 public void draw(Canvas canvas) { 668 setPaintARGBBlend(mPaint, mDestroyAnimProgress, 669 255, 63, 255, 63, 670 0, 255, 0, 0); 671 672 canvas.save(Canvas.MATRIX_SAVE_FLAG); 673 canvas.translate(mPositionX, mPositionY); 674 canvas.rotate(mHeadingAngle * TO_DEGREES); 675 canvas.drawPath(mPath, mPaint); 676 canvas.restore(); 677 } 678 679 @Override 680 public float getDestroyAnimDuration() { 681 return 1.0f; 682 } 683 } 684 685 private class Bullet extends Sprite { 686 private final Paint mPaint; 687 688 public Bullet() { 689 mPaint = new Paint(); 690 mPaint.setStyle(Style.FILL); 691 692 setSize(mBulletSize); 693 } 694 695 @Override 696 public boolean step(float tau) { 697 if (!super.step(tau)) { 698 return false; 699 } 700 return !isOutsidePlayfield(); 701 } 702 703 public void draw(Canvas canvas) { 704 setPaintARGBBlend(mPaint, mDestroyAnimProgress, 705 255, 255, 255, 0, 706 0, 255, 255, 255); 707 canvas.drawCircle(mPositionX, mPositionY, mSize, mPaint); 708 } 709 710 @Override 711 public float getDestroyAnimDuration() { 712 return 0.125f; 713 } 714 } 715 716 private class Obstacle extends Sprite { 717 private final Paint mPaint; 718 719 public Obstacle() { 720 mPaint = new Paint(); 721 mPaint.setARGB(255, 127, 127, 255); 722 mPaint.setStyle(Style.FILL); 723 } 724 725 @Override 726 public boolean step(float tau) { 727 if (!super.step(tau)) { 728 return false; 729 } 730 wrapAtPlayfieldBoundary(); 731 return true; 732 } 733 734 public void draw(Canvas canvas) { 735 setPaintARGBBlend(mPaint, mDestroyAnimProgress, 736 255, 127, 127, 255, 737 0, 255, 0, 0); 738 canvas.drawCircle(mPositionX, mPositionY, 739 mSize * (1.0f - mDestroyAnimProgress), mPaint); 740 } 741 742 @Override 743 public float getDestroyAnimDuration() { 744 return 0.25f; 745 } 746 } 747 }