Being a complete noob at games programming, I initially approached particle rendering by calling Draw() for each particle. I'm not sure if you know this, but calling Draw() several thousand times per frame results in a slideshow.

So here's my solution to particle explosions. Each particle is represented by two vertices that form a line. As the particle gets older, its velocity decreases, along with its length. This produces a sort of tracer bullet effect.

To avoid multiple Draw() calls, I create a large vertex buffer that holds the total number of vertices that represent all of the particles i.e. particles * 2. Each vertex is translated to the position of the particle before being inserted into this giant vertex buffer.

The resulting vertex buffer can then be rendered using one DrawUserPrimitives() call. If I remember correctly, Shawn Hargraves says that DrawUserPrimitives() is the best way to draw vertex buffers that change frequently i.e. per frame. I also tried using a DynamicVertexBuffer but this seems to be the best way.

In addition to the particle lines, I decided to add some additively blended sprites to give it a more interesting look. The sprite I used is a simple circle with a gradient. To position the sprite I calculate the 2D position from the particle's 3D coordinates using the Graphics.Viewport.Project() method.

You can see the drawing code here below. The code for the creation of particles uses standard object pooling to avoid garbage collection. An object is taken from the pool, given an initial position, direction and velocity and placed into an array of "alive" particles. After a certain time the particle "dies", is removed from the live particles array and is ready to be re-used in the object pool.

You can see a video of the particle effects below. The fireball part of the explosion is a simple textured sphere. As it gets older, its scale increases and its alpha value decreases.




    1 public static int MAX_PARTICLES = 2000;

    2 VertexPositionColor[] explosionVertices;

    3 

    4 protected void InitParticleVertices()

    5 {

    6     explosionVertices = new VertexPositionColor[MAX_PARTICLES * 2];

    7 }

    8 

    9 public void Draw(GraphicsDevice Graphics, BasicEffect shader)

   10 {

   11     if (aliveParticles.Count > 0)

   12     {

   13         int vertexCount = 0;

   14         int particleCount = 0;

   15         foreach (ExplosionParticle particle in aliveParticles)

   16         {

   17             Vector3 v1 = Vector3.Zero;

   18             Vector3 v2 = new Vector3(0, 0, -1);

   19             Vector3 v3;

   20             //shorten the particle line based on its age

   21             Vector3.Multiply(ref v2, particle.Age, out v3);

   22             Vector3.Subtract(ref v2, ref v3, out v2);

   23 

   24             Matrix R = Matrix.CreateRotationY(particle.Angle);

   25             Matrix T = Matrix.CreateTranslation(particle.Pos);

   26 

   27             //rotate and position the line

   28             Matrix W;

   29             Matrix.Multiply(ref R, ref T, out W);

   30 

   31             Vector3.Transform(ref v1, ref W, out v1);

   32             Vector3.Transform(ref v2, ref W, out v2);

   33 

   34             explosionVertices[vertexCount].Position = v1;

   35             explosionVertices[vertexCount].Color = particle.Color;

   36 

   37             vertexCount++;

   38             explosionVertices[vertexCount].Position = v2;

   39             explosionVertices[vertexCount].Color = particle.Color;

   40 

   41 

   42             vertexCount++;

   43             particleCount++;

   44         }

   45 

   46         BlendState oldBlend = Graphics.BlendState;

   47         Graphics.BlendState = Game.additiveBlendState;

   48 

   49         shader.Alpha = 1.0f;

   50         shader.LightingEnabled = false;

   51         shader.DiffuseColor = Color.White.ToVector3();

   52         shader.VertexColorEnabled = true;

   53         shader.World = Matrix.Identity;

   54 

   55         //draw particle lines in one Draw() call

   56         foreach (EffectPass pass in shader.CurrentTechnique.Passes)

   57         {

   58             pass.Apply();

   59             Graphics.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.LineList, explosionVertices, 0, aliveParticles.Count);

   60         }

   61 

   62         //draw a sprite for each particle, slightly offset from the particle line

   63         Game.SpriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.Additive);

   64         for (int x = 0; x < aliveParticles.Count; x++)

   65         {

   66             ExplosionParticle particle = aliveParticles[x];

   67             //use Project() to determine the position in 2D screen space

   68             Vector3 screenCoords = Graphics.Viewport.Project(particle.Pos + (2f * particle.dir), shader.Projection, shader.View, Matrix.Identity);

   69 

   70             //As with the line lengths, reduce the size of the sprite based on the age of the particle

   71             float size = 8f;

   72             size *= (1f - particle.Age);

   73 

   74             Color color = Color.White;

   75             if (Utils.GetRandomNumber(0, 2) == 1)

   76             {

   77                 color = particle.Color;

   78             }

   79 

   80             Rectangle rect = new Rectangle((int)screenCoords.X, (int)screenCoords.Y, (int)size, (int)size);

   81             Game.SpriteBatch.Draw(bloomTexture, rect, color);

   82         }

   83         Game.SpriteBatch.End();

   84 

   85 

   86         Graphics.BlendState = oldBlend;

   87         shader.Alpha = 1f;

   88         shader.VertexColorEnabled = false;

   89     }

   90 }