Advertisement
  1. Code
  2. Effects

Starling Particle Effects for Stage3D Shooter Games

Scroll to top
Read Time: 18 min
This post is part of a series called Shoot-'Em-Up.
Quick Tip: Add a Blurry Trail Effect to Your Bullets
Build a Stage3D Shoot-'Em-Up: Explosions, Parallax, and Collisions

I'm sure Stage3D is not foreign to most readers; it's the new API that gives AS3 programmers access to the GPU. However, coding against opcodes in Stage3D may not be everyone's preferred choice, so it's fortunate that there's a shortcut: Starling, a library developed to encapsulate this low level programming to make it much easier. And along with Starling comes its particle effects extension. In this tutorial, we'll check out the particle systems of this framework, and see its applications applied to a shoot-'em-up game.


Step 1: Basic Setup

Skip this step if you've been working with FlashDevelop for some time. For beginners, here's how you install a library package - in this case Starling and its particle extension. Note that these two items don't come in one package, so we'll have to download them separately.

First, download Starling framework and its particle extension from their repositories. Unzip upon successful download. Scan the first unzipped directory for src folder and paste the Starling framework library, highlighted in the image below, into your project source folder.

Install Starling libraryInstall Starling libraryInstall Starling library

Scan the second folder for the particle extension and combine them together. You can pull the highlighted folder below into the Starling folder. The image below is the end result you should arrive at.

Install particle extension onto Starling

For more info about FlashDevelop and using external libraries, see these tutorials:


Step 2: Main Classes

If you've not been introduced to Starling and its particle extension already, I strongly encourage a visit to Lee Brimelow's video tutorials on Starling and particle effects, and Matthew Chung's tutorial about handling animation states with Starling.

We're going to just breeze through the basics in two steps here. If you're already aquainted with Starling and its particle extension, feel free to jump to Step 4.

You can see from the second image of the previous step (the lower part) that two classes are being created: Main.as and Testing.as. The first acts as a launcher for the latter. So, most of our Starling code lives in Testing.as. I've highlighted the important code in Main.as here:

1
2
private function init(e:Event = null):void 
3
{
4
    removeEventListener(Event.ADDED_TO_STAGE, init);
5
    // entry point

6
    
7
    var myStarling:Starling = new Starling(Testing, stage);
8
    myStarling.simulateMultitouch = true;
9
    myStarling.start();
10
    
11
    //initiate Starling onto stage

12
    //allow mouse/ touch events to happen in Starling

13
    //turn key and start the engine!

14
}

... and Testing.as should look like this:

1
2
public class Testing extends Sprite
3
{
4
    public function Testing() {
5
        addEventListener(Event.ADDED_TO_STAGE, init);
6
    }
7
    private function init(e:Event):void {
8
        removeEventListener(Event.ADDED_TO_STAGE, init);
9
        // code goes here.

10
        
11
        stage.color = 0;	//turn stage colour to black

12
        
13
        //Draw a little quad on stage,

14
        //just to make sure everything's in place

15
        //Note the top left corner of sprite is aligned to middle of stage

16
        var q:Quad = new Quad(30, 30); addChild(q);
17
        q.color = 0xEEEEEE;
18
        q.x = stage.stageWidth >> 1; q.y = stage.stageHeight >> 1;
19
    }
20
}

And if everything is set up correctly, you should arrive at the result as shown below. Nothing much, just a sprite on the stage.

Note: Testing.as is extending Sprite from Starling.display.Sprite, not flash.display.Sprite. The classes have the same name, but are not the same.


Step 3: Particle Extension in Starling

The particle extension of Starling contains three important classes. Their functionalities are tabulated below.

Name of Class Functionality
Particle.as A single particle with unique attributes.
ParticleSystem.as Controls the flow of particles: generation, animation and recycling.
ParticleDesignerPS.as An extension of ParticleSystem.as to enable easy manipulation of particle system.

We shall create an instance of ParticleDesignerPS.as. This requires inputting the following arguments into the class constructor:

  • an XML file that contains all initiation value for particle properties (quite a number of them)
  • a image to sample for all particles

No worries, onebyonedesign.com will help you out with this. Visit their particle designer page and tweak all these initiation values to your heart's content. Then export all the data to ZIP format. This ZIP file will contain the XML file and the image for the particle effect you just designed through their webpage!

Save particle details.Save particle details.Save particle details.

Unzip and pull all these into your source folder in FlashDevelop. See the highlighted items in image below.

Integrate with source folder.

Generate import statements to get these two items into your Testing class. You will also need to rewrite the init method in Testing. They are all below.

1
2
[Embed(source="particle.pex", mimeType="application/octet-stream")]
3
private var InitValues:Class
4
5
[Embed(source = "texture.png")]
6
private var Sample:Class
1
2
private function init(e:Event):void {
3
    removeEventListener(Event.ADDED_TO_STAGE, init);
4
    // code goes here.

5
    
6
    stage.color = 0;	//turn stage colour to black

7
    var flow1:ParticleDesignerPS = 
8
    new ParticleDesignerPS(
9
            XML(new InitValues()), 
10
            Texture.fromBitmap(new Sample())
11
        );
12
    addChild(flow1);
13
    flow1.emitterX = stage.stageWidth >> 1; flow1.emitterY = stage.stageHeight >> 1;
14
    flow1.start();
15
    Starling.juggler.add(flow1);
16
}

Here's the result you should arrive at. Rather simple, right?


Step 4: Understanding the Particle System

Our next step is to enable interaction with the particle system (that little fire) at runtime. We'll use the mouse to control the properties of fire. However before we do, I'd like to sidetrack a little to brief you on the concept of particle systems.

Particles are just sprites that get emitted from a coordinate. After birth, they will animate in a certain pattern. This pattern can be unique for each particle or common to all particles. But rest assured, their physical properties will change over time. For example:

  • Distance from birth coordinate
  • Direction and speed
  • Color and alpha level
  • Size

Also, their life on the stage is determined at birth. If each particle is allowed to live forever on stage, we'll have overpopulation and the application performance will suffer due to having a lot of graphical assets to handle. At some point, the particle dies but it doesn't get shovelled off the stage. It's recycled instead, by being relocated to a birth coordinate and assume the role of a new particle. It's a new particle because a new set of properties will be defined for it then before its animation kicks in and another cycle continues.

(This is object pooling, and you can see how to apply it to your own non-Starling Flash projects here.)

Okay, so how does this relate to our fire? Well, we can use easing functions to animate properties of this fire over time. Within the Starling framework, ParticleDesignerPS is positioned at the end of this hierarchy:

ParticleDesignerPS > ParticleSystem > DisplayObject > EventDispatcher > Object

To strike a balance, we'll just trace inherited properties from ParticleSystem. Let's have a look at these the next step...


Step 5: Properties of ParticleSystem and ParticleDesignerPS

Below are the properties of ParticleSystem.

Property Description
capacity Maximum particles the system can carry at any moment. Increases in steps of 500 once the number of particles exceeds its current capacity. Read-only.
numParticles Number of particles in the system at a given time. Read-only.
emissionRate Number of particles generated from birth coordinate each second.
emitterX, emitterY Control point for the container in which all particles live.

blendFactorSource, blendFactorDestination

Context3DBlendFactor definition for source and destination. Destination refers to pixel colour from latest render and source refers to new pixel colour to draw onto destination.
texture Current image sampled as texture of particles. Read-only.

Those of ParticleDesignerPS are tabulated in the next table. Most of these properties can be fine tuned by its start and end state. For example, all generated particles will initiate with a size of 1.0 and terminate at 0.1. However, particle flow will be boring if all particles are initiated and terminated at such similar states, so ParticleDesignerPS provisions for variance of the initial value, and sometimes variance on termination value as well.

Quoting their example, if we give a variance of 0.2 on the particle's initial size, consecutive particles birthed or recycled into the system will initiate their size somewhere between 0.8 ~ 1.2 and end at 0.1.

Property Description
emitterXVariance, emitterYVariance Variation of the birth coordinate.
startSize, startSizeVariance Initial size and variance
endSize, endSizeVariance Termination size and variance
emitAngle, emitAngleVariance Particle's initial direction and variance
speed, speedVariance Particle's initial speed and variance
gravityX Acceleration along x-axis on all particles' initial velocity
gravityY Acceleration along y-axis on all particles' initial velocity
tangentialAcceleration, tangentialAccelerationVariation Rate of rotation on particle's velocity, and variance

There are two types of particle flow provisioned for in ParticleDesignerPS: gravity and radial. Shown in the table above are properties you can tweak if you're using gravity flow. For gravity, birth coordinate of particles is located at emitterX and emitterY. For radial, birth coordinate for particles is located some point away from emitterX and emitterY, and they move towards it. The table below shows the properties of radial flow.

Property Description
maxRadius, maxRadiusVariance Maximum radius from center, and variance
minRadius Minimum radius from center
rotationPerSecond, rotationPerSecondVariance Rate of rotation on particle's velocity
startColor, startColorVariance Initial color and variance
endColor, endColorVariance Termination color and variance

Step 6: Adding Interactivity

Well, thanks for accommodating that little detour. Now for some ActionScript. In the init method, we will add a listener onto the stage for touch events.

1
2
stage.addEventListener(TouchEvent.TOUCH, track);

And the listener as below.

1
2
private function track(e:TouchEvent):void {
3
    var touch:Touch = e.getTouch(stage);	//mapping onto stage's coordinate grid

4
    //when user presses mouse and moves

5
    if (touch.phase == TouchPhase.MOVED) {
6
    	
7
        //calculate the angle to point particle flow to

8
        var distX:Number = touch.globalX - flow1.emitterX;
9
        var distY:Number = touch.globalY - flow1.emitterY;
10
        var angle:Number = Math.atan2(distY, distX);
11
        
12
        t = new Tween(flow1, 1.5, Transitions.EASE_OUT_BACK);
13
        t.animate("emitAngle", angle);
14
        Starling.juggler.add(t);
15
    }
16
}

In order to perform the animation, a Tween instance is defined. Its manipulation is similar to that of popular Tween engines in many ways. Then, we add it to the juggler of current instance of Starling. This juggler object will help to progressively update the Tween instance over time.

The result is below. Click and drag your mouse around the stage.


Step 7: Setting Up Ship

Let's set up our ship now and put a trail on it. The assets are from opengameart.org and I've included them in the download package. Check that site out for other free game art.

We'll start afresh with another class, TestingShip.as. First, import the spaceship image "boss1.png".

1
2
[Embed(source = "boss1.png")]
3
private var Ship:Class

...followed by a little setup to initialise it in the init method:

1
2
//setup the graphical appearance

3
var shipBMP:Bitmap = new Ship() as Bitmap;	//import asset into a bmp

4
var shipTEX:Texture = Texture.fromBitmap(shipBMP);	//sample bmp as texture to image

5
var shipIMG:Image = new Image(shipTEX);	//image created with texture

6
7
//setup ship's orientation & position

8
shipIMG.rotation -= Math.PI*0.5;	//reorient the image

9
shipIMG.x -= shipIMG.width >> 1;	//because image's origin is at top left corner,

10
shipIMG.y += shipIMG.height >> 1;	//we reposition the image

11
theShip = new Sprite();  	//and put it into a sprite. Now registration point is centered.

12
theShip.addChild(shipIMG);	//sprite placed onto stage

13
addChildAt(theShip, 0);
14
15
//navigational properties of ship

16
loc = new Vector2D(stage.stageWidth >> 1, stage.stageHeight >> 1);
17
lof = new Vector2D(0, 10);
18
19
updateShip();

Update its position and orientation according to loc (location) and lof (line of sight).

1
2
private function updateShip():void {
3
    theShip.x = loc.x;
4
    theShip.y = loc.y;
5
    theShip.rotation = lof.getAngle();
6
}

Again, click and drag within the stage to see the effect:


Step 8: Setting Up Exhaust Trail

Okay, the exhaust of the ship is on top of the ship itself, and the spaceship is not responding to mouse event. We'll fix that now. Just offset the emitterX and emitterY of the particle flow some distance off from the spaceship and update spaceship's rotation using lof.

(Note that lof is updated on mouse events. You will see the script next step.)

1
2
private function updateShip():void {
3
    theShip.x = loc.x;
4
    theShip.y = loc.y;
5
    theShip.rotation = lof.getAngle();
6
    
7
    //update particle trail

8
    offset = new Vector2D(60, 0);
9
    offset.setAngle(lof.getAngle());
10
    flow1.emitterX = loc.x - offset.x;
11
    flow1.emitterY = loc.y - offset.y;
12
}

Step 9: Navigating Ship

Let's try to program ship navigation now, at the dispatch of a mouse event. I've only commented the important lines:

1
2
private function track(e:TouchEvent):void {
3
    var touch:Touch = e.getTouch(stage);
4
    if (touch.phase == TouchPhase.MOVED) {
5
        var distX:Number = touch.globalX - flow1.emitterX;
6
        var distY:Number = touch.globalY - flow1.emitterY;
7
        angle = Math.atan2(distY, distX);
8
        
9
        t = new Tween(flow1, 1.5, Transitions.EASE_OUT_BACK);
10
        t.animate("emitAngle", angle + Math.PI);
11
        
12
        t2 = new Tween(theShip, 1.5, Transitions.EASE_OUT);
13
        t2.moveTo(touch.globalX, touch.globalY);	//move the ship

14
        t2.onUpdate = refresh	//call upon this function whenever tween engine runs

15
        
16
        Starling.juggler.add(t);
17
        Starling.juggler.add(t2);	//add to juggler

18
    }
19
}
20
21
private function refresh():void {
22
    loc.x = theShip.x;	//refresh location

23
    loc.y = theShip.y;
24
    lof.setAngle(angle);	//refresh orientation

25
    
26
    updateShip();	// update 

27
}

And here's a show of the end result. Drag the mouse around the stage and the spaceship will go there.


Step 10: Animate Exhaust

Let's fine tune our exhaust. When the ship is moving, the exhaust will definitely be blowing harder, right? We can step up the emissionRate and speed when moving the ship, and step down emissionRate when it's stopped. Here's the event, highlighted:

1
2
t2 = new Tween(theShip, 1.5, Transitions.EASE_OUT);
3
t2.moveTo(touch.globalX, touch.globalY);	//move the ship

4
t2.onUpdate = refresh	//call upon this function whenever tween engine runs

5
t2.onStart = beginState	//when ship starts moving

6
t2.onComplete = endState	//when ship animation stops

And here are the function calls on those events.

1
2
private function beginState():void {
3
    flow1.emissionRate = 250
4
    flow1.speed = 100;
5
}
6
7
private function endState():void {
8
    flow1.emissionRate = 50
9
    flow1.speed = 10;
10
}

Click and drag again, and pay attention to the length of the exhaust.


Step 11: Parallax Effect

Particles can also express how fast the ship is moving relative to its environment. Check out the output below. Click and drag you mouse around. Observe the speed at which the surrounding particles move. They increase as you interact with the ship, and slows down when you stop the interaction. They also orient their rotation accordingly.

Configuring this effect is relatively easy on the app showcased by onebyonedesign.com. However, we'll need to code some ActionScript in order to change it at runtime, and this will take the next few steps.


Step 12: One Emitter in Front

The initiation of the particles follows a similar format to the previous example. You may choose to tweak your effect with the app from onebyonedesign.com and import into your stage. I just code straight into ActionScript for ease.

1
2
envr = new ParticleDesignerPS( XML(new InitValues()), 					
3
    Texture.fromBitmap(new Sample())	
4
    );
5
addChildAt(envr,0);
6
envr.blendFactorSource = Context3DBlendFactor.ONE
7
envr.blendFactorDestination = Context3DBlendFactor.ONE
8
envr.speed = 10;			envr.speedVariance = 20;
9
envr.startSize = 15;	envr.startSizeVariance = 0;
10
envr.endSize = 20;		envr.endSizeVariance = 20
11
envr.lifespan = 5.0;	envr.lifespanVariance = 4.0;
12
envr.emissionRate = 10
13
envr.start(); Starling.juggler.add(envr);

We'll also need to put the particle emitter a little distance ahead of the ship.

1
2
envrLoc = new Vector2D(100, 0);
3
envrLoc.setAngle(angle);

And update this vector at runtime.

1
2
//update the environment

3
envr.gravityX = -40*lof.x;	//particle is accelerating in opposite direction

4
envr.gravityY = -40*lof.y;	//of the line of sight vector

5
envr.emitterX = loc.x + envrLoc.x;
6
envr.emitterY = loc.y + envrLoc.y;

Step 13: The Spread

You see, the emitterXVariance and emitterYVariance handle the axes separately. This means that if we rotate the spaceship, we need some means of determining the length of the spread along these two axes.

Spread issue in emitterSpread issue in emitterSpread issue in emitter

Now check out the vector for line of sight. It's always perpendicular to that of spread line (the thin dark line). We can scale up this vector accordingly and swizzle its x and y with those of emitter's variance on start point . Check the demo below. Click and drag your mouse around. You'll see the particle flow more vividly.

Lastly, scale the magnitude of spread up and put it a little farther ahead of the ship so players won't see their emission point.

1
2
envrLoc = new Vector2D(200, 0);
3
envrLoc.setAngle(angle);
1
2
//update the spread

3
spread = envrLoc.clone();
4
spread.scale(0.5);
5
envr.emitterXVariance  = spread.y;
6
envr.emitterYVariance  = spread.x;

Step 14: Pump Up the Acceleration

Finally as the ship is accelerating, let's increase the magnitude of gravityX and gravityY, plus the exhaust, accordingly.

1
2
if (touch.phase == TouchPhase.MOVED) {
3
    var distX:Number = touch.globalX - flow1.emitterX;
4
    var distY:Number = touch.globalY - flow1.emitterY;
5
    angle = Math.atan2(distY, distX);
6
    
7
    //animate the exhaust

8
    t = new Tween(flow1, 1.5, Transitions.EASE_OUT_BACK);
9
    t.animate("emitAngle", angle + Math.PI);
10
    Starling.juggler.add(t);	
11
    
12
    //control the exhaust

13
    flow1.speed = 350;
14
    flow1.endSize = 70;
15
    
16
    //orient the ship & parallax's angle

17
    lof.setAngle(angle);
18
    lof.setMagnitude(10);	//adjust the magnitude of acceleration

19
    envrLoc.setAngle(angle);
20
}
21
if (touch.phase == TouchPhase.ENDED) {
22
    
23
    //control the exhaust

24
    flow1.speed = 100;
25
    flow1.endSize = 10;
26
    
27
    lof.setMagnitude(5);	//adjust the magnitude of acceleration

28
}

Step 15: Ship Under Attack

As you progress in the game, you will definitely take hits and suffer damage. As damage becomes severe, you ship will burn up. Such an effect can be generated here; we can make use of the emissionXVariance and emissionYVariance to define the area of burn. I've highlighted them in the code below.

1
2
envr = new ParticleDesignerPS( XML(new InitValues()), 					
3
    Texture.fromBitmap(new Sample())	
4
    );
5
addChildAt(envr,2);
6
envr.blendFactorSource = Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA
7
envr.blendFactorDestination = Context3DBlendFactor.ONE;
8
envr.emitterXVariance = theShip.width >> 2;
9
envr.emitterYVariance = theShip.height >> 2;
10
envr.emitAngle = 0;		envr.emitAngleVariance = Math.PI;
11
envr.speed = 0;			
12
envr.startSize = 40;	envr.startSizeVariance = 0;
13
envr.endSize = 10;		envr.endSizeVariance = 0
14
envr.lifespan = 5.0;	envr.lifespanVariance = 3.0;
15
envr.emissionRate = 10;
16
envr.start(); Starling.juggler.add(envr);

Damage severity is indicated by the area and intensity of burn. Increase and decrease emissionRate to simulate this. I've added controls on keyboard "A" and "S" to emulate this.

1
2
private function controlBurn(e:KeyboardEvent):void {
3
    if (e.keyCode == Keyboard.A) {
4
        if(envr.emissionRate < 150)	envr.emissionRate += 10;
5
        if (envr.lifespan < 8)	envr.lifespan += 0.5;
6
    }
7
    if (e.keyCode == Keyboard.S) {
8
        if(envr.emissionRate > 10)	envr.emissionRate -= 10;
9
        if (envr.lifespan > 5)	envr.lifespan -= 0.5;
10
    }
11
}

Note that if you increase the lifespan of particles, the fire seems to burn more intensely. Well, it takes time to interpolate particles from its initial size to end size, so if you increase the lifespan, its longer to transition from the bigger start to smaller end size. As more big particles stay in the same spot longer, they blend together to give the impression of a more intense fire.


Step 16: Burning Ship

Press the "A" key to see the fire burn more intensely and the "S" key to slightly quench it. The exhaust's color has been change, to differentiate it from the burning:


Step 17: Ship Exploded

All good games must end at some point. Regardless of who's being eliminated, a good explosion for a finish should not be missed. So how about a nuclear mushroom cloud? Click on the demo below to see one.

Now, let's code it.


Step 18: Radial Particle Flow

This particle flow is a little different than the ones we have seen. Previously we have been using particle flow type 0 (gravity) and this is type 1 (radial). The particles are actually moving to the middle at a constant velocity.

I turned the emission angle variance to its highest so you can see all the generated particles form a circle. Then, by animating the maximum radius and the minimum radius these particles should live in over time, using a Tween, we achieve this result.

1
2
explosion = new ParticleDesignerPS(
3
        XML(new InitValues()), 					
4
        Texture.fromBitmap(new Sample())
5
    );
6
addChild(explosion);
7
explosion.emitterX = stage.stageWidth >> 1; 	
8
explosion.emitterY = stage.stageHeight >> 1;
9
explosion.emitterType = 1;
10
explosion.emitAngle = 0;	explosion.emitAngleVariance = Math.PI;
11
explosion.maxRadius = 10;	explosion.maxRadiusVariance = 0;
12
explosion.minRadius = 0;

Here's the code to perform the animation.

1
2
private function track(e:TouchEvent):void {
3
    var touch:Touch = e.getTouch(stage);
4
    if (touch.phase == TouchPhase.BEGAN) {
5
        explosion.emitterX = touch.globalX;
6
        explosion.emitterY = touch.globalY;
7
        explosion.start();
8
        t = new Tween(explosion, 1.0, Transitions.EASE_IN);
9
        t.animate("maxRadius", 150);
10
        t.animate("minRadius", 130);
11
        t.onStart = freeze
12
        t.onComplete = reset;
13
        Starling.juggler.add(t);
14
    }
15
}
16
17
private function freeze():void {
18
    stage.removeEventListener(TouchEvent.TOUCH, track);
19
}
20
21
private function reset():void {
22
    stage.addEventListener(TouchEvent.TOUCH, track);
23
    explosion.stop();
24
    explosion.maxRadius = 10;	
25
    explosion.minRadius = 0;
26
}

Step 19: Recap

So this has been a lengthy tutorial. Let's do a little recap here. We've gone through:

  • Setting up Starling and its particle extension
  • Properties of the particle system
  • Examples of property manipulations based on the scenario of a shooter game.

Conclusion

We have covered quite a bit here. However, one important aspect I haven't gone through is to extend from ParticleSystem. This will really give you the power to code your own particle trails instead of relying on ParticleDesignerPS. I'll have to postpone this to another tutorial.

Thanks for reading and see you in the next tutorial. Do leave comments on errors and this particle engine's use in your project if you choose to adopt it.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.