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.hypot(x, 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