ActionScript 3.0 Particle Systems #5: Smoke Effect

I’ve basically spent the day trying to get this one right, and it’s not too bad, although it’s pretty heavy on the CPU. Really, you want about 150 particles and a slower fade out, or 150 particles and add two per frame instead of just one, but on my rig it’s getting jerky with that many particles, so in the animation below we’ve got 80 particles as a compromise =( Also, because we’ve only got 80 particles I’ve deliberately ramped up the alpha decay (i.e. how quickly the particles fade out) and upped the vertical speed to get them off the stage before the particle is forced to vanish by our particle limit. If you put the mouse cursor right down near the bottom of the stage and look at the particles at the top you’ll just catch them vanishing instead of fading out fully, but these are the compromises we make for a smoother framerate ;)

If it’s running poorly on your machine, try right clicking on the stage and lowering the quality, or just grab the files from after the jump and mess around in Flash CS4 (free trials available) with all the different variables (especially the ones that start with min or max!!).

I’ve set the window-mode to use GPU acceleration on this one, so if nothing displays and you’ve got an older graphics card (and you want to see it), just view the source code for this page, grab the object section and change wmode=”gpu” to wmode=”direct”.

Credits: LearnFlash.com’s particle systems vids for the how-to, kaioa.com for the FPSCounter Class, and me for coding it up and running with it!

EnJoY!

Update: I’ve modified the above example to use the RateController Class I wrote yesterday. Aside from adding a RateController object, the only other modification was adding a listener to toggle the animation on a MOUSE_LEAVE event (cursor leaving the stage), and then changing the toggleAnimation function to wait for a click (and not MOUSE_MOVE i.e. cursor returns to stage) before adding particles again. Too easy… :)

Update 2: Many thanks to Joel for pointing out that in my zeal to optimise things I’d actually added some null pointer errors as well as some extra fps, and for correctly suggesting that using Sprites instead of MovieClips would result in a small speed boost, too. The swf file above and code & zip file below have received suitably stern words, and everything now works (relatively) swiftly and error free.

As usual, source code and flash files after the hop skip jump…

Flash File (ActionScript 3.0) Code:

import flash.events.MouseEvent;
import flash.events.Event;
import flash.events.TimerEvent;
import flash.system.System;
import FPSCounter;
import RateController;
import SmokeParticle;
 
// Add our FPS counter to the stage
addChild(new FPSCounter(510, 0));
 
// Add our RateController to the stage
addChild(new RateController(stage, 5, 30, 5000));
 
// Create the array we'll use for our particles
var particleArray:Array = new Array();
 
// Define our number of bounce particles to display on the screen and a start/stop animation flag
var numberOfParticles:uint = 1; // How many particles to add per frame
var particleLimit:uint = 80; // 150 is a good number, but the frame-rate suffers =/
var animating:Boolean = false; // Flag to keep track of whether we are currently animating
 
// Create a new movie clip to add the particles to.
// The reason we do this is so that we can add the particles to the
// new sprite then apply a blur filter to our sprite canvas. This means
// we can apply the blur to our particles without blurring absolutely
// everything that we add to the stage (i.e. fps etc. remains unblurred)
var particleCanvas:Sprite = new Sprite();
addChild(particleCanvas);
 
// Add our blur to the canvas sprite we just created.
// BlurFilter switches: x blur, y-blur-amount, quality (1 = low, 2 = normal, 3 = high)
// Notice that we're using a number which is a power of 2 for the blur amount;
// this increases performance because the filters are optimised for powers of 2!
particleCanvas.filters = [new BlurFilter(8, 8, 1)];
 
// Blur the entire stage using the below code, if we want to.
//this.filters = [new BlurFilter(8, 8, 1)];
 
// Create our controls message, center it horizontally and add it to the screen
var msg:ControlsMessage = new ControlsMessage();
msg.x = stage.stageWidth / 2;
msg.y = 50;
stage.addChild(msg);
 
// Create a flag used to only show our controls message once
var showMessage:Boolean = true;
 
// Bind an event listener for the mouse leaving the stage to stop new particles
stage.addEventListener(Event.MOUSE_LEAVE, toggleAnimation);
 
// Bind an event listener to a mouse click to start/pause animation
stage.addEventListener(MouseEvent.MOUSE_DOWN, toggleAnimation);
 
function toggleAnimation(e:Event):void
{
	if (showMessage == true)
	{
		stage.removeChild(msg);
		showMessage = false;
	}
 
	// If we're not animating start the animation UNLESS this was triggered by a MOUSE_LEAVE event
	if ((animating == false) && (e.type != "mouseLeave")) {
		// Add our particles to the stage via an event listener tied to the frame rate
 		stage.addEventListener(Event.ENTER_FRAME, addParticles);
		animating = true;
	}
	else // If we want to pause the animation...
	{
		// .. remove the event listener to stop calling addParticles each frame
		stage.removeEventListener(Event.ENTER_FRAME, addParticles);
		animating = false;
 
		// When we stop animating, force garbage collection to occcur
		System.gc();
	}
 
} // End of toggleAnimation function
 
function addParticles(e:Event):void
{
	var smoke:SmokeParticle;
 
	// Loop to create numberOfParticles per frame
	for (var loop:uint = 0; loop < numberOfParticles; loop++) {
 
		// Create a new particle and push it into our array
		smoke = new SmokeParticle(mouseX, mouseY)
		particleArray.push(smoke);
 
		// Add our particle to the particleCanvas sprite, not directly to the stage!
		particleCanvas.addChild(smoke);
 
		// If we've hit our limit for the number of particles allowed on stage...
		if (particleArray.length == particleLimit) {
 
			// ...call our particle destructor to unbind the particle's event listener
			// and remove the particle from the stage
			particleArray[0].ParticleDestructor();
 
			// Destroy the particle.
			// Note: SOME-ARRAY.shift gets rid of the first element of the array
			// and returns it, so if we weren't removing the particle from the stage
			// in the destructor (which we are!), we could call shift and pass it to
			// removeChild for removal from our canvas and destroy it in one fell swoop
			// like this: particleCanvas_mc.removeChild(particleArray.shift());
			particleArray.shift();
		}
 
	} // End of for loop
 
} // End of addParticles function

ActionScript 3.0 SmokeParticle Class Code:

package
{
	// Import the classes we're going to use
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.geom.ColorTransform;
 
	public class SmokeParticle extends Sprite
	{
		// Define our class properties for each individual instance
		private var xSpeed:Number;
		private var ySpeed:Number;
		private var alphaDecay:Number;
		private var particleTargetAlpha:Number;
		private var particleScale:Number;
		private var fadeIn:Boolean;
		private var rotationRate:Number;
		private var removed:Boolean;
 
		// Define our class-wide variables (one copy shared between all instances)
		static var minXSpeed:Number = -1.0;
		static var minYSpeed:Number = -3.0;
 
		static var maxXSpeed:Number = 1.0;
		static var maxYSpeed:Number = -5.0;
 
		static var fadeInRate:Number = 0.1;
		static var minParticleAlpha:Number = 0.4;
		static var maxParticleAlpha:Number = 1.0;
		static var minAlphaDecay:Number = 0.01; // Keep this value less than fadeInRate!
		static var maxAlphaDecay:Number = 0.02;
 
		static var minInitialScale:Number = 0.4;
		static var maxInitialScale:Number = 1.0;
 
		static var rad2deg = 180 / Math.PI; // Constant to convert radians to degrees
		static var particleExpansionRate:Number = 0.03;
		static var minRotationSpeed = 1.5; // In degrees per frame
		static var rotationModifier = 2; // Fudges rotation per frame change into desired rate range
 
		// I don't like having to generate a large number of random numbers per frame because
		// the buggers are computationally expensive (i.e. they take a long time to generate!)
		// so if I generate a bunch of just them just ONCE as soon as we instatiate the class,
		// I can then just pull a number from the array whenever I want one instead of
		// generating a random number on-the-fly. If I keep track of which element we're on by
		// going to the next element once the number's been used and wrapping-around when we
		// hit the end, and assuming our source of random numbers is of sufficient size, there
		// shouldn't be any visual difference between generating on-the-fly and using the
		// pre-calculated array, only the cpu time per frame should be lower, so everything 
		// runs faster and smoother. In theory =D
		static var randArray = new Array();
		static var randElement = 0;				
		initialiseRandArray();
 
		// Does what it says on the tin.
		public static function initialiseRandArray():void
		{
			for (var i:uint = 0; i < 1000; i++)
			{
				randArray[i] = Math.random();
			}
		}
 
		// Function to return a random number from our random number array
		public function getRandNumber():Number
		{
			if (randElement < 999) {
				randElement++;
			}
			else
			{
				randElement = 0;
			}
			return randArray[randElement];
		}
 
		// Function to return us a random number within a specified range.
		// Uses our array of random numbers to try to lower CPU load
		function randRange(low:Number, high:Number):Number {
			return (getRandNumber() * (high - low)) + low;
		} 
 
		// Class Constructor
		public function SmokeParticle(theXLocation:Number, theYLocation:Number):void
		{
			// Add an event listener to each instance of our particle so it's updated
			// when each new frame is drawn
			this.addEventListener(Event.ENTER_FRAME, updateParticle);
 
			// Set the initial particle location as passed to our constructor
			this.x = theXLocation;
			this.y = theYLocation;
 
			// Randomise the x and y speed of the particle between a given range
			this.randomiseSpeeds();
 
			// Uncomment this for multi-coloured smoke particles
			//this.randomiseParticleColour();
 
			// Set the alpha our particle will fade in TO
			this.particleTargetAlpha = randRange(minParticleAlpha, maxParticleAlpha);
 
			// Set our initial particle alpha to be completely transparent
			this.alpha = 0;
 
			// Set an initial random alpha decay within the given range
			this.alphaDecay = randRange(minAlphaDecay, maxAlphaDecay);
 
			// Set an initial random rotation within the given range
			this.rotation = randRange(0, 360);
 
			// Set an initial random scale within the given range
			this.particleScale = randRange(0.4, 1);
			this.scaleX = this.particleScale;
			this.scaleY = this.particleScale;
 
			// Set an initial rotation for the particle which is proportional to the
			// particle's initial scale, where bigger particles rotate slower than small ones.
			this.rotationRate = (minRotationSpeed - this.particleScale) * rotationModifier;
 
			// Initially, we want our smoke to quickly fade in
			this.fadeIn = true;
 
			// Because particles can be removed for being transparent or off the stage,
			// as well as for being old, we need to keep track of whether the particle
			// has been removed so we don't remove it twice (i..e for BOTH of these reasons).
			this.removed = false;
		}
 
		// Destructor to unbind the particle's ENTER_FRAME event listener when we destroy it
		public function ParticleDestructor():void
		{
			// Remove the event listener from our particle
			// Note: We're not removing the particle from the stage here because
			// we're going to do that in the flash file
 
 
			// Remove the particle from the canvas. This gets called when either:
			// 1.) The particle is entirely transparent or off the stage, or
			// 2.) The particle limit is hit and we need to remove old particles.
			// Checking our removed property stops us from trying to remove the particle
			// when both conditions are true (because the particle ALWAYS gets old, but
			// it CAN be transparent/off-stage before that happens).
			// The sooner we can stop drawing the particle, the higher our framerate will be...
			if (this.removed == false) {
				this.removeEventListener(Event.ENTER_FRAME, updateParticle);
				parent.removeChild(this);
				this.removed = true;
			}
		}
 
		public function randomiseParticleColour():void
		{
			var myColourTransform:ColorTransform = this.transform.colorTransform;
 
			// This will change the color of all layers and all sub-symbols within this symbol:
			//myColourTransform.color = 0xff0000;
 
			// Shift each colour channel (you can shift in the range: -255 to 255).
			myColourTransform.redOffset   = (Math.random() * 200) - 100; // Range: -255 to +255
			myColourTransform.greenOffset = (Math.random() * 200) - 100; // Range: -255 to +255
			myColourTransform.blueOffset  = (Math.random() * 200) - 100; // Range: -255 to +255
 
			// This number will multiply by the green channel only (decimal value).  There is also a redMultiplier and blueMultiplier.
			// This will essentially saturate/desaturate the color channel.
			// colorTransform.greenMultiplier = 2;
 
			// re-assign the ColorTransform back to this symbol
			this.transform.colorTransform = myColourTransform;
		}
 
		private function randomiseSpeeds():void
		{
			// Randomise our x and y speeds within a given range
			this.xSpeed = randRange(minXSpeed, maxXSpeed); 
			this.ySpeed = randRange(minYSpeed, maxYSpeed);
		}
 
		// Function to update a particle. Bound to Event.ENTER_FRAME, so called once per frame 
		public function updateParticle(e:Event):void
		{
			// Add our randomised x and y speeds to our particle position
			this.x += xSpeed;
			this.y += ySpeed;
 
			// Subtract a small amount from our object's alpha so it fades out
			this.alpha -= this.alphaDecay;
 
			// Increase the size of our particle as it drifts upwards and then set the new scale
			this.particleScale += particleExpansionRate;
			this.scaleX = this.particleScale;
			this.scaleY = this.particleScale;
 
			// Rotate our particle slightly as it rises
			this.rotation += this.rotationRate;
 
			// If the particle has just been created and is fading in...
			if (this.fadeIn == true) {
 
				// ...then fade that bad-boy in already ;)
				this.alpha += fadeInRate;
 
				// If our particle's alpha has increased to at least it's target alpha then
				// iwe're done fading in, so set the flag accordingly
				if (this.alpha >= this.particleTargetAlpha) {
					this.fadeIn = false;
				}
			}
 
			// Unbind the event listener and remove the particle from the stage as soon
			// as the alpha reaches 0 (i.e. particle is completely transparent) OR the particle
			// has risen off the top of the stage to try to eke out a little bit more performance!
			if ( (this.alpha <= 0) || (this.y < (0 - this.height / 2)) ) {
				this.ParticleDestructor();
			}
 
		} // End of updateParticle function
 
	} // End of SmokeParticle class
 
} // End of package

ActionScript 3.0 FPSCounter Class:

package{
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.text.TextField;
    import flash.text.TextFieldAutoSize;
    import flash.utils.getTimer;
 
    public class FPSCounter extends Sprite{
        private var last:uint = getTimer();
        private var ticks:uint = 0;
        private var tf:TextField;
 
        public function FPSCounter(xPos:int=0, yPos:int=0, color:uint=0xffffff, fillBackground:Boolean=false, backgroundColor:uint=0x000000) {
            this.x = xPos;
            this.y = yPos;
            tf = new TextField();
            tf.textColor = color;
            tf.text = "----- fps";
            tf.selectable = false;
            tf.background = fillBackground;
            tf.backgroundColor = backgroundColor;
            tf.autoSize = TextFieldAutoSize.LEFT;
            addChild(tf);
            width = tf.textWidth;
            height = tf.textHeight;
            addEventListener(Event.ENTER_FRAME, tick);
        }
 
        public function tick(evt:Event):void {
            ticks++;
            var now:uint = getTimer();
            var delta:uint = now - last;
            if (delta >= 1000) {
                //trace(ticks / delta * 1000+" ticks:"+ticks+" delta:"+delta);
                var fps:Number = (ticks / delta * 1000);
                tf.text = fps.toFixed(1) + " fps";
				ticks = 0;
                last = now;
            }
        }
    }
}

As promised, Adobe Flash CS4 files for the above can be found: here.

3 thoughts on “ActionScript 3.0 Particle Systems #5: Smoke Effect”

  1. Thanks for sharing. Given my lack of experience I can’t offer much to help speed up the animation, but I did notice you use a MovieClip for your canvas. Could you get by using a Sprite instead? Perhaps that’s a negligible improvement, but I thought I’d mention nonetheless. Also, the debug flash player throws a null reference error relating to particleDestructor() and addParticles().

    1. Hey Joel – thanks for your input.

      It seems that a Sprite is pretty much a MovieClip without a timeline (although there are other static/dynamic property issues too, they don’t come into play in this particular piece of code), so a Sprite makes more sense to use than a MC, and will indeed speed things up, but by a very small amount – but hey, it all counts!

      I hadn’t noticed the null reference was introduced when I recently updated the code to remove the particles as soon as they’re entirely transparent or above the stage – I was just trying to eke out every bit of performance I knew how by removing particles from the stage as soon as I possibly could.

      The unintended side effect of this was that flash then tries to remove the particles twice:
      – Once when they’re transparent or above the stage (called from within the SmokeParticle Class), and
      – Again when the particle limit gets rid of the oldest particle (i.e. array element 0) to add a new particle (called from the main flash file)

      Hence the null pointer reference for any particle that has already been “removed” when the transparent/above remover kicked in from the updateParticle function.

      If you comment out the parent.removeChild(this); line in the ParticleDestructor function, normal service is resumed, but performance suffers because we’re not eliminating the particle as soon as we possibly can.

      I’ll have a look at the code again this evening and either wrap the removeChild calls in some conditions, or add a SmokeParticle property to keep track of whether the particle’s already been removed so hopefully we can keep our speed gain from nuking the particle early, but lose the null-pointer shenanigans :)

      Update: Done!

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.