/* * Copyright (c) 2003 Shaven Puppy Ltd * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of 'Shaven Puppy' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package net.puppygames.applet.effects; import net.puppygames.applet.Game; import net.puppygames.applet.Screen; import org.lwjgl.util.Color; import org.lwjgl.util.ReadableColor; import org.lwjgl.util.ReadablePoint; import com.shavenpuppy.jglib.interpolators.LinearInterpolator; import com.shavenpuppy.jglib.openal.ALBuffer; import com.shavenpuppy.jglib.resources.Data; import com.shavenpuppy.jglib.resources.Feature; import com.shavenpuppy.jglib.resources.Range; import com.shavenpuppy.jglib.sound.SoundEffect; import com.shavenpuppy.jglib.sprites.Appearance; /** * $Id: EmitterFeature.java,v 1.11 2010/08/03 20:44:07 foo Exp $ * The Emitter feature describes a particle effect that is either instantaneous * or lasts an amount of time. * @author $Author: foo $ * @version $Revision: 1.11 $ */ public class EmitterFeature extends Feature { private static final long serialVersionUID = 1L; private static final int MARGIN = 48; /* * Resource data */ /** Debug tag */ @Data private String tag; /** The duration of the emitter. If null then the emitter is removed after 1 emission */ private Range duration; /** Infinite: continue emitting until removed */ private boolean infinite; /** The number of particles to emit per tick */ private Range particlesPerTick; /** Maximum particles to emit before puttering out. */ private int maxParticles; /** Appearance */ private String appearance; /** Particle layer */ private int layer, subLayer; /** Chained emitter */ private String next; /** Velocity */ private Range velocity; /** Acceleration */ private Range acceleration; /** Spawning radius */ private Range radius; /** Initial scale */ private Range startScale; /** Scale at end of particle duration */ private Range scale; /** Final scale at end of fade */ private Range endScale; /** Gravity */ private Range gravityX, gravityY; /** Duration */ private Range particleDuration; /** Fade */ private Range fadeDuration; /** Start hue range */ private Range startHue; /** Saturation */ private Range startSaturation; /** Brightness */ private Range startBrightness; /** End hue range */ private Range endHue; /** Saturation */ private Range endSaturation; /** Brightness */ private Range endBrightness; /** Angle range */ private Range angle; /** Sound effect */ private String sound; /** Start / end pitch */ private float startPitch, endPitch; /** Start / end volume */ private float startVolume, endVolume; /** Embedded next */ private EmitterFeature chain; /** Slave emitter attached to particles */ private EmitterFeature slave; /** Optional coordinates to spawn at */ private float x, y; /** Y offset */ private float yOffset; /** Delay before we start emitting */ private Range delay; /** Delay after we finish emitting, in a loop */ private Range delayAfter; /** Whether particles should be rotated according to their emitted angle */ private boolean rotate; /** Whether particles should rotate according to their own angle as well */ private boolean relativeRotate; /** Interpolation: number of pixels */ private int interpolation; /** Scale all values */ private Range emitterScale; /** Don't attenuate sounds */ private boolean dontAttenuate; /** Don't move Y coordinate; just use sprite offset instead */ private boolean doYOffset; /** Force emission even if offscreen */ private boolean forceEmit; /* * Transient data */ private transient EmitterFeature nextFeature; private transient Appearance appearanceResource; private transient ALBuffer soundResource; /** * An Emitter effect */ public class EmitterInstance extends Emitter { private boolean delayed; private int tick; private int actualDuration; private int actualDelay; private float ix, iy; // initial location private float iyo; // Initial Y offset private final Screen screen; private final EmitterInstance parent; private EmitterInstance chainInstance; private boolean finished, done; private SoundEffect soundEffect; private boolean soundWasLooped; private float oldx, oldy, oldYoffset; private boolean doInterpolate; private boolean delayingAfter; private float instanceScale; private int numParticles; private EmitterInstance(EmitterInstance parent, Screen screen) { this.screen = screen; this.parent = parent; if (parent != null) { setLocation(parent.ix + EmitterFeature.this.x, parent.iy + EmitterFeature.this.y); setYOffset(parent.iyo + EmitterFeature.this.yOffset); } else { setLocation(EmitterFeature.this.x, EmitterFeature.this.y); setYOffset(EmitterFeature.this.yOffset); } } @Override protected void doSpawnEffect() { initEmitter(); } private EmitterFeature getFeature() { return EmitterFeature.this; } @Override public void setYOffset(float yOffset) { this.iyo = yOffset; if (chainInstance != null) { chainInstance.setYOffset(yOffset + chainInstance.getFeature().yOffset); } } @Override protected void playSound(ALBuffer sound) { // Override to stop sounds playing here. } @Override public String getTag() { return tag; } private void initEmitter() { if (delay == null) { actualDelay = 0; if (soundResource != null) { if (soundEffect == null || !soundWasLooped) { if (getAttenuator() != null && !dontAttenuate) { soundEffect = Game.allocateSound(soundResource, getAttenuator().getVolume(ix, iy), 1.0f, this); } else { soundEffect = Game.allocateSound(soundResource, 1.0f, 1.0f, this); } soundWasLooped = soundResource.isLooped(); } } } else { actualDelay = (int) delay.getValue(); delayed = actualDelay > 0; } if (duration == null) { actualDuration = 0; } else { actualDuration = (int) duration.getValue(); } // Maybe spawn some chained emitters? if (nextFeature != null) { if (chainInstance != null) { chainInstance.remove(); chainInstance = null; } chainInstance = (EmitterInstance) nextFeature.spawn(this, screen); } if (chain != null) { if (chainInstance != null) { chainInstance.remove(); chainInstance = null; } chainInstance = (EmitterInstance) chain.spawn(this, screen); } // Choose scale at this point instanceScale = emitterScale == null ? 1.0f : emitterScale.getValue(); doInterpolate = false; numParticles = 0; } @Override public void setLocation(float x, float y) { this.ix = x; this.iy = y; if (chainInstance != null) { chainInstance.setLocation(x + chainInstance.getFeature().x, y + chainInstance.getFeature().y); } } @Override public void setOffset(ReadablePoint offset) { super.setOffset(offset); // Also set offset of chained emitter instances if any if (chainInstance != null) { chainInstance.setOffset(offset); } } @Override public void setGain(float gain) { this.gain = gain; if (soundEffect != null) { if (getAttenuator() != null && !dontAttenuate) { soundEffect.setGain(soundResource.getGain() * gain * Game.getSFXVolume() * getAttenuator().getVolume(ix, iy + iyo), this); } else { soundEffect.setGain(soundResource.getGain() * gain * Game.getSFXVolume(), this); } } } @Override protected void doUpdate() { // Update sound gain if (soundEffect == null || soundResource == null) { return; } if (getAttenuator() != null && !dontAttenuate) { soundEffect.setGain(soundResource.getGain() * gain * Game.getSFXVolume() * getAttenuator().getVolume(ix, iy + iyo), this); } } @Override protected void doTick() { if (finished) { return; } tick ++; float xx = ix, yy = iy, oyy = iyo; if (delayed) { if (tick <= actualDelay) { return; } delayed = false; tick = 0; if (delayingAfter) { delayingAfter = false; initEmitter(); } else if (soundResource != null) { if (soundEffect != null && soundWasLooped) { soundEffect.setLooped(false, this); soundEffect = null; } if (getAttenuator() != null && !dontAttenuate) { soundEffect = Game.allocateSound(soundResource, getAttenuator().getVolume(xx, yy + oyy), 1.0f, this); } else { soundEffect = Game.allocateSound(soundResource, 1.0f, 1.0f, this); } soundWasLooped = soundResource.isLooped(); } } if (getInterpolation() == 0 || !doInterpolate) { // Just emit at the new location emit(xx, yy, oyy); doInterpolate = true; } else { // Interpolate double dx = xx - oldx; double dy = yy - oldy; double doy = oyy - oldYoffset; double dist = Math.sqrt(dx * dx + dy * dy + doy * doy); int steps = (int) (dist / getInterpolation() + 0.5); for (int i = 1; i <= steps && (maxParticles == 0 || numParticles < maxParticles); i++) { float ratio = (float) i / (float) steps; float xxx = LinearInterpolator.instance.interpolate(oldx, xx, ratio); float yyy = LinearInterpolator.instance.interpolate(oldy, yy, ratio); float oyyy = LinearInterpolator.instance.interpolate(oldYoffset, oyy, ratio); emit(xxx, yyy, oyyy); } } // Remember last position oldx = xx; oldy = yy; oldYoffset = iyo; } private void emit(float xx, float yy, float oyy) { if (isVisible()) { int n = particlesPerTick == null ? 0 : (int) (particlesPerTick.getValue() + 0.5f); // If emitter is way outside screen bounds, emit nothing ReadablePoint offset = getOffset(); float offsetXpos, offsetYpos; if (offset != null) { offsetXpos = offset.getX() + xx; offsetYpos = offset.getY() + yy + oyy; } else { offsetXpos = xx; offsetYpos = yy + oyy; } if (!forceEmit && (offsetXpos < -MARGIN || offsetYpos < -MARGIN || offsetXpos > screen.getWidth() + MARGIN || offsetYpos > screen.getHeight() + MARGIN)) { numParticles += n; } else { for (int i = 0; i < n && (maxParticles == 0 || numParticles < maxParticles); i ++) { numParticles ++; Color startColor; if (startHue == null) { startColor = new Color(ReadableColor.WHITE); } else { startColor = new Color(); startColor.fromHSB( startHue.getValue(), startSaturation == null ? 1.0f : startSaturation.getValue(), startBrightness == null ? 1.0f : startBrightness.getValue()); } Color endColor; if (endHue == null) { endColor = startColor; } else { endColor = new Color(); endColor.fromHSB( endHue.getValue(), endSaturation == null ? 1.0f : endSaturation.getValue(), endBrightness == null ? 1.0f : endBrightness.getValue()); } float xxx = xx, yyy = yy, oyyy = oyy; if (radius != null) { float r = radius.getValue() * instanceScale; double randomRadiusAngle = Math.random() * 2.0 * Math.PI; xxx += r * Math.cos(randomRadiusAngle); if (doYOffset) { yyy = yy; oyyy += r * Math.sin(randomRadiusAngle); } else { yyy += r * Math.sin(randomRadiusAngle); } } Particle p = Particle.POOL.obtain ( this, xxx, yyy, oyyy, EmitterFeature.this.angle == null ? (float) Math.random() * 360.0f : EmitterFeature.this.angle.getValue() + angle, velocity == null ? 0.0f : velocity.getValue() * instanceScale, acceleration == null ? 0.0f : acceleration.getValue() * instanceScale, (int) particleDuration.getValue(), (int) fadeDuration.getValue(), startColor, endColor ); p.setAx(gravityX == null ? 0.0f : gravityX.getValue() * instanceScale); p.setAy(gravityY == null ? 0.0f : gravityY.getValue() * instanceScale); p.setAppearance(appearanceResource); p.setLayer(layer); p.setSubLayer(subLayer); p.setDoYOffset(doYOffset); p.setStartScale(startScale == null ? instanceScale : startScale.getValue() * instanceScale); p.setEndScale(endScale == null ? instanceScale : endScale.getValue() * instanceScale); if (EmitterFeature.this.scale != null) { p.setScale(EmitterFeature.this.scale.getValue() * instanceScale); } else { p.setScale(instanceScale); } if (ceilingSet) { p.setCeiling(ceiling); } if (floorSet) { p.setFloor(floor); } if (leftWallSet) { p.setLeftWall(leftWall); } if (rightWallSet) { p.setRightWall(rightWall); } if (rotate) { p.setRotation(p.getAngle()); } p.setRelativeRotation(relativeRotate); if (slave != null) { p.setForced(true); } p.spawn(screen); if (slave != null && isVisible()) { Emitter slaveEmitter = slave.spawn(screen); slaveEmitter.setOffset(getOffset()); p.setEmitter(slaveEmitter); slaveEmitter.update(); } } } } if (infinite) { if (actualDuration > 0 && tick >= actualDuration) { tick = 0; if (delayAfter != null) { actualDelay = (int) delayAfter.getValue(); delayed = actualDelay > 0; delayingAfter = delayed; } else { // Back to the beginning initEmitter(); } } } else { if (tick >= actualDuration) { finish(); } else if (soundEffect != null) { float ratio = (float) tick / (float) (actualDuration + 1); if (startVolume != endVolume) { soundEffect.setGain(LinearInterpolator.instance.interpolate(startVolume, endVolume, ratio), Game.class); } if (startPitch != endPitch) { soundEffect.setPitch(LinearInterpolator.instance.interpolate(startPitch, endPitch, ratio), Game.class); } } if (maxParticles > 0 && numParticles >= maxParticles) { finish(); } } oldx = xx; oldy = yy; oldYoffset = iyo; } @Override protected void doRemove() { done = true; if (soundEffect != null) { soundEffect.setLooped(false, this); } soundEffect = null; // Remove chained emitters if (chainInstance != null) { chainInstance.finish(); chainInstance = null; } } @Override public void finish() { // Stop emission and wait for sound effect to finish. If there's no sound effect, we're done if (finished) { return; } finished = true; if (soundEffect == null) { remove(); } else { soundEffect.setLooped(false, this); } } @Override public boolean isEffectActive() { return !done; } } /** * C'tor */ public EmitterFeature() { } /** * @param name */ public EmitterFeature(String name) { super(name); } /** * Spawn an Emitter effect. * @param screen * @return an Emitter */ public Emitter spawn(Screen screen) { Emitter ret = new EmitterInstance(null, screen); ret.spawn(screen); return ret; } /** * Spawn a chained Emitter effect. * @param screen * @return an Emitter */ private Emitter spawn(EmitterInstance parent, Screen screen) { Emitter ret = new EmitterInstance(parent, screen); ret.spawn(screen); return ret; } /** * @return Returns the acceleration. */ public Range getAcceleration() { return acceleration; } /** * @param acceleration The acceleration to set. */ public void setAcceleration(Range acceleration) { this.acceleration = acceleration; } /** * @return Returns the angle. */ public Range getAngle() { return angle; } /** * @param angle The angle to set. */ public void setAngle(Range angle) { this.angle = angle; } /** * @return Returns the appearance. */ public String getAppearance() { return appearance; } /** * @param appearance The appearance to set. */ public void setAppearance(String appearance) { this.appearance = appearance; } /** * @return Returns the chain. */ public EmitterFeature getChain() { return chain; } /** * @param chain The chain to set. */ public void setChain(EmitterFeature chain) { this.chain = chain; } /** * @return Returns the duration. */ public Range getDuration() { return duration; } /** * @param duration The duration to set. */ public void setDuration(Range duration) { this.duration = duration; } /** * @return Returns the endBrightness. */ public Range getEndBrightness() { return endBrightness; } /** * @param endBrightness The endBrightness to set. */ public void setEndBrightness(Range endBrightness) { this.endBrightness = endBrightness; } /** * @return Returns the endHue. */ public Range getEndHue() { return endHue; } /** * @param endHue The endHue to set. */ public void setEndHue(Range endHue) { this.endHue = endHue; } /** * @return Returns the endPitch. */ public float getEndPitch() { return endPitch; } /** * @param endPitch The endPitch to set. */ public void setEndPitch(float endPitch) { this.endPitch = endPitch; } /** * @return Returns the endSaturation. */ public Range getEndSaturation() { return endSaturation; } /** * @param endSaturation The endSaturation to set. */ public void setEndSaturation(Range endSaturation) { this.endSaturation = endSaturation; } /** * @return Returns the endScale. */ public Range getEndScale() { return endScale; } /** * @param endScale The endScale to set. */ public void setEndScale(Range endScale) { this.endScale = endScale; } /** * @return Returns the endVolume. */ public float getEndVolume() { return endVolume; } /** * @param endVolume The endVolume to set. */ public void setEndVolume(float endVolume) { this.endVolume = endVolume; } /** * @return Returns the fadeDuration. */ public Range getFadeDuration() { return fadeDuration; } /** * @param fadeDuration The fadeDuration to set. */ public void setFadeDuration(Range fadeDuration) { this.fadeDuration = fadeDuration; } /** * @return Returns the gravityX. */ public Range getGravityX() { return gravityX; } /** * @param gravityX The gravityX to set. */ public void setGravityX(Range gravityX) { this.gravityX = gravityX; } /** * @return Returns the gravityY. */ public Range getGravityY() { return gravityY; } /** * @param gravityY The gravityY to set. */ public void setGravityY(Range gravityY) { this.gravityY = gravityY; } /** * @return Returns the infinite. */ public boolean isInfinite() { return infinite; } /** * @param infinite The infinite to set. */ public void setInfinite(boolean infinite) { this.infinite = infinite; } /** * @return Returns the particleDuration. */ public Range getParticleDuration() { return particleDuration; } /** * @param particleDuration The particleDuration to set. */ public void setParticleDuration(Range particleDuration) { this.particleDuration = particleDuration; } /** * @return Returns the particlesPerTick. */ public Range getParticlesPerTick() { return particlesPerTick; } /** * @param particlesPerTick The particlesPerTick to set. */ public void setParticlesPerTick(Range particlesPerTick) { this.particlesPerTick = particlesPerTick; } /** * @return Returns the radius. */ public Range getRadius() { return radius; } /** * @param radius The radius to set. */ public void setRadius(Range radius) { this.radius = radius; } /** * @return Returns the scale. */ public Range getScale() { return scale; } /** * @param scale The scale to set. */ public void setScale(Range scale) { this.scale = scale; } /** * @param startScale the startScale to set */ public void setStartScale(Range startScale) { this.startScale = startScale; } /** * @return the startScale */ public Range getStartScale() { return startScale; } /** * @return Returns the slave. */ public EmitterFeature getSlave() { return slave; } /** * @param slave The slave to set. */ public void setSlave(EmitterFeature slave) { this.slave = slave; } /** * @return Returns the sound. */ public String getSound() { return sound; } /** * @param sound The sound to set. */ public void setSound(String sound) { this.sound = sound; } /** * @return Returns the startBrightness. */ public Range getStartBrightness() { return startBrightness; } /** * @param startBrightness The startBrightness to set. */ public void setStartBrightness(Range startBrightness) { this.startBrightness = startBrightness; } /** * @return Returns the startHue. */ public Range getStartHue() { return startHue; } /** * @param startHue The startHue to set. */ public void setStartHue(Range startHue) { this.startHue = startHue; } /** * @return Returns the startPitch. */ public float getStartPitch() { return startPitch; } /** * @param startPitch The startPitch to set. */ public void setStartPitch(float startPitch) { this.startPitch = startPitch; } /** * @return Returns the startSaturation. */ public Range getStartSaturation() { return startSaturation; } /** * @param startSaturation The startSaturation to set. */ public void setStartSaturation(Range startSaturation) { this.startSaturation = startSaturation; } /** * @return Returns the startVolume. */ public float getStartVolume() { return startVolume; } /** * @param startVolume The startVolume to set. */ public void setStartVolume(float startVolume) { this.startVolume = startVolume; } /** * @param layer */ public void setLayer(int layer) { this.layer = layer; } /** * @return */ public int getLayer() { return layer; } /** * @return Returns the velocity. */ public Range getVelocity() { return velocity; } /** * @param velocity The velocity to set. */ public void setVelocity(Range velocity) { this.velocity = velocity; } /** * @param delay the delay to set */ public void setDelay(Range delay) { this.delay = delay; } /** * @return the delay */ public Range getDelay() { return delay; } /** * @return the delayAfter */ public Range getDelayAfter() { return delayAfter; } /** * @param delayAfter the delayAfter to set */ public void setDelayAfter(Range delayAfter) { this.delayAfter = delayAfter; } /** * @return the relativeRotate flag */ public boolean isRelativeRotate() { return relativeRotate; } /** * @return the rotate flag */ public boolean getRotate() { return rotate; } /** * @param rotate the rotate to set */ public void setRotate(boolean rotate) { this.rotate = rotate; } /** * @param relativeRotate the relativeRotate to set */ public void setRelativeRotate(boolean relativeRotate) { this.relativeRotate = relativeRotate; } /** * @param interpolation the interpolation to set */ public void setInterpolation(int interpolation) { this.interpolation = interpolation; } /** * @return the interpolation */ public int getInterpolation() { return interpolation; } /** * @return the emitterScale */ public Range getEmitterScale() { return emitterScale; } /** * @param emitterScale the emitterScale to set */ public void setEmitterScale(Range emitterScale) { this.emitterScale = emitterScale; } /** * @param dontAttenuate the dontAttenuate to set */ public void setDontAttenuate(boolean dontAttenuate) { this.dontAttenuate = dontAttenuate; } /** * @return the dontAttenuate */ public boolean isDontAttenuate() { return dontAttenuate; } /** * @return the doYOffset */ public boolean isDoYOffset() { return doYOffset; } /** * @param doYOffset the doYOffset to set */ public void setDoYOffset(boolean doYOffset) { this.doYOffset = doYOffset; } /** * @return the subLayer */ public int getSubLayer() { return subLayer; } /** * @param subLayer the subLayer to set */ public void setSubLayer(int subLayer) { this.subLayer = subLayer; } public int getMaxParticles() { return maxParticles; } public void setMaxParticles(int maxParticles) { this.maxParticles = maxParticles; } }