PixelKit Particles Manual
From Pixelwave
Contents |
PixelKit includes a powerful particle engine that allows you to create essentially any effect your fantastical mind can think up.
The beauty of it is its simplicity: an effect is composed of several actions and initializers which define the behavior of the particles on a granular level. Each component is responsible for a tiny part of the effect, making it very modular and interchangeable. When different components come together they can create beautiful, funny, provocative, and sometimes scary particle effects. Ok ok, maybe I went a little overboard.
Additionally you can load particle effects created with third party applications such as 71 squared's Particle Designer. Because the PixelKit particle engine is so flexible it can handle almost any type of 2d particle system which can be represented by a set of actions an initializers.
Note: The PixelKit particle engine is currently in alpha testing. To use it check out the particles branch on github.
These are some of the samples available with the Pixelwave download package:
1 Credit where credit is due
This particle engine is inspired by the awesome Flint Particle System for AS3.
Many of the core concepts are the same, while several details have been changed to better fit the loading of external definition files and making some performance enhancements.
2 The Core objects
Let's start by describing all of the base objects of the particle engine, and how they interact with each other.
2.1 PKParticleEmitter
As you can imagine, this little guy is responsible for excreting particles into 2d space. But it actually does a lot more: it's in charge of creating particles, simulating them, and destroying them when their time to live is up. In essence the emitter manages everything about a particular effect apart from the rendering. We'll discuss the renderers and how the work with emitters further down.
Lets illustrate what using an emitter is like with code. Here's how we'd create a new emitter:
PKParticleEmitter *emitter = [[PKParticleEmitter alloc] init];
The look and behavior of an emitter's particles, as well as the fashion in which they are created is dictated by the emitter's actions, initializers, and flow.
Once an emitter has been set up it can be run it like so:
[PKParticleEmitter start]
If you'd like to do your own per-frame updating of the emitter manually just call the [PKParticleEmitter updateWithDeltaTime:] method, passing in the amount of time to simulate.
To move the emitter around in space simply update its x, y, and rotation properties as you would a display object.
2.2 PKParticleInitializer
An emitter contains a list of initializers which are applied onto each newly created particle. Initializers are in charge of setting up the particles' initial states. For example, there's an initializer which sets a particle's starting position, one that sets up its velocity, and one that sets up its color. There are more initializers you can use, and you can also create your own. More on those later.
Lets add some theoretical initializers to the emitter:
// Have all the particles start 10 pixels to the right and 10 pixels // down from the emitter. PKPointZone *posZone = [PKPointZone pointZoneWithX:10.0 y:10.0]; PKPositionInitializer *positionInitializer = [PKPositionInitializer positionInitializerWithZone:posZone]; // Initialize a particle's velocity to a value between 50 and 100, in // any direction PKDiscZone *velZone = [PKDiscZone discZoneWithOuterRadius:100.0 innerRadius:50]; PKVelocityInitializer *velocityInitializer = [PKVelocityInitializer velocityInitializerWithZone:velZone]; [emitter addInitializer:positionInitializer]; [emitter addInitializer:velocityInitializer];
We just told the emitter to set up the position and velocity of every particle it creates according to the initializers we passed in. Each initializer of course has parameters which can be tweaked.
2.3 PKParticleAction
After a particle is created it needs to be simulated. To do that the emitter holds onto a list of actions, applying them on each of its particles every frame. Each action is usually responsible for a single task. For example, there's an action which moves a particle according to its velocity, one that applies gravity on it, and one that fades it out according to its age.
Here's some code that will add actions to our emitter:
// Move the particle forward in time using simple euler integration PKMoveAction *move = [PKMoveAction moveAction]; // Apply a force of 300 points / sec squared PKAccelerateAction *gravity = [PKAccelerateAction accelerateActionWithX:0.0 y:300.0]; [emitter addAction:move]; [emitter addAction:gravity];
Here we're instructing the emitter to move each particle as time passes (pretty important) and apply gravity on it.
2.4 PKParticleFlow
Each emitter has a property named flow. An emitter's flow dictates how particles will be released from it. For example, to create an emitter which emits at a steady flow of 20 particle per second we'd write:
emitter.flow = [PKSteadyFlow steadyFlowWithRate:20.0];
Alternatively you can have the emitter emit a pulse of 20 particles every half a second:
emitter.flow = [PKPulseFlow pulseFlowWithCount:20 period:0.5];
Given the pre-made flow objects and the fact you can create your own, the options are pretty much endless.
2.5 PKParticleRenderer
So by now we hopefully realize that there's this emitter object, and using initializers, actions, and flow, it defines an emission behavior, also known as a particle effect. But all of our fancy effects won't be very cool unless we can render them to the screen, now would they?
Remember when I mentioned that the emitter does pretty much everything except rendering? The reason is that we wanted the logic of a particle effect to be as separate as possible from its visuals.
So to render an emitter (and thus all of its particles) we create what we call a 'Particle Renderer' and add the emitter into it. You can think of a renderer as a container for emitters. Every frame the renderer draws all the emitters within it onto its rendering surface. All of the renderers we coded for the particle engine use a PXDisplayObject as their rendering surface (so they can be easily added to a Pixelwave display list), but in theory the rendering surface can be just about anything.
There are different renderers, each one for a different purpose. For example the PKQuadRenderer draws all of its particles using OpenGL quads, which is the fastest way to render images. Alternatively the PKPointRenderer draws all of its particles using OpenGL points, which are great if you want to draw solid circles or squares.
Let's write code to illustrate how we'd render all of our particles as textured quads (if you're not sure what those are think 'quickly rendered images'):
PKQuadRenderer *renderer = [PKQuadRenderer quadRendererWithSmoothing:YES]; // A renderer can hold any number of emitters [renderer addEmitter:emitter]; // PKQuadRenderer is a subclass of PXSprite, so we can add it to the display list [self addChild:renderer];
Alternatively if we wanted to render the same emitter as solid circles we'd write:
PKPointRenderer *renderer = [PKPointRenderer pointRendererWithSmoothing:YES]; [renderer addEmitter:emitter]; [self addChild:renderer];
There are more renderers, each serving a different purpose. The cool thing is that the renderer you choose isn't intertwined with the logic behind your effect and can be easily swaped.
2.6 PKParticleEffect
A particle effect encapsulates everything which defines an effect, namely an emitter and a renderer.
Sure, you could make your own subclasses of PKParticleEffect that encapsulate an entire effect that you can reuse. What you'll use it for most of the time though is loading effects that were defined in a file.
Let's say for example that we had created an effect using the 3rd party tool Particle Designer. We decided to call our effect file "DragonBreathingSmilies.pex". I'm not quite sure why but it seems appropriate.
Here's some code that'll load the effect and display it on the screen:
PKParticleEffect *effect = [PKParticleEffect particleEffectWithContentsOfFile:@"DragonBreathingSmilies.pex"]; PKParticleEmitter *emitter = [effect spawnEmitter]; id< PKParticleRenderer > renderer = [effect spawnRenderer]; [renderer addEmitter:emitter]; [self addChild:renderer]; [emitter start];
If you enjoy a good shortcut, you can do the same thing with even fewer lines of code like so:
PKParticleEffect *effect = [PKParticleEffect particleEffectWithContentsOfFile:@"DragonBreathingSmilies.pex"]; PKParticleEmitter *emitter = nil; id< PKParticleRenderer > renderer = [effect spawnRendererContainingEmitter:&emitter]; [self addChild:renderer]; [emitter start];
3 Other objects
There are a few other objects at play within the particle system. These aren't as central to the functioning of the system, but important nonetheless:
3.1 PKParticleFactory
The emitter has one more property I didn't mention quite yet: particleFactory. I guess it just slipped my mind. No, just kidding, it didn't; I was just trying to avoid confusion. It's not because I don't like you or anything, I swear.
A particle factory is any class that creates instances of PKParticle or any of its subclasses. Each emitter uses a particle factory when it needs to create a new particle, and returns particles to their factory when they are destroyed.
For simplicity's sake, PixelKit provides a concrete particle factory named PKParticleCreator. By default there's a single PKParticleCreator object which is shared by each emitter. The PKParticleCreator pools all of the particles it creates. If for some reason you want to use a factory other than the default one, simply set your emitter's particleFactory property to reference it. Whenever a new emitter is created, it's particleFactory property defaults to the shared factory.
3.2 PKZone
A zone in PixelKit is simply a two-dimensional shape. For example, there's the PKPointZone, the PKPRectangleZone, and the PKDiscZone. The particle system uses zones in conjunction with some initializers and actions is order to provide the highest degree of flexibility.
The PKPositionInitializer, which sets the starting position of particles in relation to the emitter, holds on to a single zone property which dictates how the particles will be positioned. If for example you want the particles to start within a rectangle, use the rectangle zone. If you want them to start out around the circumference of a circle, use the disc zone.
There are other useful zones you can use, and you can always create your own.
3.3 PKParticleEffectLoader
We created a subclass of PXLoader that handles the loading of particle effects. The behavior and usage pattern of this loader is identical to the other loaders in Pixelwave.
Most of the time, you won't need to access this class directly. To quickly load a particle effect you can just use the utility methods:
[PKParticleEffect particleEffectWithContentsOfFile:]
and
[PKParticleEffect particleEffectWithContentsOfURL:]
4 Creating your own components
4.1 Initializers
To create a custom initializer, create a class which conforms to the PKParticleInitializer protocol. You'll be required to implement the following methods:
- (void) initializeParticle:(PKParticle *)particle emitter:(PKParticleEmitter *)emitter; - (void) disposeParticle:(PKParticle *)particle emitter:(PKParticleEmitter *)emitter;
In the initialize method you should set up any of the particle's properties which are the responsibility of your initializer. In the dispose method you should clean up any memory you allocated in the initializeParticle method. You don't need to worry about zeroing out primitive type properties like x, y, and rotation, but if you allocated an object and set it the particle's graphic for example, make sure to release it in the dispose method.
Each particle is guaranteed to pass through both the initialize and dispose methods of the initializer in it's lifetime.
4.2 Actions
Actions are very simple to create. Simply construct a class that conforms to the PKParticleAction protocol. You'll be required to implement the method:
- (void) updateParticle:(PKParticle *)particle emitter:(PKParticleEmitter *)emitter deltaTime:(float)dt;
The update method will be called once for each particle in the emitter, each frame. If your action is based on time make sure to take the deltaTime argument into account, as it represents the amount of time, in seconds, elapsed since the last update.
If your action has determined that a particle needs to be destroyed at the end of the current frame, it should set its isExpired ivar to YES:
// Mark the particle to be removed at the end of the frame particle->isExpired = YES;
5 Next steps
For some practical examples take a look at the sample project 'Particles' available with Pixelwave. It contains several effects you can study and modify.
- This page was last modified on 5 October 2011, at 17:18.