1 /* 2 * Copyright (C) 2013 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.controllersample; 18 19 import com.example.inputmanagercompat.InputManagerCompat; 20 import com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener; 21 22 import android.annotation.SuppressLint; 23 import android.annotation.TargetApi; 24 import android.content.Context; 25 import android.graphics.Canvas; 26 import android.graphics.Paint; 27 import android.graphics.Paint.Style; 28 import android.graphics.Path; 29 import android.os.Build; 30 import android.os.SystemClock; 31 import android.os.Vibrator; 32 import android.util.AttributeSet; 33 import android.util.SparseArray; 34 import android.view.InputDevice; 35 import android.view.KeyEvent; 36 import android.view.MotionEvent; 37 import android.view.View; 38 39 import java.util.ArrayList; 40 import java.util.HashMap; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.Random; 44 45 /* 46 * A trivial joystick based physics game to demonstrate joystick handling. If 47 * the game controller has a vibrator, then it is used to provide feedback when 48 * a bullet is fired or the ship crashes into an obstacle. Otherwise, the system 49 * vibrator is used for that purpose. 50 */ 51 @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) 52 public class GameView extends View implements InputDeviceListener { 53 private static final int MAX_OBSTACLES = 12; 54 55 private static final int DPAD_STATE_LEFT = 1 << 0; 56 private static final int DPAD_STATE_RIGHT = 1 << 1; 57 private static final int DPAD_STATE_UP = 1 << 2; 58 private static final int DPAD_STATE_DOWN = 1 << 3; 59 60 private final Random mRandom; 61 /* 62 * Each ship is created as an event comes in from a new Joystick device 63 */ 64 private final SparseArray<Ship> mShips; 65 private final Map<String, Integer> mDescriptorMap; 66 private final List<Bullet> mBullets; 67 private final List<Obstacle> mObstacles; 68 69 private long mLastStepTime; 70 private final InputManagerCompat mInputManager; 71 72 private final float mBaseSpeed; 73 74 private final float mShipSize; 75 76 private final float mBulletSize; 77 78 private final float mMinObstacleSize; 79 private final float mMaxObstacleSize; 80 private final float mMinObstacleSpeed; 81 private final float mMaxObstacleSpeed; 82 83 public GameView(Context context, AttributeSet attrs) { 84 super(context, attrs); 85 86 mRandom = new Random(); 87 mShips = new SparseArray<Ship>(); 88 mDescriptorMap = new HashMap<String, Integer>(); 89 mBullets = new ArrayList<Bullet>(); 90 mObstacles = new ArrayList<Obstacle>(); 91 92 setFocusable(true); 93 setFocusableInTouchMode(true); 94 95 float baseSize = getContext().getResources().getDisplayMetrics().density * 5f; 96 mBaseSpeed = baseSize * 3; 97 98 mShipSize = baseSize * 3; 99 100 mBulletSize = baseSize; 101 102 mMinObstacleSize = baseSize * 2; 103 mMaxObstacleSize = baseSize * 12; 104 mMinObstacleSpeed = mBaseSpeed; 105 mMaxObstacleSpeed = mBaseSpeed * 3; 106 107 mInputManager = InputManagerCompat.Factory.getInputManager(this.getContext()); 108 mInputManager.registerInputDeviceListener(this, null); 109 } 110 111 // Iterate through the input devices, looking for controllers. Create a ship 112 // for every device that reports itself as a gamepad or joystick. 113 void findControllersAndAttachShips() { 114 int[] deviceIds = mInputManager.getInputDeviceIds(); 115 for (int deviceId : deviceIds) { 116 InputDevice dev = mInputManager.getInputDevice(deviceId); 117 int sources = dev.getSources(); 118 // if the device is a gamepad/joystick, create a ship to represent it 119 if (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) || 120 ((sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK)) { 121 // if the device has a gamepad or joystick 122 getShipForId(deviceId); 123 } 124 } 125 } 126 127 @Override 128 public boolean onKeyDown(int keyCode, KeyEvent event) { 129 int deviceId = event.getDeviceId(); 130 if (deviceId != -1) { 131 Ship currentShip = getShipForId(deviceId); 132 if (currentShip.onKeyDown(keyCode, event)) { 133 step(event.getEventTime()); 134 return true; 135 } 136 } 137 138 return super.onKeyDown(keyCode, event); 139 } 140 141 @Override 142 public boolean onKeyUp(int keyCode, KeyEvent event) { 143 int deviceId = event.getDeviceId(); 144 if (deviceId != -1) { 145 Ship currentShip = getShipForId(deviceId); 146 if (currentShip.onKeyUp(keyCode, event)) { 147 step(event.getEventTime()); 148 return true; 149 } 150 } 151 152 return super.onKeyUp(keyCode, event); 153 } 154 155 @Override 156 public boolean onGenericMotionEvent(MotionEvent event) { 157 mInputManager.onGenericMotionEvent(event); 158 159 // Check that the event came from a joystick or gamepad since a generic 160 // motion event could be almost anything. API level 18 adds the useful 161 // event.isFromSource() helper function. 162 int eventSource = event.getSource(); 163 if ((((eventSource & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) || 164 ((eventSource & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK)) 165 && event.getAction() == MotionEvent.ACTION_MOVE) { 166 int id = event.getDeviceId(); 167 if (-1 != id) { 168 Ship curShip = getShipForId(id); 169 if (curShip.onGenericMotionEvent(event)) { 170 return true; 171 } 172 } 173 } 174 return super.onGenericMotionEvent(event); 175 } 176 177 @Override 178 public void onWindowFocusChanged(boolean hasWindowFocus) { 179 // Turn on and off animations based on the window focus. 180 // Alternately, we could update the game state using the Activity 181 // onResume() 182 // and onPause() lifecycle events. 183 if (hasWindowFocus) { 184 mLastStepTime = SystemClock.uptimeMillis(); 185 mInputManager.onResume(); 186 } else { 187 int numShips = mShips.size(); 188 for (int i = 0; i < numShips; i++) { 189 Ship currentShip = mShips.valueAt(i); 190 if (currentShip != null) { 191 currentShip.setHeading(0, 0); 192 currentShip.setVelocity(0, 0); 193 currentShip.mDPadState = 0; 194 } 195 } 196 mInputManager.onPause(); 197 } 198 199 super.onWindowFocusChanged(hasWindowFocus); 200 } 201 202 @Override 203 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 204 super.onSizeChanged(w, h, oldw, oldh); 205 206 // Reset the game when the view changes size. 207 reset(); 208 } 209 210 @Override 211 protected void onDraw(Canvas canvas) { 212 super.onDraw(canvas); 213 // Update the animation 214 animateFrame(); 215 216 // Draw the ships. 217 int numShips = mShips.size(); 218 for (int i = 0; i < numShips; i++) { 219 Ship currentShip = mShips.valueAt(i); 220 if (currentShip != null) { 221 currentShip.draw(canvas); 222 } 223 } 224 225 // Draw bullets. 226 int numBullets = mBullets.size(); 227 for (int i = 0; i < numBullets; i++) { 228 final Bullet bullet = mBullets.get(i); 229 bullet.draw(canvas); 230 } 231 232 // Draw obstacles. 233 int numObstacles = mObstacles.size(); 234 for (int i = 0; i < numObstacles; i++) { 235 final Obstacle obstacle = mObstacles.get(i); 236 obstacle.draw(canvas); 237 } 238 } 239 240 /** 241 * Uses the device descriptor to try to assign the same color to the same 242 * joystick. If there are two joysticks of the same type connected over USB, 243 * or the API is < API level 16, it will be unable to distinguish the two 244 * devices. 245 * 246 * @param shipID 247 * @return 248 */ 249 @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 250 private Ship getShipForId(int shipID) { 251 Ship currentShip = mShips.get(shipID); 252 if (null == currentShip) { 253 254 // do we know something about this ship already? 255 InputDevice dev = InputDevice.getDevice(shipID); 256 String deviceString = null; 257 Integer shipColor = null; 258 if (null != dev) { 259 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 260 deviceString = dev.getDescriptor(); 261 } else { 262 deviceString = dev.getName(); 263 } 264 shipColor = mDescriptorMap.get(deviceString); 265 } 266 267 if (null != shipColor) { 268 int color = shipColor; 269 int numShips = mShips.size(); 270 // do we already have a ship with this color? 271 for (int i = 0; i < numShips; i++) { 272 if (mShips.valueAt(i).getColor() == color) { 273 shipColor = null; 274 // we won't store this value either --- if the first 275 // controller gets disconnected/connected, it will get 276 // the same color. 277 deviceString = null; 278 } 279 } 280 } 281 if (null != shipColor) { 282 currentShip = new Ship(shipColor); 283 if (null != deviceString) { 284 mDescriptorMap.remove(deviceString); 285 } 286 } else { 287 currentShip = new Ship(getNextShipColor()); 288 } 289 mShips.append(shipID, currentShip); 290 currentShip.setInputDevice(dev); 291 292 if (null != deviceString) { 293 mDescriptorMap.put(deviceString, currentShip.getColor()); 294 } 295 } 296 return currentShip; 297 } 298 299 /** 300 * Remove the ship from the array of active ships by ID. 301 * 302 * @param shipID 303 */ 304 private void removeShipForID(int shipID) { 305 mShips.remove(shipID); 306 } 307 308 private void reset() { 309 mShips.clear(); 310 mBullets.clear(); 311 mObstacles.clear(); 312 findControllersAndAttachShips(); 313 } 314 315 private void animateFrame() { 316 long currentStepTime = SystemClock.uptimeMillis(); 317 step(currentStepTime); 318 invalidate(); 319 } 320 321 private void step(long currentStepTime) { 322 float tau = (currentStepTime - mLastStepTime) * 0.001f; 323 mLastStepTime = currentStepTime; 324 325 // Move the ships 326 int numShips = mShips.size(); 327 for (int i = 0; i < numShips; i++) { 328 Ship currentShip = mShips.valueAt(i); 329 if (currentShip != null) { 330 currentShip.accelerate(tau); 331 if (!currentShip.step(tau)) { 332 currentShip.reincarnate(); 333 } 334 } 335 } 336 337 // Move the bullets. 338 int numBullets = mBullets.size(); 339 for (int i = 0; i < numBullets; i++) { 340 final Bullet bullet = mBullets.get(i); 341 if (!bullet.step(tau)) { 342 mBullets.remove(i); 343 i -= 1; 344 numBullets -= 1; 345 } 346 } 347 348 // Move obstacles. 349 int numObstacles = mObstacles.size(); 350 for (int i = 0; i < numObstacles; i++) { 351 final Obstacle obstacle = mObstacles.get(i); 352 if (!obstacle.step(tau)) { 353 mObstacles.remove(i); 354 i -= 1; 355 numObstacles -= 1; 356 } 357 } 358 359 // Check for collisions between bullets and obstacles. 360 for (int i = 0; i < numBullets; i++) { 361 final Bullet bullet = mBullets.get(i); 362 for (int j = 0; j < numObstacles; j++) { 363 final Obstacle obstacle = mObstacles.get(j); 364 if (bullet.collidesWith(obstacle)) { 365 bullet.destroy(); 366 obstacle.destroy(); 367 break; 368 } 369 } 370 } 371 372 // Check for collisions between the ship and obstacles --- this could 373 // get slow 374 for (int i = 0; i < numObstacles; i++) { 375 final Obstacle obstacle = mObstacles.get(i); 376 for (int j = 0; j < numShips; j++) { 377 Ship currentShip = mShips.valueAt(j); 378 if (currentShip != null) { 379 if (currentShip.collidesWith(obstacle)) { 380 currentShip.destroy(); 381 obstacle.destroy(); 382 break; 383 } 384 } 385 } 386 } 387 388 // Spawn more obstacles offscreen when needed. 389 // Avoid putting them right on top of the ship. 390 int tries = MAX_OBSTACLES - mObstacles.size() + 10; 391 final float minDistance = mShipSize * 4; 392 while (mObstacles.size() < MAX_OBSTACLES && tries-- > 0) { 393 float size = mRandom.nextFloat() * (mMaxObstacleSize - mMinObstacleSize) 394 + mMinObstacleSize; 395 float positionX, positionY; 396 int edge = mRandom.nextInt(4); 397 switch (edge) { 398 case 0: 399 positionX = -size; 400 positionY = mRandom.nextInt(getHeight()); 401 break; 402 case 1: 403 positionX = getWidth() + size; 404 positionY = mRandom.nextInt(getHeight()); 405 break; 406 case 2: 407 positionX = mRandom.nextInt(getWidth()); 408 positionY = -size; 409 break; 410 default: 411 positionX = mRandom.nextInt(getWidth()); 412 positionY = getHeight() + size; 413 break; 414 } 415 boolean positionSafe = true; 416 417 // If the obstacle is too close to any ships, we don't want to 418 // spawn it. 419 for (int i = 0; i < numShips; i++) { 420 Ship currentShip = mShips.valueAt(i); 421 if (currentShip != null) { 422 if (currentShip.distanceTo(positionX, positionY) < minDistance) { 423 // try to spawn again 424 positionSafe = false; 425 break; 426 } 427 } 428 } 429 430 // if the position is safe, add the obstacle and reset the retry 431 // counter 432 if (positionSafe) { 433 tries = MAX_OBSTACLES - mObstacles.size() + 10; 434 // we can add the obstacle now since it isn't close to any ships 435 float direction = mRandom.nextFloat() * (float) Math.PI * 2; 436 float speed = mRandom.nextFloat() * (mMaxObstacleSpeed - mMinObstacleSpeed) 437 + mMinObstacleSpeed; 438 float velocityX = (float) Math.cos(direction) * speed; 439 float velocityY = (float) Math.sin(direction) * speed; 440 441 Obstacle obstacle = new Obstacle(); 442 obstacle.setPosition(positionX, positionY); 443 obstacle.setSize(size); 444 obstacle.setVelocity(velocityX, velocityY); 445 mObstacles.add(obstacle); 446 } 447 } 448 } 449 450 private static float pythag(float x, float y) { 451 return (float) Math.sqrt(x * x + y * y); 452 } 453 454 private static int blend(float alpha, int from, int to) { 455 return from + (int) ((to - from) * alpha); 456 } 457 458 private static void setPaintARGBBlend(Paint paint, float alpha, 459 int a1, int r1, int g1, int b1, 460 int a2, int r2, int g2, int b2) { 461 paint.setARGB(blend(alpha, a1, a2), blend(alpha, r1, r2), 462 blend(alpha, g1, g2), blend(alpha, b1, b2)); 463 } 464 465 private static float getCenteredAxis(MotionEvent event, InputDevice device, 466 int axis, int historyPos) { 467 final InputDevice.MotionRange range = device.getMotionRange(axis, event.getSource()); 468 if (range != null) { 469 final float flat = range.getFlat(); 470 final float value = historyPos < 0 ? event.getAxisValue(axis) 471 : event.getHistoricalAxisValue(axis, historyPos); 472 473 // Ignore axis values that are within the 'flat' region of the 474 // joystick axis center. 475 // A joystick at rest does not always report an absolute position of 476 // (0,0). 477 if (Math.abs(value) > flat) { 478 return value; 479 } 480 } 481 return 0; 482 } 483 484 /** 485 * Any gamepad button + the spacebar or DPAD_CENTER will be used as the fire 486 * key. 487 * 488 * @param keyCode 489 * @return true of it's a fire key. 490 */ 491 private static boolean isFireKey(int keyCode) { 492 return KeyEvent.isGamepadButton(keyCode) 493 || keyCode == KeyEvent.KEYCODE_DPAD_CENTER 494 || keyCode == KeyEvent.KEYCODE_SPACE; 495 } 496 497 private abstract class Sprite { 498 protected float mPositionX; 499 protected float mPositionY; 500 protected float mVelocityX; 501 protected float mVelocityY; 502 protected float mSize; 503 protected boolean mDestroyed; 504 protected float mDestroyAnimProgress; 505 506 public void setPosition(float x, float y) { 507 mPositionX = x; 508 mPositionY = y; 509 } 510 511 public void setVelocity(float x, float y) { 512 mVelocityX = x; 513 mVelocityY = y; 514 } 515 516 public void setSize(float size) { 517 mSize = size; 518 } 519 520 public float distanceTo(float x, float y) { 521 return pythag(mPositionX - x, mPositionY - y); 522 } 523 524 public float distanceTo(Sprite other) { 525 return distanceTo(other.mPositionX, other.mPositionY); 526 } 527 528 public boolean collidesWith(Sprite other) { 529 // Really bad collision detection. 530 return !mDestroyed && !other.mDestroyed 531 && distanceTo(other) <= Math.max(mSize, other.mSize) 532 + Math.min(mSize, other.mSize) * 0.5f; 533 } 534 535 public boolean isDestroyed() { 536 return mDestroyed; 537 } 538 539 /** 540 * Moves the sprite based on the elapsed time defined by tau. 541 * 542 * @param tau the elapsed time in seconds since the last step 543 * @return false if the sprite is to be removed from the display 544 */ 545 public boolean step(float tau) { 546 mPositionX += mVelocityX * tau; 547 mPositionY += mVelocityY * tau; 548 549 if (mDestroyed) { 550 mDestroyAnimProgress += tau / getDestroyAnimDuration(); 551 if (mDestroyAnimProgress >= getDestroyAnimCycles()) { 552 return false; 553 } 554 } 555 return true; 556 } 557 558 /** 559 * Draws the sprite. 560 * 561 * @param canvas the Canvas upon which to draw the sprite. 562 */ 563 public abstract void draw(Canvas canvas); 564 565 /** 566 * Returns the duration of the destruction animation of the sprite in 567 * seconds. 568 * 569 * @return the float duration in seconds of the destruction animation 570 */ 571 public abstract float getDestroyAnimDuration(); 572 573 /** 574 * Returns the number of cycles to play the destruction animation. A 575 * destruction animation has a duration and a number of cycles to play 576 * it for, so we can have an extended death sequence when a ship or 577 * object is destroyed. 578 * 579 * @return the float number of cycles to play the destruction animation 580 */ 581 public abstract float getDestroyAnimCycles(); 582 583 protected boolean isOutsidePlayfield() { 584 final int width = GameView.this.getWidth(); 585 final int height = GameView.this.getHeight(); 586 return mPositionX < 0 || mPositionX >= width 587 || mPositionY < 0 || mPositionY >= height; 588 } 589 590 protected void wrapAtPlayfieldBoundary() { 591 final int width = GameView.this.getWidth(); 592 final int height = GameView.this.getHeight(); 593 while (mPositionX <= -mSize) { 594 mPositionX += width + mSize * 2; 595 } 596 while (mPositionX >= width + mSize) { 597 mPositionX -= width + mSize * 2; 598 } 599 while (mPositionY <= -mSize) { 600 mPositionY += height + mSize * 2; 601 } 602 while (mPositionY >= height + mSize) { 603 mPositionY -= height + mSize * 2; 604 } 605 } 606 607 public void destroy() { 608 mDestroyed = true; 609 step(0); 610 } 611 } 612 613 private static int sShipColor = 0; 614 615 /** 616 * Returns the next ship color in the sequence. Very simple. Does not in any 617 * way guarantee that there are not multiple ships with the same color on 618 * the screen. 619 * 620 * @return an int containing the index of the next ship color 621 */ 622 private static int getNextShipColor() { 623 int color = sShipColor & 0x07; 624 if (0 == color) { 625 color++; 626 sShipColor++; 627 } 628 sShipColor++; 629 return color; 630 } 631 632 /* 633 * Static constants associated with Ship inner class 634 */ 635 private static final long[] sDestructionVibratePattern = new long[] { 636 0, 20, 20, 40, 40, 80, 40, 300 637 }; 638 639 private class Ship extends Sprite { 640 private static final float CORNER_ANGLE = (float) Math.PI * 2 / 3; 641 private static final float TO_DEGREES = (float) (180.0 / Math.PI); 642 643 private final float mMaxShipThrust = mBaseSpeed * 0.25f; 644 private final float mMaxSpeed = mBaseSpeed * 12; 645 646 // The ship actually determines the speed of the bullet, not the bullet 647 // itself 648 private final float mBulletSpeed = mBaseSpeed * 12; 649 650 private final Paint mPaint; 651 private final Path mPath; 652 private final int mR, mG, mB; 653 private final int mColor; 654 655 // The current device that is controlling the ship 656 private InputDevice mInputDevice; 657 658 private float mHeadingX; 659 private float mHeadingY; 660 private float mHeadingAngle; 661 private float mHeadingMagnitude; 662 663 private int mDPadState; 664 665 /** 666 * The colorIndex is used to create the color based on the lower three 667 * bits of the value in the current implementation. 668 * 669 * @param colorIndex 670 */ 671 public Ship(int colorIndex) { 672 mPaint = new Paint(); 673 mPaint.setStyle(Style.FILL); 674 675 setPosition(getWidth() * 0.5f, getHeight() * 0.5f); 676 setVelocity(0, 0); 677 setSize(mShipSize); 678 679 mPath = new Path(); 680 mPath.moveTo(0, 0); 681 mPath.lineTo((float) Math.cos(-CORNER_ANGLE) * mSize, 682 (float) Math.sin(-CORNER_ANGLE) * mSize); 683 mPath.lineTo(mSize, 0); 684 mPath.lineTo((float) Math.cos(CORNER_ANGLE) * mSize, 685 (float) Math.sin(CORNER_ANGLE) * mSize); 686 mPath.lineTo(0, 0); 687 688 mR = (colorIndex & 0x01) == 0 ? 63 : 255; 689 mG = (colorIndex & 0x02) == 0 ? 63 : 255; 690 mB = (colorIndex & 0x04) == 0 ? 63 : 255; 691 692 mColor = colorIndex; 693 } 694 695 public boolean onKeyUp(int keyCode, KeyEvent event) { 696 697 // Handle keys going up. 698 boolean handled = false; 699 switch (keyCode) { 700 case KeyEvent.KEYCODE_DPAD_LEFT: 701 setHeadingX(0); 702 mDPadState &= ~DPAD_STATE_LEFT; 703 handled = true; 704 break; 705 case KeyEvent.KEYCODE_DPAD_RIGHT: 706 setHeadingX(0); 707 mDPadState &= ~DPAD_STATE_RIGHT; 708 handled = true; 709 break; 710 case KeyEvent.KEYCODE_DPAD_UP: 711 setHeadingY(0); 712 mDPadState &= ~DPAD_STATE_UP; 713 handled = true; 714 break; 715 case KeyEvent.KEYCODE_DPAD_DOWN: 716 setHeadingY(0); 717 mDPadState &= ~DPAD_STATE_DOWN; 718 handled = true; 719 break; 720 default: 721 if (isFireKey(keyCode)) { 722 handled = true; 723 } 724 break; 725 } 726 return handled; 727 } 728 729 /* 730 * Firing is a unique case where a ship creates a bullet. A bullet needs 731 * to be created with a position near the ship that is firing with a 732 * velocity that is based upon the speed of the ship. 733 */ 734 private void fire() { 735 if (!isDestroyed()) { 736 Bullet bullet = new Bullet(); 737 bullet.setPosition(getBulletInitialX(), getBulletInitialY()); 738 bullet.setVelocity(getBulletVelocityX(), 739 getBulletVelocityY()); 740 mBullets.add(bullet); 741 vibrateController(20); 742 } 743 } 744 745 public boolean onKeyDown(int keyCode, KeyEvent event) { 746 // Handle DPad keys and fire button on initial down but not on 747 // auto-repeat. 748 boolean handled = false; 749 if (event.getRepeatCount() == 0) { 750 switch (keyCode) { 751 case KeyEvent.KEYCODE_DPAD_LEFT: 752 setHeadingX(-1); 753 mDPadState |= DPAD_STATE_LEFT; 754 handled = true; 755 break; 756 case KeyEvent.KEYCODE_DPAD_RIGHT: 757 setHeadingX(1); 758 mDPadState |= DPAD_STATE_RIGHT; 759 handled = true; 760 break; 761 case KeyEvent.KEYCODE_DPAD_UP: 762 setHeadingY(-1); 763 mDPadState |= DPAD_STATE_UP; 764 handled = true; 765 break; 766 case KeyEvent.KEYCODE_DPAD_DOWN: 767 setHeadingY(1); 768 mDPadState |= DPAD_STATE_DOWN; 769 handled = true; 770 break; 771 default: 772 if (isFireKey(keyCode)) { 773 fire(); 774 handled = true; 775 } 776 break; 777 } 778 } 779 return handled; 780 } 781 782 /** 783 * Gets the vibrator from the controller if it is present. Note that it 784 * would be easy to get the system vibrator here if the controller one 785 * is not present, but we don't choose to do it in this case. 786 * 787 * @return the Vibrator for the controller, or null if it is not 788 * present. or the API level cannot support it 789 */ 790 @SuppressLint("NewApi") 791 private final Vibrator getVibrator() { 792 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && 793 null != mInputDevice) { 794 return mInputDevice.getVibrator(); 795 } 796 return null; 797 } 798 799 private void vibrateController(int time) { 800 Vibrator vibrator = getVibrator(); 801 if (null != vibrator) { 802 vibrator.vibrate(time); 803 } 804 } 805 806 private void vibrateController(long[] pattern, int repeat) { 807 Vibrator vibrator = getVibrator(); 808 if (null != vibrator) { 809 vibrator.vibrate(pattern, repeat); 810 } 811 } 812 813 /** 814 * The ship directly handles joystick input. 815 * 816 * @param event 817 * @param historyPos 818 */ 819 private void processJoystickInput(MotionEvent event, int historyPos) { 820 // Get joystick position. 821 // Many game pads with two joysticks report the position of the 822 // second 823 // joystick 824 // using the Z and RZ axes so we also handle those. 825 // In a real game, we would allow the user to configure the axes 826 // manually. 827 if (null == mInputDevice) { 828 mInputDevice = event.getDevice(); 829 } 830 float x = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_X, historyPos); 831 if (x == 0) { 832 x = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_HAT_X, historyPos); 833 } 834 if (x == 0) { 835 x = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_Z, historyPos); 836 } 837 838 float y = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_Y, historyPos); 839 if (y == 0) { 840 y = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_HAT_Y, historyPos); 841 } 842 if (y == 0) { 843 y = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_RZ, historyPos); 844 } 845 846 // Set the ship heading. 847 setHeading(x, y); 848 GameView.this.step(historyPos < 0 ? event.getEventTime() : event 849 .getHistoricalEventTime(historyPos)); 850 } 851 852 public boolean onGenericMotionEvent(MotionEvent event) { 853 if (0 == mDPadState) { 854 // Process all historical movement samples in the batch. 855 final int historySize = event.getHistorySize(); 856 for (int i = 0; i < historySize; i++) { 857 processJoystickInput(event, i); 858 } 859 860 // Process the current movement sample in the batch. 861 processJoystickInput(event, -1); 862 } 863 return true; 864 } 865 866 /** 867 * Set the game controller to be used to control the ship. 868 * 869 * @param dev the input device that will be controlling the ship 870 */ 871 public void setInputDevice(InputDevice dev) { 872 mInputDevice = dev; 873 } 874 875 /** 876 * Sets the X component of the joystick heading value, defined by the 877 * platform as being from -1.0 (left) to 1.0 (right). This function is 878 * generally used to change the heading in response to a button-style 879 * DPAD event. 880 * 881 * @param x the float x component of the joystick heading value 882 */ 883 public void setHeadingX(float x) { 884 mHeadingX = x; 885 updateHeading(); 886 } 887 888 /** 889 * Sets the Y component of the joystick heading value, defined by the 890 * platform as being from -1.0 (top) to 1.0 (bottom). This function is 891 * generally used to change the heading in response to a button-style 892 * DPAD event. 893 * 894 * @param y the float y component of the joystick heading value 895 */ 896 public void setHeadingY(float y) { 897 mHeadingY = y; 898 updateHeading(); 899 } 900 901 /** 902 * Sets the heading as floating point values returned by a joystick. 903 * These values are normalized by the Android platform to be from -1.0 904 * (left, top) to 1.0 (right, bottom) 905 * 906 * @param x the float x component of the joystick heading value 907 * @param y the float y component of the joystick heading value 908 */ 909 public void setHeading(float x, float y) { 910 mHeadingX = x; 911 mHeadingY = y; 912 updateHeading(); 913 } 914 915 /** 916 * Converts the heading values from joystick devices to the polar 917 * representation of the heading angle if the magnitude of the heading 918 * is significant (> 0.1f). 919 */ 920 private void updateHeading() { 921 mHeadingMagnitude = pythag(mHeadingX, mHeadingY); 922 if (mHeadingMagnitude > 0.1f) { 923 mHeadingAngle = (float) Math.atan2(mHeadingY, mHeadingX); 924 } 925 } 926 927 /** 928 * Bring our ship back to life, stopping the destroy animation. 929 */ 930 public void reincarnate() { 931 mDestroyed = false; 932 mDestroyAnimProgress = 0.0f; 933 } 934 935 private float polarX(float radius) { 936 return (float) Math.cos(mHeadingAngle) * radius; 937 } 938 939 private float polarY(float radius) { 940 return (float) Math.sin(mHeadingAngle) * radius; 941 } 942 943 /** 944 * Gets the initial x coordinate for the bullet. 945 * 946 * @return the x coordinate of the bullet adjusted for the position and 947 * direction of the ship 948 */ 949 public float getBulletInitialX() { 950 return mPositionX + polarX(mSize); 951 } 952 953 /** 954 * Gets the initial y coordinate for the bullet. 955 * 956 * @return the y coordinate of the bullet adjusted for the position and 957 * direction of the ship 958 */ 959 public float getBulletInitialY() { 960 return mPositionY + polarY(mSize); 961 } 962 963 /** 964 * Returns the bullet speed Y component. 965 * 966 * @return adjusted Y component bullet speed for the velocity and 967 * direction of the ship 968 */ 969 public float getBulletVelocityY() { 970 return mVelocityY + polarY(mBulletSpeed); 971 } 972 973 /** 974 * Returns the bullet speed X component 975 * 976 * @return adjusted X component bullet speed for the velocity and 977 * direction of the ship 978 */ 979 public float getBulletVelocityX() { 980 return mVelocityX + polarX(mBulletSpeed); 981 } 982 983 /** 984 * Uses the heading magnitude and direction to change the acceleration 985 * of the ship. In theory, this should be scaled according to the 986 * elapsed time. 987 * 988 * @param tau the elapsed time in seconds between the last step 989 */ 990 public void accelerate(float tau) { 991 final float thrust = mHeadingMagnitude * mMaxShipThrust; 992 mVelocityX += polarX(thrust) * tau * mMaxSpeed / 4; 993 mVelocityY += polarY(thrust) * tau * mMaxSpeed / 4; 994 995 final float speed = pythag(mVelocityX, mVelocityY); 996 if (speed > mMaxSpeed) { 997 final float scale = mMaxSpeed / speed; 998 mVelocityX = mVelocityX * scale * scale; 999 mVelocityY = mVelocityY * scale * scale; 1000 } 1001 } 1002 1003 @Override 1004 public boolean step(float tau) { 1005 if (!super.step(tau)) { 1006 return false; 1007 } 1008 wrapAtPlayfieldBoundary(); 1009 return true; 1010 } 1011 1012 @Override 1013 public void draw(Canvas canvas) { 1014 setPaintARGBBlend(mPaint, mDestroyAnimProgress - (int) (mDestroyAnimProgress), 1015 255, mR, mG, mB, 1016 0, 255, 0, 0); 1017 1018 canvas.save(Canvas.MATRIX_SAVE_FLAG); 1019 canvas.translate(mPositionX, mPositionY); 1020 canvas.rotate(mHeadingAngle * TO_DEGREES); 1021 canvas.drawPath(mPath, mPaint); 1022 canvas.restore(); 1023 } 1024 1025 @Override 1026 public float getDestroyAnimDuration() { 1027 return 1.0f; 1028 } 1029 1030 @Override 1031 public void destroy() { 1032 super.destroy(); 1033 vibrateController(sDestructionVibratePattern, -1); 1034 } 1035 1036 @Override 1037 public float getDestroyAnimCycles() { 1038 return 5.0f; 1039 } 1040 1041 public int getColor() { 1042 return mColor; 1043 } 1044 } 1045 1046 private static final Paint mBulletPaint; 1047 static { 1048 mBulletPaint = new Paint(); 1049 mBulletPaint.setStyle(Style.FILL); 1050 } 1051 1052 private class Bullet extends Sprite { 1053 1054 public Bullet() { 1055 setSize(mBulletSize); 1056 } 1057 1058 @Override 1059 public boolean step(float tau) { 1060 if (!super.step(tau)) { 1061 return false; 1062 } 1063 return !isOutsidePlayfield(); 1064 } 1065 1066 @Override 1067 public void draw(Canvas canvas) { 1068 setPaintARGBBlend(mBulletPaint, mDestroyAnimProgress, 1069 255, 255, 255, 0, 1070 0, 255, 255, 255); 1071 canvas.drawCircle(mPositionX, mPositionY, mSize, mBulletPaint); 1072 } 1073 1074 @Override 1075 public float getDestroyAnimDuration() { 1076 return 0.125f; 1077 } 1078 1079 @Override 1080 public float getDestroyAnimCycles() { 1081 return 1.0f; 1082 } 1083 1084 } 1085 1086 private static final Paint mObstaclePaint; 1087 static { 1088 mObstaclePaint = new Paint(); 1089 mObstaclePaint.setARGB(255, 127, 127, 255); 1090 mObstaclePaint.setStyle(Style.FILL); 1091 } 1092 1093 private class Obstacle extends Sprite { 1094 1095 @Override 1096 public boolean step(float tau) { 1097 if (!super.step(tau)) { 1098 return false; 1099 } 1100 wrapAtPlayfieldBoundary(); 1101 return true; 1102 } 1103 1104 @Override 1105 public void draw(Canvas canvas) { 1106 setPaintARGBBlend(mObstaclePaint, mDestroyAnimProgress, 1107 255, 127, 127, 255, 1108 0, 255, 0, 0); 1109 canvas.drawCircle(mPositionX, mPositionY, 1110 mSize * (1.0f - mDestroyAnimProgress), mObstaclePaint); 1111 } 1112 1113 @Override 1114 public float getDestroyAnimDuration() { 1115 return 0.25f; 1116 } 1117 1118 @Override 1119 public float getDestroyAnimCycles() { 1120 return 1.0f; 1121 } 1122 } 1123 1124 /* 1125 * When an input device is added, we add a ship based upon the device. 1126 * @see 1127 * com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener 1128 * #onInputDeviceAdded(int) 1129 */ 1130 @Override 1131 public void onInputDeviceAdded(int deviceId) { 1132 getShipForId(deviceId); 1133 } 1134 1135 /* 1136 * This is an unusual case. Input devices don't typically change, but they 1137 * certainly can --- for example a device may have different modes. We use 1138 * this to make sure that the ship has an up-to-date InputDevice. 1139 * @see 1140 * com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener 1141 * #onInputDeviceChanged(int) 1142 */ 1143 @Override 1144 public void onInputDeviceChanged(int deviceId) { 1145 Ship ship = getShipForId(deviceId); 1146 ship.setInputDevice(InputDevice.getDevice(deviceId)); 1147 } 1148 1149 /* 1150 * Remove any ship associated with the ID. 1151 * @see 1152 * com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener 1153 * #onInputDeviceRemoved(int) 1154 */ 1155 @Override 1156 public void onInputDeviceRemoved(int deviceId) { 1157 removeShipForID(deviceId); 1158 } 1159 } 1160