[翻译]Oreilly.Learning.XNA.3.0之nine


PS:自己翻译的,转载请著明出处

                                                                        第七章   全部放在一起

                              好吧,你已经建立了一个坚实的设计,并且你有一个很好的开始,它会成为一个很酷的游戏。同样游戏的概念是一个玩家可以控制一个精灵并且试图去避免碰撞追逐的精灵,因为它们飞过屏幕,同时试图抓住逃避的精灵(译者:前面避免的精灵是碰撞后会损失自己的生命,而后面逃避的精灵抓住后会使你强大)。现在你需要去添加一些分数和一些游戏逻辑,并做些别的稍微的调整在你需要的位置上。
                              首先你能做的在这章是添加一些分数到你的游戏中。第一步,当你写一个游戏,并且你开始看这个分数就决定什么事件触发分数的改变。有时候,如果一个某种类型的武器撞击到了玩家,分数将会被改变。在其他时候,你也会改变这个分数当玩家撞击某个它自己。还有一些时候,你想改变分数当用户完成某个事情(回答个问题个,解决了疑惑等等)。
                              在这个游戏中,你改变这个分数当一个敌人精灵离开屏幕没有跑向玩家(意思是玩家成功躲避那个精灵)。
                              这将很有意义,赋予一个得分机制,为你去添加一个分数值到每个独立的精灵。某些精灵可以比其他精灵值更多分,基于它们的速度或者别的因素,这个你决定。
                              除此之外决定如何计算分数,你可能需要去绘制分数在屏幕上。我们将先解决得分的问题,然后看如何调整分数每当一个精灵穿过屏幕没有撞到玩家。
                              除了添加得分到你的游戏中,并且了解如何绘制文本使用SpriteFonts,在本章,你同样添加一些变量到你的精灵上通过引入不同的精灵图象和不同的声音为每个精灵类型。你同样添加一个背景图象,在游戏的状态,并且添加一个power-up到这个游戏。

Drawing 2D Text(绘制2D文本)

                              这章的代码建立在你完成第6章的基础上。打开游戏的项目,并且使用它贯穿本章。
                              首先,你需要添加一个整数变量代表一个精灵的分数值到Sprite基类中(注意增加publi访问器,通过自动执行属性):

1 public int scoreValue {getprotected set;}
                              修改两个结构在Spirte类中,去接收一个整数值为分数值为精灵。第一个构造可以传入值到第二结构中,并且第二个结构应该使用这个值去设置scoreValue成员变量。Sprite类的结构现在看起来象这样:
 1 public Sprite(Texture2D textureImage, Vector2 position, Point frameSize,
 2 int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed,
 3 string collisionCueName, int scoreValue)
 4 this(textureImage, position, frameSize, collisionOffset, currentFrame,
 5 sheetSize, speed, defaultMillisecondsPerFrame, collisionCueName,
 6 scoreValue)
 7 {
 8 }
 9 public Sprite(Texture2D textureImage, Vector2 position, Point frameSize,
10 int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed,
11 int millisecondsPerFrame, string collisionCueName, int scoreValue)
12 {
13    this.textureImage = textureImage;
14    this.position = position;
15    this.frameSize = frameSize;
16    this.collisionOffset = collisionOffset;
17    this.currentFrame = currentFrame;
18    this.sheetSize = sheetSize;
19    this.speed = speed;
20    this.collisionCueName = collisionCueName;
21    this.millisecondsPerFrame = millisecondsPerFrame;
22    this.scoreValue = scoreValue;
23 }

                              你同样可以改变这个这两个类的结构(AutomatedSprite,ChasingSprite,EvadingSprite,和UserControlledSprite)去接收一个整数参数为这个分数值和传递这个值在这个基类结构。精灵的这个结构看起来应该象这样:
            1。AutomatedSprite类的结构:

 1 public AutomatedSprite(Texture2D textureImage, Vector2 position,
 2 Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize,
 3 Vector2 speed, string collisionCueName, int scoreValue)
 4 base(textureImage, position, frameSize, collisionOffset, currentFrame,
 5 sheetSize, speed, collisionCueName, scoreValue) 
 6 {
 7 }
 8 public AutomatedSprite(Texture2D textureImage, Vector2 position,
 9 Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize,
10 Vector2 speed, int millisecondsPerFrame, string collisionCueName,
11 int scoreValue)
12 base(textureImage, position, frameSize, collisionOffset, currentFrame,
13 sheetSize, speed, millisecondsPerFrame, collision
CueName, scoreValue)
14 {
15 }
              2。ChasingSprite类的结构
 1 public ChasingSprite(Texture2D textureImage, Vector2 position,
 2 Point frameSize, int collisionOffset, Point currentFrame,
 3 Point sheetSize, Vector2 speed, string collisionCueName,
 4 SpriteManager spriteManager, int scoreValue)
 5 base(textureImage, position, frameSize, collisionOffset,
 6 currentFrame, sheetSize, speed, collisionCueName, scoreValue)
 7 {
 8      this.spriteManager = spriteManager;
 9 }
10 public ChasingSprite(Texture2D textureImage, Vector2 position,
11 Point frameSize, int collisionOffset, Point currentFrame,
12 Point sheetSize, Vector2 speed, int millisecondsPerFrame,
13 string collisionCueName, SpriteManager spriteManager,
14 int scoreValue)
15 base(textureImage, position, frameSize, collisionOffset,
16 currentFrame, sheetSize, speed, millisecondsPerFrame,
17 collisionCueName, scoreValue)
18 {
19        this.spriteManager = spriteManager;
20 }

             3。EvadingSprite类的结构:

 1 public EvadingSprite(Texture2D textureImage, Vector2 position,
 2 Point frameSize, int collisionOffset, Point currentFrame,
 3 Point sheetSize, Vector2 speed, string collisionCueName,
 4 SpriteManager spriteManager, float evasionSpeedModifier,
 5 int evasionRange, int scoreValue)
 6 base(textureImage, position, frameSize, collisionOffset,
 7 currentFrame, sheetSize, speed, collisionCueName, scoreValue)
 8 {
 9     this.spriteManager = spriteManager;
10     this.evasionSpeedModifier = evasionSpeedModifier;
11     this.evasionRange = evasionRange;
12 }
13 public EvadingSprite(Texture2D textureImage, Vector2 position,
14 Point frameSize, int collisionOffset, Point currentFrame,
15 Point sheetSize, Vector2 speed, int millisecondsPerFrame,
16 string collisionCueName, SpriteManager spriteManager,
17 float evasionSpeedModifier, int evasionRange,
18 int scoreValue)
19 base(textureImage, position, frameSize, collisionOffset,
20 currentFrame, sheetSize, speed, millisecondsPerFrame,
21 collisionCueName, scoreValue)
22 {
23      this.spriteManager = spriteManager;
24      this.ev
asionSpeedModifier = evasionSpeedModifier;
25      this.evasionRange = evasionRange;
26 }
             4。UserControlledSprite类的构造:
 1 public UserControlledSprite(Texture2D textureImage, Vector2 position,
 2 Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize,
 3 Vector2 speed)
 4 base(textureImage, position, frameSize, collisionOffset, currentFrame,
 5 sheetSize, speed, null0)
 6 {
 7 }
 8 public UserControlledSprite(Texture2D textureImage, Vector2 position,
 9 Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize,
10 Vector2 speed, int millisecondsPerFrame)
11 base(textureImage, position, frameSize, collisionOffset, currentFrame,
12 sheetSize, speed, millisecondsPerFrame, null0)
13 {
14 }
注意:UserControlledSprite将没有一个分数值与它结合,因为玩家躲避自己不能获得分数。因此,你不需要去添加一个新的参数到这个结构为这个类,但是你需要传入一个0到这个scoreValue参数的结构为这个基类。
                           最后,在SpriteManager类,你需要添加分数值作为最后参数在这个结构中,当初始化一个新的Sprite对象。你目前只有创建EvadingSprite类的对象,并且你做这个在SpawnEnemy方法的结尾。为你创建的EvadingSprite添加一个0作为分数值(稍后添加一些逻辑在这章,它将创建不同的精灵类型并且基于它们的类型分配不同的分数值到这些精灵中。)这个代码创建你的EvadingSprite对象在SpawnEnemy方法现在应该象这样:
1 spriteList.Add(
2   new EvadingSprite (Game.Content.Load<Texture2D>(@"images\skullball"),
3   position, new Point(7575), 10new Point(00),
4   new Point(68), speed, "skullcollision"this, .75f, 1500));

                           你现在有一个方法去计算这个分数基于不同的精灵事件在这个游戏中。即使你目前只使用0作为分数值,这里的底层代码,所以可以开始写一些分数逻辑为这个游戏。首先,你需要添加到Game1类一个变量,它代表当前游戏的总分:

1 int currentScore = 0;

                           现在,你准备在你的屏幕上绘制分数了。绘制文本在3D上,这和你绘制精灵有相似之处。为每一祯它都被绘制,你将绘制文本在每一祯使用一个SpriteBatch和一个对象称为一个SpriteFont.当绘制2D图象在屏幕上时,你指定一个图象文件去使用,一个Texture2D对象去保存着个图象。然后XNA接受这个图象从内存中,并发送这个数据到图形卡中。
                           同样的事情发生在当用SpriteFont绘制对象时。在这种情况下,一个spritefont文件被创建。这是一个XML文件一个被给予的字体的特征:font family,字体大小,字体间隔,等等。一个SpriteFont对象被使用在内存中,去代表spritefont.当SpriteFont对象被绘制,XNA将创建一个2D图象使用这个你想要绘制的文本,并且指定的文本在XML文件中。这个图象然后发送到图形设备中,将会绘制到屏幕上。
                           当绘制游戏的分数,你想要使用currentScore变量的值。因为这变量的值变换了,在屏幕上的分数将会被更新。
                           为了绘制文本在屏幕上,使用一个SpriteFont,你需要去添加一个SpriteFont资源到你的项目中。首先,添加一个新资源文件夹到你的项目中为字体对象。右击这个Content节点在解决方案中,并选择Add-New Folder.命名文件夹为Fonts.现在,添加这个实际的spritefont资源。右击这个新Content\Fonts文件夹在这个解决方案中,并选择Add-New Item...在你的AddNew Item窗口的左边,选择Visual C#种类。选择SpriteFont为这个模板,并且命名这个spritefont为Arial.spritefont(图7-1)。
注意:Spritefont是个资源,它被挑出来并且通过内容管道被处理。例如,它们必须在你的项目的Content节点下创建。
                            Spritefont文件应该总是被命名在一个指定的font family.XNA使用这个你指定的名字在这个文件中作为font family的名字去使用,当创建XML文档去代表字体的时候。Spritefonts应该总是被命名一些象这样的<font name>.spritefont.在这种情况下,你将会使用一个Arial 字体,所以你的文件应该被命名为Arial.spritefont。
                            一旦你点击添加按钮,这个spritefont文件将会创建并打开在Visual Studio.你会注意到它是一个XML文件,它包含信息如字体的名字,大小,间距,样式等等。你可以修改他们的值去自定义你需要的字体。

                            接下来,你需要添加一个SpriteFont变量,它将会保存你的spritefont在内存中。添加下面的类别变量在你的Game1类的上部:

1 SpriteFont scoreFont;
                            现在,加载你的SpriteFont对象通过内容管道,同样的方法,你加载更多的游戏资源。你重新找回这个字体从内容管道,使用Content.Load方法。添加下面的代码行到你的Game1类的LoadContent方法中:
1 scoreFont = Content.Load<SpriteFont>(@"fonts\arial");
                            下一步是实际绘制文本在屏幕上。正如前面所提到的,这个被实现通过一个SpriteBatch对象。而不是使用SpriteBatch的Draw方法,你将使用一个叫做DrawString的方法使用一个SpriteFont去绘制文本。添加下面的代码到你的Game1类的Draw方法,在调用GraphicsDevice.Clear和调用base.Draw之间:
1 spriteBatch.Begin( );
2 // Draw fonts
3 spriteBatch.DrawString(scoreFont, "Score: " + currentScore,new Vector2(1010), Color.DarkBlue, 0, Vector2.Zero,1, SpriteEffects.None, 1);
4 spriteBatch.End( );
                           请注意,字体需要被绘制在调用SpriteBatch.Begin和SpriteBatch.End。这是因为XNA对待spritefonts就象任何别的2D图象资源。
                           这DrawString方法有个参数,它类似与被使用在2D图象调用的SpriteBatch.Draw方法。你可以调整被绘制的字体的位置,颜色,缩放,等等。第一个参数传入到DrawString方法是你希望使用SpriteFont对象去绘制,并且第二个参数是实际你希望被绘制的文本。
注意:在XNA2.0里,等宽字体不能被适当的绘制。XNA对待它们作为正常字体(失去等宽字体的特点)。在XNA3.0中这个问题被解决了,一个等宽字体现在可以被完全的绘制。
                           编译运行游戏在这时,你应该可以看见分数绘制在屏幕的上部,如图7-2。

                           做的不错!你现在有一个全部的游戏分数,也就是你已经接近完成你的游戏。然而,你现在的问题是你不能更新游戏的得分。在你做这个之前,你需要添加逻辑去创建不同的精灵类型用一个随机间隔,而不是总是创建同一类型的精灵。一旦你执行了这些变换,你可以添加分数为这个不同的精灵类型。


Randomly Generating Different Sprite Types(随机产生不同的精灵类型)
                           为了随机生成不同类型的精灵,你需要首先检测每个类型被创建的可能性。大多数精灵在这个游戏中将会AutomatedSprite.ChasingSprites将会是下一个最常见的,EvadingSprite将只会偶然出现。在这节,你会分配一个可能的百分数到精灵的每个类型中。每一次一个新精灵被产生,你将会检测到基于这些百分数哪个类型的精灵会被创建。确切的百分比可能性为每个精灵类型是作为你的游戏测试来玩,你可以调整这个点的这个值,以至于他们觉得非常好。
                           首先,打开SpriteManager类并且添加三个新类别变量代表这个类型的精灵产生的可能性:

1 int likelihoodAutomated = 75;
2 int likelihoodChasing = 20;
3 int likelihoodEvading = 5;
                           您会发现,这个值增加等于100(代表100%)。从本质上讲,75%的时间你会生成一个AutomatedSprite,20%的时间一个ChasingSprite,5%一个EvadingSprite.
                           现在,你必须添加一些代码,它会生成一个随机数,基于随机数的值,创建三个精灵之一个类型。打开你的SpriteManager类,并且替换这个调用到SpriteList.Add,它是在SpawnEnemy方法的结尾:
1 spriteList.Add(new EvadingSprite (Game.Content.Load<Texture2D>(@"images\skullball"),position, new Point(7575), 10new Point(00),new Point(68), speed, "skullcollision"this, .75f, 1500));
                           用下面的代码替换:
 1 // Get random number
 2 int random = ((Game1)Game).rnd.Next(100);
 3 if (random < likelihoodAutomated)
 4 {
 5    // Create AutomatedSprite
 6    spriteList.Add(new AutomatedSprite(Game.Content.Load<Texture2D>(@"images\skullball"),position, new Point(7575), 10new Point(0,0), new Point(68),speed, "skullcollision"0));
 7 }
 8 else if (random < likelihoodAutomated +likelihoodChasing)
 9 {
10    // Create ChasingSprite
11    spriteList.Add(new ChasingSprite(Game.Content.Load<Texture2D>(@"images\skullball"),position, new Point(7575), 10new Point(0,0), new Point(68),speed, "skullcollision"this0)); }
12 else
13 {
14    // Create EvadingSprite
15    spriteList.Add(new EvadingSprite(Game.Content.Load<Texture2D>(@"images\skullball"),position, new Point(7575), 10new Point(00), new Point(68),speed, "skullcollision"this, .75f, 1500));
16 }

                            这个代码首先生成一个数字在0到99之间,然后比较生成的数字到这个值去代表多久一个AutomatedSprite应该被产生。如果生成数少于这个值,一个AutomatedSprite被创建。如果这个随机产生的数字不少于这个值,然后它与这个值的总和来比较,表示多久一个AutomatedSprite应该被产生,这个值代表多久一个ChasingSprite应该被产生。如果产生的值少于这两个值的总和,一个ChasingSprite被产生。如果它不少于这个值,一个EvadingSprite被产生。
                             如果这样做没有意义,用这种方法考虑下:使用来自于我们当前列子的值,这个随机数(某个在0到99之间的数)是第一个评估去看下是否小于7.5。如果它是,一个AutomatedSprite被产生。这代表一个75%机会,这个AutomatedSprite被创建,这就是你真正想要的。如果这个随机数被产生大于75,然后检查它是否大于95(创建一个AutomatedSprite的机会的总和,它是75%,并且创建一个ChasingSprite的机会,它是20%)。由于这个随机数低于75时,这个比较将不会被执行,从本质上讲检查去看这个值是否在75和94之间---它代表一个20%机会一个ChasingSprite将会被创建(这就是你想要的)。最后,一个EvadingSprite将会被生成,如果两种情况都不成立----换句话说,如果随机值在95到99之间,它这里是5%的机会正是你想要的。
                             噢!现在开始把它们混合在一起。编译并且运行这个游戏,并且你应该注意到多数被产生的精灵是AutomateSprites,一些ChasingSprite被混合在一起,偶尔一个EvadingSprite显示出来。好运!
                             在这时你的游戏有个问题,虽然精灵是有不同的行为,但是它们全看起来一样。这不仅让事情有些无聊,同样让我感觉到困惑-----人们会期望长得十分相似的精灵有类似的行动。您需要添加一些多样性的不同类型精灵。

Adding Some Variety to Your Sprites(添加一些种类的精灵)
                             首先的事情是,你需要一些更多的图象。如果你没有准备做这个,下载资源代码为这本书的本章。在这章的源代码中(在AnimatedSprites\Content\Images文件夹中),你会找到一些精灵表单文件为不同类型的精灵。

                             右击Content\Images文件夹在解决方案中,并选择Add-Existing Item...浏览到AnimatedSprites\Images文件夹添加bolt.png,fourblade.png,threeblades.png,和plus.png图象到你的项目中。
                             除此之外添加一个新图象为没个新精灵,你想要不同的类型不同的一些不同的声音。同样从本章的资源中得到,在AnimatedSprite\Content\Audio文件夹,你会找到一些.wav文件。复制boltcollision.wav,fourbladescollision.wav,pluscollision.wav和threebladescollision.wav文件从这个文件夹到你自己的项目的Content\Audio文件夹中。记住当你处理真实的声音文件,你不用添加它们到Visual Studio工程中。你需要复制它们到你的项目的Content\Audio目录,但是你添加它们到这个工程使用XACT而不是在Visual Studio。
                             一旦你复制这些文件,开始XACT,因此你可以添加这些new.wav文件到你的XACT工程文件中。
                             在XACT,为这个游戏加载这些声音项目(这个文件应该被称为GameAudio.xap,并且被加载在你的项目的Content\Audio文件夹中)。打开Wave Bank和Sound Bank窗口为你的XACT工程通过双击在Wave Bank和Sound Bank节点在左边的菜单树中。
                             在Wave Bank窗口,右击和选择插入Wave File(s)....选择这四个新.wav文件,你复制它到你的项目的Content\Audio目录中,如图7-3所示。
                             当你点击打开.wav文件,将会被添加到你的Wave Bank窗口,它现在看起来象图7-4.
                             下一步,拖这个你刚才添加的项目从Wave Bank窗口到Sound Bank窗口的Cue Name部分。你需要去放下它们在Cue Name部分,而不是Sound Name部分,因为你需要cue的这些声音的名字,为了从你的代码中播放它们。你的窗口现在应该有新的声音列表在Wave Bank窗口,并且在Sound Name和Sound Bank窗口的Cue Name部分,如图7-5显示。



                             你可以调整你喜欢的声音的音量,正如第5章所描述的。一旦你感到满意,保存你的工程,推出XACT,返回到Visual Studio中。
                             现在,你需要分配新的图象和声音到不同的精灵类型中,当他们产生时。幸运的是,因为你分配你的SpriteManager类的这些方法,这会变的很容易。你已经准备随机创建不同的精灵类型,并且现在所有你需要做的是应用这个不同的图象和声音到这些精灵中。
                             你也许感到很惊奇一些你刚才添加的文件,因为现在,你只有三个类型的精灵。不过,这里实际上有六个不同的精灵表单在你的工程中。看下表7-1去看看每个精灵在这个游戏里有什么用途。


                             在SpriteManager类的SpawnEnemy方法中,你添加这个代码,它随机产生不同的精灵类型。你需要去改变这段代码,去创建五个之一的不是玩家的精灵类型,在上面的表中提及多的。如果随机数的比较表明,你需要创建一个AutomatedSprite,你需要生成另外一个随机计算并检测什么时候你应该去创建一个三刀片对象或者一个四刀片的对象(译者:怪异的精灵图片)(两个都应该是50%的可能性)。同样,如果一个ChasingSprite被选中,你添加并生成一个随机计算去检测是否要创建一个加号或头骨球(同样,都是50%的机会)。最后,如果一个EvadingSprite被创建,你将创建螺钉精灵。替换下面的代码,它刚才被添加到SpawnEnemy方法的末尾:

 1 // Get random number
 2 int random = ((Game1)Game).rnd.Next(100);
 3 if (random < likelihoodAutomated)
 4 {
 5    // Create AutomatedSprite
 6    spriteList.Add(new AutomatedSprite(Game.Content.Load<Texture2D>(@"images\skullball"),position, new Point(7575), 10new Point(0,0), new Point(68),speed, "skullcollision"0));
 7 }
 8 else if (random < likelihoodAutomated +likelihoodChasing)
 9 {
10    // Create ChasingSprite
11    spriteList.Add(new ChasingSprite(Game.Content.Load<Texture2D>(@"images\skullball"),position, new Point(7575), 10new Point(0,0), new Point(68),speed, "skullcollision"this0)); 
12 }
13 else
14 {
15    // Create EvadingSprite
16    spriteList.Add(new EvadingSprite(Game.Content.Load<Texture2D>(@"images\skullball"),position, new Point(7575), 10new Point(00), new Point(68),speed, "skullcollision"this, .75f, 1500));
17 }

                              用这段代码替换:

 

 1 // Get random number between 0 and 99
 2 int random = ((Game1)Game).rnd.Next(100);
 3 if (random < likelihoodAutomated)
 4 {
 5    // Create an AutomatedSprite.
 6    // Get new random number to determine whether to
 7    // create a three-blade or four-blade sprite.
 8    if (((Game1)Game).rnd.Next(2== 0)
 9    {
10       // Create a four-blade enemy
11       spriteList.Add(new AutomatedSprite(Game.Content.Load<Texture2D>(@"images\fourblades"),position, new Point(7575), 10new Point(00),new Point(68), speed, "fourbladescollision"0));
12    }
13    else
14    {
15       // Create a three-blade enemy
16       spriteList.Add(new AutomatedSprite(Game.Content.Load<Texture2D>(@"images\threeblades"),position, new Point(7575), 10new Point(00),new Point(68), speed, "threebladescollision"0));
17    }
18 }
19    else if (random < likelihoodAutomated +likelihoodChasing)
20    {
21       // Create a ChasingSprite.
22       // Get new random number to determine whether
23       // to create a skull or a plus sprite.
24       if (((Game1)Game).rnd.Next(2== 0)
25       {
26          // Create a skull
27          spriteList.Add(new ChasingSprite(Game.Content.Load<Texture2D>(@"images\skullball"),position, new Point(7575), 10new Point(00),new Point(68), speed, "skullcollision"this0));
28       }
29       else
30       {
31          // Create a plus
32          spriteList.Add(new ChasingSprite(Game.Content.Load<Texture2D>(@"images\plus"),position, new Point(7575), 10new Point(00),new Point(64), speed, "pluscollision"this0));
33       }
34 }
35 else
36 {
37     // Create an EvadingSprite
38     spriteList.Add(new EvadingSprite(Game.Content.Load<Texture2D>(@"images\bolt"),position, new Point(7575), 10new Point(00),new Point(68), speed, "boltcollision"this,.75f, 1500));
39 }

                               一个重要的事情需要注意的是精灵表单为加号ChasingSprite有六列并且只有四行,同时所有其他的有六列八行。如果你运行你的游戏并且加号精灵活动起来,消失或者再出现,这很可能是你的问题的原因。
                              现在编译运行游戏,你可以看见多种的对象在屏幕上乱跑它们有不同的行为,如图7-6所示。这些当它们与玩家碰撞后,对象应该也会造成不同的声音。
                              你可能接收一个编译警告关于likelinhoodEvading变量在AnimatedSprite类中。这个变量被分配但是从来没有使用过。如果你看你的代码,您会看到该变量实际上只存在可能性的百分比去显示精灵。这个值不被这里的代码使用。为了避免警报,你可以注释掉这行(不要删除它,因为,它有利于维护的目的---这个变量很快的显示出了一个躲避精灵被创建的百分数)。
                    
Adding a Background Image(添加一个背景图片)
                              接下来,你添加更多的趣味到你的游戏中通过添加一个背景图片。在本章的资源代码中(在AnimatedSprites\Content\Images文件夹),你会发现一个图象命名为background.jpg.添加这个图象到这个工程用同样你添加其他图片的方法(右击Content\Images文件夹,选择Add-Existing Item...,并且浏览到background.jpg图象包括这些资源代码)。

                              你的SpriteManager类被创建去处理活动的精灵和派生类。一些图片背景很简单,只需将添加到您的Game1类。你需要添加一个Texture2D变量为这个图象:

 

1 Texture2D backgroundTexture;

                               加载Texture2D图象在LoadContent方法中:

 

1 backgroundTexture = Content.Load<Texture2D>(@"Images\background");

                               接下来,你需要添加代码去绘制图象。因为你现在有多个精灵被绘制在你的Game1类(SpriteFont作为一个精灵被计算,所以你绘制一个分数精灵以及背景精灵),你需要确保,分数文本总是在背景图象的上面。通常上讲,当你试图保证一个精灵在另外一个上面,你修改SpriteBatch.Begin调用,包括一个适当的SpriteSortMode.然而,这是一种情况下你只能绘制两个项目,并且你知道你总是想要去绘制分数在背景上。
                              修改你的Game1类的Draw方法象下面这样:

 

 1 protected override void Draw(GameTime gameTime)
 2 {
 3    GraphicsDevice.Clear(Color.White);
 4    spriteBatch.Begin( );
 5    // Draw background image
 6    spriteBatch.Draw(backgroundTexture,new Rectangle(00, Window.ClientBounds.Width,Window.ClientBounds.Height), null,Color.White, 0, Vector2.Zero,SpriteEffects.None, 0);
 7    // Draw fonts
 8    spriteBatch.DrawString(scoreFont, "Score: " + currentScore,new Vector2(1010), Color.DarkBlue, 0, Vector2.Zero,1, SpriteEffects.None, 1);
 9    spriteBatch.End( );
10    base.Draw(gameTime);
11 }

                               编译运行这个游戏,并且你会看见影响到背景的图象,对游戏的整体外观也有影响(图7-7)。这个让人太兴奋了---所有的元素真正开始走到一起!
                              做得非常好。你已经得到一个背景和多种精灵类型,它们有多种行为。现在,让我们完成游戏的得分逻辑。

Game Scoring(游戏分数)
                              正如我们前面所讨论的这个话题,首先你需要做的是检测什么事件将会被除法改变分数。对于这个游戏,你将会更新这些分数即使用户成功回避了三刀片,四刀片,骷髅头,或者正号精灵。你实际上已经准备添加这个逻辑去检测当其中一个精灵已经成功的被回避---当它们从屏幕边缘消失,代码中删除了这个精灵。如果一个精灵已经使它穿过这个屏幕,并且需要被删除,意思用户已经回避了这个精灵,如果它是一个三叶片,四叶片,头骨球,或加号精灵,您需要给用户提供它们的分数。

注意:
任何时候你开发一个游戏,得分规则和计算是你需要考虑的。你最有可能明确的表达一个想法,执行它,然后调整它同时测试你的游戏看看是否它是正确的,是否按着你想的方式进行。就本书的目的,分数的计算和规则让你很荣誉去学习。但是,正如你开始感觉的很舒服正是这书的理念,感觉很容易的去改变规则和调整游戏,只要你感觉是正确。
                               在SpriteManager类,添加三个新的类别变量代表三个精灵类型,你将会发送at the player,以及公开的属性为每个变量:

1 int automatedSpritePointValue = 10;
2 int chasingSpritePointValue = 20;
3 int evadingSpritePointValue = 0;

                                追赶的精灵要比自动的要严格,它只能以直线穿越屏幕。因此,它们值更多的分数。回避的对象将会用于power-ups,同时玩家想要跟踪它们去获得一个奖励,如果没有碰撞到这些精灵就没有分数惩罚和奖励。
                               现在,你需要去添加到你的Game1类一个公有的方法,他将允许你的SpriteManager去添加到游戏的分数。因为删除的精灵发生在SpriteManager里,它的意义是计算分数在程序中。添加下面的方法到你的Game1类中:

1 public void AddScore(int score)
2 {
3     currentScore += score;
4 }

                               接下来,你需要查找代码,它删除精灵当它们飞离屏幕的边缘时。这个代码在你的SpriteManager类的Update方法中。这个方法实际上有两个不同的地方,精灵在那里被删除:一个是因为它们离开了屏幕,另一个是它们与玩家发生了碰撞。两种情况都使用SpriteList.RemoveAt(i)去删除精灵从游戏的精灵表中。
                               查找代码并删除精灵因为它们已经离开了屏幕边缘。当前,代码应该看上去象这样:

1 // Remove object if it is out of bounds
2 if(s.IsOutOfBounds(Game.Window.ClientBounds))
3 {
4     spriteList.RemoveAt(i);
5     --i;
6 }
                               你需要修改代码去添加分数为这些精灵,在删除它们之前。改变代码如这里所显示的:
1 // Remove object if it is out of bounds
2 if(s.IsOutOfBounds(Game.Window.ClientBounds))
3 {
4     ((Game1)Game).AddScore(spriteList[i].scoreValue);
5     spriteList.RemoveAt(i);
6     --i;
7 }

                               所以,你可以核实你布置正确的代码行到正确的位置,你的Update方法应该看起来象这样:

 1 public override void Update(GameTime gameTime)
 2 {
 3     // Update player
 4     player.Update(gameTime, Game.Window.ClientBounds);
 5     // Check to see if it's time to spawn a new enemy
 6     nextSpawnTime -= gameTime.ElapsedGameTime.Milliseconds;
 7     if (nextSpawnTime < 0)
 8     {
 9        SpawnEnemy( );
10        // Reset spawn timer
11        nextSpawnTime = ((Game1)Game).GetRandom.Next(((Game1)Game).EnemySpawnMinMilliseconds,((Game1)Game).EnemySpawnMaxMilliseconds);
12     }
13     // Update all sprites
14     for (int i = 0; i < spriteList.Count; ++i)
15     {
16         Sprite s = spriteList[i];
17         s.Update(gameTime, Game.Window.ClientBounds);
18         // Check for collisions
19         if (s.collisionRect.Intersects(player.collisionRect))
20         {
21             // Play collision sound
22             if(s.GetCollisionCueName != null)
23                  ((Game1)Game).PlayCue(s.GetCollisionCueName);
24             // Remove collided sprite from the game
25             spriteList.RemoveAt(i);
26             --i;
27         }
28         // Remove object if it is out of bounds
29         if(s.IsOutOfBounds(Game.Window.ClientBounds))
30         {
31              ((Game1)Game).AddScore(spriteList[i].GetScoreValue);
32              spriteList.RemoveAt(i);
33              --i;
34         }
35     }
36      base.Update(gameTime);
37 }

                            现在,你的SpriteManager类的Update方法是越来越晦涩,所以是时候重构了。创建一个叫做UpdateSprites的方法,它接收一个GameTime类型的参数。移动Update方法的代码的部分,它将更新你的精灵(玩家和非玩家),把它放置在UpdateSprites方法中。在Update方法的初始代码的位置,调用UpdateSprites。你的Update方法看上去应该象这样:

 1 public override void Update(GameTime gameTime)
 2 {
 3    // Time to spawn enemy?
 4    nextSpawnTime -= gameTime.ElapsedGameTime.Milliseconds;
 5    if (nextSpawnTime < 0)
 6    {
 7        SpawnEnemy( );
 8        // Reset spawn timer
 9        ResetSpawnTime( );
10    }
11    UpdateSprites(gameTime);
12    base.Update(gameTime);
13 }
                            啊哈哈,是的....非常好。你的UpdateSprites方法,反过来,看起来应该象这样:
 1 protected void UpdateSprites(GameTime gameTime)
 2 {
 3    // Update player
 4    player.Update(gameTime, Game.Window.ClientBounds);
 5    // Update all non-player sprites
 6    for (int i = 0; i < spriteList.Count; ++i)
 7    {
 8        Sprite s = spriteList[i];
 9        s.Update(gameTime, Game.Window.ClientBounds);
10        // Check for collisions
11        if (s.collisionRect.Intersects(player.collisionRect))
12        {
13           // Play collision sound
14           if (s.collisionCueName != null)
15              ((Game1)Game).PlayCue(s.collisionCueName);
16           // Remove collided sprite from the game
17           spriteList.RemoveAt(i);
18           --i;
19         }
20         // Remove object if it is out of bounds
21         if (s.IsOutOfBounds(Game.Window.ClientBounds))
22         {
23             ((Game1)Game).AddScore(spriteList[i].scoreValue);
24              spriteList.RemoveAt(i);
25              --i;
26          }
27     }
28 }

                           最后,你需要去添加适当的分数值到这个使用的结构中,这个结构创建每一个精灵。每一个AutomatedSprite,它被产生,最后的参数(它代表这个精灵的分数值)应该是automatedSpritePointValue成员变量。同样,每个ChasingSprite产生,这最后的参数应该是chasingSpritePointValue,并且每个EvadingSprite的最后的参数应该是evadingSpritePointValue属性。
                           在这个每个精灵类型的结构中,你不得不改变这些变量在SpriteManager类的SpawnEnemy方法中。为了很容易找到这个结构,在SpriteManager.cs文件里寻找每一个spriteList.Add实例。每一次spriteList.Add被调用,你传入一个新的Sprite对象,它的结构你需要去修改。为了更清晰,你的SpawnEnemy方法应该现在看起来象这样(唯一的变化是在结构中每个精灵的类型的最终参数):

Code
                               噢!现在编译运行你的游戏,你可以看见精灵成功回避并且移动了屏幕,这些精灵的分数值被添加到游戏的记分系统里了,如图7-8所示。
                               真可怕!你让一些精灵到处跑了起来,并且游戏实际上不停的更新分数!现在你做了一切,对吗?呃...嗯... ...等一下比赛永远不会结束。意思是每一次你在玩的时候,你可以潜在得到一个高分通过你只要坐在那里看。恩..,我们一个方法去解决这问题。让我们添加一些逻辑去添加不同的游戏状态,当一个玩家达到某一个分值游戏结束。
                               
Game States(游戏状态)
                               你的游戏开始了,但还需要一个方式去结束游戏。通常上来说,当一个游戏结束,游戏窗口不只是消失;通常这里有某种game-over一幕,它显示你的分数或者至少让你知道你的任务已经失败了(或者成功了)。你需要添加一段文本。同样需要某种东西在游戏开始时显示出来(可能是一组菜单使玩家能够去选择项目,或者至少是一个启动画面或者游戏介绍,也可以显示这个伟大游戏的作者的名字)。在下面的几节里,你将会添加一个介绍画面和一个关闭游戏的game-over画面。

                               贯穿任何游戏的开始和结尾,这些游戏通过不同的状态。有些时候这些状态说明一个玩家已经移动到这个游戏的不同的关卡或者不同的场景去了。有时候游戏的状态描述玩家身份的变化(就象Pac-Man,当你装成鬼魂并且去追赶它们而不是被追赶)。不管具体情况,游戏的变化通过不同的状态,并且在这些不同的状态,游戏的行为也不同。某个方法去执行启动画面和game-over场景是通过使用这些状态实现的。
                               为了定义你游戏的某些状态,你需要列举游戏需要的不同的可能的状态。创建一个enum变量在这个类别在你的Game1类中。当前,你只需要有三个状态在你的游戏中:Statrt(这里显示你的启动画面),InGame(这里是实际运行的),和(这里显示游戏结束的画面)。你同样需要创建一个enum类型的变量,它将保存当前游戏的状态。你想要初始化当前的状态变量到游戏的状态中,表示游戏的开始:
1 enum GameState { Start, InGame, GameOver };
2 GameState currentGameState = GameState.Start; 
                               目前在你的Game1类中,你有Update和Draw方法,它们让你在屏幕上绘制并且在游戏中更新对象。当你放置代码在这些其中之一的方法中(例如绘制分数的代码和背景图象),这段代码每次运行这方法被调用(贯穿游戏生命周期的每一祯)。你想分开在Update和Draw方法中的逻辑,这样允许你去写特定的代码,它将依赖于当前的状态独立运行。你可以通过添加一个switch语句到两个方法中去实现,每种可能的游戏状态用两个不同的case语句表示。然后,当你想写特定的代码去更新或者绘制选项,这个选项引发被给定的游戏状态,在为了游戏的特定的游戏状态的情况下,你添加这段代码到Update或者Draw方法中。
                               首先,添加一个swith语句到你的Game1类的Update方法中。这个Update方法应该象这样:
 1 protected override void Update(GameTime gameTime)
 2 {
 3     // Only perform certain actions based on
 4     // the current game state
 5     switch (currentGameState)
 6     {
 7        case GameState.Start:
 8            break;
 9        case GameState.InGame:
10            break;
11        case GameState.GameOver:
12            break;
13      }
14      // Allows the game to exit
15      if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==ButtonState.Pressed)
16      this.Exit( );
17      audioEngine.Update( );
18      base.Update(gameTime);
19 }
                               其次,在Draw方法中做同样的事情。你的Draw方法已经有逻辑在它里面,这个逻辑去绘制分数和背景图象,但是当这游戏是在GameState.InGame状态下时,这些材质应该被绘制,这样你需要把这段代码放在switch语句的下。你的Draw方法,现在应该象这样:
 1 protected override void Draw(GameTime gameTime)
 2 {
 3     // Only draw certain items based on
 4     // the current game state
 5     switch (currentGameState)
 6     {
 7       case GameState.Start:
 8            break;
 9       case GameState.InGame:
10            GraphicsDevice.Clear(Color.White);
11            spriteBatch.Begin( );
12       // Draw background image
13            spriteBatch.Draw(backgroundTexture,new Rectangle(00, Window.ClientBounds.Width,Window.ClientBounds.Height), null,Color.White, 0, Vector2.Zero,SpriteEffects.None, 0);
14       // Draw fonts
15            spriteBatch.DrawString(scoreFont,"Score: " + currentScore,new Vector2(1010), Color.DarkBlue,0, Vector2.Zero,1, SpriteEffects.None, 1);
16            spriteBatch.End( );
17            break;
18       case GameState.GameOver:
19            break;
20      }
21      base.Draw(gameTime);
22 }

                                 如果你编译并运行你的游戏在此时,它看起来有点酷,但是有点混乱。这个分数和背景将在游戏的窗口中缺少,并且从一祯到另一祯每一祯中的动态精灵不会被抹去,它将导致动画拖影。
                                 分数和背景可能会消失是因为当前的游戏状态被默认设置成GameState.Start,并且在这种游戏状态中你不能绘制这些项目,同样,你看见的拖影是因为你不能调用GraphicsDevice.Clear在GameState.Start状态下(你只能在GameState.InGame状态下完成这些)。
                                 你仍然能看见动态的精灵是因为SpriteManager类没有被你刚添加的游戏逻辑状态所影响。你只需要添加那段代码到Game1类中;SpriteManager是一个游戏的组成部分,并且不受你刚才添加的switch语句的影响。
                                 为了使所有这些都正常的工作,你需要添加一些逻辑去显示你的SpriteManager游戏组成部分在某个游戏状态,并且使他能够在别的状态中。

Enabling/Disabling GameComponents(启用/禁用游戏组件)
                                 默认情况下,当你创建一个GameComponet的实例并且添加它到一个游戏的组件表单中,这个GameComponent是wired into游戏的循环。当游戏的Update方法被调用,因此是GameComponent的Update方法,等等。
                                 这里有两个属性,它可以被用来启用并且禁用一个GameComponent.GameComponet的Enabled属性将会检测是否它的Update方法被调用,当游戏自己的Update方法被调用时。同样,DrawableGameComponent的Visible属性将会检测它的Draw方法是否被调用,当游戏的Draw方法被调用时。这两个属性被默认的设置为true.到你的Game1类的Initialize方法中,并立即设置两个属性为false在添加这个组件到你的游戏组件表单中时:

1 spriteManager = new SpriteManager(this);
2 Components.Add(spriteManager);
3 spriteManager.Enabled = false;
4 spriteManager.Visible = false;
注意:为什么开始时SpriteManager在一个禁止状态?记住这个游戏状态在GameState.State状态,它将被用来为某个种类的一个启动画面。你不想要精灵飞入或者飞出用一个禁止的SpriteManager,然后,但启动画面关闭,你移动到一个游戏状态,并且激活SpriteManager.                               
                                 接下来,添加某些代码去显示一些文件,当游戏在GameState.start状态。这将为你的启动画面服务,并且你可以添加图形,文本,和动态效果到它里面,就象你在游戏它本身一样(译者:开发游戏的状态,往里面添素材)。现在,你只要添加一些简单的文字,它告诉用户,他(用户)需要去回避刀片精灵对象。在你的Draw方法中,添加到GameState.Start情况下的switch语句一些代码,去显示这些简单的结构给用户:
 1 case GameState.Start:
 2      GraphicsDevice.Clear(Color.AliceBlue);
 3      // Draw text for intro splash screen
 4      spriteBatch.Begin( );
 5      string text = "Avoid the blades or die!";
 6      spriteBatch.DrawString(scoreFont, text,new Vector2((Window.ClientBounds.Width / 2)- (scoreFont.MeasureString(text).X / 2),(Window.ClientBounds.Height / 2)- (scoreFont.MeasureString(text).Y / 2)),Color.SaddleBrown);
 7      text = "(Press any key to begin)";
 8      spriteBatch.DrawString(scoreFont, text,new Vector2((Window.ClientBounds.Width / 2)- (scoreFont.MeasureString(text).X / 2),(Window.ClientBounds.Height / 2)- (scoreFont.MeasureString(text).Y / 2+ 30),Color.SaddleBrown);
 9      spriteBatch.End( );
10      break;
                                  这段代码应该非常简单明了;这里没有你以前没有见过的,除了SpriteFont的MeasureString方法的使用。这个方法将会返回一个Vector2对象表示被测量的字符串的大小。当你试图居中一个spritefont在屏幕的中心,这将非常有用,这正是这段代码的作用。为了使这段文字真正的在屏幕上居中,你将Windows.ClientBounds.Width属性值除以2,这样可以找到水平的中心位置,然后减去你准备去绘制的spritefont文本的一半宽度得到偏移量。你通过使用SpriteFont.MeasureString方法去测定你要绘制文本的宽度。
                                  如果你编译运行这段代码,你可能会有点失望。毕竟你对这个游戏投入这么多,它还是不能工作!你现在有的是一个信息告诉你去躲避或者死亡;但更糟糕,这个游戏屏幕说压下任何键可以开始,但是不关你如何使劲去压这些键,还是没有改变。这是因为你还没有添加任何的功能,去从GameState.Start状态到GameState.InGame状态。
                                  为了移动到GameState.InGame状态,添加一些代码到GameState.Start情况的switch语句在Game1类的Update方法中。下面的代码将检测从用户的任何的键压下,当玩家压下一个键,改变这个游戏到GameState.InGame状态,并且激活你的SpriteManager,它将允许精灵去开始围绕屏幕飞行:
1 case GameState.Start:
2      if (Keyboard.GetState().GetPressedKeys( ).Length > 0)
3      {
4             currentGameState= GameState.InGame;
5             spriteManager.Enabled = true;
6             spriteManager.Visible = true;
7      }
8      break
                                  如果你愿意,你同样在这里添加支持玩家的点击一个鼠标按键或者压下一个按钮在游戏平台去开始这个游戏。在这种情况下,你可能想去引导玩家去压下任何键,点击一个鼠标按钮,或者压一个按钮去继续。这总是一个好主意,让玩家知道什么你可以使用去控制游戏,所以他们不必去猜----使玩家猜会导致玩家对游戏的不满意。
                                  现在编译运行这个程序,你会看见非常简单的启动画面(如图7-9),当你压下任何一键,这时游戏开始了,非常不错!

                                  现在,你发现了乐趣,schmancy启动画面,是时候去添加同样的屏幕类型在游戏的结束的时候。在你做这个之前,但是,你需要添加逻辑,它实际上使游戏结束。

 

Game-Over Logic and the Game-Over Screen
                                  所以,现在你不得不检测你的游戏如何结束。你已经有一个游戏目标:回避三片到和四片刀精灵。但是什么时候这个游戏实际上结束了?它看起来有点粗糙,当用户一撞到一个单刀片精灵就结束游戏。相反,它可以使这个游戏更有趣,如果玩家有一个确定的生命值可以使用。
                                  为实现这一目标,首先你需要去创建一个类别变量在你的Game1类,去跟踪剩余的生命,以及公有的属性get和set访问器去允许SpriteManager去访问和修改这个值:

 1 int numberLivesRemaining = 3;
 2 public int NumberLivesRemaining
 3 {
 4      get { return numberLivesRemaining; }
 5      set
 6     {
 7          numberLivesRemaining = value;
 8          if (numberLivesRemaining == 0)
 9          {
10              currentGameState = GameState.GameOver;
11              spriteManager.Enabled = false;
12              spriteManager.Visible = false;
13          }
14      }
15 }

                                     注意当属性被设置,它的值被分配到numberLivesRemaining变量中,然后这个变量被检查去看看是否它的值为0。如果它的值为0,这个游戏的状态被改变到GameState.GameOver,并且SpriteManager被禁用和隐藏。于是这允许你去检测这个值从SpriteManager类开始,当玩家生命结束,使游戏自动关闭并且进入了一个状态,你可以在它上面显示一个game-over的画面。
                                    现在,你不仅想要跟踪这个玩家有的生命数量,而且玩家需要能够看见他还有多少生命。
注意:为什么在屏幕上显示生命的数值?同样,这同样使游戏为玩家有个更好的体验。如果玩家要不断保持注意他自己的生命数,这将减低游戏的乐趣。你可以做任何事来帮助玩家解决问题,是通过显示重要的数据(例如分数和还剩下的生命数)将使玩家把精力放在最重要的事情上:一个游戏的快乐体验。
                                    为了显示剩下的生命值,你绘制一个活生生的三个环状精灵在屏幕的左上角(在分数的下面)为每个生命,这样玩家还活着。
                                    为了避免混乱,你不想让被玩家控制的精灵与实际大小相同,所以你不得不添加同样的代码,它允许你去缩放精灵。因为这些精灵不能它们自己移动并且玩家不想让它们互相碰撞,你可以使用AutomatedSprite类并指定一个(0,0)的速度并绘制这些对象。
                                     在Sprite类中,添加一个类别变量去代表支持缩放一个被绘制的精灵:

1 protected float scale = 1;

                                     指定一个缩放值为1将导致对象被绘制成精灵同样的大小,所以你应该初始化它用这个值。接下来,你需要在你的Sprite类里改变Draw方法,使用你的重新添加的Scale变量给这个缩放参数。你的Draw方法应该象下面这样:

1 public virtual void Draw(GameTime gameTime, SpriteBatch spriteBatch)
2 {
3     spriteBatch.Draw(textureImage,position,new Rectangle(currentFrame.X * frameSize.X,currentFrame.Y * frameSize.Y,frameSize.X, frameSize.Y),Color.White, 0, Vector2.Zero,scale, SpriteEffects.None, 0);
4 }
                                     最后,你需要添加到Sprite类中一个新的结构,它将接收一个缩放值做为一个参数:
1 public Sprite(Texture2D textureImage, Vector2 position, Point frameSize,
2 int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed,
3 string collisionCueName, int scoreValue, float scale)
4 this(textureImage, position, frameSize, collisionOffset, currentFrame,
5 sheetSize, speed, defaultMillisecondsPerFrame, collisionCueName,
6 scoreValue)
7 {
8     this.scale = scale;
9 }
                                    并且添加到AutomatedSprite类一个新的构造,它将接收一个缩放参数并且传递这个值到基类中:
1 public AutomatedSprite(Texture2D textureImage, Vector2 position,
2 Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize,
3 Vector2 speed, string collisionCueName, int scoreValue, float scale)
4 base(textureImage, position, frameSize, collisionOffset, currentFrame,
5 sheetSize, speed, collisionCueName, scoreValue, scale)
6 {
7 }
                                    你的AutomatedSprite类是现在准备使用去创建精灵,你将会使用它去给玩家显示剩余生命的数量。在这个SpriteManager类中,添加一个类别变量去跟踪玩家生命的精灵:
1 List<AutomatedSprite> livesList = new List<AutomatedSprite>( );
                                    在SpriteManager的LoadContent方法中,你需要去填入livesList表单用开始玩家的生命数量等于精灵生命的数量。在每一祯,你绘制项目表单在livesList变量中在屏幕的左上角。它是一个可视的指示器,告诉玩家还有多少生命。为了填入这个表单,创建一个循环,它运行与玩家生命数一样多次,每一次通过这个循环添加一个新的AutomatedSprite对象到这个表单中。
1 for (int i = 0; i < ((Game1)Game).NumberLivesRemaining; ++i)
2 {
3     int offset = 10 + i * 40;
4     livesList.Add(new AutomatedSprite(Game.Content.Load<Texture2D>(@"images\threerings"),new Vector2(offset, 35), new Point(7575), 10,new Point(00), new Point(68), Vector2.Zero,null0, .5f));
5 }
                                     唯一复杂的东西在这部分代码中是第二个参数,它代表精灵的位置。这个参数是一个Vector2类型的。你的目标在这个表单中是创建一系列的精灵,它们不能移动并且连续在屏幕的左上角的一行上。参数的X的部分是第一个10的偏移量(因此最左边的图象从屏幕的边缘有轻微的偏移),然后被40乘(所以每个图象被绘制成上一副图象的右边40个单位)。参数的Y部分被设置成35的偏移,它刚好在分数文本的下面。
                                     现在所有剩下的是更新你的livesList对象,在每一次SpriteManager类的Update被调用时,并绘制你的对象当每一次Draw被调用时。
                                     为了实现这个,添加下面的代码在你的SpriteManager类的UpdateSprites方法的末尾。
1 foreach (Sprite sprite in livesList)
2     sprite.Update(gameTime, Game.Window.ClientBounds);
                                     添加下面的代码到Draw方法中,正好在spriteBatch.End调用的上面:
1 foreach (Sprite sprite in livesList)
2     sprite.Draw(gameTime, spriteBatch);  

                                     编译运行游戏,你会看见三个精灵在屏幕的左上角,表明玩家还剩的生命的数量(图7-10)
                                     很好!现在你只需要去添加一些逻辑去删除一个生命当玩家与一个刀片精灵碰撞,并且显示一个game-over画面当游戏结束时。
                                     删除其中之一的精灵是非常简单的。你有代码,它检测玩家和屏幕上移动的精灵之间的碰撞。当一个碰撞发生,你需要去检查精灵的类型,它与玩家碰撞:如果类型是AutomatedSprite,你需要删除一个生命精灵从精灵生命表单的末尾。确保你从这个表单末尾删除这个精灵,因为你布置它们是以从左到右的顺序。
                                     除了从精灵表单里删除一个精灵,代表玩家还有生命,你需要去消耗numberLivesRemaining变量的值,通过使用它的访问器从Game1类中。

                                     碰撞检测代码布置在你的SpriteManager的UpdateSprites方法。在这个方法中,你有逻辑去删除精灵在两种情况下:当一个精灵与玩家碰撞,当一个精灵离开了屏幕。两种情况使用spriteList.RemoveAt方法从游戏中去删除精灵。寻找两个spriteList.RemoveAt的实例在SpriteManager类的UpdateSprites方法中,并找到为碰撞使用的一个(你会看到用来播放碰撞声音的代码)。添加下面的代码到这个方法中,当一个碰撞发生,正好在代码删除精灵之前。

1 if (s is AutomatedSprite)
2 {
3    if (livesList.Count > 0)
4    {
5        livesList.RemoveAt(livesList.Count - 1);
6        --((Game1)Game).NumberLivesRemaining;
7    }
8 }
                                    为了清楚起见,这整个的UpdateSprite方法被传递到这:
 1 protected void UpdateSprites(GameTime gameTime)
 2 {
 3    // Update player
 4    player.Update(gameTime, Game.Window.ClientBounds);
 5    // Update all non-player sprites
 6    for (int i = 0; i < spriteList.Count; ++i)
 7    {
 8        Sprite s = spriteList[i];
 9        s.Update(gameTime, Game.Window.ClientBounds);
10        // Check for collisions
11        if (s.collisionRect.Intersects(player.collisionRect))
12        {
13            // Play collision sound
14            if (s.collisionCueName != null)
15                ((Game1)Game).PlayCue(s.collisionCueName);
16            // If collided with AutomatedSprite
17            // remove a life from the player
18            if (s is AutomatedSprite)
19            {
20                 if (livesList.Count > 0)
21                 {
22                      livesList.RemoveAt(livesList.Count - 1);
23                      --((Game1)Game).NumberLivesRemaining;
24                 }
25            }
26            // Remove collided sprite from the game
27            spriteList.RemoveAt(i);
28            --i;
29        }
30        // Remove object if it is out of bounds
31        if (s.IsOutOfBounds(Game.Window.ClientBounds))
32        {
33            ((Game1)Game).AddScore(spriteList[i].scoreValue);
34            spriteList.RemoveAt(i);
35            --i;
36        }
37     }
38     // Update lives-list sprites
39     foreach (Sprite sprite in livesList)
40     sprite.Update(gameTime, Game.Window.ClientBounds);
41 }
                                   如果你现在运行游戏,你会注意到每次你撞上一个三刀片或四刀片精灵时一个生命被删除。当所有你的生命用完时,这个游戏将会出现冻结。它同样实际上不是冰冻;它很简单的进入了一个游戏状态,你可以在这个游戏状态里做任何事(GameState.GameOver)。最后一步在本节是创建一个game-over画面,类似你前面所创建的启动画面。
                                   首先,在Game1类的Update方法中,添加一些代码,它将允许玩家去关闭游戏的窗口,当游戏是在game-over状态。这里,你关闭游戏当玩家压下Enter键。添加下面的代码去检测当Enter键被压下,并且去调用Exit()方法,它将整个游戏关闭(如果你添加压下鼠标或者手柄按钮的去让游戏开始,你很可能添加类似的输入支持关闭游戏):
1 case GameState.GameOver:
2 if (Keyboard.GetState( ).IsKeyDown(Keys.Enter))
3     Exit( );
4     break;
                                   为了清楚,你的Game1类的Update方法应该看起来象这个样子:
 1 protected override void Update(GameTime gameTime)
 2 {
 3     // Only perform certain actions based on
 4     // the current game state
 5     switch (currentGameState)
 6     {
 7         case GameState.Start:
 8              if (Keyboard.GetState().GetPressedKeys().Length > 0)
 9              {
10                  currentGameState = GameState.InGame;
11                  spriteManager.Enabled = true;
12                  spriteManager.Visible = true;
13              }
14              break;
15         case GameState.InGame:
16              break;
17         case GameState.GameOver:
18              if (Keyboard.GetState().IsKeyDown(Keys.Enter))
19                  Exit();
20              break;
21      }
22      // Allows the game to exit
23      if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==ButtonState.Pressed)
24         this.Exit();
25      //Update audio
26      audioEngine.Update();
27      base.Update(gameTime);
28 }
                                     现在,添加一些代码,它将绘制一个game-over信息,当游戏在game-over状态。当然,游戏你将会绘制,你做这个在Game1类的Draw方法中。在这个switch语句的game-over情况下,添加下面代码:
 1 case GameState.GameOver:
 2      GraphicsDevice.Clear(Color.AliceBlue);
 3      spriteBatch.Begin( );
 4      string gameover = "Game Over! The blades win again!";
 5      spriteBatch.DrawString(scoreFont, gameover,new Vector2((Window.ClientBounds.Width / 2)- (scoreFont.MeasureString(gameover).X / 2),(Window.ClientBounds.Height / 2)- (scoreFont.MeasureString(gameover).Y / 2)),Color.SaddleBrown);
 6      gameover = "Your score: " + currentScore;
 7      spriteBatch.DrawString(scoreFont, gameover,new Vector2((Window.ClientBounds.Width / 2)- (scoreFont.MeasureString(gameover).X / 2),(Window.ClientBounds.Height / 2)- (scoreFont.MeasureString(gameover).Y / 2+ 30),Color.SaddleBrown);
 8      gameover = "(Press ENTER to exit)";
 9      spriteBatch.DrawString(scoreFont, gameover,new Vector2((Window.ClientBounds.Width / 2)- (scoreFont.MeasureString(gameover).X / 2),(Window.ClientBounds.Height / 2)- (scoreFont.MeasureString(gameover).Y / 2+ 60),Color.SaddleBrown);
10      spriteBatch.End( );
11      break;

                                      这个代码将绘制一行文本在屏幕上:一个信息表示游戏结束了,一个信息表明玩家应该按下Enter键去退出,并且一个信息显示玩家的最终代码。这个game-over画面看起来象图7-11。

Fine-Tuning Gameplay
                                      任何你开发的游戏,你想在游戏测试时调整东西,确保游戏的方式,你想要并且去挑战,同时很有乐趣。最大的一个因素是确保游戏玩的愉快。如果你只是为你自己制造了这个游戏,这显然是你自己的。如果,你为了很多的玩家开发它,那么从用户中间得到反馈是非常重要的。

                                      在这种情况下,有一件事你可以想去调整的是关于鼠标事件,你在游戏中创建的。你会注意到,使用鼠标比使用键盘要简单。为了使游戏有更多的挑战,并且强迫用户去使用一个输入,这样维持一个稳定的完家精灵速度,请尝试删除鼠标的支持。
                                      为了删除鼠标的支持,注释掉或者删除鼠标事件代码在UserControlledSprite类的Update方法中:

 1 // COMMENTED-OUT MOUSE SUPPORT
 2 // If the mouse moved, set the position of the sprite to the mouse position
 3 // MouseState currMouseState = Mouse.GetState( );
 4 // if (currMouseState.X != prevMouseState.X ||
 5 // currMouseState.Y != prevMouseState.Y)
 6 // {
 7 // position = new Vector2(currMouseState.X, currMouseState.Y);
 8 // }
 9 // prevMouseState = currMouseState;
10                                       你还应该注释掉类别prevMouseState变量在UserControlledSprite类中:
11 // COMMENTED-OUT MOUSE SUPPORT
12 // MouseState prevMouseState;
                                      在此之前,为这个游戏删除鼠标的支持,最初的玩家精灵位置被设置到鼠标的指针的位置。它根本不会工作,所以,你想让玩家在屏幕的中心开始。你创建player对象在SpriteManager类的LoadContent方法中,并且在这个玩家对象结构中,你传入Vector2.Zero作为对象位置的参数(在表单中的第二个参数)。改变这段代码这样你传入到屏幕的中心作为玩家对象的启始位置。在SpriteManager类的LoadContent方法中的你的玩家对象初始化代码应该看起来象这样:
1 player = new UserControlledSprite(Game.Content.Load<Texture2D>(@"Images/threerings"),new Vector2(Game.Window.ClientBounds.Width / 2,Game.Window.ClientBounds.Height / 2),new Point(7575), 10new Point(00),new Point(68), new Vector2(66));
                                      游戏体验的另外一个方面是,你可能想调整去使游戏增加难度。游戏在这一点上,玩家可以玩一辈子,因为这个游戏根本没有什么挑战(译者:没有难度没有乐趣)。
                                      如何使游戏变得很困难?好,这里有一些方法。你可以使刀片精灵在这游戏中移动的渐渐变快,你可以产生不同类型的精灵,这样就很难去回避它了。或者,你可以使用一个方法的联合,或者做一些完全不同的。这里的关键是创造性。这是一个视频游戏开发,新鲜又有想法是让这个游戏很伟大。随意玩游戏,并且考虑怎样才能使游戏更有趣。
                                      对于这本书的目的,我们将会使精灵产生越来越多,是为了使游戏日益变的很困难。你已经有两个变量,它检测一个最小和最大产生周期为每个新的精灵(enemySpawnMilliseconds和enemySpawnMaxMilliseconds在Game1类中),这些变量被分别设置到1000和2000毫秒(换句话说,一个新精灵被产生每1到2秒)。
                                      你不想减少产生敌人的周期在每一祯,因为游戏运行在每秒60祯,变化率太快,使事情变的有趣。相反,创建一对新的类别变量在SpriteManager类中,你可以使用它去减少产生敌人的周期时间(在这种情况下,每一秒):
1 int nextSpawnTimeChange = 5000;
2 int timeSinceLastSpawnTimeChange = 0;
                                      这些变量看起来很相似,因为这是你使用的同一概念,当实验动画速度。基本上,你添加一些代码在Game1类的Update方法中,这个方法将添加elapsed time到timeSinceLastSpawnTimeChange变量中。当这个变量的值比nextSpawnTimeChange变量的值大(它会在游戏的每5秒后发生,因为nextSpawnTimeChange被设置成5000毫秒),你将会减少spawn-timer变量的(enemySpawnMinMilliseconds和enemySpawnMaxMilliseconds)。
                                      虽然,你不想去减少这些值。如果产生敌人的周期变量达到了0,一个新的精灵可能会被产生在每一祯----每秒产生60个精灵。没有任何方法,任何人都赶不上它的速度。为了回避这种情况,你把产生敌人的周期设置在500毫秒。
                                      创建一个新的方法在SpriteManager类中,它将调整产生敌人频率的变量,使敌人的精灵产生的越来越频繁:
 1 protected void AdjustSpawnTimes(GameTime gameTime)
 2 {
 3    // If the spawn max time is > 500 milliseconds
 4    // decrease the spawn time if it is time to do
 5    // so based on the spawn-timer variables
 6    if (enemySpawnMaxMilliseconds > 500)
 7    {
 8          timeSinceLastSpawnTimeChange += gameTime.ElapsedGameTime.Milliseconds;
 9          if (timeSinceLastSpawnTimeChange > nextSpawnTimeChange)
10          {
11              timeSinceLastSpawnTimeChange -= nextSpawnTimeChange;
12              if (enemySpawnMaxMilliseconds > 1000)
13              {
14                  enemySpawnMaxMilliseconds -= 100;
15                  enemySpawnMinMilliseconds -= 100;
16              }
17              else
18           {
19           enemySpawnMaxMilliseconds -= 10;
20           enemySpawnMinMilliseconds -= 10;
21           }
22       }
23    }
24 
                                          在这段代码中,内部的if/else语句导致产生敌人增加的速度迅速下降(每次减去100,产生敌人的周期时间被递减)直到最大的产生敌人的周期时间小于1秒(1000毫秒)。在次之后,产生敌人的周期时间继续减少直到它达到一个最大的产生敌人周期为500毫秒,但是它是很慢的下降(每次只减去10产生敌人的周期时间递减)。同样,这只是调整的一部分去使游戏按照你的方式玩。这种改变产生敌人周期的具体方法将会导致游戏变得实用,并且很有意思,然后游戏越来越难直到游戏对玩家充满了挑战。
                                          你需要添加一个调用这个新的AdjustSpawnTimes方法从你的SpriteManager类的Update方法中。添加下面的代码行到Update方法中在base.Update调用的前面:
1 AdjustSpawnTimes(gameTime);

                                          编译运行这个游戏,并且你会发现你还活着,游戏变的越来越难。在某些时候,你很可能跟不上它,游戏也会结束。看图7-12,当你启动了游戏,游戏变的错综复杂。


Creating Power-Ups

                                          我们将添加一个最后的东西到本章的这个游戏中,然后它将会微调和进一步调整。你有三个精灵,它不会做任何事:头骨球,加号,和螺栓。这些精灵并不意味会带走玩家的生命,当他们与玩家对象碰撞在一起,而是有一些积极的或消极的影响对于一个玩家。

                                           正如在本章前面所述,我们要真正进入游戏开发的创造性方面,如果是你觉得会增加大量的 娱乐游戏,你应该考虑加入该功能。当你考虑执行象power-ups这类事情时,这里没有限制你去做什么的东西。
                                           对于本书的目的,这些对象将发生与玩家对象碰撞的效果将如下:
                      1。螺丝导致玩家对象将以当前速度的200%移动5秒钟。
                      2。骷髅头导致玩家对象以当前它的速度的50%移动5秒钟。
                      3。正号导致玩家精灵比它当前尺寸200%的大小持续5秒钟。
                                          从本质上讲,一个power-up(或者power-down,如果效果对玩家是负面的)将运行给定的时间,然后效果消失。在这个游戏中,这个效果将会叠加,意思是你可以在给定效果的时间内通过叠加不至一个效果来影响自己的精灵。并且你可以通过叠加相同的效果多次(例如,如果你连续撞击到两个骷髅头,你当前的速度将是你原来的速度的25%)。
注意:效果总是可以叠加吗?这完全在于你。你是开发者,并且这是你自己的虚拟世界。你想让游戏变的有趣让这充满挑战。同样,尝试不同的效果,并决定你想用哪个效果为你工作。
                                          为了创建这些效果,你需要去添加一些代码到你的Sprite基类中。首先,你需要添加一个变量,它将保留这些scale属性的初始值。当前,你初始化scale为1,但是如果这个玩家与一个加号精灵碰撞,这个scale的值将会翻倍。而不是固定这个值是1,它最好使用一个变量代表这个初始值。添加下面的类别变量到Sprite类中:

1 protected float originalScale = 1;

                                           接下来,你需要添加一个方法,他将允许SpriteManager去增加或者减少这个scale变量的值,以及SpriteManager的方法可以调用重新设置scale变量的值到它的初始值。添加下面的方法到Sprite类:

1 public void ModifyScale(float modifier)
2 {
3      scale *= modifier;
4 }
5 public void ResetScale( )
6 {
7      scale = originalScale;
8 }
                                           稍作停顿,马上回答这个问题:什么效果将会改变scale在你的碰撞检测上?没有错,它会让你陷入困境。你会改变这个缩放,但是碰撞检测将仍然使用frameSize和collisionOffset的属性,这些属性保留这祯的大小的初始值和碰撞偏移量(译者:位置)。你能做些什么呢?幸运的是,你的SpriteManager使用一个叫做CollisionRect的访问器,它创建一个Rectangle为这个碰撞的检测。你可以修改这个访问器,使用缩放属性去恰当创建这个Rectangle,当一个不同的缩放因数被应用。改变这个在你的Sprite基类中的collisionRect属性,如下:
1 public Rectangle collisionRect
2 {
3    get
4    {
5        return new Rectangle((int)(position.X + (collisionOffset * scale)),(int)(position.Y + (collisionOffset * scale)),(int)((frameSize.X - (collisionOffset * 2)) * scale),(int)((frameSize.Y - (collisionOffset * 2)) * scale));
6    }
7 }
                                           现在,你需要对Speed变量做同样的事情。首先,添加到Sprite类一个类别变量,它将保留初始的速度:
1 Vector2 originalSpeed;
                                           因为你初始化了speed在这个结构中通过一个参数而不是实例化它成一个不变的值,你不需要去实例化originalSpeed变量到任何特定的值。但是,你需要去初始化它在这个结构中,它设置speed变量。添加下面的代码正好在结构中的this.speed=speed代码行的下面:
1 originalSpeed = speed;
                                           接下来,添加两个方法(就象你对scale变量做的),它将允许SpriteManager去修改并且重新设置Sprite类的Speed变量:
1 public void ModifySpeed(float modifier)
2 {
3     speed *= modifier;
4 }
5 public void ResetSpeed( )
6 {
7     speed = originalSpeed;
8 }
                                            噢噎,这是迄今为止最好的东西。现在你已经准备去使最终的修改了。打开SpriteManager类,正如你将会使某些变化在下面的类中。
                                            每一次玩家撞击这些特别精灵中的一只,一个power-up计时器需要去设置,这样使power-up运行5秒。添加一个类别变量到这个SpriteManager类去跟踪为什么时候power-ups应该被结束:
1 int powerUpExpiration = 0;
                                            下一步,找到UpdateSprites方法。在这个方法中,你检查去看看玩家与精灵是否发生了碰撞。之前,你添加一个检查去看看是否一个碰撞与一个AutomatedSprite对象已经发生了,如果是这样,你删除一个生命值从这个玩家上。现在,你将会处理其它类型可能发生的碰撞。最简单的方法去检测被撞精灵的类型,通过检查精灵的CollisionCu属性,看它是不是一个AutomatedSprite。记住,在与那个对象碰撞后,一个精灵的CollisionCue属性检测哪个声音被播放,因此这个声音属性对于不同类型的精灵是唯一。
                                            你的代码应该有一个if的语句,看起来象下面这样:
1 if (s is AutomatedSprite)
2 {
3    if (livesList.Count > 0)
4    {
5        livesList.RemoveAt(livesList.Count - 1);
6        --((Game1)Game).NumberLivesRemaining;
7    }
8 }
                                            添加下面的代码在if语句后面(if(s is AutomatedSprite)语句,不是if(LivesList.Count>0)语句)去创建这power-up效果并且设置power-up周期记时器为5秒(5000毫秒)。
 1 else if (s.collisionCueName == "pluscollision")
 2 {
 3     // Collided with plus - start plus power-up
 4     powerUpExpiration = 5000;
 5     player.ModifyScale(2);
 6 }
 7 else if (s.collisionCueName == "skullcollision")
 8 {
 9     // Collided with skull - start skull power-up
10     powerUpExpiration = 5000;
11     player.ModifySpeed(.5f);
12 }
13 else if (s.collisionCueName == "boltcollision")
14 {
15     // Collided with bolt - start bolt power-up
16     powerUpExpiration = 5000;
17     player.ModifySpeed(2);
18 }
                                            这段代码将会执行某些动作,它开始一个power-up效果在玩家基于碰撞精灵的碰撞提示。此外,它将设置powerUpExpiration变量到5000去表示,这个power-up计时器已经开始了,并且保持了5秒。
                                            最后一件事情你需要做的是消耗周期记时器,每一次SpriteManager类的Update方法被调用的时候。添加下面的方法到SpriteManager类中:
 1 protected void CheckPowerUpExpiration(GameTime gameTime)
 2 {
 3     // Is a power-up active?
 4     if (powerUpExpiration > 0)
 5     {
 6        // Decrement power-up timer
 7        powerUpExpiration -=gameTime.ElapsedGameTime.Milliseconds;
 8        if (powerUpExpiration <= 0)
 9        {
10               // If power-up timer has expired, end all power-ups
11               powerUpExpiration = 0;
12               player.ResetScale( );
13               player.ResetSpeed( );
14         }
15       } 
16 }

                                             然后,调用这个方法从你的Update方法里面,正好要在base.Update调用之前:

1 CheckPowerUpExpiration(gameTime);

                                             在每一祯,这个新方法将会从power-up计时器减去elapsed游戏的时间,当power-up计时器达到了0,它将会重置缩放和玩家的速度。
                                             现在,编译运行这个游戏,并且你会发现,你有一个用XNA写的完整的2D游戏---它很基础,但是很有趣味。同样,你可以调整游戏的问题比如速度,产生敌人的比率,power-ups,等等。根据自己的喜好和定制游戏要使它运行的方式是它应该的。
                                             请注意power-up效果叠加的力量。例如,如果撞击到一个加号精灵,然后撞击到一个骷髅头精灵在加号效果结束之前,两个效果叠加一起到你的玩家精灵上。同样的事情发生在类似类型的精灵上;例如,如果你撞击一个加号,然后另外加号在这个效果结束之前也撞上了玩家,那么你的玩家精灵现在会是正常400%的大小(正常大小*2*2)。
                                             棘手的问题是,即使你可以叠加power-up效果,这里只有一个计时器。也就是说如果你有两个或三个或者更多的效果作用在你的玩家精灵上,它们不会周期满在不同的时间。相反,每一次你撞击一个power-up,这个power-up计时器将会重置成5秒。如果5秒内你没有撞击到其他的power-ups,那么所有的叠加power-up效果都将结束。
                                             这是通过设计得到的,但同时,这也是很不错的地方,让你去调查,你考虑游戏应该如何来玩。如果你为power-ups考虑一个不同的规则,那么实现它!你是一个开发者,你是虚拟世界的王者,不是吗?你也可以利用这个机会,使游戏按照你想象中的来实现。
源代码:http://shiba.hpe.cn/jiaoyanzu/WULI/soft/xna.aspx?classId=4
(完)

posted on 2009-08-28 21:29  一盘散沙  阅读(685)  评论(0编辑  收藏  举报

导航