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

PS:自己翻译的,转载请著明出处
                                                        第十五章   包装你的3D游戏
                             这里我们准备去做-在你第一个3D游戏开发项目的最后的事情。它上看去不错,声音也不错,玩的也很好,剩下要做的是添加一些游戏逻辑包装它。首先第一件事情,你需要一个过度画面,当你的游戏开始时。除此,你需要去提供给玩家一些不同的指示器当她达到另外一个水平(关卡)在这个游戏中。为什么不使用一个过度画面在关卡之间呢?最后,你需要添加一个场景,它显示最后的分数当游戏结束时。听起来是一个不错的解决方案,这些问题可能会创建一个splash screen
游戏部分,它将让你显示文本在屏幕上。这样,你可以重新使用相同的类所所有三个意图,就象提到的-让我们来面对它,无论何时,你可以重新使用代码,这样你可以节省时间不会头痛。
                             本章弥补了14章留下的问题。打开工程,你准备从14章代码后面开始工作。

添加一个启动游戏画面的组件
                             在我们跳转到之前,先编写你的游戏启动画面组件,让我们返回看看这些如何去工作的。你的Game1类准备要管理不同的游戏状态。几个可能的游戏状态:开始游戏,玩这个游戏,暂停游戏在两个关卡之间,在游戏的最后显示游戏结束。
                             为了帮助你管理这些状态,创建一个枚举在你的Game1类中,这样你将使用并跟踪在游戏过程中状态与状态之间的改变。添加下面的代码行在Game1类的类层次:

1 public enum GameState { START, PLAY, LEVEL_CHANGE, END}
2 GameState currentGameState = GameState.START;
                             在这些行,你首先定义个枚举称为GameState,它声明所有的可能的游戏状态,然后你创建一个变量,你使用它去跟踪当前的游戏状态并且初始这个变量为START游戏状态。这样应该很有帮助理解本章。
                             你同样需要去添加一个方法为起始画面的游戏组件,这个模型管理游戏组件去通报Game1类当一个改变在游戏状态被占用。为此,添加下面的方法到Game1类:
1 public void ChangeGameState(GameState state, int level)
2 {
3       currentGameState = state;
4 }
                             随后,你添加更多个逻辑到这个方法,它将用到第二个参数,并且执行不同的任务依赖于改变的游戏状态。
                             添加一个启动画面游戏组件,你首先需要创建一个空白游戏组件类。添加一个游戏组件到你的项目中并且调用文件的SplashScreen.cs.
                             请记住,默认情况下一个游戏组件没有一个Draw方法,因此不能绘制任何东西。但是,如果一个启动画面不能用来绘制一段文字,那么它有什么好的?改变新游戏组件的基类从GameComponent到DrawableGameComponent,它将让你的游戏组件tie into游戏的Draw循环的次序。
                             接下来,添加下面的类别变量到你的启动画面游戏组件:
1 string textToDraw;
2 string secondaryTextToDraw;
3 SpriteFont spriteFont;
4 SpriteFont secondarySpriteFont;
5 SpriteBatch spriteBatch;
6 Game1.GameState currentGameState;
                             你的启动画面有一个功能去显示一个标题用一个很大的字体和用一个很小的字体的别的文本。这两个SpriteFont变量和SpriteBatch变量包含了很容易的绘制那个文本。你的currentGameState变量将跟踪当前的游戏状态,所以你的启动画面将知道该去做什么,基于当前游戏的在什么样的状态。
                             可能你已经猜到你添加的SpriteFont变量,你现在需要去添加几个spritefonts到你的项目中。右击Content节点在解决方案中,并且选择Add-New Folder.命名这个新文件夹Fonts.然后右击这个新Fonts文件夹,并选择Add-New Item...选择Sprite Font模板在右边,并命名你的字体Arial.spritefont,如图15-1所显示的那样。
                             接下来,添加另外的spritefont(跟着同样的步骤),调用这一个Arial Black.spritefont.

                             你准备去使用这个Arial Black字体为这个窗口的大标题文本。打开文件,你会看见基于XML的内容。字体的第二个元素是<Size>元素。改变你的Arial Blank字体的大小到16通过修改这个元素,如下:
1 <Size>16</Size>
                             为了使大标题突出一点,继续修改Arial字体的大小。打开Arial.spritefont文件,并且改变字体变小
1 <Size>10</Size>
                             接下来,在你的SplashScreen类,你需要添加一个重载为LoadContent方法,这样你可以加载字体并初始化你的SpriteBatch对象。添加下面的代码在SplashScreen类中:
1 protected override void LoadContent( )
2 {
3     // Load fonts
4     spriteFont = Game.Content.Load<SpriteFont>(@"fonts\Arial Black");
5     secondarySpriteFont = Game.Content.Load<SpriteFont>(@"fonts\Arial");
6     // Create sprite batch
7     spriteBatch = new SpriteBatch(Game.GraphicsDevice);
8     base.LoadContent( );
9 }
                             你的启动画面将显示在游戏的开始,和游戏的结尾,和关卡之间。但是你如何能使游戏从启动画面到游戏画面或者推出呢?什么是最好的方式做到这一点?这是很好的问题,真的是没有正确的答案。这是游戏开发的另一方面,来自于个人的喜好。经常,启动画面是基于时间,并且会淡出到下一个游戏状态在几秒之后。当一个键被压下或者鼠标按键被单击,其他的可以被显示了。还有一些甚至没有单独的屏幕,但是只是覆盖在游戏中,并且慢慢的淡出。严格的说,究竟怎么做取决于你。
                             对于本书的目的,我们准备使屏幕转换通过按下了Enter键。为了执行这个,你需要捕捉Enter键的压下在你的SplashScreen类的Update方法中。如果一个Enter键压下被检测到,你要么通知Game1类,在游戏状态需要被改变,要么完全退出游戏。两个中的哪一个依赖于当前的游戏状态(是splashScreen组成部分,当前显示一个星星或者关卡场景,或者一个游戏结束场景?)。
                            改变你的SplashScreen类的Update方法如下:
 1 public override void Update(GameTime gameTime)
 2 {
 3     // Did the player hit Enter?
 4     if (Keyboard.GetState( ).IsKeyDown(Keys.Enter))
 5     {
 6         // If we're not in end game, move to play state
 7        if (currentGameState == Game1.GameState.LEVEL_CHANGE || currentGameState == Game1.GameState.START)
 8              ((Game1)Game).ChangeGameState(Game1.GameState.PLAY, 0);
 9        // If we are in end game, exit
10        else if (currentGameState == Game1.GameState.END)
11        Game.Exit( );
12     }
13     base.Update(gameTime);
14 }
                           由于启动画面绝不应该在Play游戏的状态被显示,唯一的状态是你检查的是START,LEVERL_CHANGE,和END.如果当前的状态是前两个中的任意一个,你准备转化到一个PLAY状态,所以你调用Game1的类的ChangeGameState和修改要改变的类。如果当前的状态是END,当玩家按下Enter键游戏退出。
                           接下来,你需要添加代码,它将实际绘制文本。当然,这会在Draw方法中完成,您目前没有。你需要去创建一个Draw方法的重写版本,并且添加代码去绘制一个大标题的文本,和小标题文本:
 1 public override void Draw(GameTime gameTime)
 2 {
 3    spriteBatch.Begin( );
 4    // Get size of string
 5    Vector2 TitleSize = spriteFont.MeasureString(textToDraw);
 6    // Draw main text
 7    spriteBatch.DrawString(spriteFont, textToDraw,new Vector2(Game.Window.ClientBounds.Width / 2- TitleSize.X / 2,Game.Window.ClientBounds.Height / 2),Color.Gold);
 8    // Draw subtext
 9    spriteBatch.DrawString(secondarySpriteFont,secondaryTextToDraw,new Vector2(Game.Window.ClientBounds.Width / 2- secondarySpriteFont.MeasureString(secondaryTextToDraw).X / 2,Game.Window.ClientBounds.Height / 2 +TitleSize.Y + 10),Color.Gold);
10    spriteBatch.End( );
11    base.Draw(gameTime);
12 }
                            请注意,第一调用DrawString使用大的SpriteFont对象,并使文本居中使用屏幕的宽度和高度,以及TitleSize Vector2对象,它将保留标题文本的大小因为通过SpriteFont.MeasureString方法被给予。这第二个DrawString同样调用水平中心文本用同样的方式,但是它放置垂直的文本正好在标题文本的下面通过使用标题文本的大小作为偏移量。
                            SplashScreen类的最后一部分是一个方法,它将会使Game1类去设置文本,它需要被显示,并且去设置当前的游戏状态,添加这个方法到SplashScreen类中:
 1 public void SetData(string main, Game1.GameState currGameState)
 2 {
 3     textToDraw = main;
 4     this.currentGameState = currGameState;
 5     switch (currentGameState)
 6     {
 7        case Game1.GameState.START:
 8        case Game1.GameState.LEVEL_CHANGE:
 9           secondaryTextToDraw = "Press ENTER to begin";
10           break;
11        case Game1.GameState.END:
12           secondaryTextToDraw = "Press ENTER to quit";
13           break;
14      }
15 }
                            次级文本被设置依靠游戏的状态,同时主要的文本被传入到方法以新的游戏状态。现在你的SplashScreen类准备好了,所有你需要做的是放置这个组件到你的游戏中。添加一个SplashScreen变量在你的Game1类的类别,和一个变量去跟踪得分。稍后,你将会添加得分到你的游戏中,你想显示玩家的得分当游戏结束时:
1 SplashScreen splashScreen;
2 int score = 0;
                            接下来,你需要初始化SplashScreen组件,并添加它到游戏组件的表单中在Game1的Initialize方法。当前,这个方法看上去象这样:
1 protected override void Initialize( )
2 {
3     camera = new Camera(thisnew Vector3(0050),Vector3.Zero, Vector3.Up);
4     Components.Add(camera);
5     modelManager = new ModelManager(this);
6     Components.Add(modelManager);
7     base.Initialize( );
8 }
                            修改Initialize方法,如这所示:
 1 protected override void Initialize( )
 2 {
 3     camera = new Camera(thisnew Vector3(0050),Vector3.Zero, Vector3.Up);
 4     Components.Add(camera);
 5     modelManager = new ModelManager(this);
 6     Components.Add(modelManager);
 7     modelManager.Enabled = false;
 8     modelManager.Visible = false;
 9     // Splash screen component
10     splashScreen = new SplashScreen(this);
11     Components.Add(splashScreen);
12     splashScreen.SetData("Welcome to Space Defender!",currentGameState);
13     base.Initialize( );
14 }
                             前两行直接被添加在Components.Add(modelManager)行的后面。这些行禁用modelManager组件。为什么你要做这个?因为你准备开始你的游戏以(splash screen)启动画面。当启动画面被激活,这个模型管理需要被禁用,反之亦然。接下来,你初始化SplashScreen组件,添加它到组件表单,设置它的初始化变量通过SetData方法。
                             接下来你就需要考虑的是更新的方法。当前,每一次Update被调用,你检查键盘为一个空格键被压下,如果发生,发射一个子弹。现在你正好准备想去做这个,如果当前游戏状态是设置成PLAY.
                             当前的你的Game1类的Update方法应该看上去象这样:
1 protected override void Update(GameTime gameTime)
2 {
3     // Allows the game to exit
4     if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==ButtonState.Pressed)
5     this.Exit( );
6     // See if the player has fired a shot
7     FireShots(gameTime);
8     base.Update(gameTime);
9 }
                             围绕调用FireShots方法以一个if声明,这样新的子弹如果游戏是在PLAY状态,它只有被发射:
1 // Only check for shots if you're in the play game state
2 if (currentGameState == GameState.PLAY)
3 {
4     // See if the player has fired a shot
5     FireShots(gameTime);
6 }
                              接下来,你需要做同样的事情在Draw方法中,正如你当前绘制一个十字准心每一次这个方法被调用。围绕这个代码以一个类似if声明,这样十字准心被绘制,只在如果游戏在PLAY状态(添加黑体代码):
 1 protected override void Draw(GameTime gameTime)
 2 {
 3     GraphicsDevice.Clear(Color.Black);
 4     // TODO: Add your drawing code here
 5     base.Draw(gameTime);
 6     // Only draw crosshair if in play game state
 7     if (currentGameState == GameState.PLAY)
 8     {
 9       // Draw the crosshair
10       spriteBatch.Begin( );
11       spriteBatch.Draw(crosshairTexture,new Vector2((Window.ClientBounds.Width / 2)- (crosshairTexture.Width / 2),(Window.ClientBounds.Height / 2)- (crosshairTexture.Height / 2)),Color.White);
12       spriteBatch.End( );
13     }
14 
                             最后,你需要充实这个ChangeGameState方法。当前,所有它做的是设置currentGameState变量。你需要添加一些动作去停止或者播放声音音乐,启用/禁用过渡屏幕(splash screen)和模型管理组件,基于游戏的状态到游戏应该过度到哪。修改方法如下:
 1 public void ChangeGameState(GameState state, int level)
 2 {
 3      currentGameState = state;
 4      switch (currentGameState)
 5      {
 6           case GameState.LEVEL_CHANGE:
 7                splashScreen.SetData("Level " + (level + 1),GameState.LEVEL_CHANGE);
 8                modelManager.Enabled = false;
 9                modelManager.Visible = false;
10                splashScreen.Enabled = true;
11                splashScreen.Visible = true;
12           // Stop the soundtrack loop
13                trackCue.Stop(AudioStopOptions.Immediate);
14                break;
15           case GameState.PLAY:
16                modelManager.Enabled = true;
17                modelManager.Visible = true;
18                splashScreen.Enabled = false;
19                splashScreen.Visible = false;
20          if (trackCue.IsPlaying)
21                trackCue.Stop(AudioStopOptions.Immediate);
22          // To play a stopped cue, get the cue from the soundbank again
23                trackCue = soundBank.GetCue("Tracks");
24                trackCue.Play();
25                break;
26           case GameState.END:
27                splashScreen.SetData("Game Over.\nLevel: " + (level + 1+"\nScore: " + score, GameState.END);
28                modelManager.Enabled = false;
29                modelManager.Visible = false;
30                splashScreen.Enabled = true;
31                splashScreen.Visible = true;
32          // Stop the soundtrack loop
33                trackCue.Stop(AudioStopOptions.Immediate);
34                break;
35        }
36 }
                                好,我们几乎没有。你有一个游戏,它开始在一个其始画面模式基于START状态。你的其始画面改变到PLAY状态或者退出游戏,基于当前的状态。最后的步骤在这个流程中,是去添加逻辑去从PLAY到LEVEL_CHANGE或者END的转换。
                                首先,让我们了解下PLAY-LEVEL_CHANGE的转变。记住你编写这个游戏去产生X数量的敌人飞船每个关卡。转换到一个新关卡应该发生当最终的飞船已经飞过相机或者被摧毁。为了使这个游戏更流畅一点,让我同样去添加约束,所有的爆炸应该同样完成。这样,当你破坏了最后的飞船,这个游戏不会立即到过度画面,而是让你看到爆炸后,然后在开始转换。
                                在你的ModelManager类的CheckToSpawnEnemy方法,你有一个if声明,它检查看敌人的数量产生在这个关卡是否比在这关允许的敌人数量低(if(enemiesThisLevel<levelInfoList[currentLevel].numberEnemies))。如果这条件是true,这里有很多的敌人被产生在这关,并且你要检查它,看看是否是产生敌人的时间。但是,如果它是false,这是你第一个声明,该移到一个新的关卡。添加到if声明中,下面的代码到else的块,它将检查去看是否所有的爆炸已经结束了,如果是这样,转换到一个新的关卡(整个方法清晰的显示在这)。
 1 protected void CheckToSpawnEnemy(GameTime gameTime)
 2 {
 3    // Time to spawn a new enemy?
 4    if (enemiesThisLevel <levelInfoList[currentLevel].numberEnemies)
 5    {
 6           timeSinceLastSpawn += gameTime.ElapsedGameTime.Milliseconds;
 7           if (timeSinceLastSpawn > nextSpawnTime)
 8           {
 9              SpawnEnemy( );
10           }
11    }
12    else
13    {
14   if (explosions.Count == 0 && models.Count == 0)
15   {
16     // ALL EXPLOSIONS AND SHIPS ARE REMOVED AND THE LEVEL IS OVER
17     ++currentLevel;
18     enemiesThisLevel = 0;
19     missedThisLevel = 0;
20     ((Game1)Game).ChangeGameState(Game1.GameState.LEVEL_CHANGE,currentLevel);
21    }
22   }
23 }
                                如果所有的飞船和爆炸已经从各自的表单中删除,敌人产生的数量在这个关卡级别已经达到了,你同样没有什么事情去做在这个级别,是时候删除它们到下一个场景中了。为了移动下一个级别,你需要去增加currentLevel变量,复位一对计数器(enemiesThisLevel和missedThisLevel),并且通知改变Game1的游戏状态。
                                这就是这么简单。现在,让我们添加一个转变从PLAY到END游戏状态。
                                在ModelManager类的UpdateModels方法,你有两个位置,你删除飞船从这个飞船表单中使用models.RemoveAt。其中之一的情况是一个玩家射击一个飞船,另外的情况是一个飞船飞过了相机并且逃走了。这中情况下游戏将会结束,当很多飞船逃离每关允许逃离的数量。你有一个变量设立去跟踪已经逃离飞船的数量(missedThisLevel),但是你不用对它做任何事。当前的UpdateModels方法看起来象这样:
 1 protected void UpdateModels( )
 2 {
 3    // Loop through all models and call Update
 4    for (int i = 0; i < models.Count; ++i)
 5    {
 6         // Update each model
 7         models[i].Update( );
 8         // Remove models that are out of bounds
 9         if (models[i].GetWorld( ).Translation.Z >((Game1)Game).camera.cameraPosition.Z + 100)
10         {
11              models.RemoveAt(i);
12              --i;
13         }
14    }
15 }
                               修改UpdateModel方法去增加missedThisLevel变量当一个飞船逃离,移动游戏中的结束游戏状态当错过的飞船最大的数量被达到了。这个新方法应该看起来象这样:
 1 protected void UpdateModels( )
 2 {
 3      // Loop through all models and call Update
 4      for (int i = 0; i < models.Count; ++i)
 5      {
 6          // Update each model
 7          models[i].Update( );
 8          // Remove models that are out of bounds
 9          if (models[i].GetWorld( ).Translation.Z >((Game1)Game).camera.cameraPosition.Z + 100)
10          {
11           // If player has missed more than allowed, game over
12             ++missedThisLevel;
13          if (missedThisLevel >levelInfoList[currentLevel].missesAllowed)
14          {
15              ((Game1)Game).ChangeGameState(Game1.GameState.END, currentLevel);
16          }
17          models.RemoveAt(i);
18          --i;
19      }
20    }
21 }

                                基本上,这里发生的是,每一次一个飞船逃离,你增加missedThisLevel变量。当这个变量超越允许错过的数量,这个游戏结束并且Game1类也被通知了。
                                好,让我们给它一弹。编译运行你的游戏,并且你应该添加一个很漂亮友好的启动画面(图15-2)。你现在同样可以播放游戏,并且它应该从这个关卡到另外一个关卡的转换,显示另外一个过渡画面当游戏结束。

                                由于你的过渡画面是一个游戏的组成部分,你可以自定义它正象你编程时,添加一个奇特的背景图象,添加声音....作任何你想去使它更令人兴奋和有趣的事情。

Keeping Score
                                现在你有关卡和结束游戏的逻辑,但是有趣的事你的分数还是0?在这节,你将会充实游戏的记分。你已经准备显示分数在游戏结束的时候,但是你想让玩家看见他们的分数当他们正在玩的时候。为了做到这个,添加一个类别SpriteFont变量到你的Game1类,用它绘制分数:

1 SpriteFont spriteFont;

                                是的,没有错...接下来的事情,你需要做的是添加一个新的spritefont到你的项目中。右击Content\Fonts文件夹在解决方案中,选择Add-New Item....选择Sprite Font模板在右边的并命名为Tahoma.spritefont,如图15-3所示

 结束游戏的逻辑
                               请注意,这里只有一个方法能结束这个游戏,就是失败。这里没有逻辑去"赢"这个游戏。为什么呢?什么是模型的利和弊呢?
                               这实际上是一个普通的模型为一个共同的游戏。一个想法是使这个游戏很难,很难直到它不可能让任何人能到下一关。这个方法的一个好处是玩家总是有一个原因去玩-这里没有结束,唯一的目标是击败之前的高分。
                               这个方法的一个缺点是你相信没有人可以通过游戏的这关,你认为"不可能"。例如,在游戏中你现在开发的,产生敌人的时间非常短,更多的飞船被产生,每一关有允许很少的飞船飞离游戏。这种情况持续下去,直到最后一关,48只飞船被产生,平均一只0-200毫秒,同时0只允许错过。这接近于不可能。然而,重要的是注意你的游戏看起来真傻如果如果真有人打破这个关。从本质上讲,你的游戏可能崩溃,如果它是一个专业开发的游戏,将是所有的论坛上的一个热门话题在互联网上。
                               这种阶段中一个游戏打破了由于玩家已经达到这个点,它没有更多的逻辑支持继续游戏被称为"杀死屏幕"。一些著名杀死屏幕的包括Pac-Man当玩家达到256级崩溃,DonkeyKong崩溃当玩家达到22关或者第117幕,Duck Hunt崩溃当玩家达到100级。查找这些在因特网上,你会看到很多当一个玩家发现如何打破一个视频游戏被产生。这并不是什么新闻,你要你的游戏去接受,这是一个好想法或者添加逻辑到你的游戏让玩家最终能赢,或者确保它是根本不可能的,任何人都不能达到游戏的结束的逻辑。

 

                                为了使你得分字体突出一点,打开Tahoma.spritefont文件和找到<Style>元素。这个元素让你调整属性如设置文本用粗体和斜体等等。改变spritefont去使用一个粗体字体通过改变<Style>标签如下:

1 <Style>Bold</Style>

注意:在<Style>标记项是区分大小写,正如它说的在实际的spritefont文件。确保你使用这个文本Bold代替bold或者BOLD.
                               接下来,在你的Game1类的LoadContent方法中,添加下面的代码加载字体:

1 spriteFont = Content.Load<SpriteFont>(@"Fonts\Tahoma");
                               现在,你想使用这个spritefont去绘制记分牌在屏幕上。另外去绘制分数,尽管,它可以帮助玩家去看看多少更多的飞船能错过在每一关。
                               要做到这一点,你需要添加一个方式为Game1类去检查多少错过还剩下(这个数据被保存在你的ModelManager类中)。添加下面公开访问到ModelManager类:
1 public int missesLeft
2 {
3    get { returnlevelInfoList[currentLevel].missesAllowed- missedThisLevel; }
4 }
                               现在,你需要信息显示在屏幕上,是时候去看看绘制文本到屏幕上了。你可能已经意识到你需要去做这个在Game1类的Draw方法中。记住spriteBatch对象,他的代码应该在SpriteBatch.Begin和SpriteBatch.End调用之间。
                               现在,添加下面的代码在SpriteBatch.Begin和SPriteBatch.End调用之间在你的Game1类的Draw方法中:
1 // Draw the current score
2 string scoreText = "Score: " + score;
3 spriteBatch.DrawString(spriteFont, scoreText,new Vector2(1010), Color.Red);
4 // Let the player know how many misses he has left
5 spriteBatch.DrawString(spriteFont, "Misses Left: " +modelManager.missesLeft,new Vector2(10, spriteFont.MeasureString(scoreText).Y + 20),Color.Red);
Keep the Player Informed
                               为什么它如此大的处理显示还剩多少可以错过的数字呢?
                               当开发任何的游戏,更多的你可以做的是让玩家尽量减少麻烦和担心,这可以让他们集中在游戏,休闲享受的经验。它是并不罕见在更复杂的游戏为屏幕更复杂的游戏通知的指标和方法,千疮百孔,使游戏体验更好。
                               因此,这里有一个问题:如果你添加一个文本指示器在屏幕上显示还剩多少能错过,是否足够呢?这是个人的观点。这是游戏开发, 它是一个创造性的过程。但是考虑到同样有什么能被做。如果你添加一个声音效果,无论何时当错过发生时播放?然后一个玩家可以告诉某些失误(译者:指的是错过飞船)而不用四处看。当玩家还剩三个或者几个可以错过的时候,一个警报音频被播放效果如何呢?也许文本可以闪烁在那时。这有一点改变并且很容易执行,但是他们可以显着提高游戏性,并且可以考虑在你的整体设计中考虑。

                               最后,这里必须有一种方法去调整分数。这个score变量是你的Game1类的一部分,但是它的意思是检测当一个变化在分数应该发生变化时(当一个子弹撞击一个飞船时)摆放在ModelManager类中。你需要添加一个方法到你的Game1类,它将让你的ModelManager调整这个分数:

1 public void AddPoints(int points)
2 {
3      score += points;
4 }
                               这是一个非常简单的方法去增加传入点作为一个整体的参数总和。
                               现在,所有剩下的是添加记分机制到ModelManager类它自己。首先,让我们计算出每次杀死值得多少个分。是另一个领域你可以有创意,想出一个公式,为你工作。一对普通的方法既不使用一个标志系统(所有飞船总是X点)也不使用一个增加的收益的方法(飞船值更多分做为游戏的发展)。为了这个游戏,你准备执行后面这种策略。但是首先,你需要一个开始的分,添加下面的类别变量到你的ModelManager类中:
1 const int pointsPerKill = 20;
                               您可以利用这个初始值,并且乘以它通过当前的关卡去得到一个实际的分数值为每次杀死飞船(关卡1杀死一只值20,关卡2杀死一只值40等等)。最后一步是添加实际分数变化。记住在ModelManager类,这里有两个地方,你调用models.RemoveAt去删除一个敌人飞船:在UpdateModels方法,当一个飞船逃离并且飞出了范围,另一个是在UpdateShots方法,当一个子弹撞击到了飞船。
                               你需要添加一些代码,它将更新分数当一个子弹撞击到了一个飞船。在UpdateShots方法的结束,这里调用到models.RemoveAt,它删除飞船,接着通过调用一个shots.RemoveAt,它删除子弹,它撞击到了一个飞船。在这些之前立即调用RemoveAt,添加下面的代码行,它将调整游戏的分数:
1 ((Game1)Game).AddPoints(pointsPerKill * (currentLevel + 1));
Scoring Logic
                               得分一定要这么简单?
                               绝对不是。这是一个非常有创造性的过程,这个方法为了计算这个分数可以是任何你希望它是。你能想到一些东西去添加进去使分数更有趣?
                               当你射击不成功,减去一点你的分数如何?这可能使游戏更有趣,因为它可能阻碍玩家简单的按下空格键不放。
                               但是,请记住,更复杂的得分,就需要更多的解释。现在我们使用一个相当简单的方法,它是被玩家期望的,所以不需要解释-你射击那个飞船,你得分。但是想象一下玩家会有多惊奇如果分数由于什么原因开始,没有任何的解释!如果你不想要你的玩家感到非常愤怒,他们需要完全理解游戏的规则,因为这个规则变的太复杂了,就需要更多的解释。
                  
                               你有了它-现在所有剩下要做的是播放你的游戏,与你朋友挑战去击败你的分数。游戏结束的画面如图15-4。


Adding a Power-Up
                               你在这里做得很好,你创建第一个3D游戏,包括分数和日益增加的难度,并充满了乐趣!在我们结束这章之前,让我们做更多的事情。倒不是游戏是无聊的,但是任何事情它打破单调的游戏规则,采取主动的朝着使一个游戏有更多的令人兴奋和入迷的方向。
                               在本节中,你添加一个power-up(译者:威力)因素,它将会被授予当一个玩家得到三次连续的击杀。这个power-up将让玩家射击在速射的模式有10秒钟。我知道....这个声音使人兴奋,让我们来实现它吧。
                               首先,您要添加声音效果,当速射power-up被授予时它会播放。这章的源代码,在3D Game\Content\Audio文件夹,你会发现一个声音效果调用RapidFire.wav。复制这个文件到你的项目的Content\Audio目录在窗口浏览器中。记住不要添加这文件到你的项目中,因为你将会添加它到你的XACT工程文件。
                               打开你的XACT工程文件从XACT里面,添加RapidFire.wav声音到wave bank,并且创建一个声音
cue为这个声音。然后,保存XACT工程文件,并且关闭XACT(参看第5章如果你需要更多关于编辑XACT工程文件)。
                               除此之外包括一个声音效果当玩家接收这个power-up,它可能会是一个好的想法去包括一个文本指示器。这将帮助你解决任何的困惑,为什么会突然之间玩家可以射的那么快。为了实现这个,添加一个新的spritefont到你的项目中在Visual Studio通过右击这个Content\Fonts文件夹并选择Add-New Item....选择这个SpriteFont模板在右边的,并命名这个文件为Cooper Blank.spritefont,如图15-5所示。
                               打开Cooper Black.spritefont和改变字体的大小通过修改<Size>元素如下:

1 <Size>26</Size>
                               同样,修改这个样式如黑体所显示的:
1 <Style>Bold</Style>
                               好,现在让我们深入这个代码并且看看它将如何工作。你将给玩家一个power-up当她连续撞击了三个飞船没有让任何飞船越过去。为了实现这个,你需要跟踪连续的杀戮没有让一个飞船逃离。power-up减少延迟的时间在射击和周期完成之间,在10秒之后。所以你必须记住这一点。打开你的Game1类并且添加下面的类别变量:
1 int originalShotDelay = 300;
2 public enum PowerUps { RAPID_FIRE }
3 int shotDelayRapidFire = 100;
4 int rapidFireTime = 10000;
5 int powerUpCountdown = 0;
6 string powerUpText = "";
7 int powerUpTextTimer = 0;
8 SpriteFont powerUpFont;
                              让我们看这些变量和它们能做什么:
           originalShotDelay
                               当power-up开始,你修改shotDelay变量去给这个速射功能。在10秒之后,你需要去设置它返回它的初始值。这个变量简单保留初始值,这样你可以重置它,当这个power-up周期满。
           PowerUps enum
                               这里列举出所有可能power-up,稍后在这种情况下你想去添加更多。
           shotDelayRapiedFire
                               这代表射击的延迟在速射模式中。正常情况下,它是300,所以你会是射击速度的3倍。 
           rapidFireTime
                               这是power-up将会持续的时间(用豪秒计算)。
           powerUpCountdown
                               这是计算跟踪power-up已经持续了这个效果多久的时间。
           powerUpText
                               这是当一个power-up被激活文本被显示
           powerUpTextTimer
                               这是时间计时器,它跟踪power-up文本已经在屏幕上多久了。
           powerUpFont
                               这指定的字体,用以绘制power-up的文本。
                               接下来,你需要加载spritefont你刚刚创建在你的powerUpFont变量中的。添加下面的代码行到你的Game1类的LoadCotent方法中:
1 powerUpFont = Content.Load<SpriteFont>(@"fonts\Cooper Black");
                               现在,你需要一种方法去关闭power-up.所有这发生在当power-up周期满,shotDelay变量被设置返回给它的初始值。添加到你的Game1类,这下面的方法,你使用它来取消你的power-up:
1 private void CancelPowerUps( )
2 {
3      shotDelay = originalShotDelay;
4 }
                               在你的Game1类的Update方法中,你将会去检查是否一个power-up周期满。这是基于时间的,所以你将会使用这个gameTime变量去消耗你的powerUpCountdown计时器。一旦计时器达到0,你将会取消power-up通过调用CancelPowerUps方法。添加到Game1类下面的方法,它将更新你的power-up计时器并终止power-up当被需要时:
 1  protected void UpdatePowerUp(GameTime gameTime)
 2 {
 3   if (powerUpCountdown > 0)
 4   {
 5          powerUpCountdown -= gameTime.ElapsedGameTime.Milliseconds;
 6     if (powerUpCountdown <= 0)
 7     {
 8        CancelPowerUps( );
 9        powerUpCountdown = 0;
10     }
11  }
12 }
                                接下来,调用UpdatPowerUp方法在你的Game1类的Update方法的结尾,在调用base.Update之前:
1 // Update power-up timer
2 UpdatePowerUp(gameTime);
                                你需要添加一个public方法到Game1,它将让这个方法管理激活一个power-up.添加下面方法到你的Game1类中:
 1 public void StartPowerUp(PowerUps powerUp)
 2 {
 3     switch (powerUp)
 4     {
 5            case PowerUps.RAPID_FIRE:
 6            shotDelay = shotDelayRapidFire;
 7            powerUpCountdown = rapidFireTime;
 8            powerUpText = "Rapid Fire Mode!";
 9            powerUpTextTimer = 1000;
10            soundBank.PlayCue("RapidFire");
11            break;
12      }
13 }
                                为什么swich声明使用PowerUps枚举?它刚好设置这代码使你添加另外的power-ups如果你想要的。在这种情况下速射power-up,首先你设置新的射击延迟,然后你设置power-up的倒数计数器。接下来,你设置power-up文本去表示速射模式并设置文本计时器去显示文本为一秒钟。最后,你播放声音告诉玩家power-up已经被激活了。
                                由于shotDelay变量已经被使用去检测延迟在子弹之间,这里没有更多要做的在速射这个功能上。但是,你想绘制power-up文本在屏幕上同时powerUpTextTimer的值大于0。为了实现这个,添加下面的代码到Game1类的Draw方法中,在调用spriteBatch.End之前;
1 // If power-up text timer is live, draw power-up text
2 if (powerUpTextTimer > 0)
3 {
4     powerUpTextTimer -= gameTime.ElapsedGameTime.Milliseconds;
5     Vector2 textSize = powerUpFont.MeasureString(powerUpText);
6     spriteBatch.DrawString(powerUpFont,powerUpText,new Vector2((Window.ClientBounds.Width / 2-(textSize.X / 2),(Window.ClientBounds.Height / 2-(textSize.Y / 2)),Color.Goldenrod);
7 }
                                这不是任何你之前所见过的。如果文本计时器大于0,递减它用公用的时间,然后绘制字符串。你居中这个字符串就象你前面所做的,使用SpriteFont.MeasureString方法去检测绘制的字体的大小。
                                最后一件事你需要做的在Game1类中,取消任何power-ups,当一个关卡结束。例如,你不想某些人得到一个power-up在关卡2的结尾,然后把这个power-up持续用到关卡3的开头。
注意:为什么不能把power-up延续到下一关?好,象在游戏开发中很多事情,这是一个人的决定。也许你更喜欢把它延续下去,也许你不希望这样。在我看来,最好是取消它在两个关卡之间,这样就是我们要在这本中要做的。虽然,随意地去创建你想要的方式,你可以添加不同的你自己的或者用户自定义的power-ups。这是你的世界,你可以做任何你想要做的。
                                添加一个调用到CancelPowerUps方法在你的Game1类的ChangeGameState方法上部分:
1 CancelPowerUps( );

                                现在,让我们移动到ModelManager类中。这个类没有什么要做的在这个特定的power-up因为power-up只影响射击,这主要处理在Game1类中。但是,你仍然需要跟踪当开始一个power-up.因为这个power-up是基于不断的击杀,而击杀在ModelManager中被处理, 它的意义是把这个逻辑驻留在这个类中。
                                添加下面的类别变量到你的Modelmanager类中:

1 int consecutiveKills = 0;
2 int rapidFireKillRequirement = 3;
                                第一个变量将跟踪当前玩家连续击杀了多少个,同时第二个变量跟踪被要求的连续击杀的数量去授予这个power-up。
                                当你删除飞船从飞船表单中,是在一个飞船逃离或者一个被击落。在这两种情况下,你将会修改consecutiveKills的值的变量(当一个飞船逃离,consecutiveKills变成0,或者当一个飞船被击落,consecutiveKills被增加)
                                首先,找到models.RemoveAt调用在UpdateModels方法的结尾,它表明一个飞船已经逃离了。在同样的代码块,你设置游戏状态END与Game1.ChangeGameState方法,如果游戏结束了。
                                立即添加代码行在调用models.RemoveAt之前,在UpdateModel方法的结尾:
1 // Reset the kill count
2 consecutiveKills = 0;
                                这是另外一个地方你调用models.RemoveAt表明一个飞船已经撞击到了一个子弹。这是在UpdateShots方法的结尾。在这个块中,你同样播放爆炸的声音用Game1.PlayCue方法。立即添加下面的代码在调用播放Explosions cue之后(这行读取PlayCue("Explosions")):
1 // Update consecutive kill count
2 // and start power-up if requirement met
3    ++consecutiveKills;
4 if (consecutiveKills == rapidFireKillRequirement)
5 {
6      ((Game1)Game).StartPowerUp(Game1.PowerUps.RAPID_FIRE);
7 }
                                在每次杀死你,增加连续击杀的数量。如果计数器等于要求的数量,则获得power-up,这个power-up被获得通过Game1.StartPowerUp方法。
                                噢!您准备测试了。编译并运行该游戏。如果你的目标是足够的,你将会看到power-up被授予在你连续撞击到第三个飞船之后,如图15-6所示。

                                不错。游戏已完成!自由的添加别的power-ups,通知指标器,声音效果,同样别的想要的你想使这个游戏,使它按你想要的方式进行。
源代码:http://shiba.hpe.cn/jiaoyanzu/WULI/soft/xna.aspx?classId=4
(完)

posted on 2009-08-26 22:30  一盘散沙  阅读(466)  评论(0编辑  收藏  举报

导航