FFParticleSystem

author:
Michael Trenkler
description:
A particle system based on the original with some new features and various performance improvements
compatible:
v1.4 (should work in older versions though)
tag:
particles, effects, 71squared
homepage:
https://github.com/shin10/Starling-FFParticleSystem
download:
https://github.com/shin10/Starling-FFParticleSystem/archive/master.zip

Overview

live demo

This is particle system is based on and compatible to the original but provides some additional features and various performance improvements. Thanks a lot to Colin Northway (Incredipede) sponsoring this extension created for his upcoming game.

Important: I did anything to make the code as fast as possible but to get the best performance I strongly recommend using the ASC 2.0 compiler and setting it up for [inline] functions.

Feature Overview:

Additional information about the new features and the live demo (~ 5 MB) can be found on my page.

Configuration

Like it's ancestor it's configured with XML files (.pex) which can be created comfortible with tools like the following.

Particle Designer from 71squared

For some of the new features however you'll have to modify your XML by hand or set the properties with AS3.

Usage

The system is similar to the original PDParticleSystem, but there are some minor differences. To get started you'll have to know a little bit about the two main classes.

Since reading XMLs is very slow (~400x slower then an Object) FFParticleSystem will not be configured directly. Instead you will parse the config, texture and atlasXML if necessary through SystemOptions which can be reused, cloned, modified and so on. Further the SystemOptions will prepare lookup tables for animated textures etc. That way the instantiation of a FFParticleSystem is much faster.

Before you create your first FFParticleSystem call the static init() function to create a particle pool and the vertex buffers. Setting this up properly can give you a massive performance boost. Read more about this in the section about batching below.

A basic example:

// embed configuration XML
[Embed(source="fire.pex", mimeType="application/octet-stream")]
private static const FireConfig:Class;
 
// embed particle texture
[Embed(source = "particle.png")]
private static const ParticleTexture:Class;
 
// instantiate embedded objects
var psConfig:XML = XML(new FireConfig());
var psTexture:Texture = Texture.fromBitmap(new ParticleTexture());
 
// create system options
var sysOpt:SystemOptions = SystemOptions.fromXML(psConfig, psTexture);
 
// init particle systems once before creating the first instance
// creates a particle pool of 1024
// creates four vertex buffers which can batch up to 512 particles each
FFParticleSystem.init(1024, false, 512, 4);
 
// create particle system
var ps:FFParticleSystem = new FFParticleSystem(sysOpt);
ps.x = 160;
ps.y = 240;
 
// add it to the stage (juggler will be managed automatically)
addChild(ps);
 
// change position where particles are emitted
ps.emitterX = 20;
ps.emitterY = 40;
 
// start emitting particles
ps.start();
 
// emit particles for two seconds, then stop
ps.start(2.0);
 
// stop emitting particles; on restart, it will continue from the current state
ps.pause();
 
// it will continue from the current state
ps.resume();
 
// stop emitting particles; on restart, it will start from scratch
ps.stop();

Batching and multi buffering

Batching is one of the new features this extension comes with. I assume you are familiar with the general requirements. In addition to those only FFParticleSystems can get batched with each other. They have to share the same parent and be siblings next to each other.

Important to know is that the particles get batched into a number of static buffer shared among all systems and created by a single call of the static init() function where the size and number of buffers will be set. So cutting it short, the buffer has to be big enough to hold the batched particles. This is made by design to avoid recreation and garbage collection. A low value may reduce batching abilities, a high value will result in increased upload time and a higher amount of memory. So choosing a reasonable value is important.

The last parameter of the init() function is the number of buffers. If you upload a buffer to the GPU and draw it, it will be locked for writing until the GPU is done with the complete frame (including all other draw calls). That means that if you want to write again to it, you'll have to wait until the image has been presented by your graphics card. The buffers will most likely not be freed for writing until the next frame but one. That's why we cycle through a Vector of buffers. This way we can avoid stalling and run additional AS3 code instead of waiting for the GPU to be done. Try to set this value to the number of draw calls caused by FFParticleSystem instances per frame multiplied by two. For example: If you have a maximum of three particle systems, further #1 and #2 will get batched; this results in a total of 2 calls of drawTriangles(); so we'll set the number of buffers to a minimum of 4 for double buffering. Of course memory on mobile devices is sacred so you'll have to trade wisely between your memory and performance.

Animations and random start frames

Using animated textures and/or random frames is simple. Just add an animation snippet like the following to your .pex or set the values for your SystemOptions directly with AS3.

<particleEmitterConfig>
  <animation>
	  <isAnimated value="1"/>
	  <randomStartFrames value="1"/>
	  <loops value="15"/>
	  <firstFrame value="bird_0"/>
	  <lastFrame value="bird_15"/>
  </animation>
  ...
</particleEmitterConfig>

All you have to do besides that is feeding the SystemOptions with the xml file for the texture atlas. That way it can look up the frame information and will create a LUT for the frames, cache and pass it on to the particle system.

// embed configuration XML
[Embed(source="fire.pex", mimeType="application/octet-stream")]
private static const FireConfig:Class;
 
// embed particle texture
[Embed(source = "particleAtlas.png")]
private static const ParticleAtlasTexture:Class;
 
// embed configuration XML
[Embed(source="particleAtlas.xml", mimeType="application/octet-stream")]
private static const ParticleAtlasXML:Class;
 
// instantiate embedded objects
var psConfig:XML = XML(new FireConfig());
var psTexture:Texture = Texture.fromBitmap(new ParticleAtlasTexture());
var psAtalasXML:XML = XML(new ParticleAtlasXML());
 
// create system options
// the atlasXML is necessary for particles with animated texture
var sysOpt:SystemOptions = SystemOptions.fromXML(psConfig, psTexture, psAtlasXML);
 
var ps:FFParticleSystem = new FFParticleSystem(sysOpt);
 
...

Optional custom sorting, code and variables

Often you won't have to care about the depth index of your particles. Many effects like fire etc. do not need this because of the used blend mode. For all other cases where the index matters you'll have to sort the particles yourself. Additionally you can use this functions to add custom code for your personal needs.

If a particle get's removed it will be swapped with the last active particle and deactivated. Let's say you're using blend mode “normal” this will lead to strange behavior. Custom sorting can help you out but also has it's pitfalls. Even though I have added the following versions to my demo code, I want to clarify that only the last is a good choice and the others have only been added for better understanding.

This example will totally fail:

	sysOps.sortFunction = ageSortDesc;
 
...
 
// super bad!
private function ageSortDesc(a:Particle, b:Particle):Number
{
	if (a.currentTime < b.currentTime)
		return 1;
	if (a.currentTime > b.currentTime)
		return -1;
	}
	return 0;
}

Since the complete Vector of particles (active and inactive) will be sorted we have to check whether the particle is alive to avoid an even crazier behavior.

This example is working but performing poor:

	sysOps.sortFunction = ageSortDesc;
 
...
 
// bad!
private function ageSortDesc(a:Particle, b:Particle):Number
{
	if (a.active && b.active)
	{
		if (a.currentTime < b.currentTime)
			return 1;
		if (a.currentTime > b.currentTime)
			return -1;
	}
	else if (a.active && !b.active)
	{
		return -1;
	}
	else if (!a.active && b.active)
	{
		return 1;
	}
	return 0;
}

That's a lot of conditionals for comparing a simple value. Luckily there is a much faster way by preparing the inactive particles for sorting.

So here is a much better example:

	sysOps.customFunction = customParticleFunction;
	sysOps.sortFunction = ageSortDesc;
 
...
 
private function customParticleFunction(particles:Vector.<Particle>, numActive:int):void
{
	// optional custom code for active particles
	var p:Particle;
	for (var i:int = 0; i < numActive; ++i)
	{
		p = particles[i];
		...
		// p.customVariables can be used to store additional data
	}
 
 
	// preparations for sorting according to age!
	var len:int = particles.length;
	for (i = numActive; i < len; ++i)
	{
		// set current time to a high value, keeping them at the end of the Vector
		particles[i].currentTime = Number.MAX_VALUE;
	}
}
 
private function ageSortDesc(a:Particle, b:Particle):Number
{
	if (a.currentTime > b.currentTime )
		return -1;
	if (a.currentTime < b.currentTime )
		return 1;
	return 0;
}

Sorting particles based on their age is only necessary if new particles get added/removed, so this can stay as it is. If you're sorting on another property which might change at any time you can set the forceSortFlag property to true.

Changelog

Source Code

You can browse the source code of the particle system on its GitHub page.

User Comments