/*
* Copyright 2014 Google Inc. All rights reserved.
*
* 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.google.fpl.gamecontroller;
import com.google.fpl.gamecontroller.particles.BaseParticle;
/**
* Handles positioning, control, and spawning of user-controlled space ships.
*/
public class Spaceship {
// The types of weapons available.
private static final int WEAPON_BASEGUN = 0;
private static final int WEAPON_MACHINEGUN = 1;
private static final int WEAPON_SHOTGUN = 2;
private static final int WEAPON_ARROWHEADS = 3;
private static final int WEAPON_SCATTERGUN = 4;
private static final int WEAPON_ROCKET = 5;
private static final int WEAPON_COUNT = 6;
// The 2D vertex positions used to render the ship.
// The ship is shaped as a single triangle, with a total width of 2 units along the x-axis
// and a height of 1 unit along the y-axis.
private static final float[] SHIP_SHAPE = {
-1.0f, 0.5f,
-1.0f, -0.5f,
1.0f, 0.0f
};
// Scales the above vertices into world-space units.
private static final float SHIP_SIZE = 5.0f;
// When the motion controller is released, the ship coasts to a stop.
// The drag is expressed as a percentage of the ship's current velocity, so 0.05 drag
// means that the ship will get 5% shower each frame.
private static final float DRAG = 0.05f;
// To prevent constant movement, the ship will stop moving if it's velocity drops below
// this minimum velocity.
private static final float MINIMUM_VELOCITY = 0.05f;
// The minimum distance a joystick must move before it is considered to have moved.
private static final float JOYSTICK_MOVEMENT_THRESHOLD = 0.1f;
// The amount of time to wait after a ship has been destroyed before the ship
// is spawned again.
private static final float RESPAWN_FRAME_COUNT = GameState.millisToFrameDelta(2000);
// When a ship spawns, it can not be killed while it is invincible.
private static final float INVINCIBILITY_FRAME_COUNT = GameState.millisToFrameDelta(2000);
// The amount of time to wait after the end of a match before starting a new match.
private static final float NEW_MATCH_RESPAWN_FRAME_COUNT = GameState.millisToFrameDelta(6000);
// The speed at which the various weapons can fire.
private static final float GUN_FIREDELAY_BASEGUN = GameState.millisToFrameDelta(250);
private static final float GUN_FIREDELAY_SHOTGUN = GameState.millisToFrameDelta(1000);
private static final float GUN_FIREDELAY_MACHINEGUN = GameState.millisToFrameDelta(33);
private static final float GUN_FIREDELAY_ARROWHEAD = GameState.millisToFrameDelta(500);
private static final float GUN_FIREDELAY_ROCKET = GameState.millisToFrameDelta(750);
private static final float GUN_FIREDELAY_SCATTERGUN = GameState.millisToFrameDelta(133);
// Speeds for the various types of bullets. All speeds are in world-space units per frame.
private static final float BULLET_SPEED_BASEGUN = 2.5f;
private static final float BULLET_SPEED_MACHINEGUN = BULLET_SPEED_BASEGUN;
private static final float BULLET_SPEED_SHOTGUN = BULLET_SPEED_BASEGUN;
private static final float BULLET_SPEED_ARROWHEAD_CENTER = BULLET_SPEED_BASEGUN;
private static final float BULLET_SPEED_SCATTERGUN = BULLET_SPEED_BASEGUN;
private static final float BULLET_SPEED_SCATTERGUN_SECONDARY = 0.95f * BULLET_SPEED_SCATTERGUN;
private static final float BULLET_SPEED_ROCKET = 2.0f;
// The shotgun fires a volley of bullets along an arc.
private static final int SHOTGUN_BULLET_COUNT = 20;
private static final float SHOTGUN_BULLET_SPREAD_ARC_DEGREES = 20.0f;
// Every other burst from the scatter gun fires a secondary set of bullets offset
// from the central aiming direction.
private static final int SCATTERGUN_SECONDARY_BULLET_COUNT = 2;
private static final float SCATTERGUN_SECONDARY_BULLET_SPREAD_ARC_DEGREES = 15.0f;
// The arrowhead weapon fires a triangular shaped volley of bullets. Each step
// behind the center of the arrow has two bullets, one on either side of the center.
private static final int ARROWHEAD_STEP_COUNT = 3;
private static final int ARROWHEAD_STEP_BULLET_COUNT = 2;
// Each step behind the central bullet travels slower than the one ahead of it.
private static final float ARROWHEAD_STEP_SPEED_DECREMENT = 0.05f;
// Each step behind the central bullet is offset further from the center.
private static final float ARROWHEAD_STEP_SPREAD_INCREMENT = 3.0f;
// The maximum lifetime for bullet particles. This is longer than it takes any bullet
// to travel across the screen, so they will always hit something before timeing out.
private static final float BULLET_LIFETIME_IN_SECONDS = 8.0f;
// Attributes for bullet particles.
private static final float BULLET_PARTICLE_SIZE = 0.75f;
private static final float BULLET_PARTICLE_ASPECT_RATIO = 3.0f;
private static final float BULLET_PARTICLE_INITIAL_POSITION_INCREMENT = 3.0f;
// Attributes for rocket particles.
private static final float ROCKET_PARTICLE_SIZE = 2.0f;
private static final float ROCKET_PARTICLE_ASPECT_RATIO = 2.0f;
private static final float ROCKET_PARTICLE_INITIAL_POSITION_INCREMENT = 3.0f;
// The number of particles to spawn when creating a ring-burst around the ship.
private static final int RINGBURST_PARTICLE_COUNT = 100;
// Primary ring bursts have particles all moving at the same speed.
private static final float RINGBURST_PRIMARY_MIN_SPEED = 1.5f;
private static final float RINGBURST_PRIMARY_MAX_SPEED = 1.5f;
// The secondary ring burst has particles moving at different speeds.
private static final float RINGBURST_SECONDARY_MIN_SPEED = 0.75f;
private static final float RINGBURST_SECONDARY_MAX_SPEED = 3.0f;
// When a ship is invincible, it will flash a darker color.
private static final float INVINCIBILITY_COLOR_DARKEN_FACTOR = 0.3f;
// The number of frames between alternate colors while in invincible mode.
private static final float INVINCIBILITY_COLOR_BLINK_RATE = GameState.millisToFrameDelta(200);
private GameState mGameState;
private float mPositionX, mPositionY;
private int mPlayerId = GameState.INVALID_PLAYER_ID;
private int mScore = 0;
// The permanent color of the ship.
private final Utils.Color mColor = new Utils.Color();
// The current color is updated each frame. It is the same as the permanent color,
// unless the ship is invincible.
private final Utils.Color mCurrentColor = new Utils.Color();
// Handles input events for this ship.
private final GamepadController mController = new GamepadController();
// The vector describing this ship's speed and direction. If the ship is not moving,
// it's velocity will be 0, but it's heading will point in the direction it was last moving.
private float mVelocityX, mVelocityY;
// The normalized direction of this ship.
private float mHeadingX, mHeadingY;
// The normalized direction vector along which bullets are fired.
private float mAimX, mAimY;
// If true, the aim direction was set by the secondary joystick.
private boolean mJoystickAiming;
// One of the available weapons.
private int mCurrentGun;
// The number of frames to wait before spawing this ship again. 0 when the ship is spawned.
private float mRespawnTimer;
// The number of frames this ship will remain invincible.
private float mInvincibilityTimer;
// The number of frames to wait before the gun can fire again.
private float mGunRechargeTimer;
// Used by the SCATTERGUN to determine how many shots to fire (every other shot fires
// additional bullets).
private int mFireCounter;
public Spaceship(GameState gameState, int playerId, Utils.Color color) {
resetPlayer();
this.mGameState = gameState;
this.mHeadingX = 0.0f;
this.mHeadingY = 1.0f;
this.mColor.set(color);
this.mPlayerId = playerId;
// Set the respawn timer to something non-zero, so that a respawn event will trigger
// in the next update.
mRespawnTimer = 1.0f;
}
public float getPositionX() {
return mPositionX;
}
public void setPositionX(float positionX) {
this.mPositionX = Utils.clamp(positionX, GameState.MAP_LEFT_COORDINATE,
GameState.MAP_RIGHT_COORDINATE);
}
public float getPositionY() {
return mPositionY;
}
public void setPositionY(float positionY) {
this.mPositionY = Utils.clamp(positionY, GameState.MAP_BOTTOM_COORDINATE,
GameState.MAP_TOP_COORDINATE);
}
public int getPlayerId() {
return mPlayerId;
}
public boolean isActive() {
return mController.isActive();
}
public int getScore() {
return mScore;
}
public void changeScore(int pointDelta) {
this.mScore = Math.max(0, mScore + pointDelta);
}
/**
* Sets the player's score, weapon, etc. back to their default state.
*/
private void resetPlayer() {
mScore = 0;
mCurrentGun = WEAPON_BASEGUN;
mRespawnTimer = 0.0f;
mInvincibilityTimer = 0.0f;
mFireCounter = 0;
mGunRechargeTimer = 0.0f;
}
public Utils.Color getColor() {
return mColor;
}
public void deactivateShip() {
mController.setDeviceId(-1);
}
public void update(float frameDelta) {
if (!updateStatus(frameDelta)) {
// The ship is not active, so bail out now.
return;
}
updateShipPosition(frameDelta);
handleKeyPressesAndFiring(frameDelta);
checkBulletCollisions();
// Tell the controller to start a new frame. This needs to be done after we're done
// reading the controller's state for this frame.
mController.advanceFrame();
}
/**
* Sets the current weapon to one of the power-up weapons (i.e. the new weapon will be any
* of the weapons except WEAPON_BASEGUN).
*/
public void giveRandomWeapon() {
mCurrentGun = Utils.randIntInRange(1, WEAPON_COUNT);
spawnRingBurstAroundShip(RINGBURST_PRIMARY_MIN_SPEED, RINGBURST_PRIMARY_MAX_SPEED);
}
/**
* Prepares a player for the start of a new match.
*
* @param winningPlayerId the Id of the winning player.
*/
public void resetAtEndOfMatch(int winningPlayerId) {
resetPlayer();
if (isActive()) {
if (mPlayerId != winningPlayerId) {
// Explode the losing ships.
spawnRingBurstAroundShip(
RINGBURST_PRIMARY_MIN_SPEED,
RINGBURST_PRIMARY_MAX_SPEED);
spawnRingBurstAroundShip(
RINGBURST_SECONDARY_MIN_SPEED,
RINGBURST_SECONDARY_MAX_SPEED);
}
// Wait before starting next match.
mRespawnTimer = NEW_MATCH_RESPAWN_FRAME_COUNT;
}
}
public GamepadController getController() {
return mController;
}
public boolean isSpawned() {
return (mRespawnTimer <= 0);
}
public boolean isInvincible() {
return (mInvincibilityTimer > 0);
}
/**
* Draws the player's ship and 0 to 4 to indicate the player's score.
*/
public void draw(ShapeBuffer sb) {
if (!isSpawned()) {
// No drawing if we're not alive yet.
return;
}
sb.add2DShape(mPositionX, mPositionY, mCurrentColor, SHIP_SHAPE, SHIP_SIZE, SHIP_SIZE,
mHeadingX, mHeadingY);
// Draw squares around the ship to indicate the score.
for (int i = 0; i < Math.min(mScore, 4); ++i) {
// Places the dots at the edges of a square the same size as the ship.
sb.add2DShape(
mPositionX + SHIP_SIZE * Utils.SQUARE_SHAPE[i * 2 + 0],
mPositionY + SHIP_SIZE * Utils.SQUARE_SHAPE[i * 2 + 1],
mColor, Utils.SQUARE_SHAPE, 1.0f, 1.0f, 0.0f, 1.0f);
}
if (mScore > 4) {
// TODO: Implement a method to display more than 4 points per player.
// For example, space the dots at equal intervals on a circle around the ship.
Utils.logDebug("Scores higher than 4 are not displayed.");
}
}
/**
* Checks the aiming joystick position and computes the player's aim direction.
*/
protected void calculateAimDirection() {
mAimX = mController.getJoystickPosition(GamepadController.JOYSTICK_2,
GamepadController.AXIS_X);
mAimY = -mController.getJoystickPosition(GamepadController.JOYSTICK_2,
GamepadController.AXIS_Y);
float magnitude = Utils.vector2DLength(mAimX, mAimY);
if (magnitude > JOYSTICK_MOVEMENT_THRESHOLD) {
// Normalize the direction vector.
mAimX /= magnitude;
mAimY /= magnitude;
mJoystickAiming = true;
} else {
// The firing joystick is not being used, so fire any shots in the direction
// the player is currently traveling.
mAimX = mHeadingX;
mAimY = mHeadingY;
mJoystickAiming = false;
}
}
/**
* Fires one burst of the current weapon.
*/
protected void fireGun() {
switch (mCurrentGun) {
case WEAPON_BASEGUN:
// Single bullet straight ahead.
fireBullets(1, 0, BULLET_SPEED_BASEGUN, GUN_FIREDELAY_BASEGUN);
break;
case WEAPON_ARROWHEADS:
// The center bullet of the arrowhead.
fireBullets(1, 0, BULLET_SPEED_ARROWHEAD_CENTER, GUN_FIREDELAY_ARROWHEAD);
// Fire the bullets that make up the steps behind the center bullet of the
// arrowhead.
for (int i = 1; i <= ARROWHEAD_STEP_COUNT; ++i) {
// The bullets farther from the center go slower.
float speedScale = 1.0f - i * ARROWHEAD_STEP_SPEED_DECREMENT;
// Each step in the arrowhead has the bullets spread farther apart.
float spread = i * ARROWHEAD_STEP_SPREAD_INCREMENT;
fireBullets(
ARROWHEAD_STEP_BULLET_COUNT,
spread,
speedScale * BULLET_SPEED_ARROWHEAD_CENTER,
GUN_FIREDELAY_ARROWHEAD);
}
break;
case WEAPON_SHOTGUN:
// The shotgun fires a volley of bullets along an arc.
fireBullets(
SHOTGUN_BULLET_COUNT,
SHOTGUN_BULLET_SPREAD_ARC_DEGREES,
BULLET_SPEED_SHOTGUN,
GUN_FIREDELAY_SHOTGUN);
break;
case WEAPON_MACHINEGUN:
// Fire a single bullet straight ahead.
fireBullets(1, 0, BULLET_SPEED_MACHINEGUN, GUN_FIREDELAY_MACHINEGUN);
break;
case WEAPON_SCATTERGUN:
// Fire the first bullet straight ahead.
fireBullets(1, 0, BULLET_SPEED_SCATTERGUN, GUN_FIREDELAY_SCATTERGUN);
mFireCounter = (mFireCounter + 1) % 2;
if (mFireCounter == 0) {
// Every other burst from the scatter gun will have 2 extra bullets.
fireBullets(
SCATTERGUN_SECONDARY_BULLET_COUNT,
SCATTERGUN_SECONDARY_BULLET_SPREAD_ARC_DEGREES,
BULLET_SPEED_SCATTERGUN_SECONDARY,
GUN_FIREDELAY_SCATTERGUN);
}
break;
case WEAPON_ROCKET:
fireRocket();
break;
default:
Utils.logDebug("Unhandled weapon type: " + mCurrentGun);
break;
}
}
/**
* Creates a rocket "particle".
*/
protected void fireRocket() {
mGunRechargeTimer = GUN_FIREDELAY_ROCKET;
BaseParticle myShot = mGameState.getShots().spawnParticle(BULLET_LIFETIME_IN_SECONDS);
if (myShot != null) {
myShot.setPosition(mPositionX, mPositionY);
myShot.setSpeed(mAimX * BULLET_SPEED_ROCKET, mAimY * BULLET_SPEED_ROCKET);
myShot.setColor(mColor);
myShot.setSize(ROCKET_PARTICLE_SIZE);
myShot.setAspectRatio(ROCKET_PARTICLE_ASPECT_RATIO);
myShot.setOwnerId(mPlayerId);
myShot.setParticleType(BaseParticle.PARTICLE_TYPE_ROCKET);
// Offset the rocket's starting position a few steps ahead of our position.
myShot.incrementPosition(ROCKET_PARTICLE_INITIAL_POSITION_INCREMENT);
}
}
/**
* Checks to see if any bullets have collided with this ship.
*/
protected void checkBulletCollisions() {
BaseParticle bullet = mGameState.getShots().checkForCollision(mPositionX, mPositionY,
SHIP_SIZE);
if (bullet != null && bullet.getOwnerId() != mPlayerId) {
bullet.handleCollision();
if (!isInvincible()) {
spawnRingBurstAroundShip(
RINGBURST_PRIMARY_MIN_SPEED,
RINGBURST_PRIMARY_MAX_SPEED);
spawnRingBurstAroundShip(
RINGBURST_SECONDARY_MIN_SPEED,
RINGBURST_SECONDARY_MAX_SPEED);
mRespawnTimer = RESPAWN_FRAME_COUNT;
mGameState.scorePoint(bullet.getOwnerId());
changeScore(-1);
}
}
}
/**
* Spawns a ring of particles radiating from the ship's current position.
*/
protected void spawnRingBurstAroundShip(float minSpeed, float maxSpeed) {
mGameState.getExplosions().spawnRingBurst(mPositionX, mPositionY, mColor, minSpeed,
maxSpeed, RINGBURST_PARTICLE_COUNT);
}
/**
* Fires one or more bullets from the ship's current location.
* @param bulletCount the number of bullets to fire.
* @param spreadArc for multiple bullets, the arc, in degrees, over which the bullets
* are spread out.
* @param speed the speed of the bullets.
* @param recharge the number of frames delay before the next shot can be fired.
*/
protected void fireBullets(int bulletCount, float spreadArc, float speed,
float recharge) {
mGunRechargeTimer = recharge;
for (int i = 0; i < bulletCount; ++i) {
float angleDegrees;
if (bulletCount > 1) {
// Compute this bullet's position along the spread arc.
angleDegrees =
-spreadArc / 2.0f + (float) i * spreadArc / ((float) bulletCount - 1.0f);
} else {
// Single bullets are always fired along the aiming direction.
angleDegrees = 0;
}
float angleRadians = (float) Math.toRadians(angleDegrees);
float angleSin = (float) Math.sin(angleRadians);
float angleCos = (float) Math.cos(angleRadians);
float shotDx = mAimX * angleCos - mAimY * angleSin;
float shotDy = mAimX * angleSin + mAimY * angleCos;
BaseParticle myShot = mGameState.getShots().spawnParticle(BULLET_LIFETIME_IN_SECONDS);
if (myShot != null) {
myShot.setPosition(mPositionX, mPositionY);
myShot.setSpeed(shotDx * speed, shotDy * speed);
myShot.setColor(mColor);
myShot.setSize(BULLET_PARTICLE_SIZE);
myShot.setAspectRatio(BULLET_PARTICLE_ASPECT_RATIO);
myShot.setOwnerId(mPlayerId);
// Offset the bullet's starting position a few steps ahead of our position.
myShot.incrementPosition(BULLET_PARTICLE_INITIAL_POSITION_INCREMENT);
}
}
}
/**
* Updates the ship's spawning state and invincibility state.
*/
private boolean updateStatus(float frameDelta) {
updateSpawningStatus(frameDelta);
if (!isSpawned()) {
return false;
}
updateInvincibilityStatus(frameDelta);
return true;
}
/**
* Picks a new starting location when the ship is spawned.
*/
private void updateSpawningStatus(float frameDelta) {
// Are we waiting to respawn.
if (mRespawnTimer > 0.0f) {
mRespawnTimer -= frameDelta;
if (mRespawnTimer <= 0.0f) {
// Time to respawn.
mRespawnTimer = 0.0f;
// Pick a new location.
setPositionX(Utils.randFloatInRange(GameState.MAP_LEFT_COORDINATE,
GameState.MAP_RIGHT_COORDINATE));
setPositionY(Utils.randFloatInRange(GameState.MAP_BOTTOM_COORDINATE,
GameState.MAP_TOP_COORDINATE));
spawnRingBurstAroundShip(RINGBURST_PRIMARY_MIN_SPEED, RINGBURST_PRIMARY_MAX_SPEED);
mInvincibilityTimer = INVINCIBILITY_FRAME_COUNT;
// Newly spawned ships don't have any powerup weapons.
mCurrentGun = WEAPON_BASEGUN;
}
}
}
/**
* Keeps track of this ship's invincibility status.
*/
private void updateInvincibilityStatus(float frameDelta) {
if (mInvincibilityTimer > 0.0f) {
mInvincibilityTimer -= frameDelta;
if (mInvincibilityTimer < 0.0f) {
mInvincibilityTimer = 0.0f;
}
}
mCurrentColor.set(mColor);
// Flash the ship while it is invincible.
if (isInvincible()
&& ((int) (mInvincibilityTimer / INVINCIBILITY_COLOR_BLINK_RATE) % 2 == 0)) {
mCurrentColor.darken(INVINCIBILITY_COLOR_DARKEN_FACTOR);
}
}
/**
* Reads the movement joystick and updates the ship's position.
*/
private void updateShipPosition(float frameDelta) {
float newHeadingX = mController.getJoystickPosition(GamepadController.JOYSTICK_1,
GamepadController.AXIS_X);
float newHeadingY = mController.getJoystickPosition(GamepadController.JOYSTICK_1,
GamepadController.AXIS_Y);
float magnitude = Utils.vector2DLength(newHeadingX, newHeadingY);
if (magnitude > JOYSTICK_MOVEMENT_THRESHOLD) {
// Normalize the direction vector.
mHeadingX = newHeadingX / magnitude;
mHeadingY = -newHeadingY / magnitude;
// Compute the new speed.
mVelocityX = newHeadingX;
mVelocityY = -newHeadingY;
if (magnitude > 1.0f) {
// Limit the max speed to "1". If the movement joystick is moved less than
// 1 unit from the center, the ship will move less than it's maximum speed.
// If the joystick moves more than 1 unit from the center, dividing by
// magnitude will limit the speed of the ship, but keep the direction of moment
// correct.
mVelocityX /= magnitude;
mVelocityY /= magnitude;
}
// Create a particle trail (exhaust) behind the ship.
GameState.getInstance().getExplosions().spawnExhaustTrail(
mPositionX, mPositionY,
mVelocityX, mVelocityY,
mColor, 1);
}
setPositionX(mPositionX + mVelocityX * frameDelta);
setPositionY(mPositionY + mVelocityY * frameDelta);
// Use drag so that the ship will coast to a stop after the movement controller
// is released.
mVelocityX *= 1.0f - frameDelta * DRAG;
mVelocityY *= 1.0f - frameDelta * DRAG;
if (Utils.vector2DLength(mVelocityX, mVelocityY) < MINIMUM_VELOCITY) {
mVelocityX = 0.0f;
mVelocityY = 0.0f;
}
}
/**
* Checks for controller key presses and fires the gun.
*/
private void handleKeyPressesAndFiring(float frameDelta) {
mGunRechargeTimer -= frameDelta;
if (mGunRechargeTimer <= 0) {
// The gun is ready to fire, so calculate the aim direction.
calculateAimDirection();
if (mJoystickAiming || mController.isButtonDown(GamepadController.BUTTON_X)) {
fireGun();
}
}
}
}