So, Dave was having some issue with creating a particle system for his game, so thought I could come up with a game specific particle system. I say game specific, because it is just that, the system is usable for the game I/you write it for, it’s not going to sit in an engine and be used, though you could with a few tweaks.
In fact, there are a tone of optimizations you could do with this, but for this sample it does enough.
My idea was, to have an emitter manager that was a registered game service, this service could then be accessed by all your game components and each component could call and place emitters as and when they want/need them. Also, you could have emitters call other emitters if you need to, in the sample, you will see I have done this for the explosions, first an explosion emitter is called and when it is halfway through it’s cycle, it calls a smoke bomb emitter.
You may notice, this sample has a bit more than the emitter code in it, I got a bit carried away and almost wrote a game using it lol, if you like feel free to finish off :)
ParticleEmitterService
First thing we will look at is the emitter service, it is derived from the DrawableGameComponent that is intrinsic to XNA, it has two lists of IParticleEmitter, this is an interface I have created to describe a particle emitter, using this interface means you can write a totally new emitter and as long as it implements this interface, the emitter service can use it. The first list is the current list of emitters in use, I then have another list that is for emitters that are being created while we are in the Update loop, we can’t action these until we are out off the loop, so I put them in a queue to be added later.
public class ParticleEmitterService : DrawableGameComponent
{
public List<IParticleEmitter> Emitters = new List<IParticleEmitter>();
List<IParticleEmitter> emitterQue = new List<IParticleEmitter>();
bool Updating = false;
We then have the constructor (ctor) for the class, and in here I get it to register it’s self as a service with the Game instance and add it’s self to the Game.Components.
public ParticleEmitterService(Game game)
: base(game)
{
// Automaticaly register myself as a service
game.Services.AddService(this.GetType(),this);
// Put myself in the component list
game.Components.Add(this);
}
I then have an add emitter method so that game entities can invoke an emitter, as you will see this method checks if we are in the update method and if we are adds the new emitter to the queue. I created an enum so you could just say what pre baked emitter you want, you could just pass the emitter instance in rather than do it this way, but for this sample this makes it a bit simpler.
public void AddEmitter(EmitterType emitterType, Vector2 position, IGameEntity entity = null, Vector2? entityOffset = null)
{
IParticleEmitter emitter = null;
if (entityOffset == null)
entityOffset = Vector2.Zero;
bool added = false;
switch (emitterType)
{
case EmitterType.Explosion:
emitter = new ExplosionEmitter(Game) { Position = position, GameEntity = entity, EnityOffset = entityOffset.Value, Alive = true };
added = true;
break;
case EmitterType.Fire:
break;
case EmitterType.SmokeBomb:
emitter = new SmokeBombEmitter(Game) { Position = position, GameEntity = entity, EnityOffset = entityOffset.Value, Alive = true };
added = true;
break;
case EmitterType.SmokeTail:
emitter = new SmokeTrailEmitter(Game) { Position = position, GameEntity = entity, EnityOffset = entityOffset.Value, Alive = true };
added = true;
break;
}
if(added)
emitter.Initialize();
if (Updating)
emitterQue.Add(emitter);
else
Emitters.Add(emitter);
}
In our update method we check to see if we have any emitters in the queue, and if we do, we merge them with the main emitter list and clear the queue out. Then set the Updating flag to true, so if any are added while we are in here they are added to the queue. Then we go through our list, and call there Update methods, and if they are no longer alive, add them to the list to be removed from the emitter list. Then loop through and remove the emitters that are no no longer alive and call there Die method, in some emitters, you may want to do some other stuff once the emitter has completed. We the set the Updating flag to false.
public override void Update(GameTime gameTime)
{
List<IParticleEmitter> removeList = new List<IParticleEmitter>();
// Update from que if need be.
foreach (IParticleEmitter emitter in emitterQue)
{
Emitters.Add(emitter);
}
emitterQue.Clear();
Updating = true;
foreach (IParticleEmitter emitter in Emitters)
{
if (!emitter.Alive)
removeList.Add(emitter);
if(emitter.Enabled)
emitter.Update(gameTime);
}
foreach (IParticleEmitter emitter in removeList)
{
emitter.Die();
Emitters.Remove(emitter);
}
Updating = false;
}
The draw call is much simpler, just iterate through our emitters and make there draw calls.
public override void Draw(GameTime gameTime)
{
foreach (IParticleEmitter emitter in Emitters)
{
if(emitter.Visible)
emitter.Draw(gameTime);
}
}
Interfaces
In the sample I have two interfaces, the aforementioned IParticleEmitter interface and the IGameEntity interface. I created the latter so I could bind an emitter to a screen object, so if you look at the clip, you can see the smoke trail is fixed to the ship, where ever the ship goes, the smoke trail follows.
IParticleEmitter
public interface IParticleEmitter : IGameEntity
{
SpriteBatch SpriteBatch { get; }
ParticleEmitterService ParticleEmitter { get; }
BlendState ParticleBlendState { get; set; }
IGameEntity GameEntity { get; set; }
Vector2 EnityOffset { get; set; }
Vector2[] particles { get; set; }
float[] scaleParticle { get; set; }
float[] rotationParticle { get; set; }
bool[] activeParticle { get; set; }
Color Color { get; set; }
Texture2D texture { get; set; }
int ParticleCount { get; }
bool Alive { get; set; }
void Die();
}
As you can see, we have a few elements in there, but firstly we implement IGamEntity, here, why? Well you might want to hang an emitter off an emitter, so, a smoke trail off a fire ball for example, also, my IGameEntity also gives us other data structures, like position, scale, rotation, as well as bounds data (not implemented in here for the particle system) and a Docollision method (again, not for particles here)
IGameEntity
public interface IGameEntity : IGameComponent, IUpdateable, IDrawable
{
Vector2 Position { get; set; }
Vector2 Origin { get; set; }
float Scale { get; set; }
float Rotation { get; set; }
Rectangle bounds { get; }
bool DoCollision(IGameEntity entity);
}
This interface has all the stuff we need for our game entities.
Actually looking back over the interfaces, we could cull out a lot of these items as they are not used in the sample by implementing the interfaces, but I have kept them in here as you may need them later on (maybe).
Emitters
So, what do the emitters look like? Well to make life a bit more easier, I decided to use a base class that the emitters can then derive from, this base class implements the IParticleEmitter interface, so all you need to do to create a new emitter is to derive from this base class and away you go.
EmitterBase
As you might expect, EmitterBase implements our interfaces, the ctor sets up our default values
public EmitterBase(Game game, string TextureAsset)
: base(game)
{
textureAsset = TextureAsset;
Position = Vector2.Zero;
Origin = Vector2.Zero;
Scale = 1f;
Rotation = 0;
ParticleBlendState = BlendState.Additive;
Color = Color.White;
}
Initialise, well intializes the lists we need to implement the emitter
public override void Initialize()
{
particles = new Vector2[ParticleCount];
scaleParticle = new float[ParticleCount];
rotationParticle = new float[ParticleCount];
activeParticle = new bool[ParticleCount];
texture = Game.Content.Load<Texture2D>(textureAsset);
Origin = new Vector2(texture.Width / 2, texture.Height / 2);
for (int p = 0; p < ParticleCount; p++)
{
rotationParticle[p] = 0;
particles[p] = Vector2.Zero;
scaleParticle[p] = Scale;
}
}
Our update method is only of use if we have given the emitter an IGameEntity to bind to, and if it does, endures that we stick to that entity.
public override void Update(GameTime gameTime)
{
if (GameEntity != null)
{
Rotation = GameEntity.Rotation;
Position = GameEntity.Position + EnityOffset;
}
}
Our draw method again, simply draws the particles where we want them
public override void Draw(GameTime gameTime)
{
SpriteBatch.Begin(SpriteSortMode.Deferred, ParticleBlendState);
for (int p = 0; p < ParticleCount; p++)
{
if (activeParticle[p])
SpriteBatch.Draw(texture, particles[p], null, Color, rotationParticle[p], Origin, scaleParticle[p], SpriteEffects.None, 0);
}
SpriteBatch.End();
}
Our DoCollision and Die methods are just place holders for the base class, but we make sure they are virtual so they can be overriden later.
public virtual bool DoCollision(IGameEntity entity)
{
return false;
}
public virtual void Die()
{
}
So, that’s the basic structure, but what does an example emitter look like? Lets have a look at the SmokeTrail emitter.
SmokeTrailEmitter
First off, we derive from EmitterBase, this way we have all the intrinsic goodness from that base class for our emitter, and it is then by default a IParticleEmitter and an IGameEntity.
In the ctor, we set the texture to be used, the number of particles and the scale, in the Initialize method, we ensure our first particle is active.
public class SmokeTrailEmitter : EmitterBase
{
public SmokeTrailEmitter(Game game)
: base(game, "Textures/Particles/Smoke")
{
particleCount = 10;
Scale = .1f;
}
public override void Initialize()
{
base.Initialize();
activeParticle[0] = true;
}
Then all we have to do, is write the Update method, in here, we are looping through all the particles in the list, if they are active we move them down the screen, and increase there scale a little and rotate them a bit. We also measure the distance from the emitter and if greater (in this case) than 150 pixels, we deactivate it.
public override void Update(GameTime gameTime)
{
// set particle positions.
for (int p = 0; p < ParticleCount; p++)
{
if (activeParticle[p])
{
rotationParticle[p] -= (p + 1) / ParticleCount;
particles[p].Y += 8f;
scaleParticle[p] += .015f;
float dist = Vector2.Distance(Position, particles[p]);
if (dist > 150)
activeParticle[p] = false;
}
If the particle is not active, we then see if we need to activate it, so we set it’s position to that of the emitter, randomly move it either left or right a bit so we get a bobbly smoke effect and set it’s scale to the starting scale value. We then compare it’s distance from the last particle and if greater that 25 make it active.
else
{
particles[p] = Position;
particles[p].X += MathHelper.Lerp(-5, 5, (float)rnd.NextDouble());
scaleParticle[p] = Scale;
int nextP = p - 1;
if (p == 0)
{
nextP = ParticleCount - 1;
}
float dist = Vector2.Distance(particles[nextP], particles[p]);
if (dist > 25)
activeParticle[p] = true;
}
}
base.Update(gameTime);
}
I know, not the most awesome particle emitter, but it’s showing you how you can create you own emitters, take a look at the ExplosionEmitter’s Update method, this gives a totally different emitter result
public override void Update(GameTime gameTime)
{
for (int p = 0; p < ParticleCount; p++)
{
//particles[p] = Position;
rotationParticle[p] += (ParticleCount / (p+1)) * .1f;
if (blowOut)
Scale += .0025f;
else
{
a -= .005f;
if (a <= 0)
a = 0;
Color = new Color(Color.R, Color.G, Color.B, (byte)(a * 255));
Scale -= .0025f;
}
if (Scale > .5f)
{
Scale = .5f;
blowOut = !blowOut;
ParticleEmitter.AddEmitter(EmitterType.SmokeBomb, Position);
}
scaleParticle[p] = Scale;
}
if (Scale <= 0)
Alive = false;
base.Update(gameTime);
}
And now the one for the SmokeBombEmitter, again totally different results.
public override void Update(GameTime gameTime)
{
for (int p = 0; p < ParticleCount; p++)
{
//particles[p] = Position;
rotationParticle[p] += (ParticleCount / (p + 1)) * .1f;
Scale += .0025f;
a -= .0025f;
if (a <= 0)
a = 0;
scaleParticle[p] = Scale;
Color = new Color(Color.R, Color.G, Color.B, (byte)(a*255));
}
if (a <= 0)
Alive = false;
base.Update(gameTime);
}
Hope you find this useful, you can get all the source code for this here.
Also, the project uses a specific font, you can download it here, or swap it for one you have.
As ever C&C welcome.