A simple Silverlight game particle system
Source code: http://www.bluerosegames.com/silverlight-games-101/samples/particles.zip
Particles are a key part of game programming. When it comes to explosions, smoke effects, and many other types of effects, it is often easier and gives a better effect to compose the full effect out of several smaller particles. These particles can vary in velocity, position, color, and opacity, among other properties, based on the effect desired. In some games, the particles need to behave according to the game physics, such as gravity. Particles typically have a short life span and then disappear. Since there can be a large amount of particles being created and destroyed on a regular basis, a particle engine is typically employed to manage them.
The first particles we will add to the game is the exhaust trail coming out of the back of the ship when the thrust key is pressed. To do this, we will create a new particle in each frame, and calculate a velocity opposite to the current direction of the ship. The particles will fade from full opacity to partial opacity over the life of the particle, and then disappear at the end of its life span.
Since particles are such an important part of game development, I have added a ParticleEngine class to the BlueRoseGames.Helpers library in the BlueRoseGames.Helpers.Sprites namespace to make managing particles simple. Along with this is a Particle class which inherits from CenteredSprite and adds some logic specific to particles, like a start opacity, end opacity, start scale, end scale, and life span. The Update method is overridden to call the base update method, check to see if its life span is over, and if still alive, set the opacity and scale based on the time the particle has been alive. Once you create a new particle, simply call ParticleEngine.AddParticle, passing in the Particle object.
First let’s look at the Particle class. In the Update method, we keep track of how long the particle has been alive, and if it is past its life span, we set its Dead property to true, otherwise we set the current scale and opacity values based on how long it’s been alive and the start and end values specified. You could easily override this Update method if you inherit from Particle and do your own thing, like use Storyboard animations, or frame based image animations, or whatever. InterpolateDouble is just a helper method that does a linear interpolation to find the current value from the start and end values and the percentage of time that has elapsed.
using System;
using System.Windows;
namespace BlueRoseGames.Helpers.Sprites
{
public class Particle : CenteredSprite
{
protected TimeSpan lifeSoFar = TimeSpan.Zero;
public Particle()
: base()
{
FromScale = ToScale = 1;
FromOpacity = ToOpacity = 1;
Dead = false;
}
public Particle(FrameworkElement content) : base(content)
{
FromScale = ToScale = 1;
FromOpacity = ToOpacity = 1;
Dead = false;
}
public TimeSpan TimeToLive { get; set; }
public double FromOpacity { get; set; }
public double ToOpacity { get; set; }
public double FromScale { get; set; }
public double ToScale { get; set; }
public bool Dead { get; set; }
double InterpolateDouble(double start, double end, double pct)
{
return start + (end - start) * pct;
}
public override void Update(TimeSpan ElapsedTime)
{
base.Update(ElapsedTime);
lifeSoFar += ElapsedTime;
if (lifeSoFar > TimeToLive)
{
Dead = true;
}
else
{
double pct = lifeSoFar.TotalSeconds / TimeToLive.TotalSeconds;
Opacity = InterpolateDouble(FromOpacity, ToOpacity, pct);
double scale = InterpolateDouble(FromScale, ToScale, pct);
Scale = new Vector(scale, scale);
}
}
}
}
In the case of the exhaust trail, we will use ellipses (actually circles), but your particle could be a canvas with many children, an image, or any other FrameworkElement because Particle inherits from CenteredSprite which inherits from Sprite, and Sprite provides this ability.
The ParticleEngine is pretty simple. It stores a collection of Particle objects, and in its Update method, calls the Update of everything in its list, and checks to see if it’s time to remove them. It also handles adding the particle to the host canvas when the particle is added, and removing it when it’s dead.
using System;
using System.Windows.Controls;
using System.Collections.Generic;
namespace BlueRoseGames.Helpers.Sprites
{
public class ParticleEngine
{
Canvas _canvas;
List<Particle> particles = new List<Particle>();
public ParticleEngine(Canvas canvas)
{
_canvas = canvas;
}
public void AddParticle(Particle p)
{
particles.Add(p);
_canvas.Children.Add(p);
}
public void Update(TimeSpan ElapsedTime)
{
for (int i = particles.Count - 1; i >= 0; i--)
{
Particle p = particles[i];
p.Update(ElapsedTime);
if (p.Dead)
{
particles.Remove(p);
_canvas.Children.Remove(p);
}
}
}
}
}
So now that we have a general purpose particle system, let’s use it. First of all, to encapsulate the slightly complex initialization logic of the exhaust particles, let’s create an ExhaustParticle class in the SpaceRocks project that inherits from Particle:
using System;
using System.Windows.Media;
using System.Windows.Shapes;
using BlueRoseGames.Helpers.Sprites;
using BlueRoseGames.Helpers;
namespace SpaceRocks
{
public class ExhaustParticle : Particle
{
static Random rand = new Random();
public ExhaustParticle(Sprite ship) : base()
{
Ellipse ellipse = new Ellipse();
ellipse.Fill = new SolidColorBrush(Colors.White);
ellipse.Width=4;
ellipse.Height=4;
SetContent(ellipse);
FromOpacity = 1;
ToOpacity = .3;
TimeToLive = TimeSpan.FromSeconds(.2);
Vector v = -MathHelpers.CreateVectorFromAngle(ship.RotationAngle, 1);
Velocity = v * rand.Next(150, 250);
Vector offset = MathHelpers.CreateVectorFromAngle(rand.Next(0, 360), rand.Next(10, 50) / 10d);
Position = ship.Position + v * 19 + offset;
}
}
}
Basically we’re creating a white ellipse as the content of the particle, giving it a start opacity of 1 and end opacity of 3, and specifying that it should live for .2 seconds. Then we calculate the velocity based on the direction the ship is facing and the starting position based on the position of the ship. The offset is calculated to give the particles a slight randomness on position so they don’t all shoot out from the same exact point in a straight line. All of the rest of the functionality is handled by the Particle base class.
Now in the Page class, let’s declare the particle engine:
ParticleEngine particleEngine;
And then in the Page’s constructor, we’ll create it, passing in the canvas we want the particles to be displayed on:
particleEngine = new ParticleEngine(gameSurface);
Now simply in the gameLoop_Update method in the Page class, any type we add thrust to the ship, also create an exhaust particle and add it to the particle engine:
void gameLoop_Update(object sender, TimeSpan elapsed)
{
// do your game loop processing here
if (keyHandler.IsKeyPressed(Key.A) || keyHandler.IsKeyPressed(Key.Left))
{
ship.RotationAngle -= rotationSpeed * elapsed.TotalSeconds;
}
else if (keyHandler.IsKeyPressed(Key.D) || keyHandler.IsKeyPressed(Key.Right))
{
ship.RotationAngle += rotationSpeed * elapsed.TotalSeconds;
}
if (keyHandler.IsKeyPressed(Key.W))
{
ship.Thrust(elapsed);
particleEngine.AddParticle(new ExhaustParticle(ship));
}
ship.Update(elapsed);
foreach (Sprite asteroid in asteroids)
{
asteroid.Update(elapsed);
}
particleEngine.Update(elapsed);
}
Notice also that we have added a call to the particle engine’s Update method. This needs to be called in our game loop so that all of the particles update properly and are removed when their life span is complete.
So 4 lines of code plus a few more to initialize the particle, and we have a pretty nice effect. For explosions, you would do something similar, but make the velocity a random direction from a center point, and we’ll see that in a future sample.
And here is the result:
