1160 lines
38 KiB
Java
1160 lines
38 KiB
Java
/*
|
|
* Copyright (C) 2013 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.controllersample;
|
|
|
|
import com.example.inputmanagercompat.InputManagerCompat;
|
|
import com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener;
|
|
|
|
import android.annotation.SuppressLint;
|
|
import android.annotation.TargetApi;
|
|
import android.content.Context;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.Paint;
|
|
import android.graphics.Paint.Style;
|
|
import android.graphics.Path;
|
|
import android.os.Build;
|
|
import android.os.SystemClock;
|
|
import android.os.Vibrator;
|
|
import android.util.AttributeSet;
|
|
import android.util.SparseArray;
|
|
import android.view.InputDevice;
|
|
import android.view.KeyEvent;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
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.
|
|
*/
|
|
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
|
|
public class GameView extends View implements InputDeviceListener {
|
|
private static final int MAX_OBSTACLES = 12;
|
|
|
|
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 final Random mRandom;
|
|
/*
|
|
* Each ship is created as an event comes in from a new Joystick device
|
|
*/
|
|
private final SparseArray<Ship> mShips;
|
|
private final Map<String, Integer> mDescriptorMap;
|
|
private final List<Bullet> mBullets;
|
|
private final List<Obstacle> mObstacles;
|
|
|
|
private long mLastStepTime;
|
|
private final InputManagerCompat mInputManager;
|
|
|
|
private final float mBaseSpeed;
|
|
|
|
private final float mShipSize;
|
|
|
|
private final float mBulletSize;
|
|
|
|
private final float mMinObstacleSize;
|
|
private final float mMaxObstacleSize;
|
|
private final float mMinObstacleSpeed;
|
|
private final float mMaxObstacleSpeed;
|
|
|
|
public GameView(Context context, AttributeSet attrs) {
|
|
super(context, attrs);
|
|
|
|
mRandom = new Random();
|
|
mShips = new SparseArray<Ship>();
|
|
mDescriptorMap = new HashMap<String, Integer>();
|
|
mBullets = new ArrayList<Bullet>();
|
|
mObstacles = new ArrayList<Obstacle>();
|
|
|
|
setFocusable(true);
|
|
setFocusableInTouchMode(true);
|
|
|
|
float baseSize = getContext().getResources().getDisplayMetrics().density * 5f;
|
|
mBaseSpeed = baseSize * 3;
|
|
|
|
mShipSize = baseSize * 3;
|
|
|
|
mBulletSize = baseSize;
|
|
|
|
mMinObstacleSize = baseSize * 2;
|
|
mMaxObstacleSize = baseSize * 12;
|
|
mMinObstacleSpeed = mBaseSpeed;
|
|
mMaxObstacleSpeed = mBaseSpeed * 3;
|
|
|
|
mInputManager = InputManagerCompat.Factory.getInputManager(this.getContext());
|
|
mInputManager.registerInputDeviceListener(this, null);
|
|
}
|
|
|
|
// Iterate through the input devices, looking for controllers. Create a ship
|
|
// for every device that reports itself as a gamepad or joystick.
|
|
void findControllersAndAttachShips() {
|
|
int[] deviceIds = mInputManager.getInputDeviceIds();
|
|
for (int deviceId : deviceIds) {
|
|
InputDevice dev = mInputManager.getInputDevice(deviceId);
|
|
int sources = dev.getSources();
|
|
// if the device is a gamepad/joystick, create a ship to represent it
|
|
if (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
|
|
((sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK)) {
|
|
// if the device has a gamepad or joystick
|
|
getShipForId(deviceId);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
int deviceId = event.getDeviceId();
|
|
if (deviceId != -1) {
|
|
Ship currentShip = getShipForId(deviceId);
|
|
if (currentShip.onKeyDown(keyCode, event)) {
|
|
step(event.getEventTime());
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return super.onKeyDown(keyCode, event);
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
|
int deviceId = event.getDeviceId();
|
|
if (deviceId != -1) {
|
|
Ship currentShip = getShipForId(deviceId);
|
|
if (currentShip.onKeyUp(keyCode, event)) {
|
|
step(event.getEventTime());
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return super.onKeyUp(keyCode, event);
|
|
}
|
|
|
|
@Override
|
|
public boolean onGenericMotionEvent(MotionEvent event) {
|
|
mInputManager.onGenericMotionEvent(event);
|
|
|
|
// Check that the event came from a joystick or gamepad since a generic
|
|
// motion event could be almost anything. API level 18 adds the useful
|
|
// event.isFromSource() helper function.
|
|
int eventSource = event.getSource();
|
|
if ((((eventSource & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
|
|
((eventSource & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK))
|
|
&& event.getAction() == MotionEvent.ACTION_MOVE) {
|
|
int id = event.getDeviceId();
|
|
if (-1 != id) {
|
|
Ship curShip = getShipForId(id);
|
|
if (curShip.onGenericMotionEvent(event)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return super.onGenericMotionEvent(event);
|
|
}
|
|
|
|
@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) {
|
|
mLastStepTime = SystemClock.uptimeMillis();
|
|
mInputManager.onResume();
|
|
} else {
|
|
int numShips = mShips.size();
|
|
for (int i = 0; i < numShips; i++) {
|
|
Ship currentShip = mShips.valueAt(i);
|
|
if (currentShip != null) {
|
|
currentShip.setHeading(0, 0);
|
|
currentShip.setVelocity(0, 0);
|
|
currentShip.mDPadState = 0;
|
|
}
|
|
}
|
|
mInputManager.onPause();
|
|
}
|
|
|
|
super.onWindowFocusChanged(hasWindowFocus);
|
|
}
|
|
|
|
@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
|
|
protected void onDraw(Canvas canvas) {
|
|
super.onDraw(canvas);
|
|
// Update the animation
|
|
animateFrame();
|
|
|
|
// Draw the ships.
|
|
int numShips = mShips.size();
|
|
for (int i = 0; i < numShips; i++) {
|
|
Ship currentShip = mShips.valueAt(i);
|
|
if (currentShip != null) {
|
|
currentShip.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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Uses the device descriptor to try to assign the same color to the same
|
|
* joystick. If there are two joysticks of the same type connected over USB,
|
|
* or the API is < API level 16, it will be unable to distinguish the two
|
|
* devices.
|
|
*
|
|
* @param shipID
|
|
* @return
|
|
*/
|
|
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
|
private Ship getShipForId(int shipID) {
|
|
Ship currentShip = mShips.get(shipID);
|
|
if (null == currentShip) {
|
|
|
|
// do we know something about this ship already?
|
|
InputDevice dev = InputDevice.getDevice(shipID);
|
|
String deviceString = null;
|
|
Integer shipColor = null;
|
|
if (null != dev) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
|
deviceString = dev.getDescriptor();
|
|
} else {
|
|
deviceString = dev.getName();
|
|
}
|
|
shipColor = mDescriptorMap.get(deviceString);
|
|
}
|
|
|
|
if (null != shipColor) {
|
|
int color = shipColor;
|
|
int numShips = mShips.size();
|
|
// do we already have a ship with this color?
|
|
for (int i = 0; i < numShips; i++) {
|
|
if (mShips.valueAt(i).getColor() == color) {
|
|
shipColor = null;
|
|
// we won't store this value either --- if the first
|
|
// controller gets disconnected/connected, it will get
|
|
// the same color.
|
|
deviceString = null;
|
|
}
|
|
}
|
|
}
|
|
if (null != shipColor) {
|
|
currentShip = new Ship(shipColor);
|
|
if (null != deviceString) {
|
|
mDescriptorMap.remove(deviceString);
|
|
}
|
|
} else {
|
|
currentShip = new Ship(getNextShipColor());
|
|
}
|
|
mShips.append(shipID, currentShip);
|
|
currentShip.setInputDevice(dev);
|
|
|
|
if (null != deviceString) {
|
|
mDescriptorMap.put(deviceString, currentShip.getColor());
|
|
}
|
|
}
|
|
return currentShip;
|
|
}
|
|
|
|
/**
|
|
* Remove the ship from the array of active ships by ID.
|
|
*
|
|
* @param shipID
|
|
*/
|
|
private void removeShipForID(int shipID) {
|
|
mShips.remove(shipID);
|
|
}
|
|
|
|
private void reset() {
|
|
mShips.clear();
|
|
mBullets.clear();
|
|
mObstacles.clear();
|
|
findControllersAndAttachShips();
|
|
}
|
|
|
|
private void animateFrame() {
|
|
long currentStepTime = SystemClock.uptimeMillis();
|
|
step(currentStepTime);
|
|
invalidate();
|
|
}
|
|
|
|
private void step(long currentStepTime) {
|
|
float tau = (currentStepTime - mLastStepTime) * 0.001f;
|
|
mLastStepTime = currentStepTime;
|
|
|
|
// Move the ships
|
|
int numShips = mShips.size();
|
|
for (int i = 0; i < numShips; i++) {
|
|
Ship currentShip = mShips.valueAt(i);
|
|
if (currentShip != null) {
|
|
currentShip.accelerate(tau);
|
|
if (!currentShip.step(tau)) {
|
|
currentShip.reincarnate();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 --- this could
|
|
// get slow
|
|
for (int i = 0; i < numObstacles; i++) {
|
|
final Obstacle obstacle = mObstacles.get(i);
|
|
for (int j = 0; j < numShips; j++) {
|
|
Ship currentShip = mShips.valueAt(j);
|
|
if (currentShip != null) {
|
|
if (currentShip.collidesWith(obstacle)) {
|
|
currentShip.destroy();
|
|
obstacle.destroy();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Spawn more obstacles offscreen when needed.
|
|
// Avoid putting them right on top of the ship.
|
|
int tries = MAX_OBSTACLES - mObstacles.size() + 10;
|
|
final float minDistance = mShipSize * 4;
|
|
while (mObstacles.size() < MAX_OBSTACLES && tries-- > 0) {
|
|
float size = mRandom.nextFloat() * (mMaxObstacleSize - mMinObstacleSize)
|
|
+ mMinObstacleSize;
|
|
float positionX, positionY;
|
|
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;
|
|
}
|
|
boolean positionSafe = true;
|
|
|
|
// If the obstacle is too close to any ships, we don't want to
|
|
// spawn it.
|
|
for (int i = 0; i < numShips; i++) {
|
|
Ship currentShip = mShips.valueAt(i);
|
|
if (currentShip != null) {
|
|
if (currentShip.distanceTo(positionX, positionY) < minDistance) {
|
|
// try to spawn again
|
|
positionSafe = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// if the position is safe, add the obstacle and reset the retry
|
|
// counter
|
|
if (positionSafe) {
|
|
tries = MAX_OBSTACLES - mObstacles.size() + 10;
|
|
// we can add the obstacle now since it isn't close to any ships
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static float pythag(float x, float y) {
|
|
return (float) Math.sqrt(x * x + y * y);
|
|
}
|
|
|
|
private static int blend(float alpha, int from, int to) {
|
|
return from + (int) ((to - from) * alpha);
|
|
}
|
|
|
|
private 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 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;
|
|
}
|
|
|
|
/**
|
|
* Any gamepad button + the spacebar or DPAD_CENTER will be used as the fire
|
|
* key.
|
|
*
|
|
* @param keyCode
|
|
* @return true of it's a fire key.
|
|
*/
|
|
private static boolean isFireKey(int keyCode) {
|
|
return KeyEvent.isGamepadButton(keyCode)
|
|
|| keyCode == KeyEvent.KEYCODE_DPAD_CENTER
|
|
|| keyCode == KeyEvent.KEYCODE_SPACE;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Moves the sprite based on the elapsed time defined by tau.
|
|
*
|
|
* @param tau the elapsed time in seconds since the last step
|
|
* @return false if the sprite is to be removed from the display
|
|
*/
|
|
public boolean step(float tau) {
|
|
mPositionX += mVelocityX * tau;
|
|
mPositionY += mVelocityY * tau;
|
|
|
|
if (mDestroyed) {
|
|
mDestroyAnimProgress += tau / getDestroyAnimDuration();
|
|
if (mDestroyAnimProgress >= getDestroyAnimCycles()) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Draws the sprite.
|
|
*
|
|
* @param canvas the Canvas upon which to draw the sprite.
|
|
*/
|
|
public abstract void draw(Canvas canvas);
|
|
|
|
/**
|
|
* Returns the duration of the destruction animation of the sprite in
|
|
* seconds.
|
|
*
|
|
* @return the float duration in seconds of the destruction animation
|
|
*/
|
|
public abstract float getDestroyAnimDuration();
|
|
|
|
/**
|
|
* Returns the number of cycles to play the destruction animation. A
|
|
* destruction animation has a duration and a number of cycles to play
|
|
* it for, so we can have an extended death sequence when a ship or
|
|
* object is destroyed.
|
|
*
|
|
* @return the float number of cycles to play the destruction animation
|
|
*/
|
|
public abstract float getDestroyAnimCycles();
|
|
|
|
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 static int sShipColor = 0;
|
|
|
|
/**
|
|
* Returns the next ship color in the sequence. Very simple. Does not in any
|
|
* way guarantee that there are not multiple ships with the same color on
|
|
* the screen.
|
|
*
|
|
* @return an int containing the index of the next ship color
|
|
*/
|
|
private static int getNextShipColor() {
|
|
int color = sShipColor & 0x07;
|
|
if (0 == color) {
|
|
color++;
|
|
sShipColor++;
|
|
}
|
|
sShipColor++;
|
|
return color;
|
|
}
|
|
|
|
/*
|
|
* Static constants associated with Ship inner class
|
|
*/
|
|
private static final long[] sDestructionVibratePattern = new long[] {
|
|
0, 20, 20, 40, 40, 80, 40, 300
|
|
};
|
|
|
|
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 final float mMaxShipThrust = mBaseSpeed * 0.25f;
|
|
private final float mMaxSpeed = mBaseSpeed * 12;
|
|
|
|
// The ship actually determines the speed of the bullet, not the bullet
|
|
// itself
|
|
private final float mBulletSpeed = mBaseSpeed * 12;
|
|
|
|
private final Paint mPaint;
|
|
private final Path mPath;
|
|
private final int mR, mG, mB;
|
|
private final int mColor;
|
|
|
|
// The current device that is controlling the ship
|
|
private InputDevice mInputDevice;
|
|
|
|
private float mHeadingX;
|
|
private float mHeadingY;
|
|
private float mHeadingAngle;
|
|
private float mHeadingMagnitude;
|
|
|
|
private int mDPadState;
|
|
|
|
/**
|
|
* The colorIndex is used to create the color based on the lower three
|
|
* bits of the value in the current implementation.
|
|
*
|
|
* @param colorIndex
|
|
*/
|
|
public Ship(int colorIndex) {
|
|
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);
|
|
|
|
mR = (colorIndex & 0x01) == 0 ? 63 : 255;
|
|
mG = (colorIndex & 0x02) == 0 ? 63 : 255;
|
|
mB = (colorIndex & 0x04) == 0 ? 63 : 255;
|
|
|
|
mColor = colorIndex;
|
|
}
|
|
|
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
|
|
|
// Handle keys going up.
|
|
boolean handled = false;
|
|
switch (keyCode) {
|
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
|
setHeadingX(0);
|
|
mDPadState &= ~DPAD_STATE_LEFT;
|
|
handled = true;
|
|
break;
|
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
|
setHeadingX(0);
|
|
mDPadState &= ~DPAD_STATE_RIGHT;
|
|
handled = true;
|
|
break;
|
|
case KeyEvent.KEYCODE_DPAD_UP:
|
|
setHeadingY(0);
|
|
mDPadState &= ~DPAD_STATE_UP;
|
|
handled = true;
|
|
break;
|
|
case KeyEvent.KEYCODE_DPAD_DOWN:
|
|
setHeadingY(0);
|
|
mDPadState &= ~DPAD_STATE_DOWN;
|
|
handled = true;
|
|
break;
|
|
default:
|
|
if (isFireKey(keyCode)) {
|
|
handled = true;
|
|
}
|
|
break;
|
|
}
|
|
return handled;
|
|
}
|
|
|
|
/*
|
|
* Firing is a unique case where a ship creates a bullet. A bullet needs
|
|
* to be created with a position near the ship that is firing with a
|
|
* velocity that is based upon the speed of the ship.
|
|
*/
|
|
private void fire() {
|
|
if (!isDestroyed()) {
|
|
Bullet bullet = new Bullet();
|
|
bullet.setPosition(getBulletInitialX(), getBulletInitialY());
|
|
bullet.setVelocity(getBulletVelocityX(),
|
|
getBulletVelocityY());
|
|
mBullets.add(bullet);
|
|
vibrateController(20);
|
|
}
|
|
}
|
|
|
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
// 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:
|
|
setHeadingX(-1);
|
|
mDPadState |= DPAD_STATE_LEFT;
|
|
handled = true;
|
|
break;
|
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
|
setHeadingX(1);
|
|
mDPadState |= DPAD_STATE_RIGHT;
|
|
handled = true;
|
|
break;
|
|
case KeyEvent.KEYCODE_DPAD_UP:
|
|
setHeadingY(-1);
|
|
mDPadState |= DPAD_STATE_UP;
|
|
handled = true;
|
|
break;
|
|
case KeyEvent.KEYCODE_DPAD_DOWN:
|
|
setHeadingY(1);
|
|
mDPadState |= DPAD_STATE_DOWN;
|
|
handled = true;
|
|
break;
|
|
default:
|
|
if (isFireKey(keyCode)) {
|
|
fire();
|
|
handled = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return handled;
|
|
}
|
|
|
|
/**
|
|
* Gets the vibrator from the controller if it is present. Note that it
|
|
* would be easy to get the system vibrator here if the controller one
|
|
* is not present, but we don't choose to do it in this case.
|
|
*
|
|
* @return the Vibrator for the controller, or null if it is not
|
|
* present. or the API level cannot support it
|
|
*/
|
|
@SuppressLint("NewApi")
|
|
private final Vibrator getVibrator() {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
|
|
null != mInputDevice) {
|
|
return mInputDevice.getVibrator();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private void vibrateController(int time) {
|
|
Vibrator vibrator = getVibrator();
|
|
if (null != vibrator) {
|
|
vibrator.vibrate(time);
|
|
}
|
|
}
|
|
|
|
private void vibrateController(long[] pattern, int repeat) {
|
|
Vibrator vibrator = getVibrator();
|
|
if (null != vibrator) {
|
|
vibrator.vibrate(pattern, repeat);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The ship directly handles joystick input.
|
|
*
|
|
* @param event
|
|
* @param historyPos
|
|
*/
|
|
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.
|
|
if (null == mInputDevice) {
|
|
mInputDevice = event.getDevice();
|
|
}
|
|
float x = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_X, historyPos);
|
|
if (x == 0) {
|
|
x = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_HAT_X, historyPos);
|
|
}
|
|
if (x == 0) {
|
|
x = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_Z, historyPos);
|
|
}
|
|
|
|
float y = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_Y, historyPos);
|
|
if (y == 0) {
|
|
y = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_HAT_Y, historyPos);
|
|
}
|
|
if (y == 0) {
|
|
y = getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_RZ, historyPos);
|
|
}
|
|
|
|
// Set the ship heading.
|
|
setHeading(x, y);
|
|
GameView.this.step(historyPos < 0 ? event.getEventTime() : event
|
|
.getHistoricalEventTime(historyPos));
|
|
}
|
|
|
|
public boolean onGenericMotionEvent(MotionEvent event) {
|
|
if (0 == mDPadState) {
|
|
// 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;
|
|
}
|
|
|
|
/**
|
|
* Set the game controller to be used to control the ship.
|
|
*
|
|
* @param dev the input device that will be controlling the ship
|
|
*/
|
|
public void setInputDevice(InputDevice dev) {
|
|
mInputDevice = dev;
|
|
}
|
|
|
|
/**
|
|
* Sets the X component of the joystick heading value, defined by the
|
|
* platform as being from -1.0 (left) to 1.0 (right). This function is
|
|
* generally used to change the heading in response to a button-style
|
|
* DPAD event.
|
|
*
|
|
* @param x the float x component of the joystick heading value
|
|
*/
|
|
public void setHeadingX(float x) {
|
|
mHeadingX = x;
|
|
updateHeading();
|
|
}
|
|
|
|
/**
|
|
* Sets the Y component of the joystick heading value, defined by the
|
|
* platform as being from -1.0 (top) to 1.0 (bottom). This function is
|
|
* generally used to change the heading in response to a button-style
|
|
* DPAD event.
|
|
*
|
|
* @param y the float y component of the joystick heading value
|
|
*/
|
|
public void setHeadingY(float y) {
|
|
mHeadingY = y;
|
|
updateHeading();
|
|
}
|
|
|
|
/**
|
|
* Sets the heading as floating point values returned by a joystick.
|
|
* These values are normalized by the Android platform to be from -1.0
|
|
* (left, top) to 1.0 (right, bottom)
|
|
*
|
|
* @param x the float x component of the joystick heading value
|
|
* @param y the float y component of the joystick heading value
|
|
*/
|
|
public void setHeading(float x, float y) {
|
|
mHeadingX = x;
|
|
mHeadingY = y;
|
|
updateHeading();
|
|
}
|
|
|
|
/**
|
|
* Converts the heading values from joystick devices to the polar
|
|
* representation of the heading angle if the magnitude of the heading
|
|
* is significant (> 0.1f).
|
|
*/
|
|
private void updateHeading() {
|
|
mHeadingMagnitude = pythag(mHeadingX, mHeadingY);
|
|
if (mHeadingMagnitude > 0.1f) {
|
|
mHeadingAngle = (float) Math.atan2(mHeadingY, mHeadingX);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bring our ship back to life, stopping the destroy animation.
|
|
*/
|
|
public void reincarnate() {
|
|
mDestroyed = false;
|
|
mDestroyAnimProgress = 0.0f;
|
|
}
|
|
|
|
private float polarX(float radius) {
|
|
return (float) Math.cos(mHeadingAngle) * radius;
|
|
}
|
|
|
|
private float polarY(float radius) {
|
|
return (float) Math.sin(mHeadingAngle) * radius;
|
|
}
|
|
|
|
/**
|
|
* Gets the initial x coordinate for the bullet.
|
|
*
|
|
* @return the x coordinate of the bullet adjusted for the position and
|
|
* direction of the ship
|
|
*/
|
|
public float getBulletInitialX() {
|
|
return mPositionX + polarX(mSize);
|
|
}
|
|
|
|
/**
|
|
* Gets the initial y coordinate for the bullet.
|
|
*
|
|
* @return the y coordinate of the bullet adjusted for the position and
|
|
* direction of the ship
|
|
*/
|
|
public float getBulletInitialY() {
|
|
return mPositionY + polarY(mSize);
|
|
}
|
|
|
|
/**
|
|
* Returns the bullet speed Y component.
|
|
*
|
|
* @return adjusted Y component bullet speed for the velocity and
|
|
* direction of the ship
|
|
*/
|
|
public float getBulletVelocityY() {
|
|
return mVelocityY + polarY(mBulletSpeed);
|
|
}
|
|
|
|
/**
|
|
* Returns the bullet speed X component
|
|
*
|
|
* @return adjusted X component bullet speed for the velocity and
|
|
* direction of the ship
|
|
*/
|
|
public float getBulletVelocityX() {
|
|
return mVelocityX + polarX(mBulletSpeed);
|
|
}
|
|
|
|
/**
|
|
* Uses the heading magnitude and direction to change the acceleration
|
|
* of the ship. In theory, this should be scaled according to the
|
|
* elapsed time.
|
|
*
|
|
* @param tau the elapsed time in seconds between the last step
|
|
*/
|
|
public void accelerate(float tau) {
|
|
final float thrust = mHeadingMagnitude * mMaxShipThrust;
|
|
mVelocityX += polarX(thrust) * tau * mMaxSpeed / 4;
|
|
mVelocityY += polarY(thrust) * tau * mMaxSpeed / 4;
|
|
|
|
final float speed = pythag(mVelocityX, mVelocityY);
|
|
if (speed > mMaxSpeed) {
|
|
final float scale = mMaxSpeed / speed;
|
|
mVelocityX = mVelocityX * scale * scale;
|
|
mVelocityY = mVelocityY * scale * scale;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean step(float tau) {
|
|
if (!super.step(tau)) {
|
|
return false;
|
|
}
|
|
wrapAtPlayfieldBoundary();
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void draw(Canvas canvas) {
|
|
setPaintARGBBlend(mPaint, mDestroyAnimProgress - (int) (mDestroyAnimProgress),
|
|
255, mR, mG, mB,
|
|
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();
|
|
vibrateController(sDestructionVibratePattern, -1);
|
|
}
|
|
|
|
@Override
|
|
public float getDestroyAnimCycles() {
|
|
return 5.0f;
|
|
}
|
|
|
|
public int getColor() {
|
|
return mColor;
|
|
}
|
|
}
|
|
|
|
private static final Paint mBulletPaint;
|
|
static {
|
|
mBulletPaint = new Paint();
|
|
mBulletPaint.setStyle(Style.FILL);
|
|
}
|
|
|
|
private class Bullet extends Sprite {
|
|
|
|
public Bullet() {
|
|
setSize(mBulletSize);
|
|
}
|
|
|
|
@Override
|
|
public boolean step(float tau) {
|
|
if (!super.step(tau)) {
|
|
return false;
|
|
}
|
|
return !isOutsidePlayfield();
|
|
}
|
|
|
|
@Override
|
|
public void draw(Canvas canvas) {
|
|
setPaintARGBBlend(mBulletPaint, mDestroyAnimProgress,
|
|
255, 255, 255, 0,
|
|
0, 255, 255, 255);
|
|
canvas.drawCircle(mPositionX, mPositionY, mSize, mBulletPaint);
|
|
}
|
|
|
|
@Override
|
|
public float getDestroyAnimDuration() {
|
|
return 0.125f;
|
|
}
|
|
|
|
@Override
|
|
public float getDestroyAnimCycles() {
|
|
return 1.0f;
|
|
}
|
|
|
|
}
|
|
|
|
private static final Paint mObstaclePaint;
|
|
static {
|
|
mObstaclePaint = new Paint();
|
|
mObstaclePaint.setARGB(255, 127, 127, 255);
|
|
mObstaclePaint.setStyle(Style.FILL);
|
|
}
|
|
|
|
private class Obstacle extends Sprite {
|
|
|
|
@Override
|
|
public boolean step(float tau) {
|
|
if (!super.step(tau)) {
|
|
return false;
|
|
}
|
|
wrapAtPlayfieldBoundary();
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void draw(Canvas canvas) {
|
|
setPaintARGBBlend(mObstaclePaint, mDestroyAnimProgress,
|
|
255, 127, 127, 255,
|
|
0, 255, 0, 0);
|
|
canvas.drawCircle(mPositionX, mPositionY,
|
|
mSize * (1.0f - mDestroyAnimProgress), mObstaclePaint);
|
|
}
|
|
|
|
@Override
|
|
public float getDestroyAnimDuration() {
|
|
return 0.25f;
|
|
}
|
|
|
|
@Override
|
|
public float getDestroyAnimCycles() {
|
|
return 1.0f;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* When an input device is added, we add a ship based upon the device.
|
|
* @see
|
|
* com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener
|
|
* #onInputDeviceAdded(int)
|
|
*/
|
|
@Override
|
|
public void onInputDeviceAdded(int deviceId) {
|
|
getShipForId(deviceId);
|
|
}
|
|
|
|
/*
|
|
* This is an unusual case. Input devices don't typically change, but they
|
|
* certainly can --- for example a device may have different modes. We use
|
|
* this to make sure that the ship has an up-to-date InputDevice.
|
|
* @see
|
|
* com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener
|
|
* #onInputDeviceChanged(int)
|
|
*/
|
|
@Override
|
|
public void onInputDeviceChanged(int deviceId) {
|
|
Ship ship = getShipForId(deviceId);
|
|
ship.setInputDevice(InputDevice.getDevice(deviceId));
|
|
}
|
|
|
|
/*
|
|
* Remove any ship associated with the ID.
|
|
* @see
|
|
* com.example.inputmanagercompat.InputManagerCompat.InputDeviceListener
|
|
* #onInputDeviceRemoved(int)
|
|
*/
|
|
@Override
|
|
public void onInputDeviceRemoved(int deviceId) {
|
|
removeShipForID(deviceId);
|
|
}
|
|
}
|