65.9K
CodeProject 正在变化。 阅读更多。
Home

基本粒子系统

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (81投票s)

2005 年 4 月 1 日

CPOL

5分钟阅读

viewsIcon

344411

downloadIcon

4429

介绍粒子系统的基本概念,以及如何创建爆炸和喷泉等基本效果。

引言

粒子系统早已渗透到游戏引擎中,成为现实环境的基本功能和基础之一。在本文中,我将向您介绍粒子系统的基本概念,并向您展示如何创建爆炸和喷泉等基本效果。本文不涉及太多图形方面的内容,并假定一旦您有了粒子系统本身,就可以随意以您喜欢的方式显示它。

单个粒子

粒子系统实际上只是一组聚集在一起并具有相同一般行为的粒子。这些粒子可以是任何东西,从汽车撞到墙壁时的碎片,到下雨时的水滴。

所有粒子都有几个共同点 - 位置、方向、颜色和年龄。每个粒子都保留其在空间中的位置、其移动的方向、自身的颜色以及它存活了多久。

在开始研究粒子之前,我们需要一个类来保存有关位置和方向的信息。由于我们处理的是 3D 世界,一个简单的 3D 向量应该就足够了。您可以在随附的文件中找到一个功能齐全的向量类。现在,理解 Vector 是一个封装了三个 float 变量的类,并具有用于添加、减去和乘以向量的函数的知识对我们来说就足够了。

现在让我们看看我们的基本粒子

using System;
using System.Drawing;

namespace Particles
{
    /// <SUMMARY>
    /// Summary description for Particle.
    /// </SUMMARY>
    public class Particle
    {
        public static readonly int MAX_LIFE = 1000;
        
        // Position of the particle
        private Vector m_Position;
        // Direction and speed the particle is moving
        private Vector m_Velocity;
        // Age of the particle
        private int m_Life;
        // Color of the particle
        private Color m_Color

        /// <SUMMARY>
        /// Default constructor
        /// </SUMMARY>
        public Particle() : this(Vector.Zero, Vector.Zero, Color.Black, 0)
        { }

        /// <SUMMARY>
        /// Constructor
        /// </SUMMARY>
        /// <PARAM name="pos">Position 
        ///   <SEE cref="Vector" /> of newly created particle</PARAM>
        /// <PARAM name="vel">Velocity 
        ///   <SEE cref="Vector" /> of newly created particle</PARAM>
        /// <param name="col">Color of newly created particle</param>
        /// <PARAM name="life">Starting age of newly created particle</PARAM>
        public Particle(Vector pos, Vector vel, Color col, int life)
        {
            // Create particle at given position
            m_Position = pos;
            // Set particle's speed to given speed
            m_Velocity = vel
            // Set particle's color to given color
            m_Color = col;
            // Make sure starting age is valid
            if (life < 0)
                m_Life = 0;
            else
                m_Life = life;
        }

        /// <SUMMARY>
        /// Update position, velocity and age of particle
        /// </SUMMARY>
        /// <RETURNS>False - if particle is too old and should be killed
        /// True - otherwise</RETURNS>
        public bool Update()
        {
            // Update particle's movement according to environment
            m_Velocity = m_Velocity - Environment.getInstance().Gravity
                                    + Environment.getInstance().Wind;
            // Update particle's position according to movement
            m_Position = m_Position + m_Velocity;
            // Update particle's age
            m_Life++;
            // If particle if too old
            if (m_Life > MAX_LIFE)
                // Notify caller to kill particle
                return false;
            return true;
        }
        #region Accesors

        /// <SUMMARY>
        /// Read Only - Position <SEE cref="Vector" /> of the particle
        /// </SUMMARY>
        public Vector Position
        {
            get { return m_Position; }
        }
        /// <SUMMARY>
        /// Read Only - Velocity <SEE cref="Vector" /> of the particle
        /// </SUMMARY>
        public Vector Velocity
        {
            get { return m_Velocity; }
        }
        /// <SUMMARY>
        /// Read Only - Age of the particle
        /// </SUMMARY>
        public int Life
        {
            get { return m_Life; }
        }
        /// <summary>
        /// Read Only - Color of the particle
        /// </summary>
        public Color Color
        {
            get { return m_Color; }
        }
        #endregion Accessors
    }
}

代码本身就解释得很清楚了,我相信唯一需要解释的部分是下面的这行代码

    // Update particle's movement according to environment 
    m_Velocity = m_Velocity - Environment.getInstance().Gravity 
                    + Environment.getInstance().Wind;

由于我们的 Particle 只是我们世界中的一个小实体,它会受到重力和风等外部力的影响。在下一节中,我们将介绍环境。

环境

我们的环境包括所有将影响所有不同系统中的所有粒子的外部力。这些力包括琐碎的重力和风,但也可以包括温度等力,或者您可能想到的任何其他想法。由于我们只需要一个环境实例,因此我将其实现为单例模式。

using System;

namespace Particles
{
    /// <SUMMARY>
    /// Summary description for Enviroment.
    /// </SUMMARY>
    public class Environment
    {
        /// <SUMMARY>
        /// Single instance of the Environment
        /// </SUMMARY>
        private static Environment m_Instance = new Environment();

        // Default Gravity vector in our world
        private Vector m_Gravity = Vector.Zero;
        // Default Wind vector in our world
        private Vector m_Wind = Vector.Zero;

        /// <SUMMARY>
        /// Protected constructor
        /// </SUMMARY>
        protected Environment()
        {
        }

        // Public accessor function to get an instance of the Environment
        public static Environment getInstance()
        {
            return m_Instance;
        }

        /// <SUMMARY>
        /// Accessor for the Gravity Vector
        /// </SUMMARY>
        public Vector Gravity
        {
            get { return m_Gravity; }
            set { m_Gravity = value; }
        }
        /// <SUMMARY>
        /// Accessor for the Wind Vector
        /// </SUMMARY>
        public Vector Wind
        {
            get { return m_Wind; }
            set { m_Wind = value; }
        }
    }
}

这里没有什么应该让您皱眉的。

系统抽象类

到目前为止,我们只看到了单个粒子。如果您尝试过,观察屏幕上一个点四处移动可能很有趣,但这并不是真正的乐趣。粒子系统的美感只有在大量粒子一起移动时才能体现出来。在本节中,我们将创建系统的基本类。这个类实际上是一个抽象类,它将处理粒子列表,并要求每个继承它的类实现一个创建新粒子的函数和一个更新这些粒子的函数。让我们看看代码

using System;
using System.Collections;
using System.Drawing;

namespace Particles
{
    /// <SUMMARY>
    /// Summary description for ParticlesList.
    /// </SUMMARY>
    public abstract class ParticlesSystem
    {
        // Array to keep all the particles of the system
        protected ArrayList m_Particles = new ArrayList();
        // Should the particles regenerate over time
        protected bool m_Regenerate = false;
        // Central position of the system
        protected Vector m_Position; 
        // Default color of a particle
        protected Color m_Color;

        /// <SUMMARY>
        /// Generate a single particle in the system.
        /// This function is used when particles
        /// are first created, and when they are regenerated
        /// </SUMMARY>
        /// <RETURNS>New particle</RETURNS>
        protected abstract Particle GenerateParticle();

        /// <SUMMARY>
        /// Update all the particles in the system
        /// </SUMMARY>
        /// <RETURNS>False - if there are no more particles in system
        /// True - otherwise</RETURNS>
        public abstract bool Update();


        /// <SUMMARY>
        /// Draw all the particles in the system
        /// </SUMMARY>
        /// <PARAM name="g">Graphics object to be painted on</PARAM>
        public virtual void Draw(Graphics g)
        {
            Pen pen;
            int intense;
            Particle part;

            // For each particle in the system
            for (int i = 0; i < m_Particles.Count; i++)
            {
                // Get the current particle
                part = this[i];
                // Calculate particle intensity
                intense = (int)((float)part.Life / PARTICLES_MAX_LIFE);
                // Generate pen for the particle
                pen = new Pen(Color.FromArgb(intense * m_Color.R , 
                         intense * m_Color.G, 
                         intense * m_Color.B));
                // Draw particle
                g.DrawEllipse(pen, part.Position.X, part.Position.Y, 
                  Math.Max(1,4 * part.Life / PARTICLES_MAX_LIFE),
                  Math.Max(1,4 * part.Life / PARTICLES_MAX_LIFE));
                pen.Dispose();
            }
        }


        /// <SUMMARY>
        /// Indexer allowing access to each particle in the system
        /// </SUMMARY>
        public Particle this[int index]
        {
            get
            {
                return (Particle)m_Particles[index];
            }
        }

        /// <SUMMARY>
        /// Accessor to the number of particles in the system
        /// </SUMMARY>
        public int CountParticles
        {
            get { return m_Particles.Count; }
        }

        /// <SUMMARY>
        /// Accessor to the maximum life of particles in the system
        /// </SUMMARY>
        public virtual int PARTICLES_MAX_LIFE
        {
            get { return particleMaxLife; }
        }
    }
}

这三个构造函数都易于理解。GenerateParticle() 函数将在创建新粒子时使用,无论是完全新粒子,还是当粒子死亡时我们希望替换它时。Update() 将用于更新系统中的粒子。Update() 需要决定何时以及是否创建新粒子。最后,Draw() 将用于在给定的 Graphics 对象上显示粒子系统。

2 个基本粒子系统

既然我们已经看到了需要实现的两个基本接口,我们就需要开始实现粒子系统。爆炸和喷泉是两个更基本的系统。我将在这里演示它们。

爆炸

在爆炸中,粒子会四处飞溅。这很容易实现 - 我们只需设置所有粒子从系统中心开始,并以随机方向、随机速度移动。重力会处理其他所有事情。

using System;

namespace Particles
{
    /// <SUMMARY>
    /// Summary description for Explosion.
    /// </SUMMARY>
    public class PSExplosion : ParticlesSystem
    {

        private static readonly int DEFAULT_NUM_PARTICLES = 150;

        // Random numbers generator
        private Random m_rand = new Random();

        /// <SUMMARY>
        /// Default constructor
        /// </SUMMARY>
        public PSExplosion() : this(Vector.Zero, Color.Black)
        { }

        /// <SUMMARY>
        /// Constructor
        /// </SUMMARY>
        /// <PARAM name="pos">Starting position of system</PARAM>
        public PSExplosion(Vector pos) : this(pos, Color.Black)
        { }

        /// <SUMMARY>
        /// Constructor
        /// </SUMMARY>
        /// <PARAM name="pos">Starting position of system</PARAM>
        /// <PARAM name="col">Color of the particles in the system</PARAM>
        public PSExplosion(Vector pos, Color col)
        {
            // Set system's position at given position
            m_Position = pos;
            // Set system color to given color
            m_Color = col;
            // Create all the particles in the system
            for (int i = 0; i < DEFAULT_NUM_PARTICLES; i++)
            {
                // Create particle, and add it to the list of particles
                m_Particles.Add(GenerateParticle());
            }
        }


        /// <SUMMARY>
        /// Update all the particles in the system
        /// </SUMMARY>
        /// <RETURNS>False - if there are no more particles in system
        /// True - otherwise</RETURNS>
        public override bool Update()
        {
            Particle part;
            // Get number of particles in the system
            int count = m_Particles.Count;

            // For each particle
            for (int i=0; i < count; i++)
            {
                // Get particle from list
                part = (Particle)m_Particles[i];
                // Update particle and check age
                if ((!part.Update()) || (part.Life > 150))
                {
                    // Remove old particles
                    m_Particles.RemoveAt(i);
                    // Update counter and index
                    i--;
                    count = m_Particles.Count;
                }
            }
            // If there are no more particles in the system
            if (m_Particles.Count <= 0)
                return false;
            return true;
        }

        /// <SUMMARY>
        /// Generate a single particle in the system.
        /// This function is used when particles
        /// are first created, and when they are regenerated
        /// </SUMMARY>
        /// <RETURNS>New particle</RETURNS>
        protected override Particle GenerateParticle()
        {
            // Generate random direction & speed for new particle
            float rndX = 2 * ((float)m_rand.NextDouble() - 0.5f);
            float rndY = 2 * ((float)m_rand.NextDouble() - 0.5f);
            float rndZ = 2 * ((float)m_rand.NextDouble() - 0.5f);

            // Create new particle at system's starting position
            Particle part = new Particle(m_Position,
                // With generated direction and speed
                new Vector(rndX, rndY, rndZ),
                // And a random starting life
                m_rand.Next(50));

            // Return newly created particle
            return part;
        }
    }
}

在此示例中,我们在系统创建时创建了所有粒子。我们将它们都放置在系统的确切起始点,尽管为了更逼真的外观,我们可能在那里也增加了一点随机性。每个新粒子都被赋予一个随机年龄 - 这样粒子就不会同时死亡。我们还决定杀死年龄超过 150 的粒子。我们可以选择其他标准,例如仅当粒子离开显示视图或撞到东西时才杀死粒子。

喷泉

喷泉示例的出现有两个原因。首先,喷泉会再生死去的粒子,以便继续“喷射”或其他喷泉会做的事情。其次,并非所有粒子都会一次性创建 - 我们先创建几个粒子,然后随着时间的推移,向系统中添加越来越多的粒子。

using System;

namespace Particles
{
    /// <SUMMARY>
    /// Summary description for Firework.
    /// </SUMMARY>
    public class PSFountain : ParticlesSystem
    {
        private static readonly int DEFAULT_NUM_PARTICLES = 250;

        // Random numbers generator
        private Random m_rand = new Random();

        /// <SUMMARY>
        /// Default constructor
        /// </SUMMARY>
        public PSFountain() : this(Vector.Zero, Color.Black)
        { }

        /// <SUMMARY>
        /// Constructor
        /// </SUMMARY>
        /// <PARAM name="pos">Starting position of system</PARAM>
        public PSFountain(Vector pos) : this(pos, Color.Black)
        { }

        /// <SUMMARY>
        /// Constructor
        /// </SUMMARY>
        /// <PARAM name="pos">Starting position of system</PARAM>
        /// <PARAM name="col">Color of the particles in the system</PARAM>
        public PSFountain(Vector pos, Color col)
        {
            // Mark that this system regenerates particles
            m_Regenerate = true;
            // Set system's position at given position
            m_Position = pos;
            // Set system color to given color
            m_Color = col;
            // Create ONLY 5 particles
            for (int i = 0; i < 5; i++)
            {
                // Create particle, and add it to the list of particles
                m_Particles.Add(GenerateParticle());
            }
        }

        /// <SUMMARY>
        /// Generate a single particle in the system. 
        /// This function is used when particles
        /// are first created, and when they are regenerated
        /// </SUMMARY>
        /// <RETURNS>New particle</RETURNS>
        protected override Particle GenerateParticle()
        {
            // Generate random direction & speed for new particle
            // In a fountain, particles move almost straight up
            float rndX = 0.5f * ((float)m_rand.NextDouble() - 0.4f);
            float rndY = -1 - 1 * (float)m_rand.NextDouble();
            float rndZ = 2 * ((float)m_rand.NextDouble() - 0.4f);

            // Create new particle at system's starting position
            Particle part = new Particle(m_Position,
                // With generated direction and speed
                new Vector(rndX, rndY, rndZ),
                // And a random starting life
                m_rand.Next(50));

            // Return newly created particle
            return part;
        }

        /// <SUMMARY>
        /// Update all the particles in the system
        /// </SUMMARY>
        /// <RETURNS>False - if there are no more particles in system
        /// True - otherwise</RETURNS>
        public override bool Update()
        {
            Particle part; 
            // Get number of particles in the system
            int count = m_Particles.Count;

            // For each particle
            for (int i=0; i < count; i++)
            {
                // Get particle from list
                part = (Particle)m_Particles[i];
                // Update particle and check age
                if ((!part.Update()) || (part.Life > 150))
                {
                    // Remove old particles
                    m_Particles.RemoveAt(i);
                    // Update counter and index
                    i--;
                    count = m_Particles.Count;
                }
            }
            // If there aren't enough particles
            if  (m_Particles.Count < DEFAULT_NUM_PARTICLES)
                // Add another particles
                m_Particles.Add(GenerateParticle());

            // Always return true, since system is regenerating
            return true;
        }
    }
}

正如您所见,与 Explosion 类相比,更改非常小。在这里,我们在系统创建时只创建了几个粒子,并在每次更新系统时添加一个新粒子。我们还稍微改变了粒子运动的数学计算 - 现在它们几乎是直线向上移动,只是稍微偏向两侧。

更多系统

创建更多系统非常简单。其他系统的例子包括下雨和下雪、龙卷风、冲水、落叶、烟雾等等。选择是无限的。在随附的演示中,我包含了一个额外的系统 - 烟花。

结论

我在随附的文件中包含了一个所述系统的简单示例。我使用的显示非常简单 - 每个粒子都是一个单独的椭圆。但是,如果您考虑每个粒子的年龄,通过更改粒子的尺寸和透明度,您就可以创造出令人惊叹的效果。

使用描述的模型,只需几分钟即可创建新系统,并且非常欢迎您将您自己的系统发送给我添加到本文中。

历史

  • 2005 年 4 月 3 日 - 更新了演示项目(感谢 Mark Treadwell)。
  • 2005 年 4 月 9 日 - 修复了 Vector 类中的一个错误。为粒子添加了颜色。在 ParticlesSystem 抽象类中添加了 Draw() 函数。在附带的项目中添加了烟花系统。
  • 2005 年 4 月 13 日 - 修复了性能问题(感谢 John Fisher)。
  • 2005 年 4 月 19 日 - Junai 建议改进构造函数。
© . All rights reserved.