A trivial joystick based physics game to demonstrate joystick handling.
/*
* 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 com.example.android.apis.R;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.InputDevice.MotionRange;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicLong;
/**
* Demonstrates how to process input events received from game controllers.
*
* This activity displays button states and joystick positions.
* Also writes detailed information about relevant input events to the log.
*
* The game controller is also uses to control a very simple game. See {@link GameView}
* for the game itself.
*/
public class GameControllerInput extends Activity {
private static final String TAG = "GameControllerInput";
private SparseArray<InputDeviceState> mInputDeviceStates;
private GameView mGame;
private ListView mSummaryList;
private SummaryAdapter mSummaryAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mInputDeviceStates = new SparseArray<InputDeviceState>();
mSummaryAdapter = new SummaryAdapter(this, getResources());
setContentView(R.layout.game_controller_input);
mGame = (GameView) findViewById(R.id.game);
mSummaryList = (ListView) findViewById(R.id.summary);
mSummaryList.setAdapter(mSummaryAdapter);
mSummaryList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
mSummaryAdapter.onItemClick(position);
}
});
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
mGame.requestFocus();
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// Update device state for visualization and logging.
InputDeviceState state = getInputDeviceState(event);
if (state != null) {
switch (event.getAction()) {
case KeyEvent.ACTION_DOWN:
if (state.onKeyDown(event)) {
mSummaryAdapter.show(state);
}
break;
case KeyEvent.ACTION_UP:
if (state.onKeyUp(event)) {
mSummaryAdapter.show(state);
}
break;
}
}
return super.dispatchKeyEvent(event);
}
@Override
public boolean dispatchGenericMotionEvent(MotionEvent event) {
// 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) {
// Update device state for visualization and logging.
InputDeviceState state = getInputDeviceState(event);
if (state != null && state.onJoystickMotion(event)) {
mSummaryAdapter.show(state);
}
}
return super.dispatchGenericMotionEvent(event);
}
private InputDeviceState getInputDeviceState(InputEvent event) {
final int deviceId = event.getDeviceId();
InputDeviceState state = mInputDeviceStates.get(deviceId);
if (state == null) {
final InputDevice device = event.getDevice();
if (device == null) {
return null;
}
state = new InputDeviceState(device);
mInputDeviceStates.put(deviceId, state);
Log.i(TAG, device.toString());
}
return state;
}
/**
* Tracks the state of joystick axes and game controller buttons for a particular
* input device for diagnostic purposes.
*/
private static class InputDeviceState {
private final InputDevice mDevice;
private final int[] mAxes;
private final float[] mAxisValues;
private final SparseIntArray mKeys;
public InputDeviceState(InputDevice device) {
mDevice = device;
int numAxes = 0;
for (MotionRange range : device.getMotionRanges()) {
if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
numAxes += 1;
}
}
mAxes = new int[numAxes];
mAxisValues = new float[numAxes];
int i = 0;
for (MotionRange range : device.getMotionRanges()) {
if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
numAxes += 1;
}
mAxes[i++] = range.getAxis();
}
mKeys = new SparseIntArray();
}
public InputDevice getDevice() {
return mDevice;
}
public int getAxisCount() {
return mAxes.length;
}
public int getAxis(int axisIndex) {
return mAxes[axisIndex];
}
public float getAxisValue(int axisIndex) {
return mAxisValues[axisIndex];
}
public int getKeyCount() {
return mKeys.size();
}
public int getKeyCode(int keyIndex) {
return mKeys.keyAt(keyIndex);
}
public boolean isKeyPressed(int keyIndex) {
return mKeys.valueAt(keyIndex) != 0;
}
public boolean onKeyDown(KeyEvent event) {
final int keyCode = event.getKeyCode();
if (isGameKey(keyCode)) {
if (event.getRepeatCount() == 0) {
final String symbolicName = KeyEvent.keyCodeToString(keyCode);
mKeys.put(keyCode, 1);
Log.i(TAG, mDevice.getName() + " - Key Down: " + symbolicName);
}
return true;
}
return false;
}
public boolean onKeyUp(KeyEvent event) {
final int keyCode = event.getKeyCode();
if (isGameKey(keyCode)) {
int index = mKeys.indexOfKey(keyCode);
if (index >= 0) {
final String symbolicName = KeyEvent.keyCodeToString(keyCode);
mKeys.put(keyCode, 0);
Log.i(TAG, mDevice.getName() + " - Key Up: " + symbolicName);
}
return true;
}
return false;
}
public boolean onJoystickMotion(MotionEvent event) {
StringBuilder message = new StringBuilder();
message.append(mDevice.getName()).append(" - Joystick Motion:\n");
final int historySize = event.getHistorySize();
for (int i = 0; i < mAxes.length; i++) {
final int axis = mAxes[i];
final float value = event.getAxisValue(axis);
mAxisValues[i] = value;
message.append(" ").append(MotionEvent.axisToString(axis)).append(": ");
// Append all historical values in the batch.
for (int historyPos = 0; historyPos < historySize; historyPos++) {
message.append(event.getHistoricalAxisValue(axis, historyPos));
message.append(", ");
}
// Append the current value.
message.append(value);
message.append("\n");
}
Log.i(TAG, message.toString());
return true;
}
// Check whether this is a key we care about.
// In a real game, we would probably let the user configure which keys to use
// instead of hardcoding the keys like this.
private static boolean isGameKey(int keyCode) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_DOWN:
case KeyEvent.KEYCODE_DPAD_LEFT:
case KeyEvent.KEYCODE_DPAD_RIGHT:
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_SPACE:
return true;
default:
return KeyEvent.isGamepadButton(keyCode);
}
}
}
/**
* A list adapter that displays a summary of the device state.
*/
private static class SummaryAdapter extends BaseAdapter {
private static final int BASE_ID_HEADING = 1 << 10;
private static final int BASE_ID_DEVICE_ITEM = 2 << 10;
private static final int BASE_ID_AXIS_ITEM = 3 << 10;
private static final int BASE_ID_KEY_ITEM = 4 << 10;
private final Context mContext;
private final Resources mResources;
private final SparseArray<Item> mDataItems = new SparseArray<Item>();
private final ArrayList<Item> mVisibleItems = new ArrayList<Item>();
private final Heading mDeviceHeading;
private final TextColumn mDeviceNameTextColumn;
private final Heading mAxesHeading;
private final Heading mKeysHeading;
private InputDeviceState mState;
public SummaryAdapter(Context context, Resources resources) {
mContext = context;
mResources = resources;
mDeviceHeading = new Heading(BASE_ID_HEADING | 0,
mResources.getString(R.string.game_controller_input_heading_device));
mDeviceNameTextColumn = new TextColumn(BASE_ID_DEVICE_ITEM | 0,
mResources.getString(R.string.game_controller_input_label_device_name));
mAxesHeading = new Heading(BASE_ID_HEADING | 1,
mResources.getString(R.string.game_controller_input_heading_axes));
mKeysHeading = new Heading(BASE_ID_HEADING | 2,
mResources.getString(R.string.game_controller_input_heading_keys));
}
public void onItemClick(int position) {
if (mState != null) {
Toast toast = Toast.makeText(
mContext, mState.getDevice().toString(), Toast.LENGTH_LONG);
toast.show();
}
}
public void show(InputDeviceState state) {
mState = state;
mVisibleItems.clear();
// Populate device information.
mVisibleItems.add(mDeviceHeading);
mDeviceNameTextColumn.setContent(state.getDevice().getName());
mVisibleItems.add(mDeviceNameTextColumn);
// Populate axes.
mVisibleItems.add(mAxesHeading);
final int axisCount = state.getAxisCount();
for (int i = 0; i < axisCount; i++) {
final int axis = state.getAxis(i);
final int id = BASE_ID_AXIS_ITEM | axis;
TextColumn column = (TextColumn) mDataItems.get(id);
if (column == null) {
column = new TextColumn(id, MotionEvent.axisToString(axis));
mDataItems.put(id, column);
}
column.setContent(Float.toString(state.getAxisValue(i)));
mVisibleItems.add(column);
}
// Populate keys.
mVisibleItems.add(mKeysHeading);
final int keyCount = state.getKeyCount();
for (int i = 0; i < keyCount; i++) {
final int keyCode = state.getKeyCode(i);
final int id = BASE_ID_KEY_ITEM | keyCode;
TextColumn column = (TextColumn) mDataItems.get(id);
if (column == null) {
column = new TextColumn(id, KeyEvent.keyCodeToString(keyCode));
mDataItems.put(id, column);
}
column.setContent(mResources.getString(state.isKeyPressed(i)
? R.string.game_controller_input_key_pressed
: R.string.game_controller_input_key_released));
mVisibleItems.add(column);
}
notifyDataSetChanged();
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public int getCount() {
return mVisibleItems.size();
}
@Override
public Item getItem(int position) {
return mVisibleItems.get(position);
}
@Override
public long getItemId(int position) {
return getItem(position).getItemId();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
return getItem(position).getView(convertView, parent);
}
private static abstract class Item {
private final int mItemId;
private final int mLayoutResourceId;
private View mView;
public Item(int itemId, int layoutResourceId) {
mItemId = itemId;
mLayoutResourceId = layoutResourceId;
}
public long getItemId() {
return mItemId;
}
public View getView(View convertView, ViewGroup parent) {
if (mView == null) {
LayoutInflater inflater = (LayoutInflater)
parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mView = inflater.inflate(mLayoutResourceId, parent, false);
initView(mView);
}
updateView(mView);
return mView;
}
protected void initView(View view) {
}
protected void updateView(View view) {
}
}
private static class Heading extends Item {
private final String mLabel;
public Heading(int itemId, String label) {
super(itemId, R.layout.game_controller_input_heading);
mLabel = label;
}
@Override
public void initView(View view) {
TextView textView = (TextView) view;
textView.setText(mLabel);
}
}
private static class TextColumn extends Item {
private final String mLabel;
private String mContent;
private TextView mContentView;
public TextColumn(int itemId, String label) {
super(itemId, R.layout.game_controller_input_text_column);
mLabel = label;
}
public void setContent(String content) {
mContent = content;
}
@Override
public void initView(View view) {
TextView textView = (TextView) view.findViewById(R.id.label);
textView.setText(mLabel);
mContentView = (TextView) view.findViewById(R.id.content);
}
@Override
public void updateView(View view) {
mContentView.setText(mContent);
}
}
}
}
/*
* 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, 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);
}
}
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;
}
}
}
//layout/game_controller_input.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<!-- Game controller input demo. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/game_controller_input_description"
android:padding="12dip" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:padding="12dip">
<ListView
android:id="@+id/summary"
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1"
android:padding="3dip">
</ListView>
<com.example.android.apis.view.GameView
android:id="@+id/game"
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#000000"
android:padding="3dip" />
</LinearLayout>
</LinearLayout>
Related examples in the same category