/******************************************************************************* * Copyright 2011 See AUTHORS file. * * 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.badlogic.gdx.graphics.g2d; import java.io.BufferedReader; import java.io.IOException; import java.io.Writer; import com.badlogic.gdx.graphics.GL10; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.math.MathUtils; // BOZO - Javadoc. // BOZO - Add a duplicate emitter button. public class ParticleEmitter { static private final int UPDATE_SCALE = 1 << 0; static private final int UPDATE_ANGLE = 1 << 1; static private final int UPDATE_ROTATION = 1 << 2; static private final int UPDATE_VELOCITY = 1 << 3; static private final int UPDATE_WIND = 1 << 4; static private final int UPDATE_GRAVITY = 1 << 5; static private final int UPDATE_TINT = 1 << 6; private RangedNumericValue delayValue = new RangedNumericValue(); private ScaledNumericValue lifeOffsetValue = new ScaledNumericValue(); private RangedNumericValue durationValue = new RangedNumericValue(); private ScaledNumericValue lifeValue = new ScaledNumericValue(); private ScaledNumericValue emissionValue = new ScaledNumericValue(); private ScaledNumericValue scaleValue = new ScaledNumericValue(); private ScaledNumericValue rotationValue = new ScaledNumericValue(); private ScaledNumericValue velocityValue = new ScaledNumericValue(); private ScaledNumericValue angleValue = new ScaledNumericValue(); private ScaledNumericValue windValue = new ScaledNumericValue(); private ScaledNumericValue gravityValue = new ScaledNumericValue(); private ScaledNumericValue transparencyValue = new ScaledNumericValue(); private GradientColorValue tintValue = new GradientColorValue(); private RangedNumericValue xOffsetValue = new ScaledNumericValue(); private RangedNumericValue yOffsetValue = new ScaledNumericValue(); private ScaledNumericValue spawnWidthValue = new ScaledNumericValue(); private ScaledNumericValue spawnHeightValue = new ScaledNumericValue(); private SpawnShapeValue spawnShapeValue = new SpawnShapeValue(); private float accumulator; private Sprite sprite; private Particle[] particles; private int minParticleCount, maxParticleCount = 4; private float x, y; private String name; private String imagePath; private int activeCount; private boolean[] active; private boolean firstUpdate; private boolean flipX, flipY; private int updateFlags; private boolean allowCompletion; private int emission, emissionDiff, emissionDelta; private int lifeOffset, lifeOffsetDiff; private int life, lifeDiff; private float spawnWidth, spawnWidthDiff; private float spawnHeight, spawnHeightDiff; public float duration = 1, durationTimer; private float delay, delayTimer; private boolean attached; private boolean continuous; private boolean aligned; private boolean behind; private boolean additive = true; public ParticleEmitter() { initialize(); } public ParticleEmitter(BufferedReader reader) throws IOException { initialize(); load(reader); } public ParticleEmitter(ParticleEmitter emitter) { sprite = emitter.sprite; name = emitter.name; setMaxParticleCount(emitter.maxParticleCount); minParticleCount = emitter.minParticleCount; delayValue.load(emitter.delayValue); durationValue.load(emitter.durationValue); emissionValue.load(emitter.emissionValue); lifeValue.load(emitter.lifeValue); lifeOffsetValue.load(emitter.lifeOffsetValue); scaleValue.load(emitter.scaleValue); rotationValue.load(emitter.rotationValue); velocityValue.load(emitter.velocityValue); angleValue.load(emitter.angleValue); windValue.load(emitter.windValue); gravityValue.load(emitter.gravityValue); transparencyValue.load(emitter.transparencyValue); tintValue.load(emitter.tintValue); xOffsetValue.load(emitter.xOffsetValue); yOffsetValue.load(emitter.yOffsetValue); spawnWidthValue.load(emitter.spawnWidthValue); spawnHeightValue.load(emitter.spawnHeightValue); spawnShapeValue.load(emitter.spawnShapeValue); attached = emitter.attached; continuous = emitter.continuous; aligned = emitter.aligned; behind = emitter.behind; additive = emitter.additive; } private void initialize() { durationValue.setAlwaysActive(true); emissionValue.setAlwaysActive(true); lifeValue.setAlwaysActive(true); scaleValue.setAlwaysActive(true); transparencyValue.setAlwaysActive(true); spawnShapeValue.setAlwaysActive(true); spawnWidthValue.setAlwaysActive(true); spawnHeightValue.setAlwaysActive(true); } public void setMaxParticleCount(int maxParticleCount) { this.maxParticleCount = maxParticleCount; active = new boolean[maxParticleCount]; activeCount = 0; particles = new Particle[maxParticleCount]; } public void addParticle() { int activeCount = this.activeCount; if (activeCount == maxParticleCount) return; boolean[] active = this.active; for (int i = 0, n = active.length; i < n; i++) { if (!active[i]) { activateParticle(i); active[i] = true; this.activeCount = activeCount + 1; break; } } } public void addParticles(int count) { count = Math.min(count, maxParticleCount - activeCount); if (count == 0) return; boolean[] active = this.active; int index = 0, n = active.length; outer: for (int i = 0; i < count; i++) { for (; index < n; index++) { if (!active[index]) { activateParticle(index); active[index++] = true; continue outer; } } break; } this.activeCount += count; } public void update(float delta) { accumulator += Math.min(delta * 1000, 250); if (accumulator < 1) return; int deltaMillis = (int) accumulator; accumulator -= deltaMillis; boolean[] active = this.active; int activeCount = this.activeCount; for (int i = 0, n = active.length; i < n; i++) { if (active[i] && !updateParticle(particles[i], delta, deltaMillis)) { active[i] = false; activeCount--; } } this.activeCount = activeCount; if (delayTimer < delay) { delayTimer += deltaMillis; return; } if (firstUpdate) { firstUpdate = false; addParticle(); } if (durationTimer < duration) durationTimer += deltaMillis; else { if (!continuous || allowCompletion) return; restart(); } emissionDelta += deltaMillis; float emissionTime = emission + emissionDiff * emissionValue.getScale(durationTimer / (float) duration); if (emissionTime > 0) { emissionTime = 1000 / emissionTime; if (emissionDelta >= emissionTime) { int emitCount = (int) (emissionDelta / emissionTime); emitCount = Math.min(emitCount, maxParticleCount - activeCount); emissionDelta -= emitCount * emissionTime; emissionDelta %= emissionTime; addParticles(emitCount); } } if (activeCount < minParticleCount) addParticles(minParticleCount - activeCount); } public void draw(SpriteBatch spriteBatch) { if (additive) spriteBatch.setBlendFunction(GL10.GL_SRC_ALPHA, GL10.GL_ONE); Particle[] particles = this.particles; boolean[] active = this.active; int activeCount = this.activeCount; for (int i = 0, n = active.length; i < n; i++) if (active[i]) particles[i].draw(spriteBatch); this.activeCount = activeCount; if (additive) spriteBatch.setBlendFunction(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); } /** * Updates and draws the particles. This is slightly more efficient than calling {@link #update(float)} and * {@link #draw(SpriteBatch)} separately. */ public void draw(SpriteBatch spriteBatch, float delta) { accumulator += Math.min(delta * 1000, 250); if (accumulator < 1) { draw(spriteBatch); return; } int deltaMillis = (int) accumulator; accumulator -= deltaMillis; if (additive) spriteBatch.setBlendFunction(GL10.GL_SRC_ALPHA, GL10.GL_ONE); Particle[] particles = this.particles; boolean[] active = this.active; int activeCount = this.activeCount; for (int i = 0, n = active.length; i < n; i++) { if (active[i]) { Particle particle = particles[i]; if (updateParticle(particle, delta, deltaMillis)) particle.draw(spriteBatch); else { active[i] = false; activeCount--; } } } this.activeCount = activeCount; if (additive) spriteBatch.setBlendFunction(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); if (delayTimer < delay) { delayTimer += deltaMillis; return; } if (firstUpdate) { firstUpdate = false; addParticle(); } if (durationTimer < duration) durationTimer += deltaMillis; else { if (!continuous || allowCompletion) return; restart(); } emissionDelta += deltaMillis; float emissionTime = emission + emissionDiff * emissionValue.getScale(durationTimer / (float) duration); if (emissionTime > 0) { emissionTime = 1000 / emissionTime; if (emissionDelta >= emissionTime) { int emitCount = (int) (emissionDelta / emissionTime); emitCount = Math.min(emitCount, maxParticleCount - activeCount); emissionDelta -= emitCount * emissionTime; emissionDelta %= emissionTime; addParticles(emitCount); } } if (activeCount < minParticleCount) addParticles(minParticleCount - activeCount); } public void start() { firstUpdate = true; allowCompletion = false; restart(); } public void reset() { emissionDelta = 0; durationTimer = duration; start(); } private void restart() { delay = delayValue.active ? delayValue.newLowValue() : 0; delayTimer = 0; durationTimer -= duration; duration = durationValue.newLowValue(); emission = (int) emissionValue.newLowValue(); emissionDiff = (int) emissionValue.newHighValue(); if (!emissionValue.isRelative()) emissionDiff -= emission; life = (int) lifeValue.newLowValue(); lifeDiff = (int) lifeValue.newHighValue(); if (!lifeValue.isRelative()) lifeDiff -= life; lifeOffset = lifeOffsetValue.active ? (int) lifeOffsetValue.newLowValue() : 0; lifeOffsetDiff = (int) lifeOffsetValue.newHighValue(); if (!lifeOffsetValue.isRelative()) lifeOffsetDiff -= lifeOffset; spawnWidth = spawnWidthValue.newLowValue(); spawnWidthDiff = spawnWidthValue.newHighValue(); if (!spawnWidthValue.isRelative()) spawnWidthDiff -= spawnWidth; spawnHeight = spawnHeightValue.newLowValue(); spawnHeightDiff = spawnHeightValue.newHighValue(); if (!spawnHeightValue.isRelative()) spawnHeightDiff -= spawnHeight; updateFlags = 0; if (angleValue.active && angleValue.timeline.length > 1) updateFlags |= UPDATE_ANGLE; if (velocityValue.active && velocityValue.active) updateFlags |= UPDATE_VELOCITY; if (scaleValue.timeline.length > 1) updateFlags |= UPDATE_SCALE; if (rotationValue.active && rotationValue.timeline.length > 1) updateFlags |= UPDATE_ROTATION; if (windValue.active) updateFlags |= UPDATE_WIND; if (gravityValue.active) updateFlags |= UPDATE_GRAVITY; if (tintValue.timeline.length > 1) updateFlags |= UPDATE_TINT; } protected Particle newParticle(Sprite sprite) { return new Particle(sprite); } private void activateParticle(int index) { Particle particle = particles[index]; if (particle == null) { particles[index] = particle = newParticle(sprite); particle.flip(flipX, flipY); } float percent = durationTimer / (float) duration; int updateFlags = this.updateFlags; particle.currentLife = particle.life = life + (int) (lifeDiff * lifeValue.getScale(percent)); if (velocityValue.active) { particle.velocity = velocityValue.newLowValue(); particle.velocityDiff = velocityValue.newHighValue(); if (!velocityValue.isRelative()) particle.velocityDiff -= particle.velocity; } particle.angle = angleValue.newLowValue(); particle.angleDiff = angleValue.newHighValue(); if (!angleValue.isRelative()) particle.angleDiff -= particle.angle; float angle = 0; if ((updateFlags & UPDATE_ANGLE) == 0) { angle = particle.angle + particle.angleDiff * angleValue.getScale(0); particle.angle = angle; particle.angleCos = MathUtils.cosDeg(angle); particle.angleSin = MathUtils.sinDeg(angle); } float spriteWidth = sprite.getWidth(); particle.scale = scaleValue.newLowValue() / spriteWidth; particle.scaleDiff = scaleValue.newHighValue() / spriteWidth; if (!scaleValue.isRelative()) particle.scaleDiff -= particle.scale; particle.setScale(particle.scale + particle.scaleDiff * scaleValue.getScale(0)); if (rotationValue.active) { particle.rotation = rotationValue.newLowValue(); particle.rotationDiff = rotationValue.newHighValue(); if (!rotationValue.isRelative()) particle.rotationDiff -= particle.rotation; float rotation = particle.rotation + particle.rotationDiff * rotationValue.getScale(0); if (aligned) rotation += angle; particle.setRotation(rotation); } if (windValue.active) { particle.wind = windValue.newLowValue(); particle.windDiff = windValue.newHighValue(); if (!windValue.isRelative()) particle.windDiff -= particle.wind; } if (gravityValue.active) { particle.gravity = gravityValue.newLowValue(); particle.gravityDiff = gravityValue.newHighValue(); if (!gravityValue.isRelative()) particle.gravityDiff -= particle.gravity; } float[] color = particle.tint; if (color == null) particle.tint = color = new float[3]; float[] temp = tintValue.getColor(0); color[0] = temp[0]; color[1] = temp[1]; color[2] = temp[2]; particle.transparency = transparencyValue.newLowValue(); particle.transparencyDiff = transparencyValue.newHighValue() - particle.transparency; // Spawn. float x = this.x; if (xOffsetValue.active) x += xOffsetValue.newLowValue(); float y = this.y; if (yOffsetValue.active) y += yOffsetValue.newLowValue(); switch (spawnShapeValue.shape) { case square: { float width = spawnWidth + (spawnWidthDiff * spawnWidthValue.getScale(percent)); float height = spawnHeight + (spawnHeightDiff * spawnHeightValue.getScale(percent)); x += MathUtils.random(width) - width / 2; y += MathUtils.random(height) - height / 2; break; } case ellipse: { float width = spawnWidth + (spawnWidthDiff * spawnWidthValue.getScale(percent)); float height = spawnHeight + (spawnHeightDiff * spawnHeightValue.getScale(percent)); float radiusX = width / 2; float radiusY = height / 2; if (radiusX == 0 || radiusY == 0) break; float scaleY = radiusX / (float) radiusY; if (spawnShapeValue.edges) { float spawnAngle; switch (spawnShapeValue.side) { case top: spawnAngle = -MathUtils.random(179f); break; case bottom: spawnAngle = MathUtils.random(179f); break; default: spawnAngle = MathUtils.random(360f); break; } float cosDeg = MathUtils.cosDeg(spawnAngle); float sinDeg = MathUtils.sinDeg(spawnAngle); x += cosDeg * radiusX; y += sinDeg * radiusX / scaleY; if ((updateFlags & UPDATE_ANGLE) == 0) { particle.angle = spawnAngle; particle.angleCos = cosDeg; particle.angleSin = sinDeg; } } else { float radius2 = radiusX * radiusX; while (true) { float px = MathUtils.random(width) - radiusX; float py = MathUtils.random(width) - radiusX; if (px * px + py * py <= radius2) { x += px; y += py / scaleY; break; } } } break; } case line: { float width = spawnWidth + (spawnWidthDiff * spawnWidthValue.getScale(percent)); float height = spawnHeight + (spawnHeightDiff * spawnHeightValue.getScale(percent)); if (width != 0) { float lineX = width * MathUtils.random(); x += lineX; y += lineX * (height / (float) width); } else y += height * MathUtils.random(); break; } } float spriteHeight = sprite.getHeight(); particle.setBounds(x - spriteWidth / 2, y - spriteHeight / 2, spriteWidth, spriteHeight); int offsetTime = (int) (lifeOffset + lifeOffsetDiff * lifeOffsetValue.getScale(percent)); if (offsetTime > 0) { if (offsetTime >= particle.currentLife) offsetTime = particle.currentLife - 1; updateParticle(particle, offsetTime / 1000f, offsetTime); } } private boolean updateParticle(Particle particle, float delta, int deltaMillis) { int life = particle.currentLife - deltaMillis; if (life <= 0) return false; particle.currentLife = life; float percent = 1 - particle.currentLife / (float) particle.life; int updateFlags = this.updateFlags; if ((updateFlags & UPDATE_SCALE) != 0) particle.setScale(particle.scale + particle.scaleDiff * scaleValue.getScale(percent)); if ((updateFlags & UPDATE_VELOCITY) != 0) { float velocity = (particle.velocity + particle.velocityDiff * velocityValue.getScale(percent)) * delta; float velocityX, velocityY; if ((updateFlags & UPDATE_ANGLE) != 0) { float angle = particle.angle + particle.angleDiff * angleValue.getScale(percent); velocityX = velocity * MathUtils.cosDeg(angle); velocityY = velocity * MathUtils.sinDeg(angle); if ((updateFlags & UPDATE_ROTATION) != 0) { float rotation = particle.rotation + particle.rotationDiff * rotationValue.getScale(percent); if (aligned) rotation += angle; particle.setRotation(rotation); } } else { velocityX = velocity * particle.angleCos; velocityY = velocity * particle.angleSin; if (aligned || (updateFlags & UPDATE_ROTATION) != 0) { float rotation = particle.rotation + particle.rotationDiff * rotationValue.getScale(percent); if (aligned) rotation += particle.angle; particle.setRotation(rotation); } } if ((updateFlags & UPDATE_WIND) != 0) velocityX += (particle.wind + particle.windDiff * windValue.getScale(percent)) * delta; if ((updateFlags & UPDATE_GRAVITY) != 0) velocityY += (particle.gravity + particle.gravityDiff * gravityValue.getScale(percent)) * delta; particle.translate(velocityX, velocityY); } else { if ((updateFlags & UPDATE_ROTATION) != 0) particle.setRotation(particle.rotation + particle.rotationDiff * rotationValue.getScale(percent)); } float[] color; if ((updateFlags & UPDATE_TINT) != 0) color = tintValue.getColor(percent); else color = particle.tint; particle.setColor(color[0], color[1], color[2], particle.transparency + particle.transparencyDiff * transparencyValue.getScale(percent)); return true; } public void setPosition(float x, float y) { if (attached) { float xAmount = x - this.x; float yAmount = y - this.y; boolean[] active = this.active; for (int i = 0, n = active.length; i < n; i++) if (active[i]) particles[i].translate(xAmount, yAmount); } this.x = x; this.y = y; } public void setSprite(Sprite sprite) { this.sprite = sprite; if (sprite == null) return; float originX = sprite.getOriginX(); float originY = sprite.getOriginY(); Texture texture = sprite.getTexture(); for (int i = 0, n = particles.length; i < n; i++) { Particle particle = particles[i]; if (particle == null) break; particle.setTexture(texture); particle.setOrigin(originX, originY); } } /** * Ignores the {@link #setContinuous(boolean) continuous} setting until the emitter is started again. This allows * the emitter to stop smoothly. */ public void allowCompletion() { allowCompletion = true; durationTimer = duration; } public Sprite getSprite() { return sprite; } public String getName() { return name; } public void setName(String name) { this.name = name; } public ScaledNumericValue getLife() { return lifeValue; } public ScaledNumericValue getScale() { return scaleValue; } public ScaledNumericValue getRotation() { return rotationValue; } public GradientColorValue getTint() { return tintValue; } public ScaledNumericValue getVelocity() { return velocityValue; } public ScaledNumericValue getWind() { return windValue; } public ScaledNumericValue getGravity() { return gravityValue; } public ScaledNumericValue getAngle() { return angleValue; } public ScaledNumericValue getEmission() { return emissionValue; } public ScaledNumericValue getTransparency() { return transparencyValue; } public RangedNumericValue getDuration() { return durationValue; } public RangedNumericValue getDelay() { return delayValue; } public ScaledNumericValue getLifeOffset() { return lifeOffsetValue; } public RangedNumericValue getXOffsetValue() { return xOffsetValue; } public RangedNumericValue getYOffsetValue() { return yOffsetValue; } public ScaledNumericValue getSpawnWidth() { return spawnWidthValue; } public ScaledNumericValue getSpawnHeight() { return spawnHeightValue; } public SpawnShapeValue getSpawnShape() { return spawnShapeValue; } public boolean isAttached() { return attached; } public void setAttached(boolean attached) { this.attached = attached; } public boolean isContinuous() { return continuous; } public void setContinuous(boolean continuous) { this.continuous = continuous; } public boolean isAligned() { return aligned; } public void setAligned(boolean aligned) { this.aligned = aligned; } public boolean isAdditive() { return additive; } public void setAdditive(boolean additive) { this.additive = additive; } public boolean isBehind() { return behind; } public void setBehind(boolean behind) { this.behind = behind; } public int getMinParticleCount() { return minParticleCount; } public void setMinParticleCount(int minParticleCount) { this.minParticleCount = minParticleCount; } public int getMaxParticleCount() { return maxParticleCount; } public boolean isComplete() { if (delayTimer < delay) return false; return durationTimer >= duration && activeCount == 0; } public float getPercentComplete() { if (delayTimer < delay) return 0; return Math.min(1, durationTimer / (float) duration); } public float getX() { return x; } public float getY() { return y; } public int getActiveCount() { return activeCount; } public String getImagePath() { return imagePath; } public void setImagePath(String imagePath) { this.imagePath = imagePath; } public void setFlip(boolean flipX, boolean flipY) { this.flipX = flipX; this.flipY = flipY; if (particles == null) return; for (int i = 0, n = particles.length; i < n; i++) { Particle particle = particles[i]; if (particle != null) particle.flip(flipX, flipY); } } public void flipY() { angleValue.setHigh(-angleValue.getHighMin(), -angleValue.getHighMax()); angleValue.setLow(-angleValue.getLowMin(), -angleValue.getLowMax()); gravityValue.setHigh(-gravityValue.getHighMin(), -gravityValue.getHighMax()); gravityValue.setLow(-gravityValue.getLowMin(), -gravityValue.getLowMax()); windValue.setHigh(-windValue.getHighMin(), -windValue.getHighMax()); windValue.setLow(-windValue.getLowMin(), -windValue.getLowMax()); rotationValue.setHigh(-rotationValue.getHighMin(), -rotationValue.getHighMax()); rotationValue.setLow(-rotationValue.getLowMin(), -rotationValue.getLowMax()); yOffsetValue.setLow(-yOffsetValue.getLowMin(), -yOffsetValue.getLowMax()); } public void save(Writer output) throws IOException { output.write(name + "\n"); output.write("- Delay -\n"); delayValue.save(output); output.write("- Duration - \n"); durationValue.save(output); output.write("- Count - \n"); output.write("min: " + minParticleCount + "\n"); output.write("max: " + maxParticleCount + "\n"); output.write("- Emission - \n"); emissionValue.save(output); output.write("- Life - \n"); lifeValue.save(output); output.write("- Life Offset - \n"); lifeOffsetValue.save(output); output.write("- X Offset - \n"); xOffsetValue.save(output); output.write("- Y Offset - \n"); yOffsetValue.save(output); output.write("- Spawn Shape - \n"); spawnShapeValue.save(output); output.write("- Spawn Width - \n"); spawnWidthValue.save(output); output.write("- Spawn Height - \n"); spawnHeightValue.save(output); output.write("- Scale - \n"); scaleValue.save(output); output.write("- Velocity - \n"); velocityValue.save(output); output.write("- Angle - \n"); angleValue.save(output); output.write("- Rotation - \n"); rotationValue.save(output); output.write("- Wind - \n"); windValue.save(output); output.write("- Gravity - \n"); gravityValue.save(output); output.write("- Tint - \n"); tintValue.save(output); output.write("- Transparency - \n"); transparencyValue.save(output); output.write("- Options - \n"); output.write("attached: " + attached + "\n"); output.write("continuous: " + continuous + "\n"); output.write("aligned: " + aligned + "\n"); output.write("additive: " + additive + "\n"); output.write("behind: " + behind + "\n"); } public void load(BufferedReader reader) throws IOException { try { name = readString(reader, "name"); reader.readLine(); delayValue.load(reader); reader.readLine(); durationValue.load(reader); reader.readLine(); setMinParticleCount(readInt(reader, "minParticleCount")); setMaxParticleCount(readInt(reader, "maxParticleCount")); reader.readLine(); emissionValue.load(reader); reader.readLine(); lifeValue.load(reader); reader.readLine(); lifeOffsetValue.load(reader); reader.readLine(); xOffsetValue.load(reader); reader.readLine(); yOffsetValue.load(reader); reader.readLine(); spawnShapeValue.load(reader); reader.readLine(); spawnWidthValue.load(reader); reader.readLine(); spawnHeightValue.load(reader); reader.readLine(); scaleValue.load(reader); reader.readLine(); velocityValue.load(reader); reader.readLine(); angleValue.load(reader); reader.readLine(); rotationValue.load(reader); reader.readLine(); windValue.load(reader); reader.readLine(); gravityValue.load(reader); reader.readLine(); tintValue.load(reader); reader.readLine(); transparencyValue.load(reader); reader.readLine(); attached = readBoolean(reader, "attached"); continuous = readBoolean(reader, "continuous"); aligned = readBoolean(reader, "aligned"); additive = readBoolean(reader, "additive"); behind = readBoolean(reader, "behind"); } catch (RuntimeException ex) { if (name == null) throw ex; throw new RuntimeException("Error parsing emitter: " + name, ex); } } static String readString(BufferedReader reader, String name) throws IOException { String line = reader.readLine(); if (line == null) throw new IOException("Missing value: " + name); return line.substring(line.indexOf(":") + 1).trim(); } static boolean readBoolean(BufferedReader reader, String name) throws IOException { return Boolean.parseBoolean(readString(reader, name)); } static int readInt(BufferedReader reader, String name) throws IOException { return Integer.parseInt(readString(reader, name)); } static float readFloat(BufferedReader reader, String name) throws IOException { return Float.parseFloat(readString(reader, name)); } static class Particle extends Sprite { int life, currentLife; float scale, scaleDiff; float rotation, rotationDiff; float velocity, velocityDiff; float angle, angleDiff; float angleCos, angleSin; float transparency, transparencyDiff; float wind, windDiff; float gravity, gravityDiff; float[] tint; public Particle(Sprite sprite) { super(sprite); } } static public class ParticleValue { boolean active; boolean alwaysActive; public void setAlwaysActive(boolean alwaysActive) { this.alwaysActive = alwaysActive; } public boolean isAlwaysActive() { return alwaysActive; } public boolean isActive() { return alwaysActive || active; } public void setActive(boolean active) { this.active = active; } public void save(Writer output) throws IOException { if (!alwaysActive) output.write("active: " + active + "\n"); else active = true; } public void load(BufferedReader reader) throws IOException { if (!alwaysActive) active = readBoolean(reader, "active"); else active = true; } public void load(ParticleValue value) { active = value.active; alwaysActive = value.alwaysActive; } } static public class NumericValue extends ParticleValue { private float value; public float getValue() { return value; } public void setValue(float value) { this.value = value; } public void save(Writer output) throws IOException { super.save(output); if (!active) return; output.write("value: " + value + "\n"); } public void load(BufferedReader reader) throws IOException { super.load(reader); if (!active) return; value = readFloat(reader, "value"); } public void load(NumericValue value) { super.load(value); this.value = value.value; } } static public class RangedNumericValue extends ParticleValue { private float lowMin, lowMax; public float newLowValue() { return lowMin + (lowMax - lowMin) * MathUtils.random(); } public void setLow(float value) { lowMin = value; lowMax = value; } public void setLow(float min, float max) { lowMin = min; lowMax = max; } public float getLowMin() { return lowMin; } public void setLowMin(float lowMin) { this.lowMin = lowMin; } public float getLowMax() { return lowMax; } public void setLowMax(float lowMax) { this.lowMax = lowMax; } public void save(Writer output) throws IOException { super.save(output); if (!active) return; output.write("lowMin: " + lowMin + "\n"); output.write("lowMax: " + lowMax + "\n"); } public void load(BufferedReader reader) throws IOException { super.load(reader); if (!active) return; lowMin = readFloat(reader, "lowMin"); lowMax = readFloat(reader, "lowMax"); } public void load(RangedNumericValue value) { super.load(value); lowMax = value.lowMax; lowMin = value.lowMin; } } static public class ScaledNumericValue extends RangedNumericValue { private float[] scaling = { 1 }; float[] timeline = { 0 }; private float highMin, highMax; private boolean relative; public float newHighValue() { return highMin + (highMax - highMin) * MathUtils.random(); } public void setHigh(float value) { highMin = value; highMax = value; } public void setHigh(float min, float max) { highMin = min; highMax = max; } public float getHighMin() { return highMin; } public void setHighMin(float highMin) { this.highMin = highMin; } public float getHighMax() { return highMax; } public void setHighMax(float highMax) { this.highMax = highMax; } public float[] getScaling() { return scaling; } public void setScaling(float[] values) { this.scaling = values; } public float[] getTimeline() { return timeline; } public void setTimeline(float[] timeline) { this.timeline = timeline; } public boolean isRelative() { return relative; } public void setRelative(boolean relative) { this.relative = relative; } public float getScale(float percent) { int endIndex = -1; float[] timeline = this.timeline; int n = timeline.length; for (int i = 1; i < n; i++) { float t = timeline[i]; if (t > percent) { endIndex = i; break; } } if (endIndex == -1) return scaling[n - 1]; float[] scaling = this.scaling; int startIndex = endIndex - 1; float startValue = scaling[startIndex]; float startTime = timeline[startIndex]; return startValue + (scaling[endIndex] - startValue) * ((percent - startTime) / (timeline[endIndex] - startTime)); } public void save(Writer output) throws IOException { super.save(output); if (!active) return; output.write("highMin: " + highMin + "\n"); output.write("highMax: " + highMax + "\n"); output.write("relative: " + relative + "\n"); output.write("scalingCount: " + scaling.length + "\n"); for (int i = 0; i < scaling.length; i++) output.write("scaling" + i + ": " + scaling[i] + "\n"); output.write("timelineCount: " + timeline.length + "\n"); for (int i = 0; i < timeline.length; i++) output.write("timeline" + i + ": " + timeline[i] + "\n"); } public void load(BufferedReader reader) throws IOException { super.load(reader); if (!active) return; highMin = readFloat(reader, "highMin"); highMax = readFloat(reader, "highMax"); relative = readBoolean(reader, "relative"); scaling = new float[readInt(reader, "scalingCount")]; for (int i = 0; i < scaling.length; i++) scaling[i] = readFloat(reader, "scaling" + i); timeline = new float[readInt(reader, "timelineCount")]; for (int i = 0; i < timeline.length; i++) timeline[i] = readFloat(reader, "timeline" + i); } public void load(ScaledNumericValue value) { super.load(value); highMax = value.highMax; highMin = value.highMin; scaling = new float[value.scaling.length]; System.arraycopy(value.scaling, 0, scaling, 0, scaling.length); timeline = new float[value.timeline.length]; System.arraycopy(value.timeline, 0, timeline, 0, timeline.length); relative = value.relative; } } static public class GradientColorValue extends ParticleValue { static private float[] temp = new float[4]; private float[] colors = { 1, 1, 1 }; float[] timeline = { 0 }; public GradientColorValue() { alwaysActive = true; } public float[] getTimeline() { return timeline; } public void setTimeline(float[] timeline) { this.timeline = timeline; } public float[] getColors() { return colors; } public void setColors(float[] colors) { this.colors = colors; } public float[] getColor(float percent) { int startIndex = 0, endIndex = -1; float[] timeline = this.timeline; int n = timeline.length; for (int i = 1; i < n; i++) { float t = timeline[i]; if (t > percent) { endIndex = i; break; } startIndex = i; } float startTime = timeline[startIndex]; startIndex *= 3; float r1 = colors[startIndex]; float g1 = colors[startIndex + 1]; float b1 = colors[startIndex + 2]; if (endIndex == -1) { temp[0] = r1; temp[1] = g1; temp[2] = b1; return temp; } float factor = (percent - startTime) / (timeline[endIndex] - startTime); endIndex *= 3; temp[0] = r1 + (colors[endIndex] - r1) * factor; temp[1] = g1 + (colors[endIndex + 1] - g1) * factor; temp[2] = b1 + (colors[endIndex + 2] - b1) * factor; return temp; } public void save(Writer output) throws IOException { super.save(output); if (!active) return; output.write("colorsCount: " + colors.length + "\n"); for (int i = 0; i < colors.length; i++) output.write("colors" + i + ": " + colors[i] + "\n"); output.write("timelineCount: " + timeline.length + "\n"); for (int i = 0; i < timeline.length; i++) output.write("timeline" + i + ": " + timeline[i] + "\n"); } public void load(BufferedReader reader) throws IOException { super.load(reader); if (!active) return; colors = new float[readInt(reader, "colorsCount")]; for (int i = 0; i < colors.length; i++) colors[i] = readFloat(reader, "colors" + i); timeline = new float[readInt(reader, "timelineCount")]; for (int i = 0; i < timeline.length; i++) timeline[i] = readFloat(reader, "timeline" + i); } public void load(GradientColorValue value) { super.load(value); colors = new float[value.colors.length]; System.arraycopy(value.colors, 0, colors, 0, colors.length); timeline = new float[value.timeline.length]; System.arraycopy(value.timeline, 0, timeline, 0, timeline.length); } } static public class SpawnShapeValue extends ParticleValue { SpawnShape shape = SpawnShape.point; boolean edges; SpawnEllipseSide side = SpawnEllipseSide.both; public SpawnShape getShape() { return shape; } public void setShape(SpawnShape shape) { this.shape = shape; } public boolean isEdges() { return edges; } public void setEdges(boolean edges) { this.edges = edges; } public SpawnEllipseSide getSide() { return side; } public void setSide(SpawnEllipseSide side) { this.side = side; } public void save(Writer output) throws IOException { super.save(output); if (!active) return; output.write("shape: " + shape + "\n"); if (shape == SpawnShape.ellipse) { output.write("edges: " + edges + "\n"); output.write("side: " + side + "\n"); } } public void load(BufferedReader reader) throws IOException { super.load(reader); if (!active) return; shape = SpawnShape.valueOf(readString(reader, "shape")); if (shape == SpawnShape.ellipse) { edges = readBoolean(reader, "edges"); side = SpawnEllipseSide.valueOf(readString(reader, "side")); } } public void load(SpawnShapeValue value) { super.load(value); shape = value.shape; edges = value.edges; side = value.side; } } static public enum SpawnShape { point, line, square, ellipse } static public enum SpawnEllipseSide { both, top, bottom } }