flixel/effects/particles/FlxEmitter.hx
package flixel.effects.particles;
import openfl.display.BlendMode;
import flixel.FlxG;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.effects.particles.FlxParticle.IFlxParticle;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.math.FlxPoint;
import flixel.math.FlxVelocity;
import flixel.system.FlxAssets.FlxGraphicAsset;
import flixel.util.FlxColor;
import flixel.util.FlxDestroyUtil;
import flixel.util.FlxDirectionFlags;
import flixel.util.helpers.FlxBounds;
import flixel.util.helpers.FlxPointRangeBounds;
import flixel.util.helpers.FlxRangeBounds;
typedef FlxEmitter = FlxTypedEmitter<FlxParticle>;
/**
* FlxTypedEmitter is a lightweight particle emitter.
* It can be used for one-time explosions or for continuous fx like rain and fire.
* `FlxEmitter` is not optimized or anything; all it does is launch `FlxParticle` objects out
* at set intervals by setting their positions and velocities accordingly.
* It is easy to use and relatively efficient, relying on `FlxGroup`'s RECYCLE POWERS.
*/
class FlxTypedEmitter<T:FlxSprite & IFlxParticle> extends FlxTypedGroup<T>
{
/**
* Set your own particle class type here. The custom class must extend `FlxParticle`. Default is `FlxParticle`.
*/
public var particleClass:Class<T> = cast FlxParticle;
/**
* Determines whether the emitter is currently emitting particles. It is totally safe to directly toggle this.
*/
public var emitting:Bool = false;
/**
* How often a particle is emitted (if emitter is started with `Explode == false`).
*/
public var frequency:Float = 0.1;
/**
* Sets particle's blend mode. `null` by default. Warning: Expensive on Flash.
*/
public var blend:BlendMode;
/**
* The x position of this emitter.
*/
public var x:Float = 0;
/**
* The y position of this emitter.
*/
public var y:Float = 0;
/**
* The width of this emitter. Particles can be randomly generated from anywhere within this box.
*/
public var width:Float = 0;
/**
* The height of this emitter. Particles can be randomly generated from anywhere within this box.
*/
public var height:Float = 0;
/**
* How particles should be launched. If `CIRCLE`, particles will use `launchAngle` and `speed`.
* Otherwise, particles will just use `velocity.x` and `velocity.y`.
*/
public var launchMode:FlxEmitterMode = FlxEmitterMode.CIRCLE;
/**
* Keep the scale ratio of the particle. Uses the `x` values of `scale`.
*/
public var keepScaleRatio:Bool = false;
/**
* Sets the velocity range of particles launched from this emitter. Only used with `FlxEmitterMode.SQUARE`.
*/
public var velocity(default, null):FlxPointRangeBounds = new FlxPointRangeBounds(-100, -100, 100, 100);
/**
* Set the speed range of particles launched from this emitter. Only used with `FlxEmitterMode.CIRCLE`.
*/
public var speed(default, null):FlxRangeBounds<Float> = new FlxRangeBounds<Float>(0, 100);
/**
* Set the angular acceleration range of particles launched from this emitter.
*/
public var angularAcceleration(default, null):FlxRangeBounds<Float> = new FlxRangeBounds<Float>(0, 0);
/**
* Set the angular drag range of particles launched from this emitter.
*/
public var angularDrag(default, null):FlxRangeBounds<Float> = new FlxRangeBounds<Float>(0, 0);
/**
* The angular velocity range of particles launched from this emitter.
*/
public var angularVelocity(default, null):FlxRangeBounds<Float> = new FlxRangeBounds<Float>(0, 0);
/**
* The angle range of particles launched from this emitter.
* `angle.end` is ignored unless `ignoreAngularVelocity` is set to `true`.
*/
public var angle(default, null):FlxRangeBounds<Float> = new FlxRangeBounds<Float>(0);
/**
* Set this if you want to specify the beginning and ending value of angle,
* instead of using `angularVelocity` (or `angularAcceleration`).
*/
public var ignoreAngularVelocity:Bool = false;
/**
* The angle range at which particles will be launched from this emitter.
* Ignored unless `launchMode` is set to `FlxEmitterMode.CIRCLE`.
*/
public var launchAngle(default, null):FlxBounds<Float> = new FlxBounds<Float>(-180, 180);
/**
* The life, or duration, range of particles launched from this emitter.
*/
public var lifespan(default, null):FlxBounds<Float> = new FlxBounds<Float>(3);
/**
* Sets `scale` range of particles launched from this emitter.
*/
public var scale(default, null):FlxPointRangeBounds = new FlxPointRangeBounds(1, 1);
/**
* Sets `alpha` range of particles launched from this emitter.
*/
public var alpha(default, null):FlxRangeBounds<Float> = new FlxRangeBounds<Float>(1);
/**
* Sets `color` range of particles launched from this emitter.
*/
public var color(default, null):FlxRangeBounds<FlxColor> = new FlxRangeBounds(FlxColor.WHITE, FlxColor.WHITE);
/**
* Sets X and Y drag component of particles launched from this emitter.
*/
public var drag(default, null):FlxPointRangeBounds = new FlxPointRangeBounds(0, 0);
/**
* Sets the `acceleration` range of particles launched from this emitter.
* Set acceleration y-values to give particles gravity.
*/
public var acceleration(default, null):FlxPointRangeBounds = new FlxPointRangeBounds(0, 0);
/**
* Sets the `elasticity`, or bounce, range of particles launched from this emitter.
*/
public var elasticity(default, null):FlxRangeBounds<Float> = new FlxRangeBounds<Float>(0);
/**
* Sets the `immovable` flag for particles launched from this emitter.
*/
public var immovable:Bool = false;
/**
* Sets the `autoUpdateHitbox` flag for particles launched from this emitter.
* If true, the particles' hitbox will be updated to match scale.
*/
public var autoUpdateHitbox:Bool = false;
/**
* Sets the `allowCollisions` value for particles launched from this emitter.
* Set to `NONE` by default. Don't forget to call `FlxG.collide()` in your update loop!
*/
public var allowCollisions:FlxDirectionFlags = NONE;
/**
* Shorthand for toggling `allowCollisions` between `ANY` (if `true`) and `NONE` (if `false`).
* Don't forget to call `FlxG.collide()` in your update loop!
*/
public var solid(get, set):Bool;
/**
* Internal helper for deciding how many particles to launch.
*/
var _quantity:Int = 0;
/**
* Internal helper for the style of particle emission (all at once, or one at a time).
*/
var _explode:Bool = true;
/**
* Internal helper for deciding when to launch particles or kill them.
*/
var _timer:Float = 0;
/**
* Internal counter for figuring out how many particles to launch.
*/
var _counter:Int = 0;
/**
* Internal point object, handy for reusing for memory management purposes.
*/
var _point:FlxPoint = FlxPoint.get();
/**
* Internal helper for automatically calling the `kill()` method
*/
var _waitForKill:Bool = false;
/**
* Creates a new `FlxTypedEmitter` object at a specific position.
* Does NOT automatically generate or attach particles!
*
* @param X The X position of the emitter.
* @param Y The Y position of the emitter.
* @param Size Optional, specifies a maximum capacity for this emitter.
*/
public function new(X:Float = 0, Y:Float = 0, Size:Int = 0)
{
super(Size);
setPosition(X, Y);
exists = false;
}
/**
* Clean up memory.
*/
override public function destroy():Void
{
velocity = FlxDestroyUtil.destroy(velocity);
scale = FlxDestroyUtil.destroy(scale);
drag = FlxDestroyUtil.destroy(drag);
acceleration = FlxDestroyUtil.destroy(acceleration);
_point = FlxDestroyUtil.put(_point);
blend = null;
angularAcceleration = null;
angularDrag = null;
angularVelocity = null;
angle = null;
speed = null;
launchAngle = null;
lifespan = null;
alpha = null;
color = null;
elasticity = null;
super.destroy();
}
/**
* This function generates a new array of particle sprites to attach to the emitter.
*
* @param Graphics If you opted to not pre-configure an array of `FlxParticle` objects,
* you can simply pass in a particle image or sprite sheet.
* @param Quantity The number of particles to generate when using the "create from image" option.
* @param BakedRotations How many frames of baked rotation to use (boosts performance).
* Set to zero to not use baked rotations.
* @param Multiple Whether the image in the `Graphics` param is a single particle or a bunch of particles
* (if it's a bunch, they need to be square!).
* @param AutoBuffer Whether to automatically increase the image size to accommodate rotated corners.
* Default is `false`. Will create frames that are 150% larger on each axis than the
* original frame or graphic.
* @return This `FlxEmitter` instance (nice for chaining stuff together).
*/
public function loadParticles(Graphics:FlxGraphicAsset, Quantity:Int = 50, bakedRotationAngles:Int = 16, Multiple:Bool = false,
AutoBuffer:Bool = false):FlxTypedEmitter<T>
{
maxSize = Quantity;
var totalFrames:Int = 1;
if (Multiple)
{
var sprite = new FlxSprite();
sprite.loadGraphic(Graphics, true);
totalFrames = sprite.numFrames;
sprite.destroy();
}
for (i in 0...Quantity)
add(loadParticle(Graphics, Quantity, bakedRotationAngles, Multiple, AutoBuffer, totalFrames));
return this;
}
function loadParticle(Graphics:FlxGraphicAsset, Quantity:Int, bakedRotationAngles:Int, Multiple:Bool = false, AutoBuffer:Bool = false, totalFrames:Int):T
{
var particle:T = Type.createInstance(particleClass, []);
var frame = Multiple ? FlxG.random.int(0, totalFrames - 1) : -1;
if (FlxG.renderBlit && bakedRotationAngles > 0)
particle.loadRotatedGraphic(Graphics, bakedRotationAngles, frame, false, AutoBuffer);
else
particle.loadGraphic(Graphics, Multiple);
if (Multiple)
particle.animation.frameIndex = frame;
return particle;
}
/**
* Similar to `FlxSprite#makeGraphic()`, this function allows you to quickly make single-color particles.
*
* @param Width The width of the generated particles. Default is `2` pixels.
* @param Height The height of the generated particles. Default is `2` pixels.
* @param Color The color of the generated particles. Default is white.
* @param Quantity How many particles to generate. Default is `50`.
* @return This `FlxEmitter` instance (nice for chaining stuff together).
*/
public function makeParticles(Width:Int = 2, Height:Int = 2, Color:FlxColor = FlxColor.WHITE, Quantity:Int = 50):FlxTypedEmitter<T>
{
maxSize = Quantity;
for (i in 0...Quantity)
{
var particle:T = Type.createInstance(particleClass, []);
particle.makeGraphic(Width, Height, Color);
add(particle);
}
return this;
}
/**
* Called automatically by the game loop, decides when to launch particles and when to "die".
*/
override public function update(elapsed:Float):Void
{
if (emitting)
{
if (_explode)
explode();
else
emitContinuously(elapsed);
}
else if (_waitForKill)
{
_timer += elapsed;
if ((lifespan.max > 0) && (_timer > lifespan.max))
{
kill();
return;
}
}
super.update(elapsed);
}
function explode():Void
{
var amount:Int = _quantity;
if (amount <= 0 || amount > length)
amount = length;
for (i in 0...amount)
emitParticle();
onFinished();
}
function emitContinuously(elapsed:Float):Void
{
// Spawn one particle per frame
if (frequency <= 0)
{
emitParticleContinuously();
}
else
{
_timer += elapsed;
while (_timer > frequency)
{
_timer -= frequency;
emitParticleContinuously();
}
}
}
function emitParticleContinuously():Void
{
emitParticle();
_counter++;
if (_quantity > 0 && _counter >= _quantity)
onFinished();
}
function onFinished():Void
{
emitting = false;
_waitForKill = true;
_quantity = 0;
}
/**
* Call this function to turn off all the particles and the emitter.
*/
override public function kill():Void
{
emitting = false;
_waitForKill = false;
super.kill();
}
/**
* Call this function to start emitting particles.
*
* @param Explode Whether the particles should all burst out at once.
* @param Frequency Ignored if `Explode` is set to `true`. `Frequency` is how often to emit a particle.
* `0` = never emit, `0.1` = 1 particle every 0.1 seconds, `5` = 1 particle every 5 seconds.
* @param Quantity How many particles to launch. `0` = "all of the particles".
* @return This `FlxEmitter` instance (nice for chaining stuff together).
*/
public function start(Explode:Bool = true, Frequency:Float = 0.1, Quantity:Int = 0):FlxTypedEmitter<T>
{
exists = true;
visible = true;
emitting = true;
_explode = Explode;
frequency = Frequency;
_quantity += Quantity;
_counter = 0;
_timer = 0;
_waitForKill = false;
return this;
}
/**
* This function can be used both internally and externally to emit the next particle.
*/
public function emitParticle():T
{
var particle:T = cast recycle(cast particleClass);
particle.reset(0, 0); // Position is set later, after size has been calculated
particle.blend = blend;
particle.immovable = immovable;
particle.solid = solid;
particle.allowCollisions = allowCollisions;
particle.autoUpdateHitbox = autoUpdateHitbox;
// Particle lifespan settings
if (lifespan.active)
{
particle.lifespan = FlxG.random.float(lifespan.min, lifespan.max);
}
if (velocity.active)
{
// Particle velocity/launch angle settings
particle.velocityRange.active = particle.lifespan > 0 && !particle.velocityRange.start.equals(particle.velocityRange.end);
if (launchMode == FlxEmitterMode.CIRCLE)
{
var particleAngle:Float = 0;
if (launchAngle.active)
particleAngle = FlxG.random.float(launchAngle.min, launchAngle.max);
// Calculate launch velocity
_point = FlxVelocity.velocityFromAngle(particleAngle, FlxG.random.float(speed.start.min, speed.start.max));
particle.velocity.x = _point.x;
particle.velocity.y = _point.y;
particle.velocityRange.start.set(_point.x, _point.y);
// Calculate final velocity
_point = FlxVelocity.velocityFromAngle(particleAngle, FlxG.random.float(speed.end.min, speed.end.max));
particle.velocityRange.end.set(_point.x, _point.y);
}
else
{
particle.velocityRange.start.x = FlxG.random.float(velocity.start.min.x, velocity.start.max.x);
particle.velocityRange.start.y = FlxG.random.float(velocity.start.min.y, velocity.start.max.y);
particle.velocityRange.end.x = FlxG.random.float(velocity.end.min.x, velocity.end.max.x);
particle.velocityRange.end.y = FlxG.random.float(velocity.end.min.y, velocity.end.max.y);
particle.velocity.x = particle.velocityRange.start.x;
particle.velocity.y = particle.velocityRange.start.y;
}
}
else
particle.velocityRange.active = false;
// Particle angular velocity settings
particle.angularVelocityRange.active = particle.lifespan > 0 && angularVelocity.start != angularVelocity.end;
if (!ignoreAngularVelocity)
{
if (angularAcceleration.active)
particle.angularAcceleration = FlxG.random.float(angularAcceleration.start.min, angularAcceleration.start.max);
if (angularVelocity.active)
{
particle.angularVelocityRange.start = FlxG.random.float(angularVelocity.start.min, angularVelocity.start.max);
particle.angularVelocityRange.end = FlxG.random.float(angularVelocity.end.min, angularVelocity.end.max);
particle.angularVelocity = particle.angularVelocityRange.start;
}
if (angularDrag.active)
particle.angularDrag = FlxG.random.float(angularDrag.start.min, angularDrag.start.max);
}
else if (angularVelocity.active)
{
particle.angularVelocity = (FlxG.random.float(angle.end.min,
angle.end.max) - FlxG.random.float(angle.start.min, angle.start.max)) / FlxG.random.float(lifespan.min, lifespan.max);
particle.angularVelocityRange.active = false;
}
// Particle angle settings
if (angle.active)
particle.angle = FlxG.random.float(angle.start.min, angle.start.max);
// Particle scale settings
if (scale.active)
{
particle.scaleRange.start.x = FlxG.random.float(scale.start.min.x, scale.start.max.x);
particle.scaleRange.start.y = keepScaleRatio ? particle.scaleRange.start.x : FlxG.random.float(scale.start.min.y, scale.start.max.y);
particle.scaleRange.end.x = FlxG.random.float(scale.end.min.x, scale.end.max.x);
particle.scaleRange.end.y = keepScaleRatio ? particle.scaleRange.end.x : FlxG.random.float(scale.end.min.y, scale.end.max.y);
particle.scaleRange.active = particle.lifespan > 0 && !particle.scaleRange.start.equals(particle.scaleRange.end);
particle.scale.x = particle.scaleRange.start.x;
particle.scale.y = particle.scaleRange.start.y;
if (particle.autoUpdateHitbox)
particle.updateHitbox();
}
else
particle.scaleRange.active = false;
// Particle alpha settings
if (alpha.active)
{
particle.alphaRange.start = FlxG.random.float(alpha.start.min, alpha.start.max);
particle.alphaRange.end = FlxG.random.float(alpha.end.min, alpha.end.max);
particle.alphaRange.active = particle.lifespan > 0 && particle.alphaRange.start != particle.alphaRange.end;
particle.alpha = particle.alphaRange.start;
}
else
particle.alphaRange.active = false;
// Particle color settings
if (color.active)
{
particle.colorRange.start = FlxG.random.color(color.start.min, color.start.max);
particle.colorRange.end = FlxG.random.color(color.end.min, color.end.max);
particle.colorRange.active = particle.lifespan > 0 && particle.colorRange.start != particle.colorRange.end;
particle.color = particle.colorRange.start;
}
else
particle.colorRange.active = false;
// Particle drag settings
if (drag.active)
{
particle.dragRange.start.x = FlxG.random.float(drag.start.min.x, drag.start.max.x);
particle.dragRange.start.y = FlxG.random.float(drag.start.min.y, drag.start.max.y);
particle.dragRange.end.x = FlxG.random.float(drag.end.min.x, drag.end.max.x);
particle.dragRange.end.y = FlxG.random.float(drag.end.min.y, drag.end.max.y);
particle.dragRange.active = particle.lifespan > 0 && !particle.dragRange.start.equals(particle.dragRange.end);
particle.drag.x = particle.dragRange.start.x;
particle.drag.y = particle.dragRange.start.y;
}
else
particle.dragRange.active = false;
// Particle acceleration settings
if (acceleration.active)
{
particle.accelerationRange.start.x = FlxG.random.float(acceleration.start.min.x, acceleration.start.max.x);
particle.accelerationRange.start.y = FlxG.random.float(acceleration.start.min.y, acceleration.start.max.y);
particle.accelerationRange.end.x = FlxG.random.float(acceleration.end.min.x, acceleration.end.max.x);
particle.accelerationRange.end.y = FlxG.random.float(acceleration.end.min.y, acceleration.end.max.y);
particle.accelerationRange.active = particle.lifespan > 0
&& !particle.accelerationRange.start.equals(particle.accelerationRange.end);
particle.acceleration.x = particle.accelerationRange.start.x;
particle.acceleration.y = particle.accelerationRange.start.y;
}
else
particle.accelerationRange.active = false;
// Particle elasticity settings
if (elasticity.active)
{
particle.elasticityRange.start = FlxG.random.float(elasticity.start.min, elasticity.start.max);
particle.elasticityRange.end = FlxG.random.float(elasticity.end.min, elasticity.end.max);
particle.elasticityRange.active = particle.lifespan > 0 && particle.elasticityRange.start != particle.elasticityRange.end;
particle.elasticity = particle.elasticityRange.start;
}
else
particle.elasticityRange.active = false;
// Set position
particle.x = FlxG.random.float(x, x + width) - particle.width / 2;
particle.y = FlxG.random.float(y, y + height) - particle.height / 2;
// Restart animation
if (particle.animation.curAnim != null)
particle.animation.curAnim.restart();
particle.onEmit();
return particle;
}
/**
* Change the emitter's midpoint to match the midpoint of a `FlxObject`.
*
* @param Object The `FlxObject` that you want to sync up with.
*/
public function focusOn(Object:FlxObject):Void
{
Object.getMidpoint(_point);
x = _point.x - (Std.int(width) >> 1);
y = _point.y - (Std.int(height) >> 1);
}
/**
* Helper function to set the coordinates of this object.
*/
public inline function setPosition(X:Float = 0, Y:Float = 0):Void
{
x = X;
y = Y;
}
public inline function setSize(Width:Float, Height:Float):Void
{
width = Width;
height = Height;
}
inline function get_solid():Bool
{
return allowCollisions.has(ANY);
}
function set_solid(Solid:Bool):Bool
{
if (Solid)
{
allowCollisions = ANY;
}
else
{
allowCollisions = NONE;
}
return Solid;
}
}
enum FlxEmitterMode
{
SQUARE;
CIRCLE;
}