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