Shoot Out Stars with the Stardust Particle Engine

In this tutorial I will introduce to you the Stardust Particle Engine. First I'm going to show you how to set up Stardust, and then I'll cover the basic Stardust class responsibilities and how they collaborate together to make Stardust work as a whole.

Next, we will look at a Stardust's general workflow and get down to creating a particle effect with stars shooting out from the mouse cursor; the stars will slow down gradually, grow larger after birth, and shrink when dying.

Finally, I'll demonstrate the flexibility of Stardust by creating several variations from the already complete example, including using animated movie clips as particles, variable particle simulation timescale, and shooting out display objects of different classes from a single emitter.

This tutorial is meant for people that are already familiar with ActionScript 3.0 object-oriented programming (OOP), so I'm assuming that you already know very well what classes, objects, inheritance, and interface mean. No problem with OOP? Then let's go shoot some stars!

Stardust Particle Engine

As its name suggests, Stardust is used for creating particle effects. If you're an experienced ActionScripter, you might have created particle effects from scratch many times, and say "I'm totally cool with creating particle effects from scratch, so why would I need a particle engine anyway?" Well, Stardust is here to help you focus more on actual particle behavior design than worrying about the tedious underlying low-level stuff, such as memory management. Instead of writing code to take care of particle data, initializing and disposing resources, with Stardust, you get to skip these boring routines and just decide how you want your particles to behave.

Stardust Features

The class structure of Stardust was inspired by FLiNT Particle System, another ActionScript 3.0 particle engine. Thus, they share some similar basic features.

2D and 3D Particle Effects - Stardust can be used to create both 2D and 3D particle effects. It has its own built-in 3D engine, and it also can be used to work in combination with other third-particle 3D engines, including ZedBox, Papervision3D, and ND3D.

High Extensibility - Stardust provides a large set of particle behaviors and renderers at your disposal. If none of them fit your needs, you can always extend the base classes and create your own custom particle behaviors; also, you can create your own renderer to work with another 3D engine that is not supported by Stardust.

In addition to these basic features, Stardust also provides several advanced features for experienced users.

Adjustable Simulation Timescale - The timescale used for particle simulation can be dynamically adjusted during run-time. For instance, if you change the timescale to half of the original, the particle effect will be half as fast as normal speed; and if you tweak the timescale to twice the original, the particle effect is going to be simulated twice as fast as normal. This feature can come in handy when you are creating a game that has slow-motion effects: the particle effect can slow down to match your game engine's speed, synchronizing with your game animation and graphics.

XML Serialization - You can transform your particle system into a file in XML format that can be stored on your hard drive, later loaded during run time and interpreted to reconstruct your orignal particle system. This is very useful when you're working with a large project. Say you just want to size up your particles a little bit, so you tweak the parameters in the source code and recompile your entire Flash application, which might take a minute, or even over five minutes if your project is extremely huge. Is it worth it? Absolutely NO. That's a total waste of time. Using the XML serialization feature to save your particle system in external XML files, you are separating parameters from the source code for the main application. So what you'll have to do is simply open the XML file, change the parameter values, save it, and reopen your main application. That's it! No recompilation is required at all. I would say this is the ideal way to work with large projects.

When it comes to particle effects, it's very important to handle massive particle data efficiently. Stardust makes heavy use of object pools and linked lists to enhance performance:

Object Pools - Used objects are stored into a pool; later, if an object of the same type is required, Stardust doesn't instantiate it immediately, but checks if there's any object previously stored in the object pool left. If yes, Stardust simply takes out that object and uses it, instead of creating a whole new object. Usually particle effects involve a lot of object instantiation, which is CPU-consuming. By using object pools, Stardust greatly reduces the instantiation overhead.

Linked Lists - It's very easy and tempting to store particle data into an array; however, in a case when particles are created and removed very frequently, a lot of array splicing takes place to remove dead particles. Array splicing is a CPU-consuming process, especially for long arrays. For a linked list, no matter how long the list is, it always takes the same short amount of time to splice out dead particles. From version 1.1, Stardust began to use linked lists internally to store particle data.

Setting Up Stardust

Before we get down to actual coding, we'll need to grab a copy of Stardust Particle Engine. It's released under MIT license, which means it's totally free of charge no matter if you want to use it in a commercial or non-commercial project.

At the time of writing, the latest version that can be downloaded from the download list is 1.1.132 Beta. You can always grab the latest revision from the SVN repository (which might not be stable, however).

Stardust Class Responsibilities

Here I'm going to briefly cover the Stardust core classes and their responsibilities.

StardustElement

This class is the superclass of all core classes, which defines properties and methods especially for XML serialization.

Random

Generally speaking, particle effects are all about controlling an amount of entities with similar but randomized appearance and behaviors. The Random class is for generating random numbers, which can be used in Stardust for randomizing particle properties. For instance, the UniformRandom class is a subclass of the Random class, and its name tells everything: the random number generated by a UniformRandom object is uniformly distributed, and I'll be using this class in particular for the entire tutorial.

Zone

There are times when a one-dimensional random number is not enough. Sometimes we need two-dimensional random numbers, which are essentially pairs of random numbers, for properties like position and velocity. The Zone class is for generating two-dimensional random number pairs. This class models a random number pair as a random point in a 2D zone. For instance, the CircleZone generates random number pairs (x, y) from random points within a circular region. The Random and Zone classes are mainly used by the Initializer class, which will be covered later. The Zone3D class is the 3D counterpart of this class, for 3D particle effects.

Emitter

The Emitter class is basically where all the low-level stuff is encapsulated. An emitter initializes newly created particles before they are added into the simulation, updates particle properties in each main loop iteration, and removes dead particles from the simulation. The Emitter.step() method is what you want to invoke repeatedly in order to keep Stardust up and running.

Clock

The Clock class determines the rate of new particle creation for emitters. One Emitter object holds exactly one reference to a Clock object. At the beginning of each Emitter.step() method call, the emitter asks the clock object how many new particles it should create. Take the SteadyClock class for example, it tells emitters to create new particles at a constant rate.

Initializer

This class is for initializing newly created particles. An Initializer object needs to be added to an emitter for it to work. Basically, one Initializer subclass initializes just one particle property. For instance, the Mass initializer class initializes the mass of new particles. Some initializers accept a Random object as a constructor paramter for initializing particles with randomized values. The following code creates a Life initializer which initializes particle lives to values centered at 50 with variation of 10, namely between the range of 40 to 60.

new Life(new UniformRandom(50, 10));

Action

Action objects update particle properties in each iteration of the main loop (the Emiter.step() method). For example, the Move action class updates particle positions according to velocity. An Action object must be added to an emitter for it to work.

General Stardust Workflow

Now that you know how the core classes collaborate together, let's take a look at a general workflow for Stardust.

You start by creating an emitter. Use the Emitter2D class for 2D particle effects and the Emitter3D class for 3D effects.

var emitter:Emitter = new Emitter2D();

To specify the rate of particle creation, we need a clock. This can either be set by the Emitter.clock property or by passing a clock as the first parameter to the emitter's constructor.

Finally, repeatedly call the Emitter.step() method to keep the particle simulation going. You might want to use the enter-frame event or a timer to do this. In a single call of the Emitter.step() method, the clock determines how many new particles should be created, these new particles are initialized by initializers, all particles are updated by actions, dead particles are removed, and finally, the renderer renders the particle effect.

Alright. That's pretty much all of it for Stardust primer. Now it's time to open the Flash IDE and get your hands dirty.

Step 1: Create a New Flash Document

Create a new Flash document with a dimension of 640X400 a frame rate of 60fps, and a dark background. Here I made a dark-blue gradient background. By the way, Stardust works well with both Flash Player 9 and 10, so it's okay no matter you're using Flash CS3 or CS4. In this tutorial I'll be using Flash CS3.

Step 2: Draw a Star

We're creating a particle effect with stars, so we'll need to draw a star and convert it to a symbol, exported for ActionScript of course. This symbol will be used later to render our particle effect. Name the symbol and the exported class "Star".

Step 3: Create the Document Class

Step 4: Extend the Emitter

As mentioned in the general workflow, the first step is to create an emitter. And the next step is adding initializers and actions to the emitter. While this can be done in the document class constructor, I strongly recommend that it be done in a separate Emitter subclass. It's always better to separate the particle behavior design form the main program; by doing so, the code is much cleaner and easier to modify in the future, without being mixed up with the main program.

We are going to create a 2D particle effect, so the Emitter2D is the emitter class we're going to extend. Extend the Emitter2D class and name it StarEmitter, since we are going to make it shoot out stars later. The Emitter constructor accepts a Clock parameter, so we'll declare a constructor parameter to pass on a Clock object reference to the superclass's constructor.

Step 5: Declare Constants

A better approach to create an emitter subclass is to declare particle parameters as static constants, grouped at a single place. So in case you want to tweak the parameters, you'll always know where to find the declarations. The meaning of these constants will be explained later when they are used.

Step 6: Adding Initializers

Which initializers do we need to create our particle effect? Let's take a look at the list below:

DisplayObjectClass - This initializer assigns a specified display object to each particle, which will be used by a DisplayObjectRenderer to render particle effects. The constructor accepts a class reference to the display object class we wish to instantiate; for this tutorial, this class will be the Star class (symbol) we created in Step 2.

Life - This initializer assigns each particle a random life value. Later, we'll add actions to the emitter to deplete this life value through time, and to mark a particle as dead if its life value reaches zero. A Random object is passed to the constructor, which will be used by this initializer to generate random value for particles' life. For most cases, the UniformRandom class is convenient and sufficient; the first parameter of the UniformRandom constructor is the center (or average) value of the random numbers generated, and the second is the radius (or variation). For instance, a UniformRandom object with center 20 and variation 5 generates random numbers within the [15, 25] range. Here we use the LIFE_AVG constant for the center value and LIFE_VAR for the radius.

Scale - Like the Life initializer, the Scale initializer initializes a particle's scale to a random value, determined by a Random object passed to the initializer's constructor. Here we use the SCALE_AVG constant for the center value and SCALE_VAR for the radius.

Position - This initializer assigns a particle a random position. Unlike the Life and Scale initializers, which need only 1D random numbers, the Position initializer requires 2D random number pair generators. As described in the Stardust Class Responsibilities section, the Zone class is exactly for this purpose. The Zone object passed to the initializer's constructor is used to generate 2D random number pairs, which will be assigned to particles as position vectors. In this tutorial we're going to make stars shoot out from a single point located at the mouse cursor, so we'll use a SinglePoint class, which is a Zone subclass. In order to dynamically adjust the coordinate of this SinglePoint object from the document class, we need to expose a reference to this point object through a public property. This is what the "point" property is for.

Velocity - Same as the Position initializer, the Velocity initializer needs a Zone object to generate 2D random value pairs to initialize particle velocities. A 2D vector generated by the Zone object, which is the coordinate of a random point in the zone, is assigned to particles as their velocities. Here we use the LazySectorZone class that represents a sector region. A sector is a portion of a circle enclosed by two radii and two angles. For the LazySectorZone, the two angles are 0 and 360 by default, representing a full angle around a circle. The first constructor parameter of the LazySectorZone class is the average of the two radii, and the second is the variation of the radii. In this case, the average of the two radii represents the average speed, and the variation of radii represents variation of speed. Here we use the SPEED_AVG constant for the first parameter and SPEED_VAR for the second.

Rotation - The Rotation initializer initializes a particle's rotation angle to a random value. And as some of the aforementioned initializers, the constructor accepts a Random object to generate a random value. Since we'd like to have particles with angles ranging from 0 to 360 degrees, we'll use 0 as the center and 180 as the radius of the UniformRandom object.

Omega - Omega, as in most of the physics textbooks, means angular velocity. With that said, the purpose of this initializer is clear: it initializes a particle's angular velocity to a random value, and the OMEGA_AVG constant is used as the center and OMEGA_VAR as the radius of the UniformRandom object.

Step 7: Adding Actions

Okay, we're done with the initializers. Now it's time to add actions to the emitter. Below is a list of actions we need:

Age - The Age action decreases a particle's life value by 1 in each emitter step.

DeathLife - When a particle's life value reaches zero, this action marks the particle as dead, changing its isDead property from false to true. At the end of an emitter step, dead particles are removed.

Move - Pretty much as its name suggests, the Move action updates particle positions according to their velocities.

Spin - Similar to the Move action, the Spin action updates a particle's rotation angle according to the particle's omega value (angular velocity).

Damping - This action multiplies a particle's veloctiy with a factor within the range [0, 1], simulating damping effects and gradually slowing down particle. A factor of one means no damping at all: particles move freely as if there were no damping effect; a factor of zero means total damping: all particles cannot move a bit. This factor is determined by the "damping coefficient" through this formula: "factor = 1 - (damping coefficient)". The parameter passed to the constructor is the damping coefficient; here we just want a little damping effect, so we use the value 0.1 for the coefficient.

ScaleCurve - The ScaleCurve action changes a particle's scale according to its life value. It grows from an initial scale to a normal scale after birth, and fades to a final scale as it dies. Of course, a particle can also have an initial or final scale value that is larger than the normal scale; it just depends on personal choice. In many cases, we'd like particles to have an initial and final scale value of zero, which is the default value. The first parameter in the constructor stands for a particle's growing time, and the second is the fading time; so we pass in the GROWING_TIME and SHRINKING_TIME constants as the first and second parameter, respectively. The growing time is 5, meaning a particle grows from zero scale to normal scale during its first 5 unit of lifespan; and the shrinking time is 15, which means a particle shrinks to zero scale at the last 15 unit of lifespan. Note that the transition is linear by default, but any easing fucntion can be used, specifically the easing equations created by Robert Penner. There's another similar action called AlphaCurve, which works on alpha values in the same way.

That's it. Our emitter is done. Here's the code for this emitter in its entirety, necessary import statements included.

Step 8: Finish the Document Class

Now it's time to go back to the document class and finish it up. Let's take a look at the remaining tasks.

Create a StarEmitter instance - We're going to instantiate the StarEmitter class we just finished.

Assign a Clock object to the emitter - We want a steady rate of particle emission, so we'll use the SteadyClock class. The parameter passed to the clock's constructor is the rate of emission, or, in other words, the number of new particles created in each emitter step; a fractional rate of 0.5 means in each emitter step, there's a 50% chance for a new particle to be created and 50% chance for no particle created.

Create a Renderer - To visualize the particle effect, we'll going to need a renderer. The DisplayObjectRenderer should be used in conjunction with the DisplayObjectClass initializer: the initializer assigns a display object to each particle, and the renderer adds these display objects to a container's display list, constantly updating them. Also, don't forget to add the emitter to the renderer.

Call the Main Loop Repeatly - This last step keeps Stardust up and running. Here we'll make use of the enter-frame event.

Below is the complete code for the document class, necessary import statements included.

Finally, we're done! Now let's take a look at the outcome. Press CTRL+ENTER in Flash to test the movie, and you'll see the result.

Variation 1: Animated Stars

We're not done yet! Let's do a few more variations. The first one is using animated movie clips for our particles.

Step 9: Create a Timeline Animation

This first variation is quite simple, not involving any extra coding. It's as simple as creating a basic timeline animation. Edit the Star symbol in Flash IDE, create another key frame, and change the star's color in this frame to red. This essentially causes the stars to blink between yellow and red. You might want to insert some more empty frames in between, since a frame rate of 60fps is too fast for a two-frame blinking.

Now test the movie and check the result. The blinking star effect looks cartoonish; this can be used for classic dizzy-star effects, which is commonly seen in cartoons.

Variation 2: Adjusting Timescale Dynamically

As I mentioned earlier, one of the Stardust feature is "adjustable simulation timescale", which means the timescale used by Stardust for particle simulation can be dynamically adjusted. All is done by changing the Emitter.stepTimeInterval property, which is 1 by default. The following code snippet changes this value to 2, resulting in particles moving twice as fast and emitter creating new particles at double rate.

emitter.stepTimeInterval = 2;

In this variation, we'll create a slider on the stage and use it to dynamically adjust the simulation timescale.

Step 10: Create a Slider

Drag a Slider component out from the Components Panel to the stage. Name it "slider".

Step 11: Setup Slider Parameters

We'd like to have the slider to slide between 0.5 and 2, which means we want our particle simulation to be at least half as fast as normal and at most twice at fast. Also, set "liveDragging" to true so we can see the update as we scrub the slider thumb.

Step 12: Slider Event Listener

Now we have to listen to the slider's change event to dynamically change the simulation timescale. First import the SliderEvent class in the document class.

import fl.events.SliderEvent;

And then listen to the slider's change event in the document class constructor.

slider.addEventListener(SliderEvent.CHANGE, changeTimescale);

Finally, change the simulation timescale in the listener. Add the following listener to the document class.

Let's look at the result. Notice that as you scrub the slider's thumb to the left, the particle simulation slows down, and if you scrub right the simulation goes faster.

Variation 3: Multiple Display Class

Stardust provides a special initializer called SwitchInitializer. Its constructor accepts two arrays as parameters; the first one is an array of initializers, and the second one, with the same length as the first, is an array of "weights". Each time Stardust uses this initializer to initialize particles, one of the initializers in the first array is picked randomly to initialize the particles. The more weight an initializer has, the more likely it'll be picked for initialization. In this variation, we're going to make the emitter shoot out not only stars but also white circles.

Step 13: Draw a Circle

Draw a white circle, convert it to a symbol, exported for ActionScript, and name the symbol and the class "Circle". This is the class we'll use for the white circles.

Step 14: Create the Switch Initializer

Open the StarEmitter class, and delete the following line. This disables the DisplayObjectClass initializer we set up in the previous steps.

addInitializer(new DisplayObjectClass(Star));

Now add the following code to the constructor. We'd like to have stars and circles to be emitted equally likely, so we'll give them both a weight of 1.

Finally, test the movie. You can see both stars and circles are emitted out from the emitter, although angular velocity doens't have much effect on circles.

End of Tutorial

This is the end of this tutorial. Thank you for reading! You might say it's a whole lot of code just to create such simple effect, but hey, do you remember writing any code other than designing the particle's behavior in high-level? In the future if you want to modify your code, would you like to see code like this:

Stardust is here to help you create particle effects at ease. Design particle behaviors in high-level, tell Stardust what to do, and let Stardust handle the low-level implementation for you!

I hope you have understood how Stardust works internally and are now familiar enough with the basic workflow. In the future, I'll write more tutorials on custom initializers/actions/renderers, 3D particle effects, and XML serialization. Thank you again for reading this tutorial. Happy Stardusting!