/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.android.apis.view; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Paint.Style; import android.os.Handler; import android.os.SystemClock; import android.os.Vibrator; import android.util.AttributeSet; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import java.util.ArrayList; import java.util.List; import java.util.Random; /** * A trivial joystick based physics game to demonstrate joystick handling. * * If the game controller has a vibrator, then it is used to provide feedback * when a bullet is fired or the ship crashes into an obstacle. Otherwise, the * system vibrator is used for that purpose. * * @see GameControllerInput */ public class GameView extends View { private final long ANIMATION_TIME_STEP = 1000 / 60; private final int MAX_OBSTACLES = 12; private final Random mRandom; private Ship mShip; private final List mBullets; private final List mObstacles; private long mLastStepTime; private InputDevice mLastInputDevice; private static final int DPAD_STATE_LEFT = 1 << 0; private static final int DPAD_STATE_RIGHT = 1 << 1; private static final int DPAD_STATE_UP = 1 << 2; private static final int DPAD_STATE_DOWN = 1 << 3; private int mDPadState; private float mShipSize; private float mMaxShipThrust; private float mMaxShipSpeed; private float mBulletSize; private float mBulletSpeed; private float mMinObstacleSize; private float mMaxObstacleSize; private float mMinObstacleSpeed; private float mMaxObstacleSpeed; private final Runnable mAnimationRunnable = new Runnable() { public void run() { animateFrame(); } }; public GameView(Context context, AttributeSet attrs) { super(context, attrs); mRandom = new Random(); mBullets = new ArrayList(); mObstacles = new ArrayList(); setFocusable(true); setFocusableInTouchMode(true); float baseSize = getContext().getResources().getDisplayMetrics().density * 5f; float baseSpeed = baseSize * 3; mShipSize = baseSize * 3; mMaxShipThrust = baseSpeed * 0.25f; mMaxShipSpeed = baseSpeed * 12; mBulletSize = baseSize; mBulletSpeed = baseSpeed * 12; mMinObstacleSize = baseSize * 2; mMaxObstacleSize = baseSize * 12; mMinObstacleSpeed = baseSpeed; mMaxObstacleSpeed = baseSpeed * 3; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // Reset the game when the view changes size. reset(); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { ensureInitialized(); // Handle DPad keys and fire button on initial down but not on auto-repeat. boolean handled = false; if (event.getRepeatCount() == 0) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: mShip.setHeadingX(-1); mDPadState |= DPAD_STATE_LEFT; handled = true; break; case KeyEvent.KEYCODE_DPAD_RIGHT: mShip.setHeadingX(1); mDPadState |= DPAD_STATE_RIGHT; handled = true; break; case KeyEvent.KEYCODE_DPAD_UP: mShip.setHeadingY(-1); mDPadState |= DPAD_STATE_UP; handled = true; break; case KeyEvent.KEYCODE_DPAD_DOWN: mShip.setHeadingY(1); mDPadState |= DPAD_STATE_DOWN; handled = true; break; default: if (isFireKey(keyCode)) { fire(); handled = true; } break; } } if (handled) { step(event.getEventTime()); return true; } return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { ensureInitialized(); // Handle keys going up. boolean handled = false; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: mShip.setHeadingX(0); mDPadState &= ~DPAD_STATE_LEFT; handled = true; break; case KeyEvent.KEYCODE_DPAD_RIGHT: mShip.setHeadingX(0); mDPadState &= ~DPAD_STATE_RIGHT; handled = true; break; case KeyEvent.KEYCODE_DPAD_UP: mShip.setHeadingY(0); mDPadState &= ~DPAD_STATE_UP; handled = true; break; case KeyEvent.KEYCODE_DPAD_DOWN: mShip.setHeadingY(0); mDPadState &= ~DPAD_STATE_DOWN; handled = true; break; default: if (isFireKey(keyCode)) { handled = true; } break; } if (handled) { step(event.getEventTime()); return true; } return super.onKeyUp(keyCode, event); } private static boolean isFireKey(int keyCode) { return KeyEvent.isGamepadButton(keyCode) || keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_SPACE; } @Override public boolean onGenericMotionEvent(MotionEvent event) { ensureInitialized(); // Check that the event came from a joystick since a generic motion event // could be almost anything. if (event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK) && event.getAction() == MotionEvent.ACTION_MOVE) { // Cache the most recently obtained device information. // The device information may change over time but it can be // somewhat expensive to query. if (mLastInputDevice == null || mLastInputDevice.getId() != event.getDeviceId()) { mLastInputDevice = event.getDevice(); // It's possible for the device id to be invalid. // In that case, getDevice() will return null. if (mLastInputDevice == null) { return false; } } // Ignore joystick while the DPad is pressed to avoid conflicting motions. if (mDPadState != 0) { return true; } // Process all historical movement samples in the batch. final int historySize = event.getHistorySize(); for (int i = 0; i < historySize; i++) { processJoystickInput(event, i); } // Process the current movement sample in the batch. processJoystickInput(event, -1); return true; } return super.onGenericMotionEvent(event); } private void processJoystickInput(MotionEvent event, int historyPos) { // Get joystick position. // Many game pads with two joysticks report the position of the second joystick // using the Z and RZ axes so we also handle those. // In a real game, we would allow the user to configure the axes manually. float x = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_X, historyPos); if (x == 0) { x = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_HAT_X, historyPos); } if (x == 0) { x = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_Z, historyPos); } float y = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_Y, historyPos); if (y == 0) { y = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_HAT_Y, historyPos); } if (y == 0) { y = getCenteredAxis(event, mLastInputDevice, MotionEvent.AXIS_RZ, historyPos); } // Set the ship heading. mShip.setHeading(x, y); step(historyPos < 0 ? event.getEventTime() : event.getHistoricalEventTime(historyPos)); } private static float getCenteredAxis(MotionEvent event, InputDevice device, int axis, int historyPos) { final InputDevice.MotionRange range = device.getMotionRange(axis, event.getSource()); if (range != null) { final float flat = range.getFlat(); final float value = historyPos < 0 ? event.getAxisValue(axis) : event.getHistoricalAxisValue(axis, historyPos); // Ignore axis values that are within the 'flat' region of the joystick axis center. // A joystick at rest does not always report an absolute position of (0,0). if (Math.abs(value) > flat) { return value; } } return 0; } @Override public void onWindowFocusChanged(boolean hasWindowFocus) { // Turn on and off animations based on the window focus. // Alternately, we could update the game state using the Activity onResume() // and onPause() lifecycle events. if (hasWindowFocus) { getHandler().postDelayed(mAnimationRunnable, ANIMATION_TIME_STEP); mLastStepTime = SystemClock.uptimeMillis(); } else { getHandler().removeCallbacks(mAnimationRunnable); mDPadState = 0; if (mShip != null) { mShip.setHeading(0, 0); mShip.setVelocity(0, 0); } } super.onWindowFocusChanged(hasWindowFocus); } private void fire() { if (mShip != null && !mShip.isDestroyed()) { Bullet bullet = new Bullet(); bullet.setPosition(mShip.getBulletInitialX(), mShip.getBulletInitialY()); bullet.setVelocity(mShip.getBulletVelocityX(mBulletSpeed), mShip.getBulletVelocityY(mBulletSpeed)); mBullets.add(bullet); getVibrator().vibrate(20); } } private void ensureInitialized() { if (mShip == null) { reset(); } } private void crash() { getVibrator().vibrate(new long[] { 0, 20, 20, 40, 40, 80, 40, 300 }, -1); } private void reset() { mShip = new Ship(); mBullets.clear(); mObstacles.clear(); } private Vibrator getVibrator() { if (mLastInputDevice != null) { Vibrator vibrator = mLastInputDevice.getVibrator(); if (vibrator.hasVibrator()) { return vibrator; } } return (Vibrator)getContext().getSystemService(Context.VIBRATOR_SERVICE); } void animateFrame() { long currentStepTime = SystemClock.uptimeMillis(); step(currentStepTime); Handler handler = getHandler(); if (handler != null) { handler.postAtTime(mAnimationRunnable, currentStepTime + ANIMATION_TIME_STEP); invalidate(); } } private void step(long currentStepTime) { float tau = (currentStepTime - mLastStepTime) * 0.001f; mLastStepTime = currentStepTime; ensureInitialized(); // Move the ship. mShip.accelerate(tau, mMaxShipThrust, mMaxShipSpeed); if (!mShip.step(tau)) { reset(); } // Move the bullets. int numBullets = mBullets.size(); for (int i = 0; i < numBullets; i++) { final Bullet bullet = mBullets.get(i); if (!bullet.step(tau)) { mBullets.remove(i); i -= 1; numBullets -= 1; } } // Move obstacles. int numObstacles = mObstacles.size(); for (int i = 0; i < numObstacles; i++) { final Obstacle obstacle = mObstacles.get(i); if (!obstacle.step(tau)) { mObstacles.remove(i); i -= 1; numObstacles -= 1; } } // Check for collisions between bullets and obstacles. for (int i = 0; i < numBullets; i++) { final Bullet bullet = mBullets.get(i); for (int j = 0; j < numObstacles; j++) { final Obstacle obstacle = mObstacles.get(j); if (bullet.collidesWith(obstacle)) { bullet.destroy(); obstacle.destroy(); break; } } } // Check for collisions between the ship and obstacles. for (int i = 0; i < numObstacles; i++) { final Obstacle obstacle = mObstacles.get(i); if (mShip.collidesWith(obstacle)) { mShip.destroy(); obstacle.destroy(); break; } } // Spawn more obstacles offscreen when needed. // Avoid putting them right on top of the ship. OuterLoop: while (mObstacles.size() < MAX_OBSTACLES) { final float minDistance = mShipSize * 4; float size = mRandom.nextFloat() * (mMaxObstacleSize - mMinObstacleSize) + mMinObstacleSize; float positionX, positionY; int tries = 0; do { int edge = mRandom.nextInt(4); switch (edge) { case 0: positionX = -size; positionY = mRandom.nextInt(getHeight()); break; case 1: positionX = getWidth() + size; positionY = mRandom.nextInt(getHeight()); break; case 2: positionX = mRandom.nextInt(getWidth()); positionY = -size; break; default: positionX = mRandom.nextInt(getWidth()); positionY = getHeight() + size; break; } if (++tries > 10) { break OuterLoop; } } while (mShip.distanceTo(positionX, positionY) < minDistance); float direction = mRandom.nextFloat() * (float) Math.PI * 2; float speed = mRandom.nextFloat() * (mMaxObstacleSpeed - mMinObstacleSpeed) + mMinObstacleSpeed; float velocityX = (float) Math.cos(direction) * speed; float velocityY = (float) Math.sin(direction) * speed; Obstacle obstacle = new Obstacle(); obstacle.setPosition(positionX, positionY); obstacle.setSize(size); obstacle.setVelocity(velocityX, velocityY); mObstacles.add(obstacle); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // Draw the ship. if (mShip != null) { mShip.draw(canvas); } // Draw bullets. int numBullets = mBullets.size(); for (int i = 0; i < numBullets; i++) { final Bullet bullet = mBullets.get(i); bullet.draw(canvas); } // Draw obstacles. int numObstacles = mObstacles.size(); for (int i = 0; i < numObstacles; i++) { final Obstacle obstacle = mObstacles.get(i); obstacle.draw(canvas); } } static float pythag(float x, float y) { return (float) Math.sqrt(x * x + y * y); } static int blend(float alpha, int from, int to) { return from + (int) ((to - from) * alpha); } static void setPaintARGBBlend(Paint paint, float alpha, int a1, int r1, int g1, int b1, int a2, int r2, int g2, int b2) { paint.setARGB(blend(alpha, a1, a2), blend(alpha, r1, r2), blend(alpha, g1, g2), blend(alpha, b1, b2)); } private abstract class Sprite { protected float mPositionX; protected float mPositionY; protected float mVelocityX; protected float mVelocityY; protected float mSize; protected boolean mDestroyed; protected float mDestroyAnimProgress; public void setPosition(float x, float y) { mPositionX = x; mPositionY = y; } public void setVelocity(float x, float y) { mVelocityX = x; mVelocityY = y; } public void setSize(float size) { mSize = size; } public float distanceTo(float x, float y) { return pythag(mPositionX - x, mPositionY - y); } public float distanceTo(Sprite other) { return distanceTo(other.mPositionX, other.mPositionY); } public boolean collidesWith(Sprite other) { // Really bad collision detection. return !mDestroyed && !other.mDestroyed && distanceTo(other) <= Math.max(mSize, other.mSize) + Math.min(mSize, other.mSize) * 0.5f; } public boolean isDestroyed() { return mDestroyed; } public boolean step(float tau) { mPositionX += mVelocityX * tau; mPositionY += mVelocityY * tau; if (mDestroyed) { mDestroyAnimProgress += tau / getDestroyAnimDuration(); if (mDestroyAnimProgress >= 1.0f) { return false; } } return true; } public abstract void draw(Canvas canvas); public abstract float getDestroyAnimDuration(); protected boolean isOutsidePlayfield() { final int width = GameView.this.getWidth(); final int height = GameView.this.getHeight(); return mPositionX < 0 || mPositionX >= width || mPositionY < 0 || mPositionY >= height; } protected void wrapAtPlayfieldBoundary() { final int width = GameView.this.getWidth(); final int height = GameView.this.getHeight(); while (mPositionX <= -mSize) { mPositionX += width + mSize * 2; } while (mPositionX >= width + mSize) { mPositionX -= width + mSize * 2; } while (mPositionY <= -mSize) { mPositionY += height + mSize * 2; } while (mPositionY >= height + mSize) { mPositionY -= height + mSize * 2; } } public void destroy() { mDestroyed = true; step(0); } } private class Ship extends Sprite { private static final float CORNER_ANGLE = (float) Math.PI * 2 / 3; private static final float TO_DEGREES = (float) (180.0 / Math.PI); private float mHeadingX; private float mHeadingY; private float mHeadingAngle; private float mHeadingMagnitude; private final Paint mPaint; private final Path mPath; public Ship() { mPaint = new Paint(); mPaint.setStyle(Style.FILL); setPosition(getWidth() * 0.5f, getHeight() * 0.5f); setVelocity(0, 0); setSize(mShipSize); mPath = new Path(); mPath.moveTo(0, 0); mPath.lineTo((float)Math.cos(-CORNER_ANGLE) * mSize, (float)Math.sin(-CORNER_ANGLE) * mSize); mPath.lineTo(mSize, 0); mPath.lineTo((float)Math.cos(CORNER_ANGLE) * mSize, (float)Math.sin(CORNER_ANGLE) * mSize); mPath.lineTo(0, 0); } public void setHeadingX(float x) { mHeadingX = x; updateHeading(); } public void setHeadingY(float y) { mHeadingY = y; updateHeading(); } public void setHeading(float x, float y) { mHeadingX = x; mHeadingY = y; updateHeading(); } private void updateHeading() { mHeadingMagnitude = pythag(mHeadingX, mHeadingY); if (mHeadingMagnitude > 0.1f) { mHeadingAngle = (float) Math.atan2(mHeadingY, mHeadingX); } } private float polarX(float radius) { return (float) Math.cos(mHeadingAngle) * radius; } private float polarY(float radius) { return (float) Math.sin(mHeadingAngle) * radius; } public float getBulletInitialX() { return mPositionX + polarX(mSize); } public float getBulletInitialY() { return mPositionY + polarY(mSize); } public float getBulletVelocityX(float relativeSpeed) { return mVelocityX + polarX(relativeSpeed); } public float getBulletVelocityY(float relativeSpeed) { return mVelocityY + polarY(relativeSpeed); } public void accelerate(float tau, float maxThrust, float maxSpeed) { final float thrust = mHeadingMagnitude * maxThrust; mVelocityX += polarX(thrust); mVelocityY += polarY(thrust); final float speed = pythag(mVelocityX, mVelocityY); if (speed > maxSpeed) { final float scale = maxSpeed / speed; mVelocityX = mVelocityX * scale; mVelocityY = mVelocityY * scale; } } @Override public boolean step(float tau) { if (!super.step(tau)) { return false; } wrapAtPlayfieldBoundary(); return true; } public void draw(Canvas canvas) { setPaintARGBBlend(mPaint, mDestroyAnimProgress, 255, 63, 255, 63, 0, 255, 0, 0); canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.translate(mPositionX, mPositionY); canvas.rotate(mHeadingAngle * TO_DEGREES); canvas.drawPath(mPath, mPaint); canvas.restore(); } @Override public float getDestroyAnimDuration() { return 1.0f; } @Override public void destroy() { super.destroy(); crash(); } } private class Bullet extends Sprite { private final Paint mPaint; public Bullet() { mPaint = new Paint(); mPaint.setStyle(Style.FILL); setSize(mBulletSize); } @Override public boolean step(float tau) { if (!super.step(tau)) { return false; } return !isOutsidePlayfield(); } public void draw(Canvas canvas) { setPaintARGBBlend(mPaint, mDestroyAnimProgress, 255, 255, 255, 0, 0, 255, 255, 255); canvas.drawCircle(mPositionX, mPositionY, mSize, mPaint); } @Override public float getDestroyAnimDuration() { return 0.125f; } } private class Obstacle extends Sprite { private final Paint mPaint; public Obstacle() { mPaint = new Paint(); mPaint.setARGB(255, 127, 127, 255); mPaint.setStyle(Style.FILL); } @Override public boolean step(float tau) { if (!super.step(tau)) { return false; } wrapAtPlayfieldBoundary(); return true; } public void draw(Canvas canvas) { setPaintARGBBlend(mPaint, mDestroyAnimProgress, 255, 127, 127, 255, 0, 255, 0, 0); canvas.drawCircle(mPositionX, mPositionY, mSize * (1.0f - mDestroyAnimProgress), mPaint); } @Override public float getDestroyAnimDuration() { return 0.25f; } } }