Home > TutorialsTechniques
4.1. Particle System - Ring of Fire 
This tutorial is very important even if you don't want to do a ring of fire because it explains the way in which particle systems work in BetaCell.



Click here to go to the forum discussion of this tutorial


 

4.1.1. Prerequisites
default.jpg
Before you start this tutorial make sure that you have:
  • Installed XNA Game Studio 3.1
  • Downloaded the starter project from the downloads section
  • Successfully run the starter project
  • For a better understanding, is preferred to have finished the previous tutorials.
 

4.1.2. Game Assets 
Game assets [2.3.2] for this tutorial:

 

4.1.3. The using statements
Besides the using statements from the starter project, this tutorial needs the following using statements:

using BetaCell.Environment.Particles;
using BetaCell.Environment.Texture;
using BetaCell.Environment.Particles.Initializers;
using BetaCell.Util.RenderStates;
using BetaCell.Environment.Texture.Feeder;
using BetaCell.Environment.Particles.Parts;


 

4.1.4. Game Attributes 
The attributes that come with the starter project are explained in tutorial section 1.1.4.

For this tutorial we are going to be needing the following attributes:

BCParticleSystem particleSystem;

 

4.1.5. Particle Systems 
You can think of a particle as a point in space. This point behaves according to some constraints that we define
A single particle won't be enough to show an effect like rain or fire, so we are going to need more particles; how much depends on the effect that we are willing to achieve.

The idea of a particle system is to have many particles that behave in a similar but not the same way, each particle must have a random component.

To achieve this, the particle system needs to create the particles, define how the particles should move and state how the particles are going to be drawn.

default.jpg
The initializer is responsible of defining the initial position and direction of each particle, there are different types of initializers. In this tutorial we will use one that places new particles near to the perimeter of a circle as shown in part 1 of the figure.

The behavior of a particle is defined by a set of particle system parts. The parts that compose the behavior in this tutorial are shown in 2, 3, 4, 5 and 6 and combined together create the effect of a ring of fire.

Section 2 of the figure is a part that makes the particles smaller as the camera gets away from them. This isn't done automatically in particle systems.

Section 3 is a rotation part, it makes a particle rotate at a given velocity.

Section 4 is an oscillation part, it makes a particle oscillate at a given frequency. Section 5 makes a particle increase it size exponentially (like an explosion). Section 6 makes a particle disappear over time.

The combination in this tutorial means that each flame in the ring rotates, oscillates, explodes and fade; also if the camera gets away from the ring, each flame size decreases.

 

Advertisement
 

4.1.6. The Initialize method 
The initialize method starts by creating a texture strategy that represents a flame:

BCMemoryTextureStrategy torch = new BCMemoryTextureStrategy(
    content.Load<Texture2D>("Content/gameAssets/torch"));

Then, the circle initializer is created:

BCCircleInitializer circleInitializer = new BCCircleInitializer(
    3.5f,
    new Color(192, 192, 192, 64),

    50, 100,
    0.10f, 0.15f,
    0.8f, 1.0f,
    -0.2f, 0.2f,
    Vector3.Zero

);
circleInitializer.rotationPlane = BCCircleInitializer.PLANE_XY;

The parameters are:
  1. The maximum amount of time in seconds that a particle will live
  2. The starting color of the particle
  3. The minimum size in pixels of a particle
  4. The maximum size in pixels of a particle
  5. The minimum value of a custom parameter
  6. The maximum value of the custom parameter, the custom parameter is used by each particle system part as it needs.
  7. The minimum radix
  8. The maximum radix, between these radixes, the particles are placed.
  9. The the minimum width
  10. The maximum width, between these last two parameters, the particles are placed in the axis perpendicular to the circle.
  11. The scale of the speed of each component of the particle's velocity.
The line after the creation of the initializer sets the circle where the particles are going to be put to the XY plane.

After the initializer is created, the particle system is created:

particleSystem = new BCParticleSystem(
    device,
    torch,
    0,
    null,
    450,
    0.005f,
    "Particles.Base",
    circleInitializer,
    new BCAdditiveAlphaRenderStrategy()
);

The parameters are as follows:
  1. The device
  2. The texture that each particle will have
  3. The starting time of the system
  4. The bounding volume of the system
  5. The maximum particle count
  6. The time in seconds per emission of a particle
  7. The name of the base effect that all particle systems should have
  8. The particle initializer 
  9. The render strategy. 
To see information about the render strategies, please see the documentation; in the current example, the alpha channels of the particles are added; this is done to make brighter the spots where two or more particles are drawn.

After the particle system is created the parts are defined:

BCParticleRotationPart rotation =
    new BCParticleRotationPart("Particles.Parts.RotateXY", 5);


The line creates the rotation part as seen in section 3 of the image of section [4.1.5], the first parameter is efffect name that will handle the rotation part; notice that the rotaton plane is embedded. The second parameter is the rotation velocity.

The oscillation part is constructed as follows:

BCParticleOscillationPart oscilation =
    new BCParticleOscillationPart("Particles.Parts.OscillateZ", 5.0f, 0.5f);


The line creates the oscillation part as seen in section 4 of the image of section [4.1.5], The first parameter is the effect name of the oscillation part, notice that the oscillation axis is embedded. The second parameter is the frequency and the third is the space that the particle travels (the amplitude).

The part that makes the particles smaller as the camera gets away from them is constructed as follows:

BCParticleDistanceResizePart resizeDistance =
    new BCParticleDistanceResizePart("Particles.Parts.DistanceResize", 800);
resizeDistance.linearDecimator = 1;

The first line creates the distance resize part as seen in section 2 of the image of section [4.1.5], the first parameter is the name of the effect that makes the aprticles smaller, the second parameter is the view port height in pixels. The next line is the linear decimator, the bigger, the faster that the particles size decrease over the distance.

The part that makes the particles bigger over time is constructed as follows:

BCParticlePow2ResizePart pow2Resize =
    new BCParticlePow2ResizePart("Particles.Parts.ParticlePow2Resize", 100);


This line creates the resize part as seen in section 5 of the image of section [4.1.5]. It makes the particles bigger. The first parameter is the name of the effect that makes the particles resize, the second parameter is a factor that makes the size of the particles increase faster over time.

BCParticleFade fade = new BCParticleFade("Particles.Parts.Fade");

This line makes the particles disappear over time as seen in section 6 of the image of section [4.1.5]. The parameter is the name of the effect that makes the particles dissapear.

particleSystem.initEffect(device, "Shader.Texture.Sprite", sampler,
    rotation, oscilation, pow2Resize, resizeDistance, fade);


This line initialize the behavior of the particles of the system. The parameters are:
  1. The device
  2. The name of the texture effect for point sprites
  3. A sampler for the texture
  4. from the third parameter on are the particle system parts that define the behavior.

 

4.1.7. The Update method
The only new line of the update method updates the particle system. It receives the total time in seconds that the application has run.

particleSystem.update((float)gameTime.TotalGameTime.TotalSeconds);

 

4.1.8. The Draw method
The new part of this method disables the cull mode, draws the particle system and enables back the cull:

CullMode currentCullMode = device.RenderState.CullMode;
device.RenderState.CullMode = CullMode.None;
particleSystem.Draw(gameTime);
device.RenderState.CullMode = currentCullMode;


 

4.1.9. Conclusion
The main benefits of the particle system of BetaCell is that it's made by composition instead of extension and that the flexibility of the creation doesn't affect the runtime performance.

Click here to go to the forum discussion of this tutorial
 
 

Advertisement
 

4.1.10. Complete source code
using System;
using System.Collections.Generic;
using System.Threading;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

using BetaCell.Debug;
using BetaCell.Util.GlobalInfo.Content;
using BetaCell.Environment.Camera;
using BetaCell.Util.GlobalInfo;

using BetaCell.Environment.Particles;
using BetaCell.Environment.Texture;
using BetaCell.Environment.Particles.Initializers;
using BetaCell.Util.RenderStates;
using BetaCell.Environment.Texture.Feeder;
using BetaCell.Environment.Particles.Parts;

namespace Starter
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        //Game attributes
        GraphicsDeviceManager graphics;
        GraphicsDevice device;
        ContentManager content;

        BCFirstPersonHumanCamera camera;
        //end game attributes

        BCParticleSystem particleSystem;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            graphics.PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8;
            content = new ContentManager(Services);

#if(XBOX360)
            graphics.PreferredBackBufferWidth = 1280;
            graphics.PreferredBackBufferHeight = 768;
#else
            graphics.PreferredBackBufferWidth = 852;
            graphics.PreferredBackBufferHeight = 480;
#endif
        }

        protected override void Initialize()
        {
            device = graphics.GraphicsDevice;
            BCLogger.instance.init(device, content);
            BCInitializationManager.initialize(content);

            OnActivated(null, null);

            BCLogger.instance.message.color = Color.White;

            //-----
            //Particle system
            //-----

            BCMemoryTextureStrategy torch = new BCMemoryTextureStrategy(
                content.Load<Texture2D>("Content/gameAssets/torch"));

            BCCircleInitializer circleInitializer = new BCCircleInitializer(
                3.5f, new Color(192, 192, 192, 64),
                150, 400,
                0.10f, 0.15f,
                0.8f, 1.0f, -0.2f, 0.2f,
                Vector3.Zero
            );
            circleInitializer.rotationPlane = BCCircleInitializer.PLANE_XY;

            particleSystem = new BCParticleSystem(
                device,
                torch,
                0,
                null,
                450,
                0.005f,
                "Particles.Base",
                circleInitializer,
                new BCAdditiveAlphaRenderStrategy()
            );

            BCSampler sampler = new BCSampler(
                "LINEAR", "LINEAR", "POINT", "4",
                "CLAMP", "CLAMP", "0xffffffff");

            BCParticleRotationPart rotation = new BCParticleRotationPart("Particles.Parts.RotateXY", 5);
            BCParticleOscillationPart oscilation = new BCParticleOscillationPart("Particles.Parts.OscillateZ", 5.0f, 0.5f);

            BCParticleDistanceResizePart resizeDistance =
                new BCParticleDistanceResizePart("Particles.Parts.DistanceResize", 800);
            resizeDistance.linearDecimator = 1;

            BCParticlePow2ResizePart pow2Resize = new BCParticlePow2ResizePart(
                "Particles.Parts.ParticlePow2Resize", 100
            );

            BCParticleFade fade = new BCParticleFade("Particles.Parts.Fade");

            particleSystem.initEffect(
                device,
                "Shader.Texture.Sprite",
                sampler, rotation, oscilation, pow2Resize, resizeDistance, fade
            );

            base.Initialize();
        }

        protected override void OnActivated(object sender, EventArgs args)
        {
            buildViewMatrix();
            base.OnActivated(sender, args);
        }

        void buildViewMatrix()
        {
            Vector3 pos = new Vector3(0, 0, -4);
            Vector3 look = new Vector3(0, 0, 1);
            look.Normalize();

            camera = new BCFirstPersonHumanCamera(look, pos,
                MathHelper.PiOver4,
                (float)this.Window.ClientBounds.Width / (float)this.Window.ClientBounds.Height,
                1f, 200);

            BCGlobalInfo.instance.setMatrix(
                BCGlobalInfo.VIEW_INDEX,
                camera.getViewMatrix(-1)
            );

            BCGlobalInfo.instance.setMatrix(
                BCGlobalInfo.PROJECTION_INDEX,
                camera.getProjectionMatrix(-1)
            );
        }

        protected override void Update(GameTime gameTime)
        {
            particleSystem.update((float)gameTime.TotalGameTime.TotalSeconds);
            moveCamera(gameTime);
        }

        private void moveCamera(GameTime gameTime)
        {

            float df = 0;
            float ds = 0;
            float dy = 0;
            float pitch = 0;
            float angleY = 0;

            float speed = 5f;
#if (XBOX360)
            GamePadState gamepad = GamePad.GetState(PlayerIndex.One);

            df = gamepad.ThumbSticks.Left.Y;
            ds = gamepad.ThumbSticks.Left.X;
            pitch = -gamepad.ThumbSticks.Right.Y * 0.05f;
            angleY = gamepad.ThumbSticks.Right.X * 0.05f;

            if (gamepad.IsButtonDown(Buttons.RightShoulder))
            {
                dy += 0.25f;
            }
            if (gamepad.IsButtonDown(Buttons.LeftShoulder))
            {
                dy -= 0.25f;
            }

            speed = new Vector3(df, ds, dy).Length() * 10;
#else

            KeyboardState keys = Keyboard.GetState();
            MouseState mouse = Mouse.GetState();

            if (keys.IsKeyDown(Keys.W))
            {
                df += 1;
            }
            if (keys.IsKeyDown(Keys.S))
            {
                df -= 1;
            }
            if (keys.IsKeyDown(Keys.D))
            {
                ds += 1;
            }
            if (keys.IsKeyDown(Keys.A))
            {
                ds -= 1;
            }
            if (keys.IsKeyDown(Keys.Q))
            {
                dy += 1;
            }
            if (keys.IsKeyDown(Keys.E))
            {
                dy -= 1;
            }
            speed = 5f;
            if (keys.IsKeyDown(Keys.LeftControl))
            {
                speed = 10f;
            }

            angleY = ((float)this.Window.ClientBounds.Width / 2.0f - (float)mouse.X) * -0.05f;
            pitch = ((float)this.Window.ClientBounds.Height / 2.0f - (float)mouse.Y) * -0.05f;

            Mouse.SetPosition(this.Window.ClientBounds.Width / 2, this.Window.ClientBounds.Height / 2);

#endif
            camera.update(df, dy, ds, pitch, angleY, speed, (float)gameTime.ElapsedGameTime.TotalSeconds);

            BCGlobalInfo.instance.setMatrix(BCGlobalInfo.VIEW_INDEX, camera.getViewMatrix(-1));
        }

        protected override void Draw(GameTime gameTime)
        {
            graphics.GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0);

            CullMode currentCullMode = device.RenderState.CullMode;
            device.RenderState.CullMode = CullMode.None;
            particleSystem.Draw(gameTime);
            device.RenderState.CullMode = currentCullMode;

            BCLogger.instance.printFPS(gameTime);
            BCLogger.instance.flush();
            base.Draw(gameTime);
        }
    }
}