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
17package com.example.android.apis.view;
18
19import android.content.Context;
20import android.graphics.Canvas;
21import android.graphics.Paint;
22import android.graphics.Path;
23import android.graphics.Paint.Style;
24import android.os.Handler;
25import android.os.SystemClock;
26import android.os.Vibrator;
27import android.util.AttributeSet;
28import android.view.InputDevice;
29import android.view.KeyEvent;
30import android.view.MotionEvent;
31import android.view.View;
32
33import java.util.ArrayList;
34import java.util.List;
35import 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 */
46public 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
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
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
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
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
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
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
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