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
{
public class Particle
{
public static readonly int MAX_LIFE = 1000;
private Vector m_Position;
private Vector m_Velocity;
private int m_Life;
private Color m_Color
public Particle() : this(Vector.Zero, Vector.Zero, Color.Black, 0)
{ }
public Particle(Vector pos, Vector vel, Color col, int life)
{
m_Position = pos;
m_Velocity = vel
m_Color = col;
if (life < 0)
m_Life = 0;
else
m_Life = life;
}
public bool Update()
{
m_Velocity = m_Velocity - Environment.getInstance().Gravity
+ Environment.getInstance().Wind;
m_Position = m_Position + m_Velocity;
m_Life++;
if (m_Life > MAX_LIFE)
return false;
return true;
}
#region Accesors
public Vector Position
{
get { return m_Position; }
}
public Vector Velocity
{
get { return m_Velocity; }
}
public int Life
{
get { return m_Life; }
}
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:
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
{
public class Environment
{
private static Environment m_Instance = new Environment();
private Vector m_Gravity = Vector.Zero;
private Vector m_Wind = Vector.Zero;
protected Environment()
{
}
public static Environment getInstance()
{
return m_Instance;
}
public Vector Gravity
{
get { return m_Gravity; }
set { m_Gravity = value; }
}
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
{
public abstract class ParticlesSystem
{
protected ArrayList m_Particles = new ArrayList();
protected bool m_Regenerate = false;
protected Vector m_Position;
protected Color m_Color;
protected abstract Particle GenerateParticle();
public abstract bool Update();
public virtual void Draw(Graphics g)
{
Pen pen;
int intense;
Particle part;
for (int i = 0; i < m_Particles.Count; i++)
{
part = this[i];
intense = (int)((float)part.Life / PARTICLES_MAX_LIFE);
pen = new Pen(Color.FromArgb(intense * m_Color.R ,
intense * m_Color.G,
intense * m_Color.B));
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();
}
}
public Particle this[int index]
{
get
{
return (Particle)m_Particles[index];
}
}
public int CountParticles
{
get { return m_Particles.Count; }
}
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
{
public class PSExplosion : ParticlesSystem
{
private static readonly int DEFAULT_NUM_PARTICLES = 150;
private Random m_rand = new Random();
public PSExplosion() : this(Vector.Zero, Color.Black)
{ }
public PSExplosion(Vector pos) : this(pos, Color.Black)
{ }
public PSExplosion(Vector pos, Color col)
{
m_Position = pos;
m_Color = col;
for (int i = 0; i < DEFAULT_NUM_PARTICLES; i++)
{
m_Particles.Add(GenerateParticle());
}
}
public override bool Update()
{
Particle part;
int count = m_Particles.Count;
for (int i=0; i < count; i++)
{
part = (Particle)m_Particles[i];
if ((!part.Update()) || (part.Life > 150))
{
m_Particles.RemoveAt(i);
i--;
count = m_Particles.Count;
}
}
if (m_Particles.Count <= 0)
return false;
return true;
}
protected override Particle GenerateParticle()
{
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);
Particle part = new Particle(m_Position,
new Vector(rndX, rndY, rndZ),
m_rand.Next(50));
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
{
public class PSFountain : ParticlesSystem
{
private static readonly int DEFAULT_NUM_PARTICLES = 250;
private Random m_rand = new Random();
public PSFountain() : this(Vector.Zero, Color.Black)
{ }
public PSFountain(Vector pos) : this(pos, Color.Black)
{ }
public PSFountain(Vector pos, Color col)
{
m_Regenerate = true;
m_Position = pos;
m_Color = col;
for (int i = 0; i < 5; i++)
{
m_Particles.Add(GenerateParticle());
}
}
protected override Particle GenerateParticle()
{
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);
Particle part = new Particle(m_Position,
new Vector(rndX, rndY, rndZ),
m_rand.Next(50));
return part;
}
public override bool Update()
{
Particle part;
int count = m_Particles.Count;
for (int i=0; i < count; i++)
{
part = (Particle)m_Particles[i];
if ((!part.Update()) || (part.Life > 150))
{
m_Particles.RemoveAt(i);
i--;
count = m_Particles.Count;
}
}
if (m_Particles.Count < DEFAULT_NUM_PARTICLES)
m_Particles.Add(GenerateParticle());
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 a Draw()
function in the ParticlesSystem
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.