| /* |
| * 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.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. |
| * |
| * @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<Bullet> mBullets; |
| private final List<Obstacle> 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<Bullet>(); |
| mObstacles = new ArrayList<Obstacle>(); |
| |
| 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.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 |
| && 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); |
| 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); |
| } |
| } |
| |
| private void ensureInitialized() { |
| if (mShip == null) { |
| reset(); |
| } |
| } |
| |
| private void reset() { |
| mShip = new Ship(); |
| mBullets.clear(); |
| mObstacles.clear(); |
| } |
| |
| 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; |
| } |
| } |
| |
| 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; |
| } |
| } |
| } |