Tech Blog: Designing a Particle System
Over the coming months we’ll be posting up a series of tech blogs direct from the ChilliSource development team. This is the first in that series and since we’ve just completely overhauled our particle system, it seems like a good place to start. We’ll discuss our design philosophy for the new system and then show a little of how it works.
What is a particle effect?
Particle effects are a rendering technique employed by many games for non-solid objects and effects which would otherwise be difficult to render. This include water, fire and smoke, as well as more abstract things such as spell effects or glowing objects.
The basic premise behind a particle effect is to render a relatively large number of, typically translucent, sprites which transform and fade in such a way that the combined object is an approximation of the desired effect. A smoke effect may be built out of a series of particles which slowly ascend while scaling and spreading out, eventually entirely fading out. Only a single smoke image is needed (though more may be desired) as the interaction between each of the particles will hide the repetition.
Image from http://bit.ly/1BAj1yL
API Design
A good particle system takes a lot of thought and careful design. The nature of a particle effect means that there is a lot going on at any one time. Each individual part of a particle system is fairly simple; the complexity comes from the quantity and the interactions between these parts. There are also a number of subtle differences in the the ways a particle effect can be used; for example, whether or not each particle is in local or world space. A small thing like this can make a significant difference to the created effect: drifting a car round a corner looks fantastic leaving a world space dust or smoke trail behind it!
Image from http://bit.ly/1Gxyoq0
Depending on the desired effect, there are a number of other small but significant things like this, and a good particle system will handle them all. Is the effect 2D or 3D? When playback stops should all particles disappear, or should the effect stop emitting and only finish when all particles die? Is the particle size in pixel coordinates or world space coordinates?
All of this means that the API needs to be carefully designed. For example in our system particle emission happens in local space meaning a developer of new emitters doesn’t need to handle world space. We expose both methods and events for completely stopping an effect and simply stopping emission allowing existing particles to finish their lifecycle.
Performance
Much like most rendering techniques, particle effects can be relatively expensive. This is especially the case on mobile where clever tricks such as performing particle calculations on the GPU aren’t really viable. Fortunately, the short development cycles of mobile games mean they often under-use concurrency, leaving up to 3 cores idling. If we utilise these, particle computations are essentially free! Particle Effects are also perfect for parallelisation as they often have little to no interaction with the rest of the world (the obvious exception being collidable particles, but that can still be handled concurrently).
We can handle concurrency using ChilliSource’s multi-threaded task system (See the Task Scheduler for more information) but we still need to ensure that we approach it in a way it scales to many cores. If we have a single task processing all particle effects we’re still not fully utilising the resources available to us. However a task per particle is overkill and the overhead of creating and running a task is likely to mean it would not be very efficient. Most games will have several particle effects running at once, so a task per effect is a reasonable approach.
In addition to concurrency, particle effects are also a great candidate for data-oriented design. It is common to think of a particle itself as an object and for it to contain further objects which apply different effects to it. This results in update loops that look something like this:
void UpdateParticles(std::vector& in_particles)
{
for (Particle* particle : in_particles)
{
particle->Update();
}
}
void Particle::Update()
{
m_position += particle.m_velocity;
...
for (Affector* affector : m_affectors)
{
affector->Update();
}
}
This isn’t particularly cache-friendly, which can be a relatively large hit to performance in big loops, which are common in particle effects. To remedy this the data for each particle can be stored contiguously in memory, after all within a single effect we know they will all have identical data. The virtual method call to update the affector can be avoided in the loop by considering an affector to be something that affects the entire effect, not just a single particle. The following demonstrates this:
void UpdateParticles(std::vector& in_particles, std::vector& in_affectors)
{
for (Particle& particle : in_particles)
{
particle.m_position += particle.m_velocity;
...
}
for (Affector* affector : in_affectors)
{
affector->Update(in_particles)
}
}
void Affector::Update(std::vector& in_particles)
{
for (Particle& particle : in_particles)
{
...
}
}
This results in contiguously memory access and avoids virtual method calls while looping over the particles which is much more cache friendly. This data-oriented aspect of the system can also typically remain behind the scenes, allowing us to keep the more user-friendly object-oriented API.
Extensibility
Ideally, a particle system should handle as many cases as possible and be flexible enough to recreate many effects out of the box. However it will never be able to handle everything, so it needs to be extensible. There are typically 3 areas which can be extended in a particle system: emission, the particles actions and the rendered object.
Emission is often handled through an emitter object. Emitters handle a variety of things from the rate of emission and different emission types through to the shape of emission. As long as the base emitter is flexible enough though, the only thing that would need to be extended by the user is the emission shape. Basic shapes such as sphere, cone and box are usually supplied by the system, but if the user wants anything more complex they will have to define it themselves.
Management of particle actions is usually handled through affectors. This exposes some form of update method allowing the affector to alter the basic properties of a particle each frame it is active. Basic affectors such as acceleration, scaling or changing of colour are usually supplied by the particle system.
Most particle systems consist of camera-facing (billboarded) sprites so it is less common to allow the user to change and extend the way a particle is drawn. Sometimes other types are desired however: sprites with frame-animation, or even 3D mesh particles.
Advanced Properties
Particle systems have a tendency to end up with a huge number of different settings, which can make their usage confusing and difficult. This is aggravated further by the fact that there can be many ways to set the same setting. Sometimes an effect might have, for example, the lifetime set to a constant, however it’s more common to randomise it between a range, or, in extreme cases, for it to change over time. If the particle system is designed to be data-driven, it can be difficult to expose these properties without convoluting the API.
{
"Lifetime": "1.0", # Constant lifetime. Used only if other lifetime values
# are not set.
"MinLifetime": "0.8", # Min and max lifetime range. Takes precedence over
"MaxLifetime": "1.2", # "Lifetime". Only used if no lifetime curve is set.
"EndLifetime": "1.2" # lifetime changes over time between start and end
"StartLifetime": "0.8", # using the given curve.
"LifetimeCurve": "Quad"
}
If each different setting has a similar number of properties, it’s easy to see how this can become a mess. This can be made worse if all settings don’t consistently support the same set of variants – or worse, handles them differently!
A tidy solution to this is to have an extensible Property type. This means both the API and the associated file format only need one way of setting any given property and how the final value is derived is described by the property type. Json is particularly good for this as it allows properties to be described as objects. Constant properties can still be set without the need for a json object.
{
"Lifetime": { # Random lifetime property.
"Type": "Random",
"Min": "0.8",
"Max": "1.2"
},
"Colour": { # Colour fades to 0 over time.
"Type": "Curve",
"Start": "1.0 1.0 1.0 1.0",
"End": "0.0 0.0 0.0 0.0",
"Curve": "Quad"
},
"Size": "1.0 1.0" # Size property is a constant, so no need for object.
}
Particles in ChilliSource
With all this in mind we set in about creating our new particle system – and we’re really happy with the result! The following describes how to create a simple particle effect using the system.
First of all you will need a csparticle file. This is a JSON file which describes each aspect of a particle effect. There are 4 sections to a csparticle file: basic information, drawable, emitter and the affectors.
The basic information section contains settings such as the duration, number of particles and simulation space of the effect.
{
"Duration": "1.0",
"MaxParticles": "1000",
"SimulationSpace": "World",
It also contains a number of properties pertaining the initial state of a particle. Properties work as described in the previous section: they can have their value set directly if a constant is desired, or they can be set to a Json object for more complex property types. All settings like this have the “Property” suffix, i.e “LifetimeProperty”.
"LifetimeProperty": {
"Type": "RandomConstant",
"LowerValue": "1.0",
"UpperValue": "1.5"
},
"ColourProperty": {
"Type": "RandomConstant",
"LowerValue": "1.0 0.0 0.0 0.5",
"UpperValue": "0.0 1.0 0.0 0.0"
},
"InitialSpeedProperty": "5.0",
The Drawable section describes how each particle will be rendered. Most particle effects will use the StaticBillboard drawable type, so this will contain settings like the material, texture atlas and how a particle is sized. Particles drawn in pixel space should use the “UsePreferredSize” size policy, meaning it is rendered at the same pixel size as the image. 3D games rarely work in pixel space though, so other size policies are provided to make conversion to world space easier. A size can be set to describe the space the particle should be in, and a size policy set to describe how aspect ratio is handled relative to this. If size is set to 1.0 x 1.0 and the “FitMaintainingAspect size” policy is used, then the image will be scale such that the image fits within 1.0 x 1.0 world space units without changing the aspect ratio of the image.
"Drawable": {
"Type": "StaticBillboard",
"MaterialPath": "Particles/Particles.csmaterial",
"AtlasPath": "Particles/Particles.csatlas",
"ImageIds": "ParticleA",
"ParticleSize": "1.0 1.0",
"SizePolicy": "FitMaintainingAspect"
},
The next section is the Emitter, describing how particles are emitted. Most properties describing the shape of emission will differ between the different emitter types, but there are some properties which apply to all. “EmissionMode” describes whether or not it is a burst or stream emitter; “ParticlesPerEmissionProperty” describes how many particles there are in each individual emission; and “EmissionRateProperty” describes how many emissions per second (this only applies to stream emitters).
"Emitter": {
"Type": "Sphere",
"EmissionMode": "Stream",
"EmissionRateProperty": "1000.0f",
"ParticlesPerEmissionProperty": "1",
"RadiusProperty": {
"Type": "Curve",
"Curve": "SmoothStepPingPong",
"StartValue": "0.5",
"EndValue": "1.5"
}
},
Finally there are the Affectors. This is simply a list of all of the different Affectors that apply to each of the particles.
"Affectors": [
{
"Type": "ColourOverLifetime",
"TargetColourProperty": "0.0 0.0 0.0 0.0"
},
{
"Type": "ScaleOverLifetime",
"ScaleProperty": "0.2 0.2"
}
]
}
Now that we have our particle effect resource, we need to load it into the game. As with other resources, this is handled via the resource pool:
auto resourcePool = CSCore::Application::Get()->GetResourcePool();
CSRendering::ParticleEffectCSPtr particleEffect = resourcePool->LoadResource(CSCore::StorageLocation::k_package, "ParticleEffect.csparticle");
Finally, we render this by creating a new Particle Effect Component, attaching it to an Entity and adding it to the Scene.
auto renderFactory = CSCore::Application::Get()->GetSystem();
CSRendering::ParticleEffectComponentSPtr particleComponent = renderFactory->CreateParticleEffectComponent(particleEffect);
CSCore::EntitySPtr entity = CSCore::Entity::Create();
entity->AddComponent(particleComponent);
GetScene()->Add(entity);
By default a particle effect will play once. We often want it to loop, which can be achieved by setting the playback type.
particleComponent->SetPlaybackType(CSRendering::ParticleEffectComponent::PlaybackType::k_looping);
That should be all you need to do to render a particle effect. This just scratches the surface however, a lot can be achieved with the particle system. Particle properties are powerful, allowing a lot of flexibility in what you can create, and with the ability to add you own emitters, affectors and even drawables, you can create a huge array of different effects. Give it a shot and let us know what you think!