wtflash.com
 

Archive for the ‘actionscript 2.0’ Category

Particle Systems in ActionScript 2.0 and 3.0 - Part 1

Saturday, May 23rd, 2009

I’ve always had a interest in particle systems stemming from playing a lot of video games as a kid (wait, still do) and wondering how explosions and blood splatters were created.  I figured by developing my own basic particle system library I would be able to 1) learn more about ActionScript, 2) have a greater understanding of OOP, and 3) do something cool!

A quick, but necessary reminder: I am a Flash designer attempting to learn more about ActionScript development so please follow at your own risk and if you see an error please leave a comment.

We’ll begin with an ActionScript 2.0 version since I need a little refresher.  Revisiting AS2 makes me appreciate AS3 that much more.  Here’s a sample usage of the particle system:

The Flash plugin is required to view this object.

(this performance test was inspired by the GreenSock Performance Tests)

These sites are good reads and helped point me in the right direction: Particle System API and “Particle Systems” by Allen Martin.  The library is simple and consists of only 2 classes: a Particle class and an Emitter class.  The Particle class will represent a single particle that is instantiated by the Emitter class.

The Particle Class

//AS2///////////////////////////////////////////////////////////////////////////
//
//  Emitter
//
//  Copyright (c) 2009 Jason Woan. All rights reserved.
//
//  Do what you want with the code.  Credit is nice but not necessary.  Use at
//  your own risk and discretion.  I won't be held responsible if your site
//  blows up.  Enjoy!
//
////////////////////////////////////////////////////////////////////////////////

/**
 *	This class is the blueprint to a single particle maintaining its inherent
 *	properties and performing position calculations.  Use as a MovieClip's
 *	linkage class.
 *
 *	@version 0.1.0
 *	@langversion ActionScript 2.0
 *	@playerversion Flash 8.0
 *
 *	@author Jason_Woan
 *	@since  2009-05-14
 */
dynamic class com.jasonwoan.speck.Particle extends MovieClip {

	//--------------------------------------
	//  PUBLIC VARIABLES
	//--------------------------------------

	public var xVelocity:Number = 1;
	public var yVelocity:Number = 1;
	public var xAccel:Number = 0;
	public var yAccel:Number = 0;
	//public var rotationalVelocity:Number = 0;
	//public var mass:Number = 1;
	public var age:Number = 0;
	public var life:Number = 10;
	public var dead:Boolean = false;
	public var direction:Number = 45;
	public var speed:Number = 1;
	public var fade:Boolean = false;
	public var scale:Number = 100;
	public var shrink:Boolean = false;
	public var xMinBound:Number;
	public var xMaxBound:Number;
	public var yMinBound:Number;
	public var yMaxBound:Number;
	public var restitution:Number;

	//--------------------------------------
	//  PRIVATE & PROTECTED VARIABLES
	//--------------------------------------

	private var _xPrevious:Number;
	private var _yPrevious:Number;
	private var _scaleStart:Number;

	//--------------------------------------
	//  CONSTRUCTOR
	//--------------------------------------
	/**
	 *	@constructor
	 */
	public function Particle() {
		// calculate directional velocity
		var radDirection:Number = this.direction * Math.PI / 180;
		xVelocity = this.speed * Math.cos(radDirection);
		yVelocity = this.speed * Math.sin(radDirection);
		this._xscale = this._yscale = this.scale;
	}

	//--------------------------------------
	//  PUBLIC METHODS
	//--------------------------------------

	public function update():Void
	{
		if (!dead) {
			// save previous position
			this._xPrevious = this._x;
			this._yPrevious = this._y;

			// check bounds
			if (this.xMinBound != undefined) {
				if (this._x + this.xVelocity < this.xMinBound) {
					this.xVelocity = -1 * this.restitution;
				}
			}
			if (this.xMaxBound != undefined) {
				if (this._x + this.xVelocity > this.xMaxBound) {
					this.xVelocity = -1 * this.restitution;
				}
			}
			if (this.yMinBound != undefined) {
				if (this._y + this.yVelocity < this.yMinBound) {
					this.yVelocity *= -1 * this.restitution;
				}
			}
			if (this.yMaxBound != undefined) {
				if (this._y + this.yVelocity > this.yMaxBound) {
					this.yVelocity *= -1 * this.restitution;
				}
			}

			// update position
			this._x += this.xVelocity;
			this._y += this.yVelocity;

			// update velocity
			this.xVelocity += this.xAccel;
			this.yVelocity += this.yAccel;

			this.age++;

			// particle shape and alpha dynamics
			if (this.fade) {
				this._alpha = 100 - Math.round(this.age / this.life * 100);
			}

			if (shrink) {
				this._xscale = this._yscale = this.scale - Math.round(this.age / this.life * scale);
			}

			// check for death
			if (this.age == this.life) {
				this.dead = true;
				this._visible = false;
			}
		}
	}

	public function revive():Void
	{
		if (this.dead) {
			this.dead = false;
			this._visible = true;
			this.age = 0;
			// reset velocities
			var radDirection:Number = this.direction * Math.PI / 180;
			this.xVelocity = this.speed * Math.cos(radDirection);
			this.yVelocity = this.speed * Math.sin(radDirection);
		}
	}
}

The Emitter Class

//AS2///////////////////////////////////////////////////////////////////////////
//
//  Emitter
//
//  Copyright (c) 2009 Jason Woan. All rights reserved.
//
//  Do what you want with the code.  Credit is nice but not necessary.  Use at
//  your own risk and discretion.  I won't be held responsible if your site
//  blows up.  Enjoy!
//
////////////////////////////////////////////////////////////////////////////////

import mx.utils.Delegate;

import com.tbwachiat.speck.Particle;

/**
 *	The Emitter manages the creation of particles.
 *
 *	@version 0.1.0
 *	@langversion ActionScript 2.0
 *	@playerversion Flash 8.0
 *
 *	@author Jason_Woan
 *	@since  2009-05-14
 */
class com.jasonwoan.speck.Emitter {

	//--------------------------------------
	//  PRIVATE & PROTECTED VARIABLES
	//--------------------------------------

	private var _gravity:Number = 0;

	// number of particles per step
	private var _birthRate:Number = 3;

	// the life of each particle in seconds
	private var _life:Number = 100;

	private var _scale:Number = 100;
	private var _scaleMin:Number;
	private var _scaleMax:Number;
	private var _speed:Number = 1;
	private var _direction:Number = 45;
	private var _randomDirection:Boolean = false;

	private var _xAccel:Number = 0;
	private var _yAccel:Number = 0;

	private var _xMin:Number = 0;
	private var _xMax:Number = 0;
	private var _yMin:Number = 0;
	private var _yMax:Number = 0;

	private var _xMinBound:Number;
	private var _xMaxBound:Number;
	private var _yMinBound:Number;
	private var _yMaxBound:Number;
	private var _restitution:Number = 1;

	private var _fade:Boolean = false;
	private var _shrink:Boolean = false;

	private var _emitter:MovieClip;
	private var _particleID:String;
	private var _particles:Array;

	private var _oldestIndex:Number = 0;

	//--------------------------------------
	//  CONSTRUCTOR
	//--------------------------------------
	/**
	 *	@constructor
	 */
	public function Emitter(pEmitter:MovieClip, pParticleID:String, pParameters:Object) {
		_emitter = pEmitter;
		_particleID = pParticleID;
		_particles = new Array();

		// check parameter object
		for (var prop in pParameters) {
			switch (prop) {
				// the life of a particle in frames
				// the particle will be recycled if dead
				case "life" :
				if (pParameters[prop] > 0) _life = pParameters[prop];
				break;

				// the number of particles generated per update on enter frame
				case "birthRate" :
				if (pParameters[prop] > 0) _birthRate = pParameters[prop];
				break;

				// a positive acceleration of a particle in the y-direction
				case "gravity" :
				_gravity = pParameters[prop];
				break;

				// the initial x and y scale of a particle
				case "scale" :
				_scale = pParameters[prop];
				break;

				// the minimum initial x and y scale of a particle
				// use in conjuction with scaleMax
				case "scaleMin" :
				_scaleMin = pParameters[prop];
				break;

				// the maximum initial x and y scale of a particle
				// use in conjuction with scaleMin
				case "scaleMax" :
				_scaleMax = pParameters[prop];
				break;

				// the distance traveled by the particle per enter frame
				case "speed" :
				_speed = pParameters[prop];
				break;

				// acceleration of the particle in the x-direction
				case "xAccel" :
				_xAccel = pParameters[prop];
				break;

				// acceleration of the particle in the y-direction
				case "yAccel" :
				_yAccel = pParameters[prop];
				break;

				// the minimum initial x position to create a particle
				case "xMin" :
				_xMin = pParameters[prop];
				break;

				// the maximum initial x position to create a particle
				case "xMax" :
				_xMax = pParameters[prop];
				break;

				// the minimum initial y position to create a particle
				case "yMin" :
				_yMin = pParameters[prop];
				break;

				// the maximum initial y position to create a particle
				case "yMax" :
				_yMax = pParameters[prop];
				break;

				// the left "wall" of the emitter
				case "xMinBound" :
				_xMinBound = pParameters[prop];
				break;

				// the right "wall" of the emitter
				case "xMaxBound" :
				_xMaxBound = pParameters[prop];
				break;

				// the "ceiling" of the emitter
				case "yMinBound" :
				_yMinBound = pParameters[prop];
				break;

				// the "floor" of the emitter
				case "yMaxBound" :
				_yMaxBound = pParameters[prop];
				break;

				// if true, the particle will fade away in proportion
				// to the particles age and life
				case "fade" :
				_fade = pParameters[prop];
				break;

				// if true, the particle will shrink away in proportion
				// to the particles age and life
				case "shrink" :
				_shrink = pParameters[prop];
				break;

				// the amount of velocity retained when a particle
				// hits a wall, ceiling or floor
				case "restitution" :
				_restitution = pParameters[prop];
				break;

				// the initial direction of a particle in degrees or
				// "random" string
				case "direction" :
				// check for random string
				if (pParameters[prop] == "random") {
					_randomDirection = true;
				} else {
					if (typeof(pParameters[prop]) == "number") _direction = pParameters[prop];
				}
				break;
			}
		}
	}

	//--------------------------------------
	//  GET / SET
	//--------------------------------------

	/**
	 *	@private
	 */
	public function get numParticles():Number
	{
		return _particles.length;
	}

	//--------------------------------------
	//  PUBLIC METHODS
	//--------------------------------------

	/**
	 *	Starts the emitter.
	 */
	public function start():Void
	{
		_emitter.onEnterFrame = Delegate.create(this, update);
	}

	/**
	 *	Stops the emitter.
	 */
	public function stop():Void
	{
		delete _emitter.onEnterFrame;
	}

	//--------------------------------------
	//  PRIVATE & PROTECTED INSTANCE METHODS
	//--------------------------------------

	/**
	 *	Step update method.
	 */
	private function update():Void
	{
		// update all particles
		var i:Number = _particles.length;
		while (i--) {
			_particles[i].update();
		}

		// create particles specified in birth rate

		var particle:MovieClip;
		var d:Number;
		var startX:Number;
		var startY:Number;
		var scale:Number;
		i = _birthRate;
		while (i--) {
			d = (_randomDirection) ? Math.round(Math.random() * 360) : _direction;
			startX = _xMin + Math.round(Math.random() * (_xMax - _xMin));
			startY = _yMin + Math.round(Math.random() * (_yMax - _yMin));

			if (_scaleMin != undefined && _scaleMax != undefined) {
				scale = _scaleMin + Math.round(Math.random() * (_scaleMax - _scaleMin));
			} else {
				scale = _scale;
			}

			// resuse dead particles first
			if (_particles[_oldestIndex].dead) {
				particle = _particles[_oldestIndex];
				particle.revive();
				particle._x = 0;
				particle._y = 0;
				if (_oldestIndex + 1 < _particles.length) {
					_oldestIndex++;
				} else {
					_oldestIndex = 0;
				}
			} else {
				// create a new particle
				particle = _emitter.attachMovie(_particleID, _particleID + _particles.length, _emitter.getNextHighestDepth(), {direction:d, speed:_speed, scale:scale});
				_particles.push(particle);
			}

			// set particle properties
			particle.life = _life;
			particle._x = startX;
			particle._y = startY;
			particle.xAccel = _xAccel;
			particle.yAccel = _yAccel + _gravity;
			particle.fade = _fade;
			particle.shrink = _shrink;

			particle.xMinBound = _xMinBound;
			particle.xMaxBound = _xMaxBound;
			particle.yMinBound = _yMinBound;
			particle.yMaxBound = _yMaxBound;
			particle.restitution = _restitution;
		}
	}
}

Usage

First add the particle system package to the class path in the Publish Settings or ActionScript 2.0 preferences. Create a particle MovieClip in Flash and point its linkage class to the Particle class location.

particle-symbol

Create an empty MovieClip that will serve as the particle container where all particles instances will be created. Create an instance of the Emitter and assign the empty container MovieClip as the first argument and the linkage identifier of the particle symbol as the second argument. Don’t forget to import the particle system package (coined speck). Use the start and stop methods of the Emitter object to control the system.

import com.jasonwoan.speck.*;

var e:Emitter = new Emitter(emitterMC, "FireParticle");
e.start();

This is the most basic usage of this particle system. To customize the default emitter, you’ll want to add an object of parameters as a third argument when instantiating an Emitter. Please refer to the comments in the Emitter class for a description of valid parameters and their effects. Try messing around with the sample usage at the beginning of the post.

Finally

This is by no means a robust particle system. Ideally, I’d like to create a ParticleSystem object that would manage any number of Emitters and apply global forces like gravity and surface friction. I would also add a cue parameter so an Emitter can be tweened. I’ll save most of those updates for the AS3 version due to performance issues with AS2. This particle system library is lightweight and will probably serve well for simple particle effects.

I came across the following “gotcha” during development:

In ActionScript 2.0, an array used as an instance property should be initialized inside the constructor or any subsequent method and NOT when declaring the instance variable. When you initialize and declare and array instance property the value seems to be treated globally and multiple instances of that object will share the value even though it is access modifier is set to private. For example:

Bad

class MyClass{
	private var myArray:Array = [];

	public function MyClass() {

	}
}

Good

class MyClass{
	private var myArray;

	public function MyClass() {
		myArray = [];
	}
}

I’ll post the AS3 version as soon as I finish it. Thanks for reading and comments/questions/suggestions/recommendations are appreciated.

Download Source