A basic Particles System






4.91/5 (81 votes)
Introduction to the basic idea of the particle systems, and how to create basic effects such as explosions and water fountains.
Introduction
Particle Systems have long ago intruded into game engines, to become one of the basic features and foundations of a realistic environment. In this article, I will introduce you to the basic idea of the particle systems, and will show you how to create basic effects such as explosions and water fountains. This article does not cover much on the graphics side, and assume that once you have the particle system itself, you're free to display it in whatever way pleases you.
The single particle
A particle system is actually just a group of particles that are grouped together and have the same general behavior. These particles can be anything, from parts of a car when it hits a wall, to drops of water when there's rain.
All particles have a couple of things in common - position, direction, color and age. Each particle keeps its location in the space, the direction where it's going, its own color, and how long it has been alive.
Before we start looking at the particle, we need a class to keep information about the location and direction. Since we're dealing with a 3D world, a simple 3D-vector should be enough. You can find a fully working vector class in the attached files. It's enough for us now to understand that a Vector
is a class that encapsulates three float
variables, with functions for adding, subtracting and multiplying vectors.
Now let's have a look at our basic particle:
using System;
using System.Drawing;
namespace Particles
{
/// <SUMMARY>
/// Summary description for Particle.
/// </SUMMARY>
public class Particle
{
public static readonly int MAX_LIFE = 1000;
// Position of the particle
private Vector m_Position;
// Direction and speed the particle is moving
private Vector m_Velocity;
// Age of the particle
private int m_Life;
// Color of the particle
private Color m_Color
/// <SUMMARY>
/// Default constructor
/// </SUMMARY>
public Particle() : this(Vector.Zero, Vector.Zero, Color.Black, 0)
{ }
/// <SUMMARY>
/// Constructor
/// </SUMMARY>
/// <PARAM name="pos">Position
/// <SEE cref="Vector" /> of newly created particle</PARAM>
/// <PARAM name="vel">Velocity
/// <SEE cref="Vector" /> of newly created particle</PARAM>
/// <param name="col">Color of newly created particle</param>
/// <PARAM name="life">Starting age of newly created particle</PARAM>
public Particle(Vector pos, Vector vel, Color col, int life)
{
// Create particle at given position
m_Position = pos;
// Set particle's speed to given speed
m_Velocity = vel
// Set particle's color to given color
m_Color = col;
// Make sure starting age is valid
if (life < 0)
m_Life = 0;
else
m_Life = life;
}
/// <SUMMARY>
/// Update position, velocity and age of particle
/// </SUMMARY>
/// <RETURNS>False - if particle is too old and should be killed
/// True - otherwise</RETURNS>
public bool Update()
{
// Update particle's movement according to environment
m_Velocity = m_Velocity - Environment.getInstance().Gravity
+ Environment.getInstance().Wind;
// Update particle's position according to movement
m_Position = m_Position + m_Velocity;
// Update particle's age
m_Life++;
// If particle if too old
if (m_Life > MAX_LIFE)
// Notify caller to kill particle
return false;
return true;
}
#region Accesors
/// <SUMMARY>
/// Read Only - Position <SEE cref="Vector" /> of the particle
/// </SUMMARY>
public Vector Position
{
get { return m_Position; }
}
/// <SUMMARY>
/// Read Only - Velocity <SEE cref="Vector" /> of the particle
/// </SUMMARY>
public Vector Velocity
{
get { return m_Velocity; }
}
/// <SUMMARY>
/// Read Only - Age of the particle
/// </SUMMARY>
public int Life
{
get { return m_Life; }
}
/// <summary>
/// Read Only - Color of the particle
/// </summary>
public Color Color
{
get { return m_Color; }
}
#endregion Accessors
}
}
The code is pretty self-explanatory, and I believe that the only part that needs explanation is the following line:
// Update particle's movement according to environment
m_Velocity = m_Velocity - Environment.getInstance().Gravity
+ Environment.getInstance().Wind;
Since our Particle
is just a small entity in our world, it is affected by outside forces such as gravity and wind. In the next section, we'll cover the Environment.
The Environment
Our environment includes all external forces that will affect all particles in all the different systems. Such forces include the trivial gravity and wind, but can also include forces such as temperature or any other idea you might have. Since we want only one instance for the environment, I have implemented it as a Singleton:using System;
namespace Particles
{
/// <SUMMARY>
/// Summary description for Enviroment.
/// </SUMMARY>
public class Environment
{
/// <SUMMARY>
/// Single instance of the Environment
/// </SUMMARY>
private static Environment m_Instance = new Environment();
// Default Gravity vector in our world
private Vector m_Gravity = Vector.Zero;
// Default Wind vector in our world
private Vector m_Wind = Vector.Zero;
/// <SUMMARY>
/// Protected constructor
/// </SUMMARY>
protected Environment()
{
}
// Public accessor function to get an instance of the Environment
public static Environment getInstance()
{
return m_Instance;
}
/// <SUMMARY>
/// Accessor for the Gravity Vector
/// </SUMMARY>
public Vector Gravity
{
get { return m_Gravity; }
set { m_Gravity = value; }
}
/// <SUMMARY>
/// Accessor for the Wind Vector
/// </SUMMARY>
public Vector Wind
{
get { return m_Wind; }
set { m_Wind = value; }
}
}
}
Nothing here should make you even raise an eye-brow.
The System Abstract Class
Until now we've seen only single particles. As much fun as it might have been for you to watch a single dot move around on the screen, if you even bothered to try it, it's no real buzz. The beauty of particle systems can only be seen when we have large numbers of particles moving together. In this section, we will create the basic class for a system. This class, which is actually an abstract class, will handle the list of particles, and will require each class that inherit from it to implement a function to create new particles, and a function to update those particles. Let's have a look at the code:
using System;
using System.Collections;
using System.Drawing;
namespace Particles
{
/// <SUMMARY>
/// Summary description for ParticlesList.
/// </SUMMARY>
public abstract class ParticlesSystem
{
// Array to keep all the particles of the system
protected ArrayList m_Particles = new ArrayList();
// Should the particles regenerate over time
protected bool m_Regenerate = false;
// Central position of the system
protected Vector m_Position;
// Default color of a particle
protected Color m_Color;
/// <SUMMARY>
/// Generate a single particle in the system.
/// This function is used when particles
/// are first created, and when they are regenerated
/// </SUMMARY>
/// <RETURNS>New particle</RETURNS>
protected abstract Particle GenerateParticle();
/// <SUMMARY>
/// Update all the particles in the system
/// </SUMMARY>
/// <RETURNS>False - if there are no more particles in system
/// True - otherwise</RETURNS>
public abstract bool Update();
/// <SUMMARY>
/// Draw all the particles in the system
/// </SUMMARY>
/// <PARAM name="g">Graphics object to be painted on</PARAM>
public virtual void Draw(Graphics g)
{
Pen pen;
int intense;
Particle part;
// For each particle in the system
for (int i = 0; i < m_Particles.Count; i++)
{
// Get the current particle
part = this[i];
// Calculate particle intensity
intense = (int)((float)part.Life / PARTICLES_MAX_LIFE);
// Generate pen for the particle
pen = new Pen(Color.FromArgb(intense * m_Color.R ,
intense * m_Color.G,
intense * m_Color.B));
// Draw particle
g.DrawEllipse(pen, part.Position.X, part.Position.Y,
Math.Max(1,4 * part.Life / PARTICLES_MAX_LIFE),
Math.Max(1,4 * part.Life / PARTICLES_MAX_LIFE));
pen.Dispose();
}
}
/// <SUMMARY>
/// Indexer allowing access to each particle in the system
/// </SUMMARY>
public Particle this[int index]
{
get
{
return (Particle)m_Particles[index];
}
}
/// <SUMMARY>
/// Accessor to the number of particles in the system
/// </SUMMARY>
public int CountParticles
{
get { return m_Particles.Count; }
}
/// <SUMMARY>
/// Accessor to the maximum life of particles in the system
/// </SUMMARY>
public virtual int PARTICLES_MAX_LIFE
{
get { return particleMaxLife; }
}
}
}
The three constructors are easy to understand. The GenerateParticle()
function will be used when a new particle is created, whether it's a completely new particle, or when a particle dies and we wish to replace it with a new one. The Update()
will be used to update the particles in the system. Update()
will need to decide if and when to create new particles. And last, Draw()
will be used to display the particle system on a given Graphics
object.
2 basic particle systems
Now that we've seen the basic interface that we need to implement, we need to start implementing particle systems. Two of the more basic systems are an explosion and a fountain. I'll demonstrate them here.
Explosion
In an explosion, particles just fly everywhere. This is quite easy to implement - we just set all the particles to start at the center of the system, and move to a random direction, with a random speed. Gravity will take care of everything else.
using System;
namespace Particles
{
/// <SUMMARY>
/// Summary description for Explosion.
/// </SUMMARY>
public class PSExplosion : ParticlesSystem
{
private static readonly int DEFAULT_NUM_PARTICLES = 150;
// Random numbers generator
private Random m_rand = new Random();
/// <SUMMARY>
/// Default constructor
/// </SUMMARY>
public PSExplosion() : this(Vector.Zero, Color.Black)
{ }
/// <SUMMARY>
/// Constructor
/// </SUMMARY>
/// <PARAM name="pos">Starting position of system</PARAM>
public PSExplosion(Vector pos) : this(pos, Color.Black)
{ }
/// <SUMMARY>
/// Constructor
/// </SUMMARY>
/// <PARAM name="pos">Starting position of system</PARAM>
/// <PARAM name="col">Color of the particles in the system</PARAM>
public PSExplosion(Vector pos, Color col)
{
// Set system's position at given position
m_Position = pos;
// Set system color to given color
m_Color = col;
// Create all the particles in the system
for (int i = 0; i < DEFAULT_NUM_PARTICLES; i++)
{
// Create particle, and add it to the list of particles
m_Particles.Add(GenerateParticle());
}
}
/// <SUMMARY>
/// Update all the particles in the system
/// </SUMMARY>
/// <RETURNS>False - if there are no more particles in system
/// True - otherwise</RETURNS>
public override bool Update()
{
Particle part;
// Get number of particles in the system
int count = m_Particles.Count;
// For each particle
for (int i=0; i < count; i++)
{
// Get particle from list
part = (Particle)m_Particles[i];
// Update particle and check age
if ((!part.Update()) || (part.Life > 150))
{
// Remove old particles
m_Particles.RemoveAt(i);
// Update counter and index
i--;
count = m_Particles.Count;
}
}
// If there are no more particles in the system
if (m_Particles.Count <= 0)
return false;
return true;
}
/// <SUMMARY>
/// Generate a single particle in the system.
/// This function is used when particles
/// are first created, and when they are regenerated
/// </SUMMARY>
/// <RETURNS>New particle</RETURNS>
protected override Particle GenerateParticle()
{
// Generate random direction & speed for new particle
float rndX = 2 * ((float)m_rand.NextDouble() - 0.5f);
float rndY = 2 * ((float)m_rand.NextDouble() - 0.5f);
float rndZ = 2 * ((float)m_rand.NextDouble() - 0.5f);
// Create new particle at system's starting position
Particle part = new Particle(m_Position,
// With generated direction and speed
new Vector(rndX, rndY, rndZ),
// And a random starting life
m_rand.Next(50));
// Return newly created particle
return part;
}
}
}
In this example, we've created all the particles when the system was created. We've placed them all at exactly the starting point of the system, although for a more realistic look, we might have added a little bit of randomness there too. Each new particle is given a random age - this way the particles don’t die all at the same time. We've also decided to kill particles that are older than 150. We could have chosen another criteria, such as to kill particles only when they leave the display view, or they bumped into something.
Fountain
The fountain example is given here due to two reasons. First, the fountain regenerates particles that die, in order to continue "fountaining" or whatever else fountains do. Secondly, not all the particles are created at once - we first create a few particles, and as time goes on, we add more and more particles to the system.
using System;
namespace Particles
{
/// <SUMMARY>
/// Summary description for Firework.
/// </SUMMARY>
public class PSFountain : ParticlesSystem
{
private static readonly int DEFAULT_NUM_PARTICLES = 250;
// Random numbers generator
private Random m_rand = new Random();
/// <SUMMARY>
/// Default constructor
/// </SUMMARY>
public PSFountain() : this(Vector.Zero, Color.Black)
{ }
/// <SUMMARY>
/// Constructor
/// </SUMMARY>
/// <PARAM name="pos">Starting position of system</PARAM>
public PSFountain(Vector pos) : this(pos, Color.Black)
{ }
/// <SUMMARY>
/// Constructor
/// </SUMMARY>
/// <PARAM name="pos">Starting position of system</PARAM>
/// <PARAM name="col">Color of the particles in the system</PARAM>
public PSFountain(Vector pos, Color col)
{
// Mark that this system regenerates particles
m_Regenerate = true;
// Set system's position at given position
m_Position = pos;
// Set system color to given color
m_Color = col;
// Create ONLY 5 particles
for (int i = 0; i < 5; i++)
{
// Create particle, and add it to the list of particles
m_Particles.Add(GenerateParticle());
}
}
/// <SUMMARY>
/// Generate a single particle in the system.
/// This function is used when particles
/// are first created, and when they are regenerated
/// </SUMMARY>
/// <RETURNS>New particle</RETURNS>
protected override Particle GenerateParticle()
{
// Generate random direction & speed for new particle
// In a fountain, particles move almost straight up
float rndX = 0.5f * ((float)m_rand.NextDouble() - 0.4f);
float rndY = -1 - 1 * (float)m_rand.NextDouble();
float rndZ = 2 * ((float)m_rand.NextDouble() - 0.4f);
// Create new particle at system's starting position
Particle part = new Particle(m_Position,
// With generated direction and speed
new Vector(rndX, rndY, rndZ),
// And a random starting life
m_rand.Next(50));
// Return newly created particle
return part;
}
/// <SUMMARY>
/// Update all the particles in the system
/// </SUMMARY>
/// <RETURNS>False - if there are no more particles in system
/// True - otherwise</RETURNS>
public override bool Update()
{
Particle part;
// Get number of particles in the system
int count = m_Particles.Count;
// For each particle
for (int i=0; i < count; i++)
{
// Get particle from list
part = (Particle)m_Particles[i];
// Update particle and check age
if ((!part.Update()) || (part.Life > 150))
{
// Remove old particles
m_Particles.RemoveAt(i);
// Update counter and index
i--;
count = m_Particles.Count;
}
}
// If there aren't enough particles
if (m_Particles.Count < DEFAULT_NUM_PARTICLES)
// Add another particles
m_Particles.Add(GenerateParticle());
// Always return true, since system is regenerating
return true;
}
}
}
As you can see, the changes from the Explosion class are quite minor. Here we've created only a few particles when the system is created, and add a new particle every time the system is updated. We've also changed a bit the math for the movement of the particles - now they move almost straight up, and just a bit to the sides.
More systems
Creating more systems is quite simple. Examples of other systems include rain and snow, tornados, water flushing, falling leaves, smoke and more. The options are endless. In the attached demo, I've included another system - a firework.
Conclusion
I've included in the attached files a simple example of the described systems. The display I've used is very simple - single ellipse for each particle. But if you take into account each particle's age, you can come up with amazing effects just by changing the particles' size and transparency.
Creating new systems can be done in just minutes using the described model, and you are more than welcome to send me your own systems to add to this article.
History
- 3rd April 2005 - Demo project updated (Thanks to Mark Treadwell).
- 9th April 2005 - Fixed a bug in the
Vector
class. Added color to the particles. Included aDraw()
function in theParticlesSystem
abstract class. Added firework system to attached project. - 13rd April 2005 - Performance issue fixed (Thanks to John Fisher).
- 19th April 2005 - Constructors improvement suggested by Junai.