Path: blob/master/SLICK_HOME/src/org/newdawn/slick/particles/ConfigurableEmitter.java
1457 views
package org.newdawn.slick.particles;12import java.io.ByteArrayInputStream;3import java.io.ByteArrayOutputStream;4import java.io.IOException;5import java.util.ArrayList;67import org.newdawn.slick.Color;8import org.newdawn.slick.Image;9import org.newdawn.slick.SlickException;10import org.newdawn.slick.geom.Vector2f;11import org.newdawn.slick.util.FastTrig;12import org.newdawn.slick.util.Log;1314/**15* An emitter than can be externally configured. This configuration can also be16* saved/loaded using the ParticleIO class.17*18* @see ParticleIO19*20* @author kevin21*/22public class ConfigurableEmitter implements ParticleEmitter {23/** The path from which the images should be loaded */24private static String relativePath = "";2526/**27* Set the path from which images should be loaded28*29* @param path30* The path from which images should be loaded31*/32public static void setRelativePath(String path) {33if (!path.endsWith("/")) {34path += "/";35}36relativePath = path;37}3839/** The spawn interval range property - how often spawn happens */40public Range spawnInterval = new Range(100, 100);41/** The spawn count property - how many particles are spawned each time */42public Range spawnCount = new Range(5, 5);43/** The initial life of the new pixels */44public Range initialLife = new Range(1000, 1000);45/** The initial size of the new pixels */46public Range initialSize = new Range(10, 10);47/** The offset from the x position */48public Range xOffset = new Range(0, 0);49/** The offset from the y position */50public Range yOffset = new Range(0, 0);51/** The spread of the particles */52public RandomValue spread = new RandomValue(360);53/** The angular offset */54public Value angularOffset = new SimpleValue(0);55/** The initial distance of the particles */56public Range initialDistance = new Range(0, 0);57/** The speed particles fly out */58public Range speed = new Range(50, 50);59/** The growth factor on the particles */60public Value growthFactor = new SimpleValue(0);61/** The factor of gravity to apply */62public Value gravityFactor = new SimpleValue(0);63/** The factor of wind to apply */64public Value windFactor = new SimpleValue(0);65/** The length of the effect */66public Range length = new Range(1000, 1000);67/**68* The color range69*70* @see ColorRecord71*/72public ArrayList colors = new ArrayList();73/** The starting alpha value */74public Value startAlpha = new SimpleValue(255);75/** The ending alpha value */76public Value endAlpha = new SimpleValue(0);7778/** Whiskas - Interpolated value for alpha */79public LinearInterpolator alpha;80/** Whiskas - Interpolated value for size */81public LinearInterpolator size;82/** Whiskas - Interpolated value for velocity */83public LinearInterpolator velocity;84/** Whiskas - Interpolated value for y axis scaling */85public LinearInterpolator scaleY;8687/** The number of particles that will be emitted */88public Range emitCount = new Range(1000, 1000);89/** The points indicate */90public int usePoints = Particle.INHERIT_POINTS;9192/** True if the quads should be orieted based on velocity */93public boolean useOriented = false;94/**95* True if the additivie blending mode should be used for particles owned by96* this emitter97*/98public boolean useAdditive = false;99100/** The name attribute */101public String name;102/** The name of the image in use */103public String imageName = "";104/** The image being used for the particles */105private Image image;106/** True if the image needs updating */107private boolean updateImage;108109/** True if the emitter is enabled */110private boolean enabled = true;111/** The x coordinate of the position of this emitter */112private float x;113/** The y coordinate of the position of this emitter */114private float y;115/** The time in milliseconds til the next spawn */116private int nextSpawn = 0;117118/** The timeout counting down to spawn */119private int timeout;120/** The number of particles in use by this emitter */121private int particleCount;122/** The system this emitter is being updated to */123private ParticleSystem engine;124/** The number of particles that are left ot emit */125private int leftToEmit;126127/** True if we're wrapping up */128private boolean wrapUp = false;129/** True if the system has completed due to a wrap up */130private boolean completed = false;131132/**133* Create a new emitter configurable externally134*135* @param name136* The name of emitter137*/138public ConfigurableEmitter(String name) {139this.name = name;140leftToEmit = (int) emitCount.random();141timeout = (int) (length.random());142143colors.add(new ColorRecord(0, Color.white));144colors.add(new ColorRecord(1, Color.red));145146ArrayList curve = new ArrayList();147curve.add(new Vector2f(0.0f, 0.0f));148curve.add(new Vector2f(1.0f, 255.0f));149alpha = new LinearInterpolator(curve, 0, 255);150151curve = new ArrayList();152curve.add(new Vector2f(0.0f, 0.0f));153curve.add(new Vector2f(1.0f, 255.0f));154size = new LinearInterpolator(curve, 0, 255);155156curve = new ArrayList();157curve.add(new Vector2f(0.0f, 0.0f));158curve.add(new Vector2f(1.0f, 1.0f));159velocity = new LinearInterpolator(curve, 0, 1);160161curve = new ArrayList();162curve.add(new Vector2f(0.0f, 0.0f));163curve.add(new Vector2f(1.0f, 1.0f));164scaleY = new LinearInterpolator(curve, 0, 1);165}166167/**168* Set the name of the image to use on a per particle basis. The complete169* reference to the image is required (based on the relative path)170*171* @see #setRelativePath(String)172*173* @param imageName174* The name of the image to use on a per particle reference175*/176public void setImageName(String imageName) {177if (imageName.length() == 0) {178imageName = null;179}180181this.imageName = imageName;182if (imageName == null) {183image = null;184} else {185updateImage = true;186}187}188189/**190* The name of the image to load191*192* @return The name of the image to load193*/194public String getImageName() {195return imageName;196}197198/**199* @see java.lang.Object#toString()200*/201public String toString() {202return "[" + name + "]";203}204205/**206* Set the position of this particle source207*208* @param x209* The x coodinate of that this emitter should spawn at210* @param y211* The y coodinate of that this emitter should spawn at212*/213public void setPosition(float x, float y) {214this.x = x;215this.y = y;216}217218/**219* Get the base x coordiante for spawning particles220*221* @return The x coordinate for spawning particles222*/223public float getX() {224return x;225}226227/**228* Get the base y coordiante for spawning particles229*230* @return The y coordinate for spawning particles231*/232public float getY() {233return y;234}235236/**237* @see org.newdawn.slick.particles.ParticleEmitter#isEnabled()238*/239public boolean isEnabled() {240return enabled;241}242243/**244* @see org.newdawn.slick.particles.ParticleEmitter#setEnabled(boolean)245*/246public void setEnabled(boolean enabled) {247this.enabled = enabled;248}249250/**251* @see org.newdawn.slick.particles.ParticleEmitter#update(org.newdawn.slick.particles.ParticleSystem,252* int)253*/254public void update(ParticleSystem system, int delta) {255this.engine = system;256257if (updateImage) {258updateImage = false;259try {260image = new Image(relativePath + imageName);261} catch (SlickException e) {262image = null;263Log.error(e);264}265}266267if ((wrapUp) ||268((length.isEnabled()) && (timeout < 0)) ||269((emitCount.isEnabled() && (leftToEmit <= 0)))) {270if (particleCount == 0) {271completed = true;272}273}274particleCount = 0;275276if (wrapUp) {277return;278}279280if (length.isEnabled()) {281if (timeout < 0) {282return;283}284timeout -= delta;285}286if (emitCount.isEnabled()) {287if (leftToEmit <= 0) {288return;289}290}291292nextSpawn -= delta;293if (nextSpawn < 0) {294nextSpawn = (int) spawnInterval.random();295int count = (int) spawnCount.random();296297for (int i = 0; i < count; i++) {298Particle p = system.getNewParticle(this, initialLife.random());299p.setSize(initialSize.random());300p.setPosition(x + xOffset.random(), y + yOffset.random());301p.setVelocity(0, 0, 0);302303float dist = initialDistance.random();304float power = speed.random();305if ((dist != 0) || (power != 0)) {306float s = spread.getValue(0);307float ang = (s + angularOffset.getValue(0) - (spread308.getValue() / 2)) - 90;309float xa = (float) FastTrig.cos(Math.toRadians(ang)) * dist;310float ya = (float) FastTrig.sin(Math.toRadians(ang)) * dist;311p.adjustPosition(xa, ya);312313float xv = (float) FastTrig.cos(Math.toRadians(ang));314float yv = (float) FastTrig.sin(Math.toRadians(ang));315p.setVelocity(xv, yv, power * 0.001f);316}317318if (image != null) {319p.setImage(image);320}321322ColorRecord start = (ColorRecord) colors.get(0);323p.setColor(start.col.r, start.col.g, start.col.b, startAlpha324.getValue(0) / 255.0f);325p.setUsePoint(usePoints);326p.setOriented(useOriented);327328if (emitCount.isEnabled()) {329leftToEmit--;330if (leftToEmit <= 0) {331break;332}333}334}335}336}337338/**339* @see org.newdawn.slick.particles.ParticleEmitter#updateParticle(org.newdawn.slick.particles.Particle,340* int)341*/342public void updateParticle(Particle particle, int delta) {343particleCount++;344345particle.adjustVelocity(windFactor.getValue(0) * 0.00005f * delta, gravityFactor346.getValue(0) * 0.00005f * delta);347348float offset = particle.getLife() / particle.getOriginalLife();349float inv = 1 - offset;350float colOffset = 0;351float colInv = 1;352353Color startColor = null;354Color endColor = null;355for (int i = 0; i < colors.size() - 1; i++) {356ColorRecord rec1 = (ColorRecord) colors.get(i);357ColorRecord rec2 = (ColorRecord) colors.get(i + 1);358359if ((inv >= rec1.pos) && (inv <= rec2.pos)) {360startColor = rec1.col;361endColor = rec2.col;362363float step = rec2.pos - rec1.pos;364colOffset = inv - rec1.pos;365colOffset /= step;366colOffset = 1 - colOffset;367colInv = 1 - colOffset;368}369}370371if (startColor != null) {372float r = (startColor.r * colOffset) + (endColor.r * colInv);373float g = (startColor.g * colOffset) + (endColor.g * colInv);374float b = (startColor.b * colOffset) + (endColor.b * colInv);375376float a;377if (alpha.isActive()) {378a = alpha.getValue(inv) / 255.0f;379} else {380a = ((startAlpha.getValue(0) / 255.0f) * offset)381+ ((endAlpha.getValue(0) / 255.0f) * inv);382}383particle.setColor(r, g, b, a);384}385386if (size.isActive()) {387float s = size.getValue(inv);388particle.setSize(s);389} else {390particle.adjustSize(delta * growthFactor.getValue(0) * 0.001f);391}392393if (velocity.isActive()) {394particle.setSpeed(velocity.getValue(inv));395}396397if (scaleY.isActive()) {398particle.setScaleY(scaleY.getValue(inv));399}400}401402/**403* Check if this emitter has completed it's cycle404*405* @return True if the emitter has completed it's cycle406*/407public boolean completed() {408if (engine == null) {409return false;410}411412if (length.isEnabled()) {413if (timeout > 0) {414return false;415}416return completed;417}418if (emitCount.isEnabled()) {419if (leftToEmit > 0) {420return false;421}422return completed;423}424425if (wrapUp) {426return completed;427}428429return false;430}431432/**433* Cause the emitter to replay it's circle434*/435public void replay() {436reset();437nextSpawn = 0;438leftToEmit = (int) emitCount.random();439timeout = (int) (length.random());440}441442/**443* Release all the particles held by this emitter444*/445public void reset() {446completed = false;447if (engine != null) {448engine.releaseAll(this);449}450}451452/**453* Check if the replay has died out - used by the editor454*/455public void replayCheck() {456if (completed()) {457if (engine != null) {458if (engine.getParticleCount() == 0) {459replay();460}461}462}463}464465/**466* Create a duplicate of this emitter.467* The duplicate should be added to a ParticleSystem to be used.468* @return a copy if no IOException occurred, null otherwise469*/470public ConfigurableEmitter duplicate() {471ConfigurableEmitter theCopy = null;472try {473ByteArrayOutputStream bout = new ByteArrayOutputStream();474ParticleIO.saveEmitter(bout, this);475ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());476theCopy = ParticleIO.loadEmitter(bin);477} catch (IOException e) {478Log.error("Slick: ConfigurableEmitter.duplicate(): caught exception " + e.toString());479return null;480}481return theCopy;482}483484/**485* a general interface to provide a general value :]486*487* @author void488*/489public interface Value {490/**491* get the current value that might depend from the given time492*493* @param time494* @return the current value495*/496public float getValue(float time);497}498499/**500* A configurable simple single value501*502* @author void503*/504public class SimpleValue implements Value {505/** The value configured */506private float value;507/** The next value */508private float next;509510/**511* Create a new configurable new value512*513* @param value514* The initial value515*/516private SimpleValue(float value) {517this.value = value;518}519520/**521* Get the currently configured value522*523* @return The currently configured value524*/525public float getValue(float time) {526return value;527}528529/**530* Set the configured value531*532* @param value533* The configured value534*/535public void setValue(float value) {536this.value = value;537}538}539540/**541* A configurable simple linear random value542*543* @author void544*/545public class RandomValue implements Value {546/** The value configured */547private float value;548549/**550* Create a new configurable new value551*552* @param value553* The initial value554*/555private RandomValue(float value) {556this.value = value;557}558559/**560* Get the currently configured value561*562* @return The currently configured value563*/564public float getValue(float time) {565return (float) (Math.random() * value);566}567568/**569* Set the configured value570*571* @param value572* The configured value573*/574public void setValue(float value) {575this.value = value;576}577578/**579* get the configured value580*581* @return the configured value582*/583public float getValue() {584return value;585}586}587588/**589* A value computed based on linear interpolation between a set of points590*591* @author void592*/593public class LinearInterpolator implements Value {594/** The list of points to interpolate between */595private ArrayList curve;596/** True if this interpolation value is active */597private boolean active;598/** The minimum value in the data set */599private int min;600/** The maximum value in the data set */601private int max;602603/**604* Create a new interpolated value605*606* @param curve The set of points to interpolate between607* @param min The minimum value in the dataset608* @param max The maximum value possible in the dataset609*/610public LinearInterpolator(ArrayList curve, int min, int max) {611this.curve = curve;612this.min = min;613this.max = max;614this.active = false;615}616617/**618* Set the collection of data points to interpolate between619*620* @param curve The list of data points to interpolate between621*/622public void setCurve(ArrayList curve) {623this.curve = curve;624}625626/**627* The list of data points to interpolate between628*629* @return A list of Vector2f of the data points to interpolate between630*/631public ArrayList getCurve() {632return curve;633}634635/**636* Get the value to use at a given time value637*638* @param t The time value (expecting t in [0,1])639* @return The value to use at the specified time640*/641public float getValue(float t) {642// first: determine the segment we are in643Vector2f p0 = (Vector2f) curve.get(0);644for (int i = 1; i < curve.size(); i++) {645Vector2f p1 = (Vector2f) curve.get(i);646647if (t >= p0.getX() && t <= p1.getX()) {648// found the segment649float st = (t - p0.getX())650/ (p1.getX() - p0.getX());651float r = p0.getY() + st652* (p1.getY() - p0.getY());653// System.out.println( "t: " + t + ", " + p0.x + ", " + p0.y654// + " : " + p1.x + ", " + p1.y + " => " + r );655656return r;657}658659p0 = p1;660}661return 0;662}663664/**665* Check if this interpolated value should be used666*667* @return True if this value is in use668*/669public boolean isActive() {670return active;671}672673/**674* Indicate if this interpoalte value should be used675*676* @param active True if this value should be used677*/678public void setActive(boolean active) {679this.active = active;680}681682/**683* Get the maxmimum value possible in this data set684*685* @return The maximum value possible in this data set686*/687public int getMax() {688return max;689}690691/**692* Set the maximum value possible in this data set693*694* @param max The maximum value possible in this data set695*/696public void setMax(int max) {697this.max = max;698}699700/**701* Get the minimum value possible in this data set702*703* @return The minimum value possible in this data set704*/705public int getMin() {706return min;707}708709/**710* Set the minimum value possible in this data set711*712* @param min The minimum value possible in this data set713*/714public void setMin(int min) {715this.min = min;716}717}718719/**720* A single element in the colour range of this emitter721*722* @author kevin723*/724public class ColorRecord {725/** The position in the life cycle */726public float pos;727/** The color at this position */728public Color col;729730/**731* Create a new record732*733* @param pos734* The position in the life cycle (0 = start, 1 = end)735* @param col736* The color applied at this position737*/738public ColorRecord(float pos, Color col) {739this.pos = pos;740this.col = col;741}742}743744/**745* Add a point in the colour cycle746*747* @param pos748* The position in the life cycle (0 = start, 1 = end)749* @param col750* The color applied at this position751*/752public void addColorPoint(float pos, Color col) {753colors.add(new ColorRecord(pos, col));754}755756/**757* A simple bean describing a range of values758*759* @author kevin760*/761public class Range {762/** The maximum value in the range */763private float max;764/** The minimum value in the range */765private float min;766/** True if this range application is enabled */767private boolean enabled = false;768769/**770* Create a new configurable range771*772* @param min773* The minimum value of the range774* @param max775* The maximum value of the range776*/777private Range(float min, float max) {778this.min = min;779this.max = max;780}781782/**783* Generate a random number in the range784*785* @return The random number from the range786*/787public float random() {788return (float) (min + (Math.random() * (max - min)));789}790791/**792* Check if this configuration option is enabled793*794* @return True if the range is enabled795*/796public boolean isEnabled() {797return enabled;798}799800/**801* Indicate if this option should be enabled802*803* @param enabled804* True if this option should be enabled805*/806public void setEnabled(boolean enabled) {807this.enabled = enabled;808}809810/**811* Get the maximum value for this range812*813* @return The maximum value for this range814*/815public float getMax() {816return max;817}818819/**820* Set the maxmium value for this range821*822* @param max823* The maximum value for this range824*/825public void setMax(float max) {826this.max = max;827}828829/**830* Get the minimum value for this range831*832* @return The minimum value for this range833*/834public float getMin() {835return min;836}837838/**839* Set the minimum value for this range840*841* @param min842* The minimum value for this range843*/844public void setMin(float min) {845this.min = min;846}847}848849public boolean useAdditive() {850return useAdditive;851}852853public boolean isOriented() {854return this.useOriented;855}856857public boolean usePoints(ParticleSystem system) {858return (this.usePoints == Particle.INHERIT_POINTS) && (system.usePoints()) ||859(this.usePoints == Particle.USE_POINTS);860}861862public Image getImage() {863return image;864}865866public void wrapUp() {867wrapUp = true;868}869870public void resetState() {871replay();872}873}874875876