Particles Tutorial

Welcome to the Particles tutorial. It will teach you how to use the particle system in the Thor Library. Fundamentally, there are multiple entities which lead in combination to the rendering of particles on the screen:

The ParticleSystem class

This class is the core of Thor’s Particles module. It holds containers of particles and provides an interface to update and draw them. Every ParticleSystem instance is initialized with a const sf::Texture& representing the particle’s texture. Inside a single system, all particles use the same texture. Let’s define a particle system that uses a given texture for the particles:

sf::Texture texture;
… // Load texture
thor::ParticleSystem system;
system.setTexture(texture);

Important are two member functions. First, a method called update() applies 2D transforms to each particle. It emits new particles and affects existing ones, depending on the passed time. The parameter dt stands for the frame time, which can be computed with a sf::Clock instance. The other important method is draw() which renders the particles on your screen, using the current sf::View.

An example of how the particle system integrates to your game loop:

sf::RenderWindow window(...);
sf::Clock clock;
thor::ParticleSystem system;
... // Initialize particle system

// Main loop
for (;;)
{
     ... // Handle events etc.

     // Update particle system
     system.update(clock.restart());

     // Draw particle system
     window.clear();
     window.draw(system);
     window.display();
}

Emitters

Before a particle system is able to manipulate and draw any particles, the latter must be created somehow. This is the task of the particle emitters. Emitters are represented by function objects; that is, you can use functors, global and member functions, results of std::bind(), and lambda expressions.

There is one concrete emitter type in Thor, namely thor::UniversalEmitter. This class can determine parameters like emission rate as well as initial particle conditions such as position, rotation or lifetime. However, UniversalEmitter itself has no geometric representation in 2D space.

An emitter object can be attached to a particle system using thor::ParticleSystem::addEmitter(). By doing so, you tell the ParticleSystem that it may use your emitter as a source of particles for the system. As soon as the particles are emitted, the ParticleSystem object takes control of them and manages them. For example, we define an emitter that creates particles inside a circular area, and attach it to the particle system:

// Create emitter that emits 30 particles per second, each of which lives for 5 seconds
thor::UniversalEmitter emitter;
emitter.setEmissionRate(30);
emitter.setParticleLifetime(sf::seconds(5));
system.addEmitter(emitter);

Now the particles are automatically emitted. But sometimes these constants aren't enough, what if you want the lifetime to be in a random interval, e.g. between 5 and 7 seconds? For this purpose, Thor's Math module implements distributions. These consist of functions that return a value based on a specific distribution (like a uniform random number in an interval, or a point in a rectangle).

To implement the given example of a random time in [5, 7] seconds, we can write this:

emitter.setParticleLifetime( thor::Distributions::uniform(sf::seconds(5), sf::seconds(7)) );

The same applies to other attributes, for example:

emitter.setParticlePosition( thor::Distributions::circle(center, radius) );   // Emit particles in given circle
emitter.setParticleVelocity( thor::Distributions::deflect(direction, 15.f) ); // Emit towards direction with deviation of 15°
emitter.setParticleRotation( thor::Distributions::uniform(0.f, 360.f) );      // Rotate randomly

Affectors

Things get interesting when you want to influence particles after their emission. For this purpose, the Thor library supplies you with particle affectors. There are predefined classes specifying different affector mechanisms. Examples include the force affector, which constantly accelerates particles, or the color affector, which applies a color gradient to particles over time.

Like emitters, affectors are represented by function objects and can be attached to a ParticleSystem. In every frame, the particle system invokes each of the attached affectors and lets it modify the particles. A simple example of an affector that applies gravity to all particles is defined as follows:

sf::Vector2f acceleration(0.f, 10.f);
thor::ForceAffector gravityAffector(acceleration);
system.addAffector(gravityAffector);

Affectors are tightly related to animations. While the former are functions that modify particles over time, the latter are functions that modify general graphical objects such as sprites for a given process. Let's say we have an animation fader that fades the particle in and out during the first and last 15% of its lifetime:

thor::FadeAnimation fader(0.15f, 0.15f);

It is now possible to use this animation as an affector, through the wrapper class thor::AnimationAffector:

system.addAffector(thor::AnimationAffector(fader));

Multiple particle variants

If you do not want to use the whole texture, it is possible to specify one or more texture rectangles that are used to represent a particle. Multiple texture rects are useful if you want different variants of a particle, but only a single particle system. For example, let's say you use thor::ParticleSystem to represent debris. Of course, interesting chunks of debris have distinct shapes, so you might use multiple texture rectangles to refer to the different variants.

Adding new texture rects is straightforward:

thor::ParticleSystem system;
system.setTexture(texture);
system.addTextureRect(sf::IntRect(...));
system.addTextureRect(sf::IntRect(...));

But how do you choose which texture rect a specific particle uses? The addTextureRect() function returns a texture index, which can be passed to an emitter. In the following example, all emitted particles are assigned index1 and therefore use the texture rectangle rect1:

unsigned int index0 = system.addTextureRect(rect0);
unsigned int index1 = system.addTextureRect(rect1);

thor::UniversalEmitter emitter;
emitter.setParticleTextureIndex(index1);

Texture indices are consecutive unsigned int numbers starting at zero. That allows you to easily choose an index at random. If you call addTextureRect() three times, the indices will be 0, 1 and 2. Thus, you can use the Math module to create a random distribution that is directly usable:

emitter.setParticleTextureIndex(thor::Distributions::uniform(0, 2));

Value and reference semantics

Because affectors and emitters are function objects, they have value semantics, meaning that they can be copied, assigned, passed to functions and returned from functions the way you are used to. Therefore, when you attach an affector or emitter to a particle system, it will be copied. This makes it possible to use temporary objects without worrying about lifetimes.

However, sometimes this is not what you want. You might want to store an emitter elsewhere and change it after you have added it to the particle system. For example, a lot of emitters are directed, depending on the orientation of game objects. In order to have reference semantics – meaning that emitters and affectors are only referenced and not owned by the particle system – Thor provides two functions thor::refAffector() and thor::refEmitter() that can enclose affectors and emitters:

// Create emitter
thor::UniversalEmitter emitter;

// Add reference to emitter to particle system
system.addEmitter(thor::refEmitter(emitter));

// Change emitter properties later
emitter.setEmissionRate(30);
emitter.setParticleLifetime(sf::seconds(5));

Of course, it is now your responsibility to keep the referenced objects alive as long as they are used by the particle system.

Removing affectors and emitters

Sometimes you may want to add emitters and affectors only temporarily. A typical example is an explosion, where an emitter emits particles for a very short time. A simple option to limit the time during which emitters or affectors remain active is to use the corresponding overloads:

// Add only for 1 second
system.addEmitter(emitter, sf::seconds(1));
system.addAffector(affector, sf::seconds(1));

Sometimes you need more control however, often you want to couple the removal to a condition. The addEmitter() and addAffector() functions return a thor::Connection instance, which has a method disconnect(). Simply call it, and the corresponding emitter or affector will be removed from the system.

thor::Connection c = system.addEmitter(emitter);
c.disconnect();

It is even possible to couple the emitter to the lifetime of another object. The RAII-style class thor::ScopedConnection automatically disconnects the associated object upon destruction.

{
    thor::ScopedConnection c = system.addEmitter(emitter);
} // End of scope: Emitter is removed

To demonstrate a use case, let's assume you have a central particle system to emit the exhaust of multiple airplanes. You add an emitter for every airplane, while the planes themselves store a scoped connection to the corresponding emitter. If an airplane is destroyed, its emitter will automatically be removed.

Customization [advanced]

You can easily provide your own emitter or affector by implementing functions for them. Both emitters and affectors are represented by callable objects, that is, you have to conform to the following signatures:

void (thor::EmissionInterface& system, sf::Time dt)  // emitter
void (thor::Particle& particle, sf::Time dt)     // affector

thor::EmissionInterface is a tiny class that acts as an interface to the particle system. It provides solely an emitParticle() function that allows you to insert a particle. For affectors, the particle being affected is passed to your affector function. The thor::Particle class stores various particle attributes, such as position, lifetime, color and texture index. For both emitters and affectors, the parameterdt represents the delta frame time.

The Fireworks example of the SDK shows you how to implement custom affectors and emitters. The result may look as follows: