[翻译]Oreilly.Learning.XNA.3.0之six
第十四章 粒子系统
在本章中,你会重新开始3D游戏,你已经创建了本书的3D部分的很多游戏。具体来说,你会加入一些粒子效果到你的游戏中。粒子效果允许游戏开发者去创建一个令人兴奋的和真实的特殊效果,开发者使用他们如烟雾,火焰,爆炸,魔术和其他效果,它可以使更多游戏非常精彩和令人兴奋和有魅力。
首先,让我们谈谈粒子。什么是粒子?在游戏开发术语,一个粒子的典型代表了一粒子效应单一组成部分。一个单一的火花在一个焰火中,一个单一的元素在一个滚滚浓烟里,一个单一的一闪一闪的灯光在一个魔术效果都是粒子的例子。多粒子的结合在一个特殊的效果流动是什么所谓的粒子效应。
粒子引擎是粒子系统背后的驱动机制。一个粒子引擎操作多个粒子去创建一个粒子的效果通过力的应用,如重力和动力去使粒子移动和现实的方式作出反应。
在本章,您将创建一个粒子引擎,爆炸产生的粒子效果,你把它插入到游戏中当打击一个敌人的飞船。
创建一个自定义的顶点
创建我们的粒子引擎,我们将工作在3D游戏代码,这些代码已经创建了很多在这本书的3D节。打开你完成的解决方案在第12章的最后,使用它作为本章的练习。
在此之前的DirectX 8,粒子效果通常被创建通过画一个矩形使用两个三角形,然后涂上纹理到那个矩形。在DirectX,一个重要的性能提高通过引进一些东西称为一个point sprite.Point sprites允许开发者去绘制单一的粒子通过指定唯一的顶点。单一的顶点能被着色和涂上纹理。这个提高允许开发者用一个单一的顶点工作而不是四个和单一点的primitive代替两个三角形的primitives为每一个粒子。这可能听起来并不很多,但是当你考虑这个效果可以使用数百甚至上千的粒子,它代表一个显着的性能提升。
在本书的3D部分的开始,你学习了相关的3D对象的绘制使用三角形的primitives.你绘制这些对象通过保存顶点为这个对象在顶点数组中(还记得VertexPositionColor和VertexPositionTexture对象?)。然后你绘制这些对象使用一个TriangleList,TriangleStrip,或者TriangleFan的primitive类型。
在本节中,你会做同样的事情,但是和point sprites.换句话说,每一个顶点你保存在你的顶点数组中,它将代表一个单一的粒子。你同样可以绘制粒子使用一个primitive类型,但是在这种情况下,你使用primitive类型的PointList,它将绘制一个point sprites的表单。
而不是使用VertexPositionColor或者另外的预先确定顶点的类型,你准备去定义你自己的顶点格式在这节。为什么?回忆起所有3D对象移动有位置和方向。你的粒子同样会有位置和方向,但是除此之外,你同样定义两个其他组成部分为每一个粒子。首先,你定义一个颜色为每个单独的粒子。颜色将会从一个2D纹理中取出通过一个纹理坐标(粒子将会被给予,颜色到相应的象素在2D纹理中在一个给定的坐标)。其次,你将会定义粒子顶点它自己的大小。这里没有预定义顶点类型,它包含所有的这些数据类型,这样你您将创建您自己的数据类型。
它实际上相当简单,创建自己的顶点。不仅是简单,但它很有乐趣-并让我们面对现实,最终,这是我们真正想要的,是不是?所以让我们开始吧。首先你需要做的是创建一个新的空的C#代码文件。做这个通过右击在你的解决方案在资源管理器中,选择Add-New Item....,选择Code File模板在窗口的右边。Code File 模板将创建一个空的C#文件。命名这个文件Particle.cs,正如图14-1所示。
一个自定义顶点格式被表示,通过一个struct在C#中。除定义这个struct的成员之外在你的自定义顶点中,你同样需要定义一个元素,它返回一个VertexElement对象的一个数组。这VertexElement的数组将允许你去创建一个VertexDeclaration对象为这个顶点。
注意:如果你还记得,VertexDeclarations被用在你的Draw方法去告诉图形设备,什么样的数据类型将要发送到它.是从来没有比现在更重要的,因为你创造自定义的顶点类型,图形设备之前从来没有看见过。可能需要去指定一个VertexDeclaration,它将让图形设备明白如何去绘制你的自定义顶点对象。
一旦你的空白代码文件被打开和准备好了,添加下面的代码到文件中:
2 using Microsoft.Xna.Framework.Graphics;
3 namespace _3D_Game
4 {
5 struct Particle
6 {
7 public Vector3 position;
8 public Vector3 direction;
9 public Vector2 textureCoordinate;
10 public float pointSize;
11 public static readonly VertexElement[] vertexElements =
12 {
13 new VertexElement(0, 0, VertexElementFormat.Vector3,VertexElementMethod.Default,VertexElementUsage.Position, 0),
14 new VertexElement(0, 24, VertexElementFormat.Single,VertexElementMethod.Default,VertexElementUsage.TextureCoordinate, 0),
15 new VertexElement(0, 32, VertexElementFormat.Single,VertexElementMethod.Default,VertexElementUsage.PointSize, 0),
16 };
17 }
18 }
第三个参数是顶点类型的。第五个参数是一个VertexElementUsage参数,告诉HLSL代码这个顶点如何去使用。这个值是什么关系数据,使用HLSL语义。另外一个参数被使用到以后的自定义顶点,但是现在你可以使用默认的值(0为参数1,VertexElementMethod.Default为参数4,和0为参数6)
第一个VertexElement是位置向量和它的Vector3类型。由于为了此元素的使用,是代表了顶点的位置,VertexElementUsage.Position被使用给第五个参数。通过使用智能感知的在Visual Studio在VertexElementUsage类,你可能会看见所有的值的位置,你可以为这个参数使用它。大多数值应该看起来有点相似,因为它们符合在前一章所讨论的语义。
谈下句法,这个VertexElementUsage参数是使这些句法工作的。还记得吗,如果你有一个POSITION0的句法分配到一个输入参数为一个顶点着色器,顶点的位置将自动的分配到那个变量中。XNA知道那个数据可以被传递到HLSL中,是通过这个VertexElementUsage参数。
为了举例说明,考虑到下面代码部分从HLSL effect文件中:
2 {
3 float4 Position : POSITION0;
4 };
5 struct VertexShaderOutput
6 {
7 float4 Position : POSITION0;
8 };
9 VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
10 {
11 VertexShaderOutput output;
12 float4 worldPosition = mul(input.Position, World);
13 float4 viewPosition = mul(worldPosition, View);
14 output.Position = mul(viewPosition, Projection);
15 return output;
16 }
顶点着色器(VertexShaderFunction)接收一个VertexShaderInput类型的参数。在VertexShaderInput的struct是一个属性称为Position,它有一个POSITION0的句法。这个句法告诉HLSL,什么时候顶点着色器运行,它应该摄取数据来作为顶点着色器的位置,并且分配数据到VertexShaderInput的struct的Position属性。你可以考虑下这个作为一个数据吸引机制(当顶点着色器运行时,HLSL将寻找指定的数据作为位置,如果数据是可用的,HLSL将拉起这个数据把它插入到适当的变量中)。
然而,什么使数据变成变量摆在首位呢?这就是先前你所做的,在你的Particle struct.VertexElementUsage参数你传递到这个元素在你的struct中,分配到句法到XNA代码它本身里。在这中情况下,第一个元素,它相应的Particle struct的位置元素,被VertexElementUsage Position给予。这告诉XNA它需要传递数据,你把数据放在这个struct的位置元素中,到HLSL代码,而且它基本上需要打上一个标签在数据上,命名它是一个位置。
That's the push side of the data.XNA把数据推入HLSL中,把它打上相应的VertexElementUsage属性标签。然后,HLSL领取这个数据并分配它到HLSL文件中的变量,基于相应的句法(译者:用相应的句法来实现上述过程)。
创建一个粒子引擎
现在你已经定义了一个自定义的顶点类型,是时候到粒子引擎的本身去了。你将会创建三个新的类在这节:ParticleSettings,它将保留设置为单独的粒子;particleExplositionSettings,它将保留设置为单独的爆炸;ParticleExplosion,它代表一个单一爆炸效果,并且为移动,更新,和绘制所有的粒子爆炸效果负责。
创建一个新的类在你的项目中,称为ParticleSettings,并且代替ParticleSettings.cs文件的内容用下面的代码:
2 {
3 class ParticleSettings
4 {
5 // Size of particle
6 public int maxSize = 2;
7 }
8 class ParticleExplosionSettings
9 {
10 // Life of particles
11 public int minLife = 1000;
12 public int maxLife = 2000;
13 // Particles per round
14 public int minParticlesPerRound = 100;
15 public int maxParticlesPerRound = 600;
16 // Round time
17 public int minRoundTime = 16;
18 public int maxRoundTime = 50;
19 // Number of particles
20 public int minParticles = 2000;
21 public int maxParticles = 3000;
22 }
23 }
在ParticleExplosionSettings类,位于同样的代码文件中,那里有很多设置。(在C#中,它是完全合法的,均可在同一文件的多个类中,因为这两个类是相关的,它是有意义的把它们两个都在你的ParticleSetting.cs文件中。)在这个类的设置中定义粒子效果的不同部分,如效果持续多长时间,多少粒子被应用于这个效果中,等。所有这些将在下面的表单里做更详细的解释。
从本质上讲,你的ParticleExplosion类,建成后,将有如下功能:
1。当一个ParticleExplosion实例被创建,你可以创建一个Particle对象的数组(粒子是自定义顶点,你在本章前面的定义过)。
数组的大小将会在minParticles和maxParticles值之间。
2。你的particleExplosion类将运行在我们称做粒子的"rounds"中。round是一个单间的时间周期表示当一个新的粒子被释放
从爆炸中。Rounds将在每个X毫秒开始,这个X是在你的minRoundTime和maxRoundTime设置之间。每一个round,某些在
minParticlesPerRound和maxParticlePerRound之间的粒子将会从爆炸中释放
3。一旦一定数量的粒子通过ParticleExplosion类释放达到粒子的数组中粒子的数量,这个爆炸效果将不被任何更多的粒子创建。
4。每次这个Update方法被调用用ModelManager类,particleExplosition类的Update方法将被调用。这个方法将循环所有
的活动粒子,并且更新它们的位置。由于我们在外太空,这里没有额外的力例如重力去处理,所以粒子将以直线移动,通过你的
粒子struct的方向元素。
5。一个粒子引擎同样有杀死粒子当它周期已满时的能力。这也就是在你的代码中设置minLife和maxLife的目的。每个
ParticleExplosion将会被给一个寿命值生成,大于minLife小于maxLife.一旦一个爆炸的寿命时间已经达到,它在每一次round中使粒子退休(译者:粒子死 掉)。
6。一旦所有的粒子退休,则爆炸结束,它被删除。
好了,现在你有一个想法,如何ParticleExplosion类可以工作,让我们把它一起扔掉。创建一个新的类在你的项目中命名为particleExplosion.确保你下面的命名空间在文件的上部分。
2 using Microsoft.Xna.Framework;
3 using Microsoft.Xna.Framework.Graphics;
接下来,添加下面的类别变量:
2 Particle[] particles;
3 // Position
4 Vector3 position;
5 // Life
6 int lifeLeft;
7 // Rounds and particle counts
8 int numParticlesPerRound;
9 int maxParticles;
10 static Random rnd = new Random( );
11 int roundTime;
12 int timeSinceLastRound = 0;
13 // Vertex and graphics info
14 VertexDeclaration vertexDeclaration;
15 GraphicsDevice graphicsDevice;
16 // Texture
17 Vector2 textureSize;
18 // Settings
19 ParticleSettings particleSettings;
20 // Array indices
21 int endOfLiveParticlesIndex = 0;
22 int endOfDeadParticlesIndex = 0;
particles
你的自定义粒子顶点对象的数组。
position
爆炸的位置,或者这个位置来自新发送出来的粒子。这同样是飞船被击中的位置,触发这个爆炸。
lifeLeft
在粒子开始被删除前还有多少有生命的粒子活着的。
numPartclesPerRound
每一round多少粒子被创建,多少被删除,爆炸没有留下活着的粒子。
maxParticles
一次爆炸能被创建的粒子的总数。
rnd
一个随机对象从Game1类传入通过构造器。这个对象是静态的因为你只想让它们中的一个存活在所有爆炸的实例中。
roundTime
在粒子rounds之间的时间数量。
timeSinceLastRound
一个计时器跟踪在下一个round开始还剩多少时间。
vertexDeclaration
一个顶点声明被创建,使用你的自定义顶点去允许图形设备了解如何绘制你的粒子顶点。
graphicsDevice
图形设备从Game1类传入。
textureSize
应用在爆炸类的纹理大小。这个纹理为你的爆炸的粒子,代表可能的不同颜色的数量。每一个粒子将会被映射到一个自由的点在这个纹理上,并分配纹理象素的颜色在那个点上。
particleSettings
一个ParticleSettings类的实例,应用这些设置到被创建的粒子上。
endofLiveParticlesIndex and endOfDeadParticlesIndex
粒子数组的索引。整个数组是示例当爆炸类被作为示例时,但是只有粒子它们被绘制,它们存活在endOfDeadParticlesIndex和endOfParticlesIndex之间。作为新的粒子被"创建"每一round,endofLiveParticlesIndex移动到数组中。作为新粒子被"删除"每一round,endOfDeadParticlesIndex移动表单中,每一次粒子被绘制,那唯一被绘制的在这两个索引之间的。
图14-2提供一个图形解释,粒子的数组在ParticleExplosion类函数中是如何工作的。当endOfDeadParticlesIndex和endOfLiveParticesIndex移动从左到右,粒子在两个索引之间的被绘制。粒子在左边表示死了,同时粒子在两个索引的右边表明还没有开始它的生命。
你同样需要提供一个方式给ModelManager类去定义当一个爆炸被完成时。添加下面的public属性到particleExplosion类:
2 {
3 get { return endOfDeadParticlesIndex == maxParticles; }
4 }
接下来,添加一个构造器到ParticleExplosion类中,如这里所示:
2 {
3 this.position = position;
4 this.lifeLeft = lifeLeft;
5 this.numParticlesPerRound = numParticlesPerRound;
6 this.maxParticles = maxParticles;
7 this.roundTime = roundTime;
8 this.graphicsDevice = graphicsDevice;
9 this.textureSize = textureSize;
10 this.particleSettings = particleSettings;
11 vertexDeclaration = new VertexDeclaration(graphicsDevice,Particle.vertexElements);
12 particles = new Particle[maxParticles];
13 InitializeParticles( );
14 }
对于构造器的结束,你实例化一个粒子对象的数组,你把maxParticles参数的大小传入。然后,那里有个调用InitializeParticles.这个方法将通过整个数组,并设置一个位置,随机方向,随机颜色用一个纹理坐标的形式,一个随机顶点大小基于maxSize设置在你的ParticleSettings类中。添加InitializeParticles方法,如下面所示:
2 {
3 // Loop until max particles
4 for (int i = 0; i < maxParticles; ++i)
5 {
6 // Assign a random texture coordinate for color
7 particles[i].textureCoordinate = new Vector2(rnd.Next(0, (int)textureSize.X) / textureSize.X,rnd.Next(0, (int)textureSize.Y) / textureSize.Y);
8 // All particles start where the explosion began
9 particles[i].position = position;
10 // Create a random velocity/direction
11 Vector3 direction = new Vector3((float)rnd.NextDouble( ) * 2 - 1,(float)rnd.NextDouble( ) * 2 - 1,(float)rnd.NextDouble( ) * 2 - 1);
12 direction.Normalize( );
13 // Multiply by NextDouble to make sure that
14 // all particles move at random speeds
15 direction *= (float)rnd.NextDouble( );
16 particles[i].direction = direction;
17 // Set random point size
18 particles[i].pointSize =(float)rnd.NextDouble( ) * particleSettings.maxSize;
19 }
20 }
此外,注意调用Normalize方向向量,在随机生成XYZ值之后。为什么你需要做这个?如果你不做,相反你的爆炸粒子爆炸成一个非常好看的圆球,它们也可以爆炸成一个立方体形式,什么原因?随机值的范围用于创建向量在开始的地方从-1到1在X,Y和Z轴。这将导致最长的水平向量 (1,0,0)和最长的对角线向量(1,1,1),当数以百计的粒子使用这个随机方向向量被创建时,将导致一个立方体的形式。还记得Normalize将使所有的向量有一个1个长度,所以规格化向量将维持随机的方向,但是将改变所有向量的数量级为1。也就是说你会有一个很漂亮的球形-但同样也意味着所有的粒子将会有同样的速度,这不是你想要的。为了解决这个问题,你乘以这个向量通过一个调用NextDouble方法从Random对象,它会有所不同的向量的长度。
现在,你需要编写ParticleExplosion类的Update方法。这个方法将负责移动这个粒子,以及添加新的粒子每一round,同时删除久的粒子(译者:死掉的粒子)每一round.添加下面的方法:
2 {
3 // Decrement life left until it's gone
4 if (lifeLeft > 0)
5 lifeLeft -= gameTime.ElapsedGameTime.Milliseconds;
6 // Time for new round?
7 timeSinceLastRound += gameTime.ElapsedGameTime.Milliseconds;
8 if (timeSinceLastRound > roundTime)
9 {
10 // New round - add and remove particles
11 timeSinceLastRound -= roundTime;
12 // Increment end of live particles index each
13 // round until end of list is reached
14 if (endOfLiveParticlesIndex < maxParticles)
15 {
16 endOfLiveParticlesIndex += numParticlesPerRound;
17 if (endOfLiveParticlesIndex > maxParticles)
18 endOfLiveParticlesIndex = maxParticles;
19 }
20 if (lifeLeft <= 0)
21 {
22 // Increment end of dead particles index each
23 // round until end of list is reached
24 if (endOfDeadParticlesIndex < maxParticles)
25 {
26 endOfDeadParticlesIndex += numParticlesPerRound;
27 if (endOfDeadParticlesIndex > maxParticles)
28 endOfDeadParticlesIndex = maxParticles;
29 }
30 }
31 }
32 // Update positions of all live particles
33 for (int i = endOfDeadParticlesIndex;i < endOfLiveParticlesIndex; ++i)
34 {
35 particles[i].position += particles[i].direction;
36 // Assign a random texture coordinate for color
37 // to create a flashing effect for each particle
38 particles[i].textureCoordinate = new Vector2(rnd.Next(0, (int)textureSize.X) / textureSize.X,rnd.Next(0, (int)textureSize.Y) / textureSize.Y);
39 }
40 }
让我们近距离的看一下这个方法中的逻辑。这个方法首先声明lifeLeft变量通过大量用去的时间,直到最后调用Update方法。记住当lifeLeft变量变成0,你将会开始删除粒子在每一round.
接下来,timeSinceLastRound计时器增加并且检查是否新的round应该开始。如果它该新的round,新的粒子被添加到被绘制的粒子表单中,通过增加endOfLiveparticlesIndex变量的值。如果索引变量准备好在数组的结尾(通过maxParticls指定),没有更多的粒子被添加到的粒子清单为了去绘制。
然后,lifeLeft变量被检查,看看是否它的值小于1(表明爆炸的生命结束)。如果是这种情况,endOfDeadParticlesIndex变量被增加,它将导致粒子从绘制粒子的表单中删除。一旦endOfDeadParticlesIndex变量达到数组的结尾,它将不在移动,同时IsDead属性你前面所添加的,将返回true(表明ModelManger,爆炸已经到了头,并准备删除)。
最后,该方法更新所有粒子,在被绘制的表单中(所有粒子在endOfDeadParticlesIndex和endOfLiveParticlesIndex变量)。
非常好。现在我们已经接近尾声!所有您需要做的是一个绘制代码方法对此次爆炸事件。此代码应该很简单,因为它没有什么不同比你以前的指定的代码:
2 {
3 // Only draw if there are live particles
4 if (endOfLiveParticlesIndex - endOfDeadParticlesIndex > 0)
5 {
6 // Set vertex declaration
7 graphicsDevice.VertexDeclaration = vertexDeclaration;
8 // Set HLSL parameters
9 effect.Parameters["WorldViewProjection"].SetValue(
10 camera.view * camera.projection);
11 // Draw particles
12 effect.Begin( );
13 // Draw particles
14 effect.Begin( );
15 foreach (EffectPass pass in effect.CurrentTechnique.Passes)
16 {
17 pass.Begin( );
18 graphicsDevice.DrawUserPrimitives<Particle>(PrimitiveType.PointList,particles, endOfDeadParticlesIndex,endOfLiveParticlesIndex - endOfDeadParticlesIndex);
19 pass.End( );
20 }
21 effect.End( );
22 }
23 }
Effect传入到你所有绘制的粒子中通过一个Effect参数与一个相机对象一起。你只是想去绘制一个实际需要被绘制的粒子(意思是这里一些在数组中的粒子在两个索引变量之间)。在设定Effect的参数后,你调用Effect的开始方法,并且循环它的每一个passes.注意调用DrawUserPrimitives和它使用PrimitiveType.PointList以绘制。同样,注意你数组中的索引,它指定从哪里提取数据(第三个参数),是你的endOfDeadParticlesIndex变量。最后的参数是多少个primitives(在这种情况下,指的是点)你想去绘制。该数字是不同于两个指数之间的变量。
这一切就这么简单。您现在有一个自定义的顶点,某些设置类去帮助声明事情的函数,一个爆炸类去操作并且绘制你的粒子。令人印象深刻!现在是时候去创建效果文件了,你会使用它去绘制你的粒子。
添加一个粒子效果文件
首先,你想创建一个文件夹为你的effects.右击Content节点在你的解决方案,然后选择Add-New Floder.命名这个文件夹为Effects.然后,右击新的Content\Effects文件夹,并选择Add-New Item...选择Effect File模版在窗口的右边,并且命名这个文件为Particle.fx,如图14-3所显示。
用下面的效果代码替换所有在你的新Particle.fx文件的代码:
2 Texture theTexture;
3 sampler ColoredTextureSampler = sampler_state
4 {
5 texture = <theTexture> ;
6 magfilter = LINEAR;
7 minfilter = LINEAR;
8 mipfilter= POINT;
9 AddressU = Clamp;
10 AddressV = Clamp;
11 };
12 struct VertexShaderInput
13 {
14 float4 Position : POSITION0;
15 float2 textureCoordinates : TEXCOORD0;
16 float pointSize : PSIZE0;
17 };
18 struct VertexShaderOutput
19 {
20 float4 Position : POSITION0;
21 float2 textureCoordinates : TEXCOORD0;
22 float pointSize : PSIZE0;
23 };
24 struct PixelShaderInput
25 {
26 float2 textureCoordinates : TEXCOORD0;
27 };
28 VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
29 {
30 VertexShaderOutput output;
31 output.Position = mul(input.Position, WorldViewProjection);
32 output.textureCoordinates = input.textureCoordinates;
33 output.pointSize = input.pointSize;
34 return output;
35 }
36 float4 PixelShaderFunction(PixelShaderInput input) : COLOR0
37 {
38 return tex2D( ColoredTextureSampler, input.textureCoordinates);
39 }
40 technique Technique1
41 {
42 pass Pass1
43 {
44 VertexShader = compile vs_1_1 VertexShaderFunction( );
45 PixelShader = compile ps_1_1 PixelShaderFunction( );
46 }
47 }
对于你来说大多数代码应该很熟悉,因为它与前面章节的例子很相似。注意你有一个纹理和一个纹理取样器。纹理坐标被发送到顶点着色器输入中,然后发送出到顶点着色器的输出里,最后到象素着色器的输入中。这个数据被设置在Particle 结构中当你初始化你的粒子在你的ParticleExplosion类的InitialzeParticles方法中。因为Particle结构的这个vertexElement方法指示这个textureCoordinate成员作为VertexUsageElement.TextureCoordinate,这个数据将自动被分配到任何的顶点着色器输入参数以一个TEXCOORD[n]句法。你不得不摄取这个数据在你的顶点着色器输入中,并且输出它在顶点着色器的输出中,为了可以再次把它挑出来在象素着色器输入中。
另外一件事,是将新的给你,是句法PSIZE[n]的用法。这声明一个单一顶点的大小。这句法是联系在C#代码中的你的Particle结构的VertexElementUsage.PointSize成员在一起,它是你的pointSize变量。这个变量同样设置在ParticleExplosion类的InitializeParticles方法中。当你从顶点着色器返回一个值,在HLSL中以一个PSIZE[n]句法,这个顶点的大小会相应的设置
添加你的粒子引擎到你的游戏中
现在你有一个效果和一个粒子引擎,你需要去修改你的代码去创建一个爆炸,当飞船被击中。
首先要做的,你需要添加纹理图象为粒子去使用。这个纹理将给粒子它们的颜色-每一个粒子将会分配一个随机的象素从这个纹理,并且给予这个颜色在纹理中的象素中。
如果你没有准备好,下载资源代码为本书的这章。在3D Game\Content\Textures文件夹,你会发现一个图象文件叫做Particle.png.添加它到你的项目中通过右击Content\Textures文件在解决方案中,选择Add-Existing Item...,然后浏览到Particle.png文件选择它。
现在打开ModelManager类,让我们去添加一些很酷的爆炸到你的游戏中,添加下面的类别变量:
2 ParticleExplosionSettings particleExplosionSettings =new ParticleExplosionSettings( );
3 ParticleSettings particleSettings = new ParticleSettings( );
4 Effect explosionEffect;
5 Texture2D explosionTexture;
接下来,你需要添加载资源为你的粒子,设置当前的粒子效果的技巧,同样设置纹理为那个效果。修改这个LoadContent方法在你的ModelManager类如下面的代码:
2 {
3 // Load explosion stuff
4 explosionTexture =Game.Content.Load<Texture2D>(@"textures\particle");
5 explosionEffect =Game.Content.Load<Effect>(@"effects\particle");
6 explosionEffect.CurrentTechnique =explosionEffect.Techniques["Technique1"];
7 explosionEffect.Parameters["theTexture"].SetValue(explosionTexture);
8 base.LoadContent( );
9 }
现在,在ModelManager的UpdateShots方法中,你将需要去找到那个位置,这个位置是你检测到是否子弹碰撞到了飞船。它是一个唯一的位置在这个类,在这里调用BasicModel CollidesWith方法。当前的代码应该看起来象这样:
2 {
3 // Collision! remove the ship and the shot.
4 models.RemoveAt(j);
5 shots.RemoveAt(i);
6 --i;
7 ((Game1)Game).PlayCue("Explosions");
8 break;
9 }
2 {
3 // Collision! add an explosion.
4 explosions.Add(new ParticleExplosion(GraphicsDevice,
5 models[j].GetWorld().Translation,
6 ((Game1)Game).rnd.Next(
7 particleExplosionSettings.minLife,
8 particleExplosionSettings.maxLife),
9 ((Game1)Game).rnd.Next(
10 particleExplosionSettings.minRoundTime,
11 particleExplosionSettings.maxRoundTime),
12 ((Game1)Game).rnd.Next(
13 particleExplosionSettings.minParticlesPerRound,
14 particleExplosionSettings.maxParticlesPerRound),
15 ((Game1)Game).rnd.Next(
16 particleExplosionSettings.minParticles,
17 particleExplosionSettings.maxParticles),
18 new Vector2(explosionTexture.Width,
19 explosionTexture.Height),
20 particleSettings));
21 // Remove the ship and the shot
22 models.RemoveAt(j);
23 shots.RemoveAt(i);
24 --i;
25 ((Game1)Game).PlayCue("Explosions");
26 break;
27 }
同时这将创建一个爆炸,你的类仍然需要更新爆炸,并且绘制它在每一祯。添加下面的方法到ModelManager类去更新爆炸,删除它们一旦它们被完成:
2 {
3 // Loop through and update explosions
4 for (int i = 0; i < explosions.Count; ++i)
5 {
6 explosions[i].Update(gameTime);
7 // If explosion is finished, remove it
8 if (explosions[i].IsDead)
9 {
10 explosions.RemoveAt(i);
11 --i;
12 }
13 }
14 }
2 UpdateExplosions(gameTime);
这个代码将会更新每一个爆炸在表单中,通过调用这个爆炸的Update方法。此外,如果一个爆炸完成了(通过IsDead访问器检测到),它被从这个表单中删除。
最后,在你的ModelManager类的Draw方法中,添加下面的代码去循环所有的爆炸,立即调用它们的Draw方法在调用base.Draw之前:
2 {
3 pe.Draw(explosionEffect, ((Game1)Game).camera);
4 }
Adding a Starfield(添加一个星空背景)
鉴于你已经创建了您和您的粒子和粒子引擎,你完全设立为创建星空在你的背景中,使这看上去很象外太空。为了实现这个,你需要创建一个新的ParticleExplosion类的版本,这个对待粒子有点不同。例如,这个粒子你用来为星星在背景里,不会围绕屏幕移动。你同样不能删除星星从要绘制的星星表单中,你的明星也不会死-星星将会同样在游戏的结束同样它们也在开始的时候。
添加一个新类到你的项目中通过右击解决方案并选择Add-Class...命名这个类ParticleStarSheet:
2 using System;
3 using System.Collections.Generic;
4 using System.Text;
5 using Microsoft.Xna.Framework;
6 using Microsoft.Xna.Framework.Graphics;
7 namespace _3D_Game
8 {
9 class ParticleStarSheet
10 {
11 //Particles
12 Particle[] particles;
13 //Behavior variables
14 Vector3 maxPosition;
15 int maxParticles;
16 Vector2 textureSize;
17 static Random rnd = new Random();
18 //Graphics related variables
19 VertexDeclaration vertexDeclaration;
20 GraphicsDevice graphicsDevice;
21 ParticleSettings particleSettings;
22 public ParticleStarSheet(GraphicsDevice graphicsDevice,Vector3 maxPosition, int maxParticles, Vector2 textureSize,ParticleSettings particleSettings)
23 {
24 this.maxPosition = maxPosition;
25 this.maxParticles = maxParticles;
26 this.graphicsDevice = graphicsDevice;
27 this.textureSize = textureSize;
28 this.particleSettings = particleSettings;
29 particles = new Particle[maxParticles];
30 InitializeParticles();
31 vertexDeclaration = new VertexDeclaration(graphicsDevice,
32 Particle.vertexElements);
33 }
34 private void InitializeParticles()
35 {
36 //Loop through array and initialize particles
37 for (int i = 0; i < maxParticles; ++i)
38 {
39 //Get random coordinate from texture for color
40 particles[i].textureCoordinate = new Vector2(
41 rnd.Next(0, (int)textureSize.X) / textureSize.X,
42 rnd.Next(0, (int)textureSize.Y) / textureSize.Y);
43 //Get random position
44 particles[i].position = new Vector3(
45 rnd.Next(-(int)maxPosition.X, (int)maxPosition.X),
46 rnd.Next(-(int)maxPosition.Y, (int)maxPosition.Y),
47 maxPosition.Z);
48 //Get random direction
49 Vector3 direction = new Vector3(
50 (float)rnd.NextDouble() * 2 - 1,
51 (float)rnd.NextDouble() * 2 - 1,
52 (float)rnd.NextDouble() * 2 - 1);
53 //Normalize the direction to create a sphere shape explosion
54 direction.Normalize();
55 direction *= (float)rnd.NextDouble();
56 particles[i].direction = direction;
57 //Get random point size
58 particles[i].pointSize =(float)rnd.NextDouble() * particleSettings.maxSize;
59 }
60 }
61 public void Update(GameTime gameTime)
62 {
63 }
64 public void Draw(Effect effect, Camera camera)
65 {
66 //Set vertex declaration
67 graphicsDevice.VertexDeclaration = vertexDeclaration;
68 //Set effect params
69 effect.Parameters["WorldViewProjection"].SetValue(camera.view * camera.projection);
70 //Draw using the effect
71 effect.Begin();
72 foreach (EffectPass pass in effect.CurrentTechnique.Passes)
73 {
74 pass.Begin();
75 graphicsDevice.DrawUserPrimitives<Particle>(PrimitiveType.PointList,particles, 0, maxParticles);
76 pass.End();
77 }
78 effect.End();
79 }
80 }
81 }
另外一个关键的不同在这个类和ParticleExplosion类之间,这个类绘制所有的粒子在数组中,并且不移动或者删除它们。
为了使用这个类,你想要添加一个不同的纹理为这些星星。星星将会比爆炸中的粒子要很白很黄,粒子经常是红的或褐色的。在3D Game\Content\Textures文件夹下的资源代码为这章,是一个文件叫做Stars.png.添加它到你的项目中通过右击Content\Textures文件夹在解决方案中,选择Add-Existing Item...,并浏览到Stars.png文件选择它。
现在,添加下面的类别变量到你的ModelManager类中:
2 Effect starEffect;
3 Texture2D starTexture;
其次,在你的ModelManager类的LoadContent方法中,添加下面的代码正好在调用base.LoadContent之前:
2 starTexture = Game.Content.Load<Texture2D>(@"textures\stars");
3 starEffect = explosionEffect.Clone(GraphicsDevice);
4 starEffect.CurrentTechnique = starEffect.Techniques["Technique1"];
5 starEffect.Parameters["theTexture"].SetValue(starTexture);
6 // Initialize particle star sheet
7 stars = new ParticleStarSheet(GraphicsDevice,new Vector3(2000, 2000, -1900),1500,new Vector2(starTexture.Width, starTexture.Height),particleSettings);
另一件事,你要在这做的是创建一个初始化恒星。都是在这里完成因为星星将一直存在这个游戏中,而不是只在特定的时间,象爆炸一样。
你的星星从来不用更新是因为它们不会移动,所以,仅剩下要做的就是添加的代码绘制星星。添加下面的代码行到你的ModelManager类的Draw方法中。正好在base.Draw之前:
嗯,你快完成。这个游戏看起来不错,而且事情按想象中发生。现在剩下的是添加一些记分牌,并且使你的游戏的关卡能运行,马上你就要完成你可怕的XNA 3D游戏了
源代码:http://shiba.hpe.cn/jiaoyanzu/WULI/soft/xna.aspx?classId=4
(完)