/*
* 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.particles;
import android.graphics.PointF;
import com.google.fpl.gamecontroller.GameState;
import com.google.fpl.gamecontroller.ShapeBuffer;
import com.google.fpl.gamecontroller.Utils;
/**
* Manages a group of particles.
*/
public class ParticleSystem {
// Constants used to create the "ring burst" effect.
private static final float RING_BURST_INITIAL_POSITION_INCREMENT = 0.0f;
private static final float RING_BURST_MAX_ALPHA = 0.25f;
private static final float RING_BURST_MIN_SIZE = 0.5f;
private static final float RING_BURST_MAX_SIZE = 2.0f;
private static final float RING_BURST_ASPECT_RATIO = 1.0f;
private static final float RING_BURST_MIN_LIFETIME = 0.25f;
private static final float RING_BURST_MAX_LIFETIME = 0.75f;
private static final int RING_BURST_OWNER_ID = GameState.INVALID_PLAYER_ID;
// Constants used to create the shrapnel effect.
private static final float SHRAPNEL_INITIAL_POSITION_INCREMENT = 3.0f;
private static final float SHRAPNEL_MAX_ALPHA = 1.0f;
private static final float SHRAPNEL_MIN_SIZE = 0.75f;
private static final float SHRAPNEL_MAX_SIZE = 0.75f;
private static final float SHRAPNEL_ASPECT_RATIO = 4.0f;
private static final float SHRAPNEL_MIN_LIFETIME = 0.08f;
private static final float SHRAPNEL_MAX_LIFETIME = 0.75f;
// Constants used to create exhaust particles.
private static final float EXHAUST_TRAIL_MIN_LIFETIME = 0.25f;
private static final float EXHAUST_TRAIL_MAX_LIFETIME = 1.0f;
private static final float EXHAUST_TRAIL_SOURCE_VELOCITY_SCALE = -0.5f;
private static final float EXHAUST_TRAIL_VELOCITY_VARIANCE = 0.1f;
private static final float EXHAUST_TRAIL_SOURCE_OFFSET_STEPS = 2.0f;
private static final float EXHAUST_TRAIL_MIN_SIZE = 1.0f;
private static final float EXHAUST_TRAIL_MAX_SIZE = 2.0f;
private static final float EXHAUST_TRAIL_MAX_ALPHA = 0.25f;
private static final int COLLISION_GRID_ZONE_SIZE = 10;
protected final BaseParticle[] mParticles;
protected final ParticleCollisionGrid mCollisionGrid;
protected int mLastOpenIndex = 0;
/**
* Constructs a new particle system.
*
* @param maxActiveParticles the most particles that will ever be active at once.
* @param generateCollisionGrid - true to create a ParticleCollisionGrid structure
* for intersection and proximity queries.
*/
public ParticleSystem(int maxActiveParticles, boolean generateCollisionGrid) {
mParticles = new BaseParticle[maxActiveParticles];
for (int i = 0; i < mParticles.length; i++) {
mParticles[i] = new BaseParticle();
}
if (generateCollisionGrid) {
mCollisionGrid = new ParticleCollisionGrid(
GameState.WORLD_WIDTH,
GameState.WORLD_HEIGHT,
COLLISION_GRID_ZONE_SIZE);
} else {
mCollisionGrid = null;
}
}
/**
* Updates all the particles in the system.
*
* @param frameDelta the number of frames that have elapsed since the last update.
*/
public void update(float frameDelta) {
if (mCollisionGrid != null) {
mCollisionGrid.clear();
for (BaseParticle particle : mParticles) {
particle.update(frameDelta);
if (particle.isActive()) {
mCollisionGrid.addParticle(particle);
}
}
} else {
for (BaseParticle particle : mParticles) {
particle.update(frameDelta);
}
}
}
/**
* Draws the system to the given shape buffer.
*/
public void draw(ShapeBuffer sb) {
for (BaseParticle particle : mParticles) {
if (particle.isActive()) {
particle.draw(sb);
}
}
}
/**
* Spawns a new particle.
*
* @param lifetimeInSeconds the number of seconds this particle will be active.
* @return the new particle, or null if too many particles have already been spawned.
*/
public BaseParticle spawnParticle(float lifetimeInSeconds) {
int slot = getNextOpenIndex();
if (slot != -1) {
mParticles[slot].reset(GameState.secondsToFrameDelta(lifetimeInSeconds));
return mParticles[slot];
}
return null;
}
/**
* Spawns a ring of particles around the given point.
*
* Useful for smoke and other effects that don't interact with the players' ships.
*
* @param centerX x center of the ring.
* @param centerY y center of the ring.
* @param color color of the ring particles.
* @param minSpeed minimum particle speed.
* @param maxSpeed maximum particle speed.
* @param particleCount number of particles to create.
*/
public void spawnRingBurst(float centerX, float centerY, Utils.Color color, float minSpeed,
float maxSpeed, int particleCount) {
spawnGroupFromPoint(
centerX, centerY, RING_BURST_INITIAL_POSITION_INCREMENT,
color, RING_BURST_MAX_ALPHA,
minSpeed, maxSpeed,
RING_BURST_MIN_SIZE, RING_BURST_MAX_SIZE, RING_BURST_ASPECT_RATIO,
RING_BURST_MIN_LIFETIME, RING_BURST_MAX_LIFETIME,
RING_BURST_OWNER_ID,
particleCount);
}
/**
* Creates an explosion centered around the given point.
*
* Useful for creating fragments that can potentially collide with players' ships.
*
* @param centerX x center of the explosion.
* @param centerY y center of the explosion.
* @param color color of explosion particles.
* @param minSpeed minimum particle speed.
* @param maxSpeed maximum particle speed.
* @param ownerId the owner of the new particles.
* @param particleCount the number of particles to create.
*/
public void spawnShrapnelExplosion(float centerX, float centerY, Utils.Color color,
float minSpeed, float maxSpeed, int ownerId,
int particleCount) {
spawnGroupFromPoint(centerX, centerY, SHRAPNEL_INITIAL_POSITION_INCREMENT,
color, SHRAPNEL_MAX_ALPHA,
minSpeed, maxSpeed,
SHRAPNEL_MIN_SIZE, SHRAPNEL_MAX_SIZE, SHRAPNEL_ASPECT_RATIO,
SHRAPNEL_MIN_LIFETIME, SHRAPNEL_MAX_LIFETIME,
ownerId,
particleCount);
}
/**
* Creates a trail of particles behind the given source point.
*
* Useful for creating ship or rocket exhaust.
*
* @param sourceX x source of the exhaust.
* @param sourceY y source of the exhaust.
* @param sourceVelocityX the x velocity of the object creating exhaust.
* @param sourceVelocityY the y velocity of the object creating exhaust.
* @param color the color of the new particles.
* @param particleCount the number of particles to create.
*/
public void spawnExhaustTrail(float sourceX, float sourceY, float sourceVelocityX,
float sourceVelocityY, Utils.Color color, int particleCount) {
for (int i = 0; i < particleCount; ++i) {
float lifetime = Utils.randFloatInRange(
EXHAUST_TRAIL_MIN_LIFETIME,
EXHAUST_TRAIL_MAX_LIFETIME);
BaseParticle trailParticle = spawnParticle(lifetime);
if (trailParticle != null) {
float velocityX = sourceVelocityX * EXHAUST_TRAIL_SOURCE_VELOCITY_SCALE;
velocityX += Utils.randFloatInRange(-EXHAUST_TRAIL_VELOCITY_VARIANCE,
EXHAUST_TRAIL_VELOCITY_VARIANCE);
float velocityY = sourceVelocityY * EXHAUST_TRAIL_SOURCE_VELOCITY_SCALE;
velocityY += Utils.randFloatInRange(-EXHAUST_TRAIL_VELOCITY_VARIANCE,
EXHAUST_TRAIL_VELOCITY_VARIANCE);
trailParticle.setPosition(
sourceX - sourceVelocityX * EXHAUST_TRAIL_SOURCE_OFFSET_STEPS,
sourceY - sourceVelocityY * EXHAUST_TRAIL_SOURCE_OFFSET_STEPS);
trailParticle.setSpeed(velocityX, velocityY);
trailParticle.setColor(color);
trailParticle.setSize(
Utils.randFloatInRange(EXHAUST_TRAIL_MIN_SIZE, EXHAUST_TRAIL_MAX_SIZE));
trailParticle.setMaxAlpha(EXHAUST_TRAIL_MAX_ALPHA);
}
}
}
/**
* Treats the particle array as a circular list, and returns the next index after the given one.
*/
private int getNextIndex(int i) {
return (i + 1) % mParticles.length;
}
/**
* Returns the next available particle index, or -1 if all slots are in use.
*/
protected int getNextOpenIndex() {
for (int i = getNextIndex(mLastOpenIndex); i != mLastOpenIndex; i = getNextIndex(i)) {
if (!mParticles[i].isActive()) {
mLastOpenIndex = i;
return mLastOpenIndex;
}
}
Utils.logDebug("Too many active particles: " + this.toString());
return -1;
}
/**
* Returns the first particle that lies within the given circle.
*
* If more than one particle are in the circle, only one is returned. The returned
* particle is not necessarily the one closest to the center of the circle.
*
* Returns null if no particle is in the given circle or if this system does not have
* collision detection enabled.
*
* @param x x center of the circle.
* @param y y center of the circle.
* @param radius the radius of the circle to test.
* @return the first particle that lies within the given circle.
*/
public BaseParticle checkForCollision(float x, float y, float radius) {
if (mCollisionGrid == null) {
return null;
}
// Get the list of all possible hits.
BaseParticle[] possibleHits =
mCollisionGrid.getRectPopulation(x - radius, y - radius, x + radius, y + radius);
BaseParticle currentParticle;
final float radiusSquared = radius * radius;
// Look for the first particle that meets our criteria (within the given circle).
for (int i = 0; possibleHits[i] != null; i++) {
currentParticle = possibleHits[i];
float xx = x - currentParticle.getPositionX();
float yy = y - currentParticle.getPositionY();
if (Utils.vector2DLengthSquared(xx, yy) <= radiusSquared) {
return currentParticle;
}
}
return null;
}
/**
* Returns a list of particles that might fall within the given rectangle.
*
* The list of particles returned is a super-set of the particles in the given
* rectangle. Finer-grained checking is needed to know exactly which particles are
* in the rectangle.
*
* @param x the center of the rectangle.
* @param y the center of the rectangle.
* @param width the width of the rectangle.
* @param height the height of the rectangle.
* @return All the particles that may be within the given rectangle.
*/
public BaseParticle[] getPotentialCollisions(float x, float y, float width, float height) {
if (mCollisionGrid == null) {
return null;
}
final float left = x - width / 2.0f;
final float right = x + width / 2.0f;
final float bottom = y - height / 2.0f;
final float top = y + height / 2.0f;
return mCollisionGrid.getRectPopulation(left, bottom, right, top);
}
/**
* Helper function for spawning a group of particles at or near a given point.
*
* @param centerX x center of the group.
* @param centerY y center of the group.
* @param initialPositionIncrement the number of frame increments to move the particle
* from its initial position.
* @param color color of the particles.
* @param maxAlpha maximum alpha value for new particles.
* @param minSpeed minimum particle speed.
* @param maxSpeed maximum particle speed.
* @param minSize minimum particle size.
* @param maxSize maximum particle size.
* @param aspectRatio aspect ration for new particles.
* @param minLifetime minimum lifetime for new particles.
* @param maxLifetime maximum lifetime for new particles.
* @param ownerId the owner of the new particles.
* @param particleCount the number of particles to create.
*/
private void spawnGroupFromPoint(float centerX, float centerY, float initialPositionIncrement,
Utils.Color color, float maxAlpha,
float minSpeed, float maxSpeed,
float minSize, float maxSize, float aspectRatio,
float minLifetime, float maxLifetime,
int ownerId,
int particleCount) {
for (int i = 0; i < particleCount; i++) {
PointF direction = Utils.randDirectionVector();
float speed = Utils.randFloatInRange(minSpeed, maxSpeed);
float size = Utils.randFloatInRange(minSize, maxSize);
float lifetime = Utils.randFloatInRange(minLifetime, maxLifetime);
BaseParticle particle = spawnParticle(lifetime);
if (particle != null) {
particle.setPosition(centerX, centerY);
particle.setSpeed(direction.x * speed, direction.y * speed);
particle.setColor(color);
particle.setMaxAlpha(maxAlpha);
particle.setSize(size);
particle.setAspectRatio(aspectRatio);
particle.setOwnerId(ownerId);
// Potentially offset the particle so it does not start exactly on the
// source location.
particle.incrementPosition(initialPositionIncrement);
}
}
}
}