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