Playing With Particles Part 2
In part 1, we used a DisplayObject (a Shape) for our particles. Now let’s get into single pixel particles; and go from 300 particles to 10,000 or more.
Firstly, i need to point out that the following wisdom is not my own; all that i am about to tell you, i learnt from a great session by Ralph Hauwert at FFK10. With the formalities dealt with, lets get on with the show…
To display thousands of particles (at a decent frame rate) we need to do things a bit differently to part 1; instead of a separate DisplayObject for each particle, we use a single Bitmap and manipulate the pixels of its bitmapData property as our particles. In addition, there are also some important changes in the way we handle the particles; resulting in faster code (important when looping thousands of times!).
Here’s a simple example with 10,000 particles (all examples have a FPS counter in the bottom-right corner to check performance; all swfs published at 30fps):
{PlayingWithParticlesExample7}
As before, we have a separate class for our particle:
package parts { final public class Particle7 { public var next:Particle7; public var x:Number; public var y:Number; public var dx:Number; public var dy:Number; public var colour:uint; public function Particle7() { //set initial properties x = 200; y = 200; var angle:Number = 2*Math.PI*Math.random(); var speed:Number = 0.1*Math.random()+1; dx = speed*Math.sin(angle); dy = speed*Math.cos(angle); colour = 0xFFFFFF*Math.random(); } } }
This time however, the class is used purely as a Value Object (it is used only to store the particle information; there is nothing visual) and does not extend any other class.
There are 3 things to point out here; firstly, the class is defined with the keyword ‘final’ – this tells Flash that the class will not be extended by a subclass, and means Flash (Player) runs this code slightly faster. Secondly, the class has a ‘next’ property of type Particle7 – this allows each particle to become part of a linked list and will allow us to loop through the particles quickly (more on that later). Thirdly, all properties (except next) are now simple datatypes (ie: no Vector3D or Point types); again, this is for efficiency – it is quicker to do this: dx = 20; than this: velocity.x = 20; (velocity is now dx and dy).
Here’s the main class:
package { import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.Sprite; import flash.events.Event; import flash.geom.Rectangle; import parts.FpsCounter; import parts.Particle7; [SWF(backgroundColor="#000000", frameRate="30", width="400", height="400")] public class PlayingWithParticlesExample7 extends Sprite { private const BMD_RECT:Rectangle = new Rectangle(0, 0, 400, 400); private const PARTICLE_NUMBER:uint = 10000; private var firstParticle:Particle7; private var bitmapData:BitmapData; public function PlayingWithParticlesExample7() { super(); init(); } private function init():void { //add bitmap to stage bitmapData = new BitmapData(BMD_RECT.width, BMD_RECT.height, false, 0x333333); addChild(new Bitmap(bitmapData)); //create linked list of particles var i:int = PARTICLE_NUMBER; var particle:Particle7; var nextParticle:Particle7; //repeat until i is false (less than zero) while (--i) { particle = new Particle7(); particle.next = nextParticle; nextParticle = particle; } firstParticle = particle; //add debug info to bottom right var fps:FpsCounter = new FpsCounter(); fps.x = BMD_RECT.width-fps.width; fps.y = BMD_RECT.height-fps.height; addChild(fps); //start looping addEventListener(Event.ENTER_FRAME, loop); } private function loop(event:Event):void { //lock and clear bitmapdata bitmapData.lock(); bitmapData.fillRect(BMD_RECT, 0x333333); //read data from bitmap var v:Vector. = bitmapData.getVector(BMD_RECT); //loop through the particles var particle:Particle7 = firstParticle; while (particle) { //update position particle.x += particle.dx; particle.y += particle.dy; //check for edge collision (and rebound if hit) if (particle.x<0) { particle.x = -particle.x; particle.dx = -particle.dx; } else if (particle.x>BMD_RECT.width) { particle.x = BMD_RECT.width-(particle.x-BMD_RECT.width); particle.dx = -particle.dx; } if (particle.y<0) { particle.y = -particle.y; particle.dy = -particle.dy; } else if (particle.y>BMD_RECT.height) { particle.y = BMD_RECT.height-(particle.y-BMD_RECT.height); particle.dy = -particle.dy; } //update data for pixel v[BMD_RECT.width*int(particle.y)+int(particle.x)] = particle.colour; //move to next particle particle = particle.next; } //write data back to bitmap bitmapData.setVector(BMD_RECT, v); //unlock bitmapdata bitmapData.unlock(); } } }
In the init() method, we setup a Bitmap (same size as the swf) and then create the particles as a linked list; instead of using an Array or Vector to store a reference to each particle, we just store a reference to a single particle (firstParticle). This particle contains a reference to the next particle (in its next property) and so on through all particles (the last particle has a next value of null). Next, we add the fps counter and start the frame loop running.
In the loop() method, we first lock the BitmapData we are going to manipulate (improves performance) and then fill it with a dark grey. Next, we copy the BitmapData (just the dark grey in this case) into a Vector object with getVector(). We use the constant BMD_RECT through-out as this is the most efficient way (as compared to calling bitmapData.rect everytime).
Now we loop through the particles – note the last line of the loop: particle = particle.next; when particle.next is null the loop will end. We update the x and y positions, check whether an edge has been hit (and bounce) and then update the appropriate item in our Vector with the particle colour. After the loop, we write the Vector back to bitmapData and unlock it.
As with everything else, we use getVector() and setVector() instead of directly updating pixels with bitmapData.setPixel() because setPixel() is a relatively slow process; get/setVector is quicker.
The behaviour of the particles above is very simple and hence doesn’t require much code. This is clearly important – we are running the code within the while loop 10,000 times on each frame so it needs to be quick. If we want more complex behaviour then we need to cheat to keep things running quickly:
{PlayingWithParticlesExample8}
First, we create a bitmapData object, fill it with Perlin Noise and save it to a Vector:
var mapBmd:BitmapData = new BitmapData(BMD_RECT.width, BMD_RECT.height); mapBmd.perlinNoise(BMD_RECT.width, BMD_RECT.height, 2, 2, true, false, BitmapDataChannel.GREEN|BitmapDataChannel.BLUE); map = mapBmd.getVector(BMD_RECT);
In the loop, we calculate velocity based on the colour of the pixel in the map Vector:
var mapCol:uint = map[BMD_RECT.width*int(particle.y)+int(particle.x)]; var mapX:uint = mapCol>>8 & 0xFF; var mapY:uint = mapCol & 0xFF; particle.x += (mapX-127.5)/64; particle.y += (mapY-127.5)/64;
First we get the colour of the pixel at position x,y in the map Vector. Then we extract the green channel value into mapX and the blue channel value into mapY. Lastly, we update the particles x and y positions based on these values. This example uses the same calculation as the DisplacementMapFilter i discussed in a previous post – many different behaviours can be achieved by substituting different mapping data or by acting upon it in a different way:
{PlayingWithParticlesExample9}
var mapCol:uint = map[BMD_RECT.width*int(particle.y)+int(particle.x)]; var mapX:uint = mapCol>>8 & 0xFF; var mapY:uint = mapCol & 0xFF; particle.dx += (mapX-127.5)/512; particle.dy += (mapY-127.5)/512; particle.dx *= 0.98; particle.dy *= 0.98; particle.x += particle.dx; particle.y += particle.dy;
This example is very similar to example 8 except that we use the mapping values to provide the acceleration rather than the velocity, and instead of Perlin Noise we use the bitmap on the right.
Note that we also reduce (dampen) the velocity (particle.dx *= 0.98) to stop particles flying off too fast. The mapping calculation interprets the bitmap colours as follows; dark blue = accelerate to the left, light blue = accelerate down, light green = accelerate to the right and dark green = accelerate upwards. This way we can give our particles complex paths to follow without a large expensive calculation being required.
And Finally
Hopefully this is enough to start you off. For more, i suggest you watch Ralph’s session at FFK10 and also visit FlashAndMath.com where they have lots of examples that use clever mapping ideas to give great effects.
SOURCE CODE: PlayingWithParticles2.zip
nice job!!!