#region File Description
//-----------------------------------------------------------------------------
// SmokePlumeParticleSystem.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
#endregion
#region Using Statements
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
#endregion
namespace RockRainIphone.Core
{
///
/// ParticleSystem is an abstract class that provides the basic functionality to
/// create a particle effect. Different subclasses will have different effects,
/// such as fire, explosions, and plumes of smoke. To use these subclasses,
/// simply call AddParticles, and pass in where the particles should exist
///
public abstract class ParticleSystem : DrawableGameComponent
{
// these two values control the order that particle systems are drawn in.
// typically, particles that use additive blending should be drawn on top of
// particles that use regular alpha blending. ParticleSystems should therefore
// set their DrawOrder to the appropriate value in InitializeConstants, though
// it is possible to use other values for more advanced effects.
public const int AlphaBlendDrawOrder = 100;
public const int AdditiveDrawOrder = 200;
private Random random;
protected SpriteBatch spriteBatch;
// the texture this particle system will use.
private Texture2D texture;
// the origin when we're drawing textures. this will be the middle of the
// texture.
private Vector2 origin;
// this number represents the maximum number of effects this particle system
// will be expected to draw at one time. this is set in the constructor and is
// used to calculate how many particles we will need.
private int howManyEffects;
// the array of particles used by this system. these are reused, so that calling
// AddParticles will not cause any allocations.
Particle[] particles;
// the queue of free particles keeps track of particles that are not curently
// being used by an effect. when a new effect is requested, particles are taken
// from this queue. when particles are finished they are put onto this queue.
Queue freeParticles;
///
/// returns the number of particles that are available for a new effect.
///
public int FreeParticleCount
{
get { return freeParticles.Count; }
}
// This region of values control the "look" of the particle system, and should
// be set by deriving particle systems in the InitializeConstants method. The
// values are then used by the virtual function InitializeParticle. Subclasses
// can override InitializeParticle for further
// customization.
#region constants to be set by subclasses
///
/// minNumParticles and maxNumParticles control the number of particles that are
/// added when AddParticles is called. The number of particles will be a random
/// number between minNumParticles and maxNumParticles.
///
protected int minNumParticles;
protected int maxNumParticles;
///
/// this controls the texture that the particle system uses. It will be used as
/// an argument to ContentManager.Load.
///
protected string textureFilename;
///
/// minInitialSpeed and maxInitialSpeed are used to control the initial velocity
/// of the particles. The particle's initial speed will be a random number
/// between these two. The direction is determined by the function
/// PickRandomDirection, which can be overriden.
///
protected float minInitialSpeed;
protected float maxInitialSpeed;
///
/// minAcceleration and maxAcceleration are used to control the acceleration of
/// the particles. The particle's acceleration will be a random number between
/// these two. By default, the direction of acceleration is the same as the
/// direction of the initial velocity.
///
protected float minAcceleration;
protected float maxAcceleration;
///
/// minRotationSpeed and maxRotationSpeed control the particles' angular
/// velocity: the speed at which particles will rotate. Each particle's rotation
/// speed will be a random number between minRotationSpeed and maxRotationSpeed.
/// Use smaller numbers to make particle systems look calm and wispy, and large
/// numbers for more violent effects.
///
protected float minRotationSpeed;
protected float maxRotationSpeed;
///
/// minLifetime and maxLifetime are used to control the lifetime. Each
/// particle's lifetime will be a random number between these two. Lifetime
/// is used to determine how long a particle "lasts." Also, in the base
/// implementation of Draw, lifetime is also used to calculate alpha and scale
/// values to avoid particles suddenly "popping" into view
///
protected float minLifetime;
protected float maxLifetime;
///
/// to get some additional variance in the appearance of the particles, we give
/// them all random scales. the scale is a value between minScale and maxScale,
/// and is additionally affected by the particle's lifetime to avoid particles
/// "popping" into view.
///
protected float minScale;
protected float maxScale;
///
/// different effects can use different blend modes. fire and explosions work
/// well with additive blending, for example.
///
protected BlendState spriteBlendMode;
#endregion
///
/// Constructs a new ParticleSystem.
///
/// The host for this particle system. The game keeps the
/// content manager and sprite batch for us.
/// the maximum number of particle effects that
/// are expected on screen at once.
/// it is tempting to set the value of howManyEffects very high.
/// However, this value should be set to the minimum possible, because
/// it has a large impact on the amount of memory required, and slows down the
/// Update and Draw functions.
protected ParticleSystem(Game game, int howManyEffects)
: base(game)
{
random = new Random(GetHashCode());
this.howManyEffects = howManyEffects;
}
///
/// override the base class's Initialize to do some additional work; we want to
/// call InitializeConstants to let subclasses set the constants that we'll use.
///
/// also, the particle array and freeParticles queue are set up here.
///
public override void Initialize()
{
InitializeConstants();
// calculate the total number of particles we will ever need, using the
// max number of effects and the max number of particles per effect.
// once these particles are allocated, they will be reused, so that
// we don't put any pressure on the garbage collector.
particles = new Particle[howManyEffects * maxNumParticles];
freeParticles = new Queue(howManyEffects * maxNumParticles);
for (int i = 0; i < particles.Length; i++)
{
particles[i] = new Particle();
freeParticles.Enqueue(particles[i]);
}
base.Initialize();
}
///
/// this abstract function must be overriden by subclasses of ParticleSystem.
/// It's here that they should set all the constants marked in the region
/// "constants to be set by subclasses", which give each ParticleSystem its
/// specific flavor.
///
protected abstract void InitializeConstants();
///
/// Override the base class LoadContent to load the texture. once it's
/// loaded, calculate the origin.
///
protected override void LoadContent()
{
// Initialize SpriteBatch
spriteBatch = new SpriteBatch(GraphicsDevice);
// make sure sub classes properly set textureFilename.
if (string.IsNullOrEmpty(textureFilename))
{
string message = "textureFilename wasn't set properly, so the " +
"particle system doesn't know what texture to load. Make " +
"sure your particle system's InitializeConstants function " +
"properly sets textureFilename.";
throw new InvalidOperationException(message);
}
// load the texture....
texture = Game.Content.Load(textureFilename);
// ... and calculate the center. this'll be used in the draw call, we
// always want to rotate and scale around this point.
origin.X = texture.Width / 2;
origin.Y = texture.Height / 2;
base.LoadContent();
}
///
/// AddParticles's job is to add an effect somewhere on the screen. If there
/// aren't enough particles in the freeParticles queue, it will use as many as
/// it can. This means that if there not enough particles available, calling
/// AddParticles will have no effect.
///
/// where the particle effect should be created
public void AddParticles(Vector2 where)
{
// the number of particles we want for this effect is a random number
// somewhere between the two constants specified by the subclasses.
int numParticles = random.Next(minNumParticles, maxNumParticles);
// create that many particles, if you can.
for (int i = 0; i < numParticles && freeParticles.Count > 0; i++)
{
// grab a particle from the freeParticles queue, and Initialize it.
Particle p = freeParticles.Dequeue();
InitializeParticle(p, where);
}
}
// a handy little function that gives a random float between two
// values. This will be used in several places in the sample, in particilar in
// ParticleSystem.InitializeParticle.
public float RandomBetween(float min, float max)
{
return min + (float)random.NextDouble() * (max - min);
}
///
/// InitializeParticle randomizes some properties for a particle, then
/// calls initialize on it. It can be overriden by subclasses if they
/// want to modify the way particles are created. For example,
/// SmokePlumeParticleSystem overrides this function make all particles
/// accelerate to the right, simulating wind.
///
/// the particle to initialize
/// the position on the screen that the particle should be
///
protected virtual void InitializeParticle(Particle p, Vector2 where)
{
// first, call PickRandomDirection to figure out which way the particle
// will be moving. velocity and acceleration's values will come from this.
Vector2 direction = PickRandomDirection();
// pick some random values for our particle
float velocity = RandomBetween(minInitialSpeed, maxInitialSpeed);
float acceleration = RandomBetween(minAcceleration, maxAcceleration);
float lifetime = RandomBetween(minLifetime, maxLifetime);
float scale = RandomBetween(minScale, maxScale);
float rotationSpeed = RandomBetween(minRotationSpeed, maxRotationSpeed);
// then initialize it with those random values. initialize will save those,
// and make sure it is marked as active.
p.Initialize(
where, velocity * direction, acceleration * direction,
lifetime, scale, rotationSpeed);
}
///
/// PickRandomDirection is used by InitializeParticles to decide which direction
/// particles will move. The default implementation is a random vector in a
/// circular pattern.
///
protected virtual Vector2 PickRandomDirection()
{
float angle = RandomBetween(0, MathHelper.TwoPi);
return new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
}
///
/// Check if the effect still active in screen
///
public bool Active
{
get
{
// go through all of the particles...
foreach (Particle p in particles)
{
if (p.Active)
{
return true;
}
}
return false;
}
}
///
/// overriden from DrawableGameComponent, Update will update all of the active
/// particles.
///
public override void Update(GameTime gameTime)
{
// calculate dt, the change in the since the last frame. the particle
// updates will use this value.
float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;
// go through all of the particles...
foreach (Particle p in particles)
{
if (p.Active)
{
// ... and if they're active, update them.
p.Update(dt);
// if that update finishes them, put them onto the free particles
// queue.
if (!p.Active)
{
freeParticles.Enqueue(p);
}
}
}
base.Update(gameTime);
}
///
/// overriden from DrawableGameComponent, Draw will use ParticleSampleGame's
/// sprite batch to render all of the active particles.
///
public override void Draw(GameTime gameTime)
{
// tell sprite batch to begin, using the spriteBlendMode specified in
// initializeConstants
spriteBatch.Begin(SpriteSortMode.Deferred, spriteBlendMode);
foreach (Particle p in particles)
{
// skip inactive particles
if (!p.Active)
continue;
// normalized lifetime is a value from 0 to 1 and represents how far
// a particle is through its life. 0 means it just started, .5 is half
// way through, and 1.0 means it's just about to be finished.
// this value will be used to calculate alpha and scale, to avoid
// having particles suddenly appear or disappear.
float normalizedLifetime = p.TimeSinceStart / p.Lifetime;
// we want particles to fade in and fade out, so we'll calculate alpha
// to be (normalizedLifetime) * (1-normalizedLifetime). this way, when
// normalizedLifetime is 0 or 1, alpha is 0. the maximum value is at
// normalizedLifetime = .5, and is
// (normalizedLifetime) * (1-normalizedLifetime)
// (.5) * (1-.5)
// .25
// since we want the maximum alpha to be 1, not .25, we'll scale the
// entire equation by 4.
float alpha = 4 * normalizedLifetime * (1 - normalizedLifetime);
Color color = new Color(new Vector4(1, 1, 1, alpha));
// make particles grow as they age. they'll start at 75% of their size,
// and increase to 100% once they're finished.
float scale = p.Scale * (.75f + .25f * normalizedLifetime);
spriteBatch.Draw(texture, p.Position, null, color,
p.Rotation, origin, scale, SpriteEffects.None, 0.0f);
}
spriteBatch.End();
base.Draw(gameTime);
}
}
}