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 GameView(Context context, AttributeSet attrs)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. findControllersAndAttachShips()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 onKeyDown(int keyCode, KeyEvent event)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 onKeyUp(int keyCode, KeyEvent event)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 onGenericMotionEvent(MotionEvent event)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 onWindowFocusChanged(boolean hasWindowFocus)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 onSizeChanged(int w, int h, int oldw, int oldh)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 onDraw(Canvas canvas)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) getShipForId(int shipID)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 */ removeShipForID(int shipID)304 private void removeShipForID(int shipID) { 305 mShips.remove(shipID); 306 } 307 reset()308 private void reset() { 309 mShips.clear(); 310 mBullets.clear(); 311 mObstacles.clear(); 312 findControllersAndAttachShips(); 313 } 314 animateFrame()315 private void animateFrame() { 316 long currentStepTime = SystemClock.uptimeMillis(); 317 step(currentStepTime); 318 invalidate(); 319 } 320 step(long currentStepTime)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 pythag(float x, float y)450 private static float pythag(float x, float y) { 451 return (float) Math.sqrt(x * x + y * y); 452 } 453 blend(float alpha, int from, int to)454 private static int blend(float alpha, int from, int to) { 455 return from + (int) ((to - from) * alpha); 456 } 457 setPaintARGBBlend(Paint paint, float alpha, int a1, int r1, int g1, int b1, int a2, int r2, int g2, int b2)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 getCenteredAxis(MotionEvent event, InputDevice device, int axis, int historyPos)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 */ isFireKey(int keyCode)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 setPosition(float x, float y)506 public void setPosition(float x, float y) { 507 mPositionX = x; 508 mPositionY = y; 509 } 510 setVelocity(float x, float y)511 public void setVelocity(float x, float y) { 512 mVelocityX = x; 513 mVelocityY = y; 514 } 515 setSize(float size)516 public void setSize(float size) { 517 mSize = size; 518 } 519 distanceTo(float x, float y)520 public float distanceTo(float x, float y) { 521 return pythag(mPositionX - x, mPositionY - y); 522 } 523 distanceTo(Sprite other)524 public float distanceTo(Sprite other) { 525 return distanceTo(other.mPositionX, other.mPositionY); 526 } 527 collidesWith(Sprite other)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 isDestroyed()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 */ step(float tau)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 isOutsidePlayfield()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 wrapAtPlayfieldBoundary()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 destroy()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 */ getNextShipColor()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 */ Ship(int colorIndex)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 onKeyUp(int keyCode, KeyEvent event)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 */ fire()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 onKeyDown(int keyCode, KeyEvent event)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") getVibrator()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 vibrateController(int time)799 private void vibrateController(int time) { 800 Vibrator vibrator = getVibrator(); 801 if (null != vibrator) { 802 vibrator.vibrate(time); 803 } 804 } 805 vibrateController(long[] pattern, int repeat)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 */ processJoystickInput(MotionEvent event, int historyPos)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 step(float tau)1004 public boolean step(float tau) { 1005 if (!super.step(tau)) { 1006 return false; 1007 } 1008 wrapAtPlayfieldBoundary(); 1009 return true; 1010 } 1011 1012 @Override draw(Canvas canvas)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 getDestroyAnimDuration()1026 public float getDestroyAnimDuration() { 1027 return 1.0f; 1028 } 1029 1030 @Override destroy()1031 public void destroy() { 1032 super.destroy(); 1033 vibrateController(sDestructionVibratePattern, -1); 1034 } 1035 1036 @Override getDestroyAnimCycles()1037 public float getDestroyAnimCycles() { 1038 return 5.0f; 1039 } 1040 getColor()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 Bullet()1054 public Bullet() { 1055 setSize(mBulletSize); 1056 } 1057 1058 @Override step(float tau)1059 public boolean step(float tau) { 1060 if (!super.step(tau)) { 1061 return false; 1062 } 1063 return !isOutsidePlayfield(); 1064 } 1065 1066 @Override draw(Canvas canvas)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 getDestroyAnimDuration()1075 public float getDestroyAnimDuration() { 1076 return 0.125f; 1077 } 1078 1079 @Override getDestroyAnimCycles()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 step(float tau)1096 public boolean step(float tau) { 1097 if (!super.step(tau)) { 1098 return false; 1099 } 1100 wrapAtPlayfieldBoundary(); 1101 return true; 1102 } 1103 1104 @Override draw(Canvas canvas)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 getDestroyAnimDuration()1114 public float getDestroyAnimDuration() { 1115 return 0.25f; 1116 } 1117 1118 @Override getDestroyAnimCycles()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 onInputDeviceAdded(int deviceId)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 onInputDeviceChanged(int deviceId)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 onInputDeviceRemoved(int deviceId)1156 public void onInputDeviceRemoved(int deviceId) { 1157 removeShipForID(deviceId); 1158 } 1159 } 1160