• [翻译]XNA外文博客文章精选之sixteen(中)



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

    <接上一篇>
                                                                             外来侵略者
                                                                                             Nick Gravelyn
    There's More to a Game than Just the Game
    Decisions, Decisions
                                        大多数游戏一般都不会直接进入游戏。所以有一个游戏包含主要的菜单是一个大的步骤正确的趋势。为了完成这个,让我们开始创建一个主要的菜单从头开始我们的MenuItem类,它将代表一个单一的操作,用户可以选择。
     1 public class MenuItem
     2 {
     3     public string Name;
     4     public event EventHandler Activate;
     5     public event EventHandler OptionLeft;
     6     public event EventHandler OptionRight;
     7     public bool IsDisabled = false;
     8     public MenuItem(string name)
     9     {
    10          Name = name;
    11     }
    12 }
                                        我们的MenuItem类十分的基础。它包含一个Name,一些事件被激活并且允许我们去改变选项(例如我们后面会添加的音量的大小),一个布尔值为禁止选项被选择。最后我们有一个结构去简单的创建给定名字的项目。
                                        接着,让我们添加一些方法,我们的Menu类可以用来触发这些我们创建的事件:
     1 public void PerformActivate()
     2 {
     3      if (Activate != null)
     4            Activate(thisnull);
     5 }
     6 public void PerformOptionLeft()
     7 {
     8      if (OptionLeft != null)
     9            OptionLeft(thisnull);
    10 }
    11 public void PerformOptionRight()
    12 {
    13      if (OptionRight != null)
    14            OptionRight(thisnull);
    15 }
                                         非常简单的东西在这里。我们刚才只包装了方法,这样我们的Menu可以调用这些事件来处理。
                                         现在,让我们继续去创建实际的Menu类:
     1 public class Menu
     2 {
     3       public List<MenuItem> Items = new List<MenuItem>();
     4       int currentItem = 0;
     5       public MenuItem SelectedItem
     6       {
     7             get
     8               {
     9                    if (Items.Count > 0)
    10                         return Items[currentItem];
    11                    else
    12                         return null;
    13               }
    14        }
    15 }
                                        我们的Menu包含一个菜单项目表单以及当前选择的项目的索引。我们同样揭露一个属性,它返回选择的项目,如果这里是一个或者没有,这里没有选项。
                                        接着,让我们添加一些不同的选项方法。
     1 public void SelectNext()
     2 {
     3        if (Items.Count > 0)
     4        {
     5             do
     6             {
     7                  currentItem = (currentItem + 1% Items.Count;
     8             } while (SelectedItem.IsDisabled);  
     9       }
    10 }
    11 public void SelectPrevious()
    12 {
    13        if (Items.Count > 0)
    14        {
    15             do
    16             {
    17                 currentItem--;
    18                 if (currentItem < 0)
    19                      currentItem = Items.Count - 1;
    20             } while (SelectedItem.IsDisabled);
    21       }
    22 
                                        在两个选择方法中,我们有一个do-while循环,它将循环直到我们找到一个non-disabled项目在项目表单中。这个SelectNext方法利用模操作去确保我们循环到表单的尾在到表单的头,而SelectPrevious方法使用一个标准if语句去检查他们。
                                        最后,这个菜单创建一个绘制它的方法。
     1 public void Draw(SpriteBatch spriteBatch, SpriteFont font)
     2 {
     3       spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
     4       for (int i = 0; i < Items.Count; i++)
     5       {
     6             MenuItem item = Items[i];
     7             Vector2 size = font.MeasureString(item.Name);
     8             Vector2 pos = new Vector2(spriteBatch.GraphicsDevice.Viewport.Width / 2,spriteBatch.GraphicsDevice.Viewport.Height / 2);
     9             pos -= size * .5f;
    10             pos.Y += i * (size.Y * 1.1f);
    11             Color color = Color.White;
    12             if (item == SelectedItem)
    13                 color = Color.Yellow;
    14             else if (item.IsDisabled)
    15                 color = Color.DarkSlateGray;
    16             spriteBatch.DrawString(font,item.Name,pos,color);
    17       }
    18       spriteBatch.End();
    19 }

                                        我们的绘制方法接收一个SpriteBatch和SpriteFont为了这个绘制。这将使它事实上非常简单去改变字体为你的绘制。然后循环所有的选项,绘制它们用一个垂直的表单在屏幕的中央。

    简单化的输入
                                        现在我们有一个非常不错的菜单系统为我们去使用,同时制造了我们的主要菜单和选项屏幕。但是一个大的问题我们将会面对的是,控制的输入。大多数输入是简单的检查每一祯基础上使它非常难和慢的通过菜单选项。所以,这就是我们要做的,去执行一个InputHelper类,它将使它非常容易为我们去处理输入在我们的菜单上。让我们开始它们的基础的外壳:

    1 public static class InputHelper
    2 {
    3     public static GamePadState GamePadState1;
    4     public static GamePadState GamePadState2;
    5     public static KeyboardState KeyboardState;
    6     public static GamePadState LastGamePadState1;
    7     public static GamePadState LastGamePadState2;
    8     public static KeyboardState LastKeyboardState;
    9 }

                                        你将会看见我们保存了两个状态在每个输入设备上(我们为这里第二个玩家创建数据,即使我们没有执行co-op)。这就是我们如何跟踪当前的祯的输入状态,和之前的祯的输入状态。接着,让我们开始通过创建一个方法去更新所有的我们的输入状态。

    1 public static void Update()
    2 {
    3     LastGamePadState1 = GamePadState1;
    4     LastGamePadState2 = GamePadState2; 
    5     LastKeyboardState = KeyboardState;
    6     GamePadState1 = GamePad.GetState(PlayerIndex.One);
    7     GamePadState2 = GamePad.GetState(PlayerIndex.Two);
    8     KeyboardState = Keyboard.GetState();
    9 }
                                        首先,我们保存当前的值在前面的祯变量中,然后我们poll新的输入状态。我们将继续我们的实际的帮助方法,我们将用于整个菜单。第一个方法将会告诉我们如果一个游戏pad按钮刚被压下在这祯:
    1 public static bool IsNewButtonPress(PlayerIndex index, Buttons button)
    2 {
    3     if (index == PlayerIndex.One)
    4          return (GamePadState1.IsButtonDown(button) && LastGamePadState1.IsButtonUp(button));
    5     else
    6          return (GamePadState2.IsButtonDown(button) && LastGamePadState2.IsButtonUp(button));
    7 }
                                       这个方法接收玩家的索引检查和按钮检查,然后执行一些简单的逻辑看看,这个按钮是否被压下在这一祯中,但是不是最后一祯。接着让我们写一个类似键盘的方法:
    1 public static bool IsNewKeyPress(Keys key)
    2 {
    3        return (KeyboardState.IsKeyDown(key) && LastKeyboardState.IsKeyUp(key));
    4 }
                                       同样我们刚才看见,这个键当前被压下了,但是我们不能在最后一祯压下。接着我们想要添加类似的功能为这个游戏的pad thumbsticks.目标是有一个方法,我们可以选择哪个thumbstick和哪个轴在一个指定的游戏pad上,看看thumbstick是否刚刚压下一个给定的值。在这点上我们将会使用这来允许我们的thumbstick运动,每次只能移动一个单一的选项。为了写这个方法,我们首先需要添加一对枚举到我们的项目中:
    1 public enum Thumbstick
    2 {Left,Right}
    3 public enum ThumbstickAxis
    4 {X,Y} 
                                       这里有两个枚举通过我们的方法将会被使用,简单的指定thumbstick和轴我们试图去使用的。现在让我们详细的描写它自身的方法。它相当的长,但是我们稍后会分解:
     1 public static bool DidThumbstickPassThreshold(PlayerIndex index, Thumbstick thumbstick,ThumbstickAxis axis, float threshold)
     2 {
     3      Vector2 lastThumbStick = Vector2.Zero;
     4      Vector2 newThumbStick = Vector2.Zero;
     5      if (thumbstick == Thumbstick.Left)
     6      {
     7          if (index == PlayerIndex.One)
     8          {
     9               lastThumbStick = LastGamePadState1.ThumbSticks.Left;
    10               newThumbStick = GamePadState1.ThumbSticks.Left;
    11          }
    12          else
    13          {
    14               lastThumbStick = LastGamePadState2.ThumbSticks.Left;
    15               newThumbStick = GamePadState2.ThumbSticks.Left;
    16          }
    17      }
    18      else
    19      {
    20            if (index == PlayerIndex.One)
    21            {
    22                lastThumbStick = LastGamePadState1.ThumbSticks.Right;
    23                newThumbStick = GamePadState1.ThumbSticks.Right;
    24            }
    25            else
    26            {
    27                lastThumbStick = LastGamePadState2.ThumbSticks.Right;
    28                newThumbStick = GamePadState2.ThumbSticks.Right;
    29            }
    30      }
    31      float lastValue = 0f, newValue = 0f;
    32      if (axis == ThumbstickAxis.X)
    33      {
    34            lastValue = lastThumbStick.X;
    35            newValue = newThumbStick.X;
    36      }
    37      else
    38      {
    39           lastValue = lastThumbStick.Y;
    40           newValue = newThumbStick.Y;
    41      }
    42      if (threshold < 0f)
    43           return (lastValue > threshold && newValue < threshold);
    44      else
    45           return (lastValue < threshold && newValue > threshold);
    46 }
                                       这里有很多的代码,但是幸运的是它们都是很简单的方法。让我们一部份一部分的看,确保什么东西都是有意义的。
     1 Vector2 lastThumbStick = Vector2.Zero;
     2 Vector2 newThumbStick = Vector2.Zero;
     3 if (thumbstick == Thumbstick.Left)
     4 {
     5      if (index == PlayerIndex.One)
     6      {
     7            lastThumbStick = LastGamePadState1.ThumbSticks.Left;
     8            newThumbStick = GamePadState1.ThumbSticks.Left;
     9      }
    10      else
    11      {
    12            lastThumbStick = LastGamePadState2.ThumbSticks.Left;
    13            newThumbStick = GamePadState2.ThumbSticks.Left;
    14      }
    15 }
    16 else
    17 {
    18         if (index == PlayerIndex.One)
    19         {
    20              lastThumbStick = LastGamePadState1.ThumbSticks.Right;
    21              newThumbStick = GamePadState1.ThumbSticks.Right;
    22         }
    23         else
    24         {
    25              lastThumbStick = LastGamePadState2.ThumbSticks.Right;
    26              newThumbStick = GamePadState2.ThumbSticks.Right;
    27         }
    28 }
    29 So this is the first part of the method. All we are doing is using the thumbstick and index parameters to determine that thumbstick�s value this frame and last frame. Pretty simple.
    30 float lastValue = 0f, newValue = 0f;
    31 if (axis == ThumbstickAxis.X)
    32 {
    33      lastValue = lastThumbStick.X;
    34      newValue = newThumbStick.X;
    35 
    36 }
    37 else
    38 {
    39      lastValue = lastThumbStick.Y;
    40      newValue = newThumbStick.Y;
    41 }
                                          这部分是相当的简单。我们只使用轴的参数去得到当前的和最后的想要的thumbstick值使用我们上面发现的这个Vector2s。
    1 if (threshold < 0f)
    2      return (lastValue > threshold && newValue < threshold);
    3 else
    4      return (lastValue < threshold && newValue > threshold);

                                          最后我们计算出threshold是否已经被算出,如果threshold是在lastValue和newValue之间,并且完成InputHelper类。

    Menu Base
                                          因为我们的游戏将会有两个画面,有菜单在它们上面(主菜单和选项),它的意义是创建一个基础的类去在这两个之间共享。这将阻止我们为每个写同样的代码两次类。让我们创建一个新的类叫做MenuBaseGameState:

     1 public class MenuBaseGameState : GameState
     2 {
     3      SpriteBatch spriteBatch;
     4      SpriteFont font;
     5      protected Menu Menu = new Menu();
     6      string title;
     7      public MenuBaseGameState(Game game, string title): base(game)
     8      {
     9           spriteBatch = new SpriteBatch(GraphicsDevice);
    10           font = Content.Load<SpriteFont>("Courier New");
    11           this.title = title; 
    12      }
    13 }
                                        我们的基类包含一个SpriteBatch和SpriteFont,它将会被使用去绘制菜单,这个Menu它本身,和一个标题去显示在屏幕的上部。这个结构简单的接收游戏的实例和期望的标题。
                                         现在我们将会跳到Update方法中,它将会大量使用这个InputHelper我们之前写的去处理所有的菜单控制:
    1 public override void Update(GameTime gameTime)
    2 {
    3 }
                                         首先我们添加一些逻辑,他将允许我们去激活当前的选项。我们允许玩家去使用A或者Start在游戏pad上以及Space和Enter在键盘上去激活这些选项:
    1 if (InputHelper.IsNewButtonPress(PlayerIndex.One, Buttons.A) ||InputHelper.IsNewButtonPress(PlayerIndex.One, Buttons.Start) ||InputHelper.IsNewButtonPress(PlayerIndex.Two, Buttons.A) ||InputHelper.IsNewButtonPress(PlayerIndex.Two, Buttons.Start) ||InputHelper.IsNewKeyPress(Keys.Space) ||InputHelper.IsNewKeyPress(Keys.Enter))
    2 {
    3     if (Menu.SelectedItem != null)
    4           Menu.SelectedItem.PerformActivate();
    5 }
                                         既然我已经写了这些,你可以想象一下我不得不放入了多少代码,if语句在没有InputHelper类的情况下做了同样的事情。接着我们添加逻辑去允许我们去选择下一个或者上一个选项为这个选择的项目。我们将会允许玩家去使用其中的left thumbsticks,或者DPad,这个Left和Right允许键和A和D在键盘上所有移动左和右在这个菜单上:
     1 if (InputHelper.DidThumbstickPassThreshold(PlayerIndex.One, Thumbstick.Left, ThumbstickAxis.X, -.3f) ||InputHelper.DidThumbstickPassThreshold(PlayerIndex.Two, Thumbstick.Left, ThumbstickAxis.X, -.3f) ||InputHelper.IsNewButtonPre(PlayerIndex.One, Buttons.DPadLeft) ||InputHelper.IsNewButtonPress(PlayerIndex.Two, Buttons.DPadLeft) ||nputHelper.IsNewKeyPress(Keys.Left) ||InputHelper.IsNewKeyPress(Keys.A))
     2 {
     3      if (Menu.SelectedItem != null)
     4          Menu.SelectedItem.PerformOptionLeft();
     5 }
     6 if (InputHelper.DidThumbstickPassThreshold(PlayerIndex.One, Thumbstick.Left, ThumbstickAxis.X, .3f) ||InputHelper.DidThumbstickPassThreshold(PlayerIndex.Two, Thumbstick.Left, ThumbstickAxis.X, .3f) ||InputHelper.IsNewButtonPress(PlayerIndex.One, Buttons.DPadRight) ||InputHelper.IsNewButtonPress(PlayerIndex.Two, Buttons.DPadRight) ||putHelper.IsNewKeyPress(Keys.Right) ||InputHelper.IsNewKeyPress(Keys.D))
     7 {
     8      if (Menu.SelectedItem != null)
     9          Menu.SelectedItem.PerformOptionRight();
    10 }
                                         你将会看见这两个逻辑非常长和使用了很多相同的逻辑。好处是有InputHelper类在周围。想象一下写这三个方法的内容反复在if语句中。
                                         最后,我们需要去添加逻辑到实际滚动菜单项。同样我们将会使用任何游戏pad的thumbstick或者DPad以及使用Up/Down箭头和W/S键盘键去允许玩家一个最大数量的输入选择。
    1 if (InputHelper.DidThumbstickPassThreshold(PlayerIndex.One, Thumbstick.Left, ThumbstickAxis.Y, .3f) ||InputHelper.DidThumbstickPassThreshold(PlayerIndex.Two, Thumbstick.Left, ThumbstickAxis.Y, .3f) ||InputHelper.IsNewButtonPress(PlayerIndex.One, Buttons.DPadUp) ||InputHelper.IsNewButtonPress(PlayerIndex.Two, Buttons.DPadUp) ||InputHelper.IsNewKeyPress(Keys.Up) ||InputHelper.IsNewKeyPress(Keys.W))
    2 {
    3      Menu.SelectPrevious();
    4 }
    5 if (InputHelper.DidThumbstickPassThreshold(PlayerIndex.One, Thumbstick.Left, ThumbstickAxis.Y, -.3f) ||InputHelper.DidThumbstickPassThreshold(PlayerIndex.Two, Thumbstick.Left, ThumbstickAxis.Y, -.3f) ||InputHelper.IsNewButtonPress(PlayerIndex.One, Buttons.DPadDown) ||InputHelper.IsNewButtonPress(PlayerIndex.Two, Buttons.DPadDown) ||InputHelper.IsNewKeyPress(Keys.Down) ||InputHelper.IsNewKeyPress(Keys.S))
    6 {
    7      Menu.SelectNext();
    8 }
                                         这里有大量的代码。但是现在我们有它在一个基础的类中,我们可以很容易的重新在使用它为所有的菜单,我们游戏也许需要的菜单。最后MenuBaseGameState是Draw方法去绘制菜单的标题和菜单的选项:
    1 public override void Draw(GameTime gameTime)
    2 {
    3     spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
    4     Vector2 titlePos = new Vector2(GraphicsDevice.Viewport.Width / 2, 100f);
    5     titlePos -= font.MeasureString(title) * .5f;
    6     spriteBatch.DrawString(font,title,titlePos,Color.White);
    7     spriteBatch.End();
    8     Menu.Draw(spriteBatch, font);
    9 }

                                        同样我们做相同的计算标题到屏幕的中心,然后我们使用Menu.Draw方法去绘制菜单和它的选择到屏幕上。现在我们可以继续去创建我们的红色菜单游戏状态。

    Main Destination
                                        我们的MainMenuGameState将会让玩家可以选择开始任何一个单一的玩家或者co-op游戏,移动到选项画面或者退出游戏,让我们开始通过创建一个MainMenuGameState,它将扩展这个MenuBaseGameState.我们同样将会填满这个结构去为我们创建所有的菜单选择。

     1 public class MainMenuGameState : MenuBaseGameState
     2 {
     3     public MainMenuGameState(Game game, string title): base(game, title)
     4     {
     5         MenuItem item;
     6         item = new MenuItem("Single Player");
     7         item.Activate += SinglePlayerActivated;
     8         Menu.Items.Add(item);
     9         item = new MenuItem("Co-Op");
    10         item.Activate += CoopActivated;
    11         Menu.Items.Add(item);
    12         item = new MenuItem("Options");
    13         item.Activate += OptionsActivated;
    14         Menu.Items.Add(item);
    15         item = new MenuItem("Quit");
    16         item.Activate += QuitActivated;
    17         Menu.Items.Add(item);
    18     }
    19 }

                                           你将会看见我们创建所有我们的四个菜单选项和挂起事件处理到某些方法中。接着让我们添加所有的处理这些选项的方法:

     1 public void SinglePlayerActivated(object sender, EventArgs args)
     2 {}
     3 public void CoopActivated(object sender, EventArgs args)
     4 {}
     5 public void OptionsActivated(object sender, EventArgs args)
     6 {}
     7 public void QuitActivated(object sender, EventArgs args)
     8 {
     9      Game.Exit(); 
    10 }

                                          现在我们只一个填写是我们的QuitActivated方法,我们将会回到那里并且填写它剩下的部分。
                                         接着让我们添加我们的Update方法。这个Update方法允许我们退出游戏通过压在游戏Pad上的Back键或者键盘上的Escape键。我们同样必须非常肯定去调用base.Update在某时候去更新所有我们的菜单逻辑在MenuBaseGameState里找。

    1 public override void Update(GameTime gameTime)
    2 {
    3      if (InputHelper.IsNewButtonPress(PlayerIndex.One, Buttons.Back) ||InputHelper.IsNewButtonPress(PlayerIndex.Two, Buttons.Back) ||InputHelper.IsNewKeyPress(Keys.Escape))
    4      {
    5           Game.Exit();
    6      }
    7      base.Update(gameTime);
    8 }
                                         现在,让我们制造一些变化在Game1.cs文件里,看看我们创建了什么。第一个变化是去添加这个新的MainMenuGameState到状态管理中,并且确保初始化游戏的状态是设置到主菜单中,在此时我们的游戏的初始化方法应该看起来象这样:
     1 protected override void Initialize()
     2 {
     3      base.Initialize();
     4      stateManager.GameStates.Add(AAGameState.Playing, new PlayingGameState(this));
     5      EndPlayingGameState epgs = new EndPlayingGameState(this);
     6      stateManager.GameStates.Add(AAGameState.Win, epgs);
     7      stateManager.GameStates.Add(AAGameState.Lose, epgs);
     8      stateManager.GameStates.Add(AAGameState.MainMenu, new MainMenuGameState(this"Alien Aggressors!"));
     9      stateManager.CurrentState = AAGameState.MainMenu;
    10 }
                                        接着,让我们产生一些变化在游戏的Update方法中,我们想要删除这个允许我们从这个Update方法中退出的逻辑,因为我们有同样感到功能在游戏状态的主菜单中。我们同样需要去调用我们的InputHelper.Update方法去保证我们总是有正确的输入。这将减少我们的Update方法象这样:
    1 protected override void Update(GameTime gameTime)
    2 {
    3     InputHelper.Update();
    4     base.Update(gameTime);
    5 }

                                        现在你可以运行你的游戏了,可以看看你辛苦工作的成果。你将会看见我们的主菜单,你可以使用游戏pads以及键盘来浏览。你可以选择Quit选项和激活它去退出这个游戏。接着让我们配合我们的MainMenuGameState在我们的PlayingGameState,这样我们可以开始我们的新游戏。

    Starting Something
                                        现在我们有一个菜单,我们需要允许它开始我们的游戏。为了开始这个,首先我们首先需要添加重置PlayingGameState的能力.这将允许我们开始多个游戏,不用退出游戏。让我们开始通过分解一些PlayingGameState的结构成一个新的方法叫做Initialize:

     1 public PlayingGameState(Game game): base(game)
     2 {
     3      Bullet.Texture = Content.Load<Texture2D>("bullet");
     4      Bullet.Origin = new Vector2(Bullet.Texture.Width / 2, Bullet.Texture.Height / 2);
     5      spriteBatch = new SpriteBatch(GraphicsDevice);
     6 }
     7 public void Initialize()
     8 {
     9      player1 = new PlayerShip(Content.Load<Texture2D>("player1"),PlayerIndex.One);
    10      player1.Position = new Vector2(GraphicsDevice.Viewport.Width / 2 - player1.Bounds.Width / 2,GraphicsDevice.Viewport.Height - player1.Bounds.Height);
    11      playerBullets.Clear();
    12      alienBullets.Clear();
    13      alienGrid.Initialize(Content.Load<Texture2D>("alien"), 105);
    14 }
                                       现在我们可以调用这个Initialize方法只要你想重新设置我们的游戏到默认的状态。接着我们可以添加代码在我们的主要的菜单事件处理为了开始一个单一玩家的游戏,这将设置好所有东西并且开始我们游戏:
    1 public void SinglePlayerActivated(object sender, EventArgs args)
    2 {
    3       (Manager.GameStates[AAGameState.Playing] as PlayingGameState).Initialize();
    4       Manager.CurrentState = AAGameState.Playing;
    5 }

                                       所有我们要做的是得到PlayingGameState从这个管理类中,并且调用Initialize方法。最后我们改变管理类的CurrentState为Playing状态,这时你可以能够开始一个游戏通过激活这个Single Player菜单选项在这个主菜单中。

    Do Overs
                                      现在我们有一个非常不错的菜单开始我们的游戏。让我们修补下我们的EndPlayingGameState允许一旦一个游戏完成,我们的回到菜单中。为了实现这个我们只需要去添加一些代码到Update方法中:

    1 public override void Update(GameTime gameTime)
    2 
    3      if (InputHelper.IsNewButtonPress(PlayerIndex.One, Buttons.A) ||InputHelper.IsNewButtonPress(PlayerIndex.One, Buttons.Start) ||InputHelper.IsNewButtonPress(PlayerIndex.Two, Buttons.A) ||InputHelper.IsNewButtonPress(PlayerIndex.Two, Buttons.Start) ||InputHelper.IsNewKeyPress(Keys.Space) ||InputHelper.IsNewKeyPress(Keys.Enter))
    4      {
    5            Manager.CurrentState = AAGameState.MainMenu;
    6      }
    7 }
                                      所有我们要做的是检查A和Start游戏的Pad按钮以及Enter和Space键盘键。接着让我们更新这个Draw代码去写一些说明,这样让玩家知道屏幕上做了什么。首先我们只产生字符串并且布置它。
    1 string info = "Press A, Start, Enter, or Space to continue";
    2 Vector2 halfInfoSize = spriteFont.MeasureString(info) / 2;
    3 Vector2 infoPos = centerScreen - halfInfoSize + new Vector2(0f, 100f);
                                      然后我们可以绘制它到我们的屏幕上:
    1 spriteBatch.DrawString(spriteFont,info,infoPos,Color.White);

                                      我们添加一些新变量包括信息字符串并且布置它在屏幕上,在我们先前绘制的结果字符串的下面一点。现在你可以玩这个游戏,你想玩多少次就玩多少次,每次不需要重新开始整个程序。


    Fleshing out the Rest of the Game(充实剩下的游戏代码)

    Bring a Friend
                                      接着,让我们添加第二个玩家。这是全部一个非常简单的过程,但是将允许我们有更多的乐趣在我们的游戏中。首先确保你已经添加了player2.png精灵到你的游戏的内容项目中,然后让我们head over到PlayingGameState并且添加另外的玩家到我们的类的数据中:

    1 PlayerShip player2;
                                      我们同样想要一个布尔值表示我们是否正在做一个co-op游戏。这将让我们使用相同的类为这个单一的玩家并且co-op游戏非常简单:
    1 bool coOp = false;
                                      现在我们需要制造一些改变到这个Initialize方法,这样我们可以告诉状态是否是我们想要的co-op游戏并且同样,它可以创建第二个玩家:
     1 public void Initialize(bool isCoOp)
     2 {
     3      player1 = new PlayerShip(Content.Load<Texture2D>("player1"),PlayerIndex.One);
     4      player1.Position = new Vector2(GraphicsDevice.Viewport.Width / 2 - player1.Bounds.Width / 2,GraphicsDevice.Viewport.Height - player1.Bounds.Height);
     5      coOp = isCoOp;
     6      player2 = new PlayerShip(Content.Load<Texture2D>("player2"),PlayerIndex.Two);
     7      player2.Position = new Vector2(GraphicsDevice.Viewport.Width / 2 - player2.Bounds.Width / 2,GraphicsDevice.Viewport.Height - player2.Bounds.Height);
     8      playerBullets.Clear();
     9      alienBullets.Clear();
    10      alienGrid.Initialize(Content.Load<Texture2D>("alien"), 105);
    11 }
                                      所有我们准备做的是复制我们的玩家1创建的代码,保存这个isCoOp值,它被传入这个方法中。接着让我们添加更新代码去更新新的玩家在我们的Update方法中:
    1 if (coOp && player2.IsAlive) 
    2      player2.Update(gameTime, playerBullets, GraphicsDevice.Viewport.Width);
                                      现在我们必须更新我们的foreach循环,它处理alienBullets去考虑我们第二个玩家:
     1 foreach (Bullet b in alienBullets)
     2 {
     3      b.Update();
     4      if (!screenRect.Intersects(b.Bounds))
     5            bulletsToRemove.Add(b);
     6      else if (player1.IsAlive && player1.CollideBullet(b))
     7      {
     8            bulletsToRemove.Add(b);
     9            player1.ExtraLives--;
    10            player1.Position.X = player1.Bounds.Width / 2
    11            CheckPlayerLives(); 
    12      }
    13      else if (coOp && player2.IsAlive && player2.CollideBullet(b))
    14      {
    15            bulletsToRemove.Add(b);
    16            player2.ExtraLives--;
    17            player2.Position.X = player2.Bounds.Width / 2
    18            CheckPlayerLives(); 
    19      }
    20 }
                                      首先注意新的else if语句去处理第二个玩家的碰撞。其他的改变游戏的Lost(丢失)状态的情况下。你会看见我们用一个调用CheckPlayerLives的新方法来替换旧的逻辑。这个新方法被用来检查我们玩家的是否活着:
    1 private void CheckPlayerLives()
    2 {
    3     if (coOp && !player1.IsAlive && !player2.IsAlive)
    4           Manager.CurrentState = AAGameState.Lose;
    5     else if (!coOp && !player1.IsAlive)
    6           Manager.CurrentState = AAGameState.Lose;
    7 }
                                     我检查我们是否玩的是一个co-op游戏,并且使用它去检测我们是否需要检查两个玩家的IsAlive属性。
                                     接着,我们将会更新draw代码为我们的玩家去确保他们不能绘制,当死了并确保玩家2被绘制:
    1 if (player1.IsAlive)
    2     player1.Draw(spriteBatch);
    3 if (coOp && player2.IsAlive)
    4     player2.Draw(spriteBatch);
                                    这个PlayingGameState现在是一个完整的功能为我们的第二个玩家。我们只需要返回并且更新我们的MainMenuGameState去处理新的Initialize方法,和让我们开始一个co-op游戏:
     1 public void SinglePlayerActivated(object sender, EventArgs args)
     2 {
     3      (Manager.GameStates[AAGameState.Playing] as PlayingGameState).Initialize(false);
     4      Manager.CurrentState = AAGameState.Playing;
     5 }
     6 public void CoopActivated(object sender, EventArgs args)
     7 {
     8      (Manager.GameStates[AAGameState.Playing] as PlayingGameState).Initialize(true);
     9      Manager.CurrentState = AAGameState.Playing;
    10 }

                                    现在运行这个游戏,你可以开始一个co-op的游戏和你的朋友。


    Keeping Score

                                    让我们完成游戏的一部份通过添加一个分数到这个游戏中以及显示每个玩家剩下的额外生命。首先让我们添加一个新的变量到PlayingGameState去保存我们当前的分数以及SpriteFont为了绘制它。

    1 int score = 0
    2 SpriteFont spriteFont;
                                    让我们首先添加这些代码去加在字体到我们的结构中:
    1 spriteFont = Content.Load<SpriteFont>("Courier New");
                                    接着,让我们更新我们的Initialize方法去确保每一次新游戏开始我们的分数为0,
    1 public void Initialize(bool isCoOp, bool newGame)
    2 {
    3     //
    4     if (newGame)
    5     {
    6         score = 0;
    7     }
    8 }
                                    由于我们为Initialize改变了方法的签名,确保通过和更新调用这个方法。任何调用从我们的MainMenuGameState应该传入ture,然而任何从PlayingGameState的调用应该传入false.接着我们可以head down到Update方法中,并且在每一次外星飞船被一个子弹击中时奖励分数:
    1 foreach (Bullet b in playerBullets)
    2 {
    3     //
    4     else if (alienGrid.CollideBullet(b))
    5     {
    6         //
    7         score += 100;
    8     }
    9 }
                                    在我们的情况下,我们选择去奖励100点每一次一个外星飞船被其中一个玩家的子弹击中。接着让我们head down到我们的Draw方法中并且绘制这个到玩家可以看见的地方:
    1 spriteBatch.DrawString(spriteFont,"Score: " + score.ToString(),new Vector2(10f),Color.White);
                                    接着,我们想要去绘制一些每个玩家的生命,在屏幕的右上角的分数。我们可以通过使用一对简单的循环实现它:
     1 for (int i = 1; i <= player1.ExtraLives; i++)
     2 {
     3     spriteBatch.Draw(player1.Texture,new Rectangle(GraphicsDevice.Viewport.Width - (player1.Texture.Width * i),10,25,25),Color.White);
     4 }
     5 if (coOp)
     6 {
     7     for (int i = 1; i <= player2.ExtraLives; i++)
     8     {
     9          spriteBatch.Draw(player2.Texture,new Rectangle(GraphicsDevice.Viewport.Width - (player2.Texture.Width * i),35,25,25),Color.White);
    10     }
    11 }
                                    这个绘制每条生命在左上角。我们只会绘制生命为玩家2如果我们在一个co-op游戏里。现在我们可以运行这个游戏,看看一个分数去表示我们做的怎么样。让我们接续通过显示最终的分数在我们的EndPlayingGameState.让我们添加一个新的变量到EndPlayingGameState类中:
    1 public int FinalScore = 0;
                                    接着,我们需要去更新我们的Draw方法去产生必须的数据为绘制分数到屏幕上,就象我们前面为信息做的那样:
    1 string score = "Final Score: " + FinalScore.ToString();
    2 Vector2 halfScoreSize = spriteFont.MeasureString(score) / 2
    3 Vector2 scorePos = centerScreen - halfScoreSize - new Vector2(0f, 100f);
                                   然后,我们只绘制出象前面的文本:
    1 spriteBatch.DrawString(spriteFont,score,scorePos,Color.White);
                                   最后一件事,我们需要去确保分数被设置当游戏结束时。为了实现这个我们必须更新一些地方。首先让我们更新foreach循环,它通过playerBullets和添加必须的代码去设置我们这里的最终的分数:
     1 foreach (Bullet b in playerBullets)
     2 {
     3     //
     4     else if (alienGrid.CollideBullet(b))
     5     {
     6         //
     7         if (alienGrid.Count == 0)
     8         { 
     9              GameState winState = Manager.GameStates[AAGameState.Win];
    10             (winState as EndPlayingGameState).FinalScore = score; Manager.CurrentState = AAGameState.Win;
    11         }
    12     }
    13 
                                 接着,我们需要更新我们的CheckPlayersLives方法去做些相同的事情。我们可以简单化这个通过添加这些代码块到方法的结尾:
    1 if (Manager.CurrentState == AAGameState.Lose)
    2 {
    3     GameState loseState = Manager.GameStates[AAGameState.Lose];
    4        (loseState as EndPlayingGameState).FinalScore = score;
    5 }

                                 完成后我们现在看看在游戏完成后,我们的游戏做的如何。


    Waves of Fleets

                                 我们的游戏已经接近完成。所有我们需要添加的是支持多关卡在这个游戏中。毕竟它只有一个波浪的敌人去射击是相当乏味的。让我们创建一个系统,它给我们的游戏多个关卡,你几乎任何的限制。现在我们将会限制我们10。为了开始让我们转倒PlayingGameState类并且添加这两个新的变量:

    1 int level = 1;
    2 const int maxLevel = 10;
                                 接着我们需要添加支持关卡到我们的Initialize方法,这样我们的外星飞船grid(组)被正常设置基于关卡并且同样确保我们每一次重新设置关卡。
     1 public void Initialize(bool isCoOp, bool newGame)
     2 {
     3    //
     4    if (newGame)
     5    {
     6       score = 0;
     7       level = 1;
     8    }
     9    alienGrid.Initialize(Content.Load<Texture2D>("alien"), 10 + (level / 3), 5 + (level / 2));
    10 }

                                 现在,外星飞船gird(组)的大小是基于关卡,如每第三关添加另外的列和其他关添加一行的外星飞船。
                                 接着,让我们添加逻辑去允许我们取得进展。我们实现这个通过添加更多的代码到我们的foreach循环迭代整个playerBullets:

     1 foreach (Bullet b in playerBullets)
     2 {
     3     //
     4     else if (alienGrid.CollideBullet(b))
     5     {
     6          //
     7          if (alienGrid.Count == 0)
     8          {
     9               if (level == maxLevel)
    10               {
    11                    GameState winState = Manager.GameStates[AAGameState.Win];
    12                    (winState as EndPlayingGameState).FinalScore = score;
    13                    Manager.CurrentState = AAGameState.Win;
    14               }
    15               else
    16               {
    17                   level++;
    18                   Initialize(coOp); 
    19                   return;
    20               }
    21          }
    22     }
    23 }
                                现在每一次外星grid是空的,我们检查哪关我们在。如果我们在最后一关,意思我们已经赢了,所以我们执行我们的end-game逻辑就象正常的。如果我们不是在最后一关,我们增加关卡并且再一次运行Initialize方法,使用我们的coOp值作为参数。我们必须确保调用return,直到我们的修改playerBullets集合在Initialize方法中。如果我们忘记这个return语句,我们的游戏将抛出一个异常。
                                 我们的游戏现在有10关,然后它是非常生硬的在他们之间转换。我们可以修正这个通过添加一个新的类来作为过渡在这些关之间。让我们添加一个TransitionGameState到我们的游戏项目中:
     1 public class TransitionGameState : GameState
     2 {
     3     SpriteBatch spriteBatch;
     4     SpriteFont font; 
     5     public string Caption = string.Empty;
     6     float transitionTimer = 0f;
     7     const float transitionLength = 2f;
     8     public TransitionGameState(Game game): base(game)
     9     {
    10           spriteBatch = new SpriteBatch(GraphicsDevice);
    11           font = Content.Load<SpriteFont>("Courier New");
    12     }
    13 }
                                 我们保存类的内部的一个SpriteBatch和SpriteFont为了绘制以及一个Caption字符串去绘制到屏幕上。我们同样保存两个浮点型的值,我们将使用它记时这个状态持续了多久。这将确保我们的状态只保持在屏幕上,设置一定数量的时间(当前是2秒)。
                                 现在让我们创建Update方法去处理我们的记时器逻辑:
    1 public override void Update(GameTime gameTime)
    2 {
    3    transitionTimer += (float)gameTime.ElapsedGameTime.TotalSeconds;
    4    if (transitionTimer >= transitionLength) 
    5    {
    6        transitionTimer = 0f;
    7        Manager.CurrentState = AAGameState.Playing;
    8    }
    9 }
                                 接着我们必须使Draw命令去绘制我们的标题到屏幕上:
    1 public override void Draw(GameTime gameTime)
    2 {
    3      spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
    4      Vector2 pos = new Vector2(GraphicsDevice.Viewport.Width / 2,GraphicsDevice.Viewport.Height / 2);
    5      pos -= font.MeasureString(Caption) * .5f;
    6      spriteBatch.DrawString(font, Caption, pos, Color.White);
    7      spriteBatch.End();
    8 }
                                 接着让我们更新我们的主要彩旦事件处理,使用新的过渡游戏状态,而不是直接跳入游戏中:
     1 public void SinglePlayerActivated(object sender, EventArgs args)
     2 {
     3      (Manager.GameStates[AAGameState.Playing] as PlayingGameState).Initialize(false); 
     4      (Manager.GameStates[AAGameState.LevelTransition] as TransitionGameState).Caption = "Level 1";
     5      Manager.CurrentState = AAGameState.LevelTransition;
     6 }
     7 public void CoopActivated(object sender, EventArgs args)
     8 {
     9      (Manager.GameStates[AAGameState.Playing] as PlayingGameState).Initialize(true); 
    10      (Manager.GameStates[AAGameState.LevelTransition] as TransitionGameState).Caption = "Level 1";
    11      Manager.CurrentState = AAGameState.LevelTransition;
    12 }
                                 这里的主要的不同是,我们设置Manager.CurrentState到LevelTransation而不是Playing.我们同样设置过渡的标题状态是Level1.
                                 接着我们需要更新我们的PlayingGameState去使用这个过渡在关卡之间。这将使游戏非常容易理解,通过允许玩家级时去准备为一个新的一波敌人。让我们转到我们更新我们的关卡并且计划它去利用这个过渡游戏状态:
    1 level++;
    2 Initialize(coOp, false);
    3 GameState transState = Manager.GameStates[AAGameState.LevelTransition];
    4 (transState as TransitionGameState).Caption = "Level " + level.ToString();
    5 Manager.CurrentState = AAGameState.LevelTransition;
                                 现在我们得到一个非常不错的暂停在所有的关卡之间去给玩家几秒休息。新的一波外星飞船马上就要出现了。
                                 最后让我们到Game1.Initialize方法并且创建这个过渡对象并且把它放到manager里。
    1 stateManager.GameStates.Add(AAGameState.LevelTransition, new TransitionGameState(this));
                                 现在你可以运行游戏看看过渡的工作。当你开始一个游戏得到Level1的显示,每一次你完成一关你被告诉哪一关将要开始。这非常的棒对于这个游戏来说。接着我们将继续polish(译者:使游戏精良)通过添加声音效果在我们的游戏里。

     

     Make Some Noise
                                 我们的游戏接近完成,但是我们失去了经历的大部分,它来自游戏:声音。我们当前有一个十分安静的太空战争,而可能是准确的科学,难道不是十分有趣吗。现在让我们添加些声音到这里面。作为说明,本节将涵盖如何去处理我们在游戏代码中的声音。有关创建声音的效果和设置XACT项目。请看Appendix B。
                                 第一步添加音频去确保我们有必须做这个的文件。我们需要开始通过添加所有文件从Audio文件夹找到AlienAggressorsContent.zip文件夹到我们的Content文件夹。这是文件夹包含我们的WAV文件和XACT项目文件需要通过我们的游戏。在这点上,你的内容目录应该包含所有的这些文件。

                                  接着进入你的游戏中的Content项目,右击,并选择Add Existing选项并且添加XACT项目文件到内容中。你需要做这个,或者想要,去添加他们的WAV文件;他们引用XACT项目,这样,我们需要他们在这个项目的目录中,我们不想要他们在游戏的项目中。我们不想要它们在游戏项目中。
                                  现在,我们有声音内容块,让我们开始在我们的支持的声音里计划下。我们将会实现这个通过创造一个SoundManager类,它将包含并控制所有的声音数据为我们的游戏:

    1 public static class SoundManager
    2 {
    3     static AudioEngine engine;
    4     static WaveBank waveBank;
    5     static SoundBank soundBank;
    6     static Cue music;
    7 }

                                  我们的管理只包含三段音频系统(一个AuidoEngine,WaveBank,和SoundBank)以及一个Cue为我们的背景音乐。
                                 接着,让我们创建一个Initialize方法,它将加载所有的这些文件为我们:

    1 public static void Initialize(Game game, string engineFile, string waveBankFile, string soundBankFile)
    2 {
    3      string contentDir = game.Content.RootDirectory + "/";
    4      engine = new AudioEngine(contentDir + engineFile);
    5      waveBank = new WaveBank(engine, contentDir + waveBankFile);
    6      soundBank = new SoundBank(engine, contentDir + soundBankFile);
    7 }

                                  我们有用户传入到Game 实例中以及我们需要加载的但个文件的名字。我们传入到Game中,这样我们可以得到游戏的内容根目录。这将允许我们去改变内容项目的根目录,每一次不用必须去改变所有三个文件路径。
                                 现在,让我们添加一个方法去播放我们其中之一个的声音。

    1 public static void PlayCue(string name)
    2 {
    3      soundBank.PlayCue(name);
    4 }

                                  它非常的简单正如你看见的,我们刚才调用了PlayCue在soundBand并且传入了相同的声音,我们想要播放的。接着让我们添加一些方法去控制我们的背景音乐:

     1 public static void PlayMusic(string name)
     2 {
     3     music = soundBank.GetCue(name);
     4     music.Play();
     5 }
     6 public static void StopMusic()
     7 {
     8     if (music != null && music.IsPlaying)
     9     {
    10         music.Stop(AudioStopOptions.Immediate);
    11         music = null;
    12     }
    13 }

                                  这一次,我们使用GetCue去得到一个涉及到我们想要的背景音乐。然后我们调用Play在这个Cue它本身上。我们实现这个,所以在StopMusic我们能够去停止音乐。我们想要确保这个音乐是non-null和IsPlaying在我们停止它之前并且设置它为空。
                                 现在让我们添加一对方法去控制我们的声音的音量。

    1 public static void SetSoundFXVolume(float volume)
    2 {
    3       engine.GetCategory("SoundFx").SetVolume(volume);
    4 }
    5 public static void SetMusicVolume(float volume)
    6 {
    7       engine.GetCategory("Music").SetVolume(volume);
    8 
                                 这个音量值我们传入的应该在[0,1]的范围,0是减弱,1是满音量,在XACT里这是默认的。虽然SetVolume方法允许值大于1在这种情况下,我们想要增加音量超过我们在XACT定义的。
                                 最后的方法我们需要写的是一个Update方法,它将更新声音系统和确保我们的声音能正确的播放:
    1 public static void Update()
    2 {
    3     engine.Update();
    4 }
                                 再次,这个方法只封装了引擎的Update方法。
                                 为了把它溶入我们的游戏中,我们首先需要确保我们调用SoundManager.Initialize在我们的Game1.Initialize方法中:
    1 SoundManager.Initialize(this"AlienAggressors.xgs""AlienAggressors.xwb""AlienAggressors.xsb");
                                 接着让我们更新我们的Game1.Update方法去调用我们的SoundManger.Update确保我们调用这个方法在每一祯:
    1 protected override void Update(GameTime gameTime)
    2 {
    3    InputHelper.Update();
    4    SoundManager.Update();
    5    base.Update(gameTime);
    6 }
                                 现在让我们开始这个SoundManager去播放一些声音在我们的游戏中,首先让我们开始到Ship类中,并且使它播放一个声音,每一次飞船发射。为了实现这个我们添加一个调用到SoundManager.PlayCue在我们的Fire方法中:
     1 public void Fire(List<Bullet> bullets)
     2 {
     3     if (fireTimer <= 0f)
     4     {
     5         fireTimer = fireRate;
     6         Bullet b = new Bullet();
     7         b.Position = Position + BulletOrigin;
     8         b.Velocity = BulletVelocity;
     9         b.Color = BulletColor;
    10         bullets.Add(b); 
    11         SoundManager.PlayCue("laser"); 
    12     }
    13 }
                                  现在所有的飞船在屏幕上,我们将播放Laser声音,当他们发射。现在让我们添加到这个爆炸声音中,每一次一个飞船被撞击。我们可以做这个通过更新CollideBullet方法在Ship类中:
    1 public bool CollideBullet(Bullet b)
    2 {
    3      bool hit = Bounds.Contains((int)b.Position.X, (int)b.Position.Y);
    4      if (hit)
    5          SoundManager.PlayCue("explode");
    6      return hit;
    7 }
                                  让我们继续通过添加一些音频反馈到我们的菜单上,让我们打开我们的MenuBaseGameState和考虑下Update方法。我们想要去添加这行在每一个if语句的内部在这里面,这样任何的动作将导致它被播放:
    1 SoundManager.PlayCue("bwoop"); 

                                  最后,让我们播放背景音乐为这个游戏。让我们转到我们的Game1.Initialize语句中,并且添加这个调用到SoundManager.PlayMusic:

    1 SoundManager.PlayMusic("tear_it_down");

                                  现在如果我们运行这个游戏我们发现所有的行为创建声音的反馈和我们有一些fase-paced背景音乐。

    Everyone Loves Options
                                  现在,我们有了音频在我们的游戏中,让我们添加在选项中为这个游戏,这样玩家可以选择他们想要音频如何大声。并且同样他们是否要全屏幕。让我们开始通过添加一个新的类叫做OptionsGameState,它来自我们的MenuBaseGameState类:

     1 public class OptionsGameState : MenuBaseGameState
     2 {
     3    int soundFxVolume = 100;
     4    int musicVolume = 100;
     5    MenuItem soundFxItem;
     6    MenuItem musicItem;
     7    public OptionsGameState(Game game): base(game, "Options")
     8    {
     9    }
    10 }
                                  我们的状态保存一对整型值,它代表音量,我们将显示它在屏幕上。我们同样保存一对相关的soundFxItem和musicItem,我们将使用它更新这个文本,只要音量改变了。接着让我们填入这个结构去创建我们想要的选项:
     1 soundFxItem = new MenuItem("Sound FX Volume: 100%");
     2 soundFxItem.OptionLeft += SoundFxOptionLeft;
     3 soundFxItem.OptionRight += SoundFxOptionRight;
     4 Menu.Items.Add(soundFxItem);
     5 musicItem = new MenuItem("Music Volume: 100%");
     6 musicItem.OptionLeft += MusicOptionLeft;
     7 musicItem.OptionRight += MusicOptionRight;
     8 Menu.Items.Add(musicItem);
     9 MenuItem item = new MenuItem("Toggle Fullscreen");
    10 item.Activate += FullScreenActivate;
    11 Menu.Items.Add(item);
    12 item = new MenuItem("");
    13 item.IsDisabled = true;
    14 Menu.Items.Add(item);
    15 item = new MenuItem("Done");
    16 item.Activate += DoneActivated;
    17 Menu.Items.Add(item);
                                  这与我们在MainMenuGameState里做的类似。一个很酷的事情,我们要创建一个空的菜单,在ToggleFullscreen和Done选项之间。通过给它一个空的字符串并且指定它是禁用的,我们从本质上添加一个空的空间在我们的菜单里,它将帮助使这些事情很容易去读。接着让我们继续创建所有的我们使用的事件处理。让我们开始这个SoundFxOptionLeft和SoundFxOptionRight:
     1 private void SoundFxOptionLeft(object sender, EventArgs e)
     2 {
     3     soundFxVolume = (int)Math.Max(soundFxVolume - 100);
     4     soundFxItem.Name = string.Format("Sound FX Volume: {0}%", soundFxVolume);
     5     SoundManager.SetSoundFXVolume(soundFxVolume / 100f);
     6 }
     7 private void SoundFxOptionRight(object sender, EventArgs e)
     8 {
     9     soundFxVolume = (int)Math.Min(soundFxVolume + 10100);
    10     soundFxItem.Name = string.Format("Sound FX Volume: {0}%", soundFxVolume);
    11     SoundManager.SetSoundFXVolume(soundFxVolume / 100f);
    12 }
                                  在这些方法中,我们简单的调整我们的soundFxVolume变量,同时确保它保持在0和100之间。我们然后更新这个菜单选项的名字映射新的值。最后我们调用SoundManger.SetSoundFxVolume方法去更新这个音量在音频系统里。我们划分我们的音量通过100,这样我们在0-1的范围中为这个方法的输入:
                                  接着,我们做这个音乐音量在某种意义上:
     1 private void MusicOptionLeft(object sender, EventArgs e)
     2 {
     3     musicVolume = (int)Math.Max(musicVolume - 100);
     4     musicItem.Name = string.Format("Music Volume: {0}%", musicVolume);
     5     SoundManager.SetMusicVolume((float)musicVolume / 100f);
     6 }
     7 private void MusicOptionRight(object sender, EventArgs e)
     8 {
     9     musicVolume = (int)Math.Min(musicVolume + 10100);
    10     musicItem.Name = string.Format("Music Volume: {0}%", musicVolume);
    11     SoundManager.SetMusicVolume((float)musicVolume / 100f);
    12 }
                                   接着我们需要添加toggle全屏处理。它很简单调用GraphicsManager.ToggleFullscreen方法:
    1 private void FullScreenActivate(object sender, EventArgs e)
    2 {
    3     GraphicsManager.ToggleFullScreen();
    4 }
                                   最后我们添加事件处理为我们的Done按钮,它把我们返回到主菜单中:
    1 private void DoneActivated(object sender, EventArgs e)
    2 {
    3     Manager.CurrentState = AAGameState.MainMenu;
    4 }
                                   现在我们事件处理已经被设置好,让我们创建一个Update方法去添加一些可以用的到这个游戏,而不是要求用户压下Done每一次。我们允许他们同样使用Back或者B游戏pad按钮以及Escape键盘键去返回到主菜单选项:
    1 public override void Update(GameTime gameTime)
    2 {
    3     if (InputHelper.IsNewButtonPress(PlayerIndex.One, Buttons.Back) ||InputHelper.IsNewButtonPress(PlayerIndex.Two, Buttons.Back) ||InputHelper.IsNewButtonPress(PlayerIndex.One, Buttons.B) ||InputHelper.IsNewButtonPress(PlayerIndex.Two, Buttons.B) ||InputHelper.IsNewKeyPress(Keys.Escape))
    4     { 
    5         DoneActivated(nullnull);
    6     }
    7     base.Update(gameTime);
    8 }
                                  为什么我门调用DoneActivated手动而不是改变状态?它让我们改变行为留下选项状态不用去更新两个地方。
                                  现在我们的类被设立,让我们添加代码在Game1.Initialize去为我们创建新的状态:
    1 stateManager.GameStates.Add(AAGameState.Options,new OptionsGameState(this));
                                  接着我们只要添加这个代码到我们的MainMenuGameState的OptionsActivated事件处理去选择游戏的状态:
    1 public void OptionsActivated(object sender, EventArgs args)
    2 {
    3      Manager.CurrentState = AAGameState.Options;
    4 }
                                   现在,我们可以运行游戏并且使用一个屏幕选项去改变我们的音频音量水平并且toggle和切换出全屏。

     Take a Break
                                   最后一项,我们想做的是允许我们的游戏可以被暂停。这是一个很好的方式实现这个,当你正在游戏的中间时(译者:打游戏时,想上厕所),电话响了后者可能你的晚饭开始了。为了实现暂停功能系统,我们将创建一个新的PausedGameState
    类,这样扩展MenuBaseGameState类:

     1 public class PausedGameState : MenuBaseGameState
     2 {
     3       public PausedGameState(Game game): base(game, "Paused")
     4       {
     5              MenuItem item = new MenuItem("Continue");
     6              item.Activate += ContinueActivated;
     7              Menu.Items.Add(item);
     8              item = new MenuItem("Quit To Main Menu");
     9              item.Activate += QuitActivated;
    10              Menu.Items.Add(item);
    11        }
    12        private void ContinueActivated(object sender, EventArgs e)
    13        {
    14             Manager.CurrentState = AAGameState.Playing;
    15        }
    16        private void QuitActivated(object sender, EventArgs e)
    17        {
    18             Manager.CurrentState = AAGameState.MainMenu;
    19        }
    20 }
                                   这个简单的类只显示一个菜单允许玩家去重新开始这个游戏或者去退出主菜单。我们将同样添加一个简单的Update方法允许玩家压下Back或者B在一个游戏Pad或者Escape在键盘去返回到这个游戏:
    1 public override void Update(GameTime gameTime)
    2 {
    3     if (InputHelper.IsNewButtonPress(PlayerIndex.One, Buttons.Back) ||InputHelper.IsNewButtonPress(PlayerIndex.Two, Buttons.Back) ||InputHelper.IsNewButtonPress(PlayerIndex.One, Buttons.B) ||InputHelper.IsNewButtonPress(PlayerIndex.Two, Buttons.B) ||InputHelper.IsNewKeyPress(Keys.Escape))
    4     {
    5            Manager.CurrentState = AAGameState.Playing;
    6     }
    7     base.Update(gameTime);
    8 }
                                   接着我们需要去创建状态并且把它放入我们的游戏中。让我们到Game1.Initialize方法中并且创建新的状态:
    1 stateManager.GameStates.Add(AAGameState.Paused,new PausedGameState(this));
                                   现在,让我添加代码到我们的PlayingGameState去引起暂停状态,只要玩家压下game pad的Start或者键盘上的Enter键。我们将放置这段代码在我们的Update方法的上部:
    1 if (InputHelper.IsNewButtonPress(PlayerIndex.One, Buttons.Start) ||InputHelper.IsNewButtonPress(PlayerIndex.Two, Buttons.Start) ||InputHelper.IsNewKeyPress(Keys.Enter))
    2 {
    3      Manager.CurrentState = AAGameState.Paused;
    4      return;
    5 }

                                  这段代码不仅设置我们的状态到暂停,而且它退出update方法,确保这个游戏在没有更新其余的PlayingGameState下很快的暂停。


    Meeting Expectations

                                   在这点上,我们现在可以暂停我们的游戏并且选择继续它。我们有的一个问题是,我们的暂停菜单不能重新设置选择的项目。所以如果你退出这个主菜单,开始一个新的游戏,点击暂停,你将会看见,Quit To Main Menu对象被选择。这不是大多数游戏期望的,所以我们将会添加一个新的方法到Menu类,让我们设置选择我们想要的选项:

    1 public void SelectItem(int index)
    2 {
    3     if (index >= 0 && index < Items.Count)
    4     {
    5          currentItem = index + 1;
    6          SelectPrevious();
    7     }
    8 }
                                  让我们解释这段代码中的少许部分。首先我们证实渴望的索引是在我们表单的有效的范围内。然后我们设置当前的选项的索引加一,并且调用SelectPrevious方法。为什么要做这个?为了处理这种情况,索引被传入涉及一个禁止的项目。我们的这种方法将会通过这个表单找到一个有效的选项去选择并返回它。如果这个渴望的索引是有效的,它将wind up选择,一个在SelectPreviouse 方法中。
                                  现在,我们只必须更新我们的PausedGameState事件处理重新设置菜单选项在改变状态之前:
     1 private void ContinueActivated(object sender, EventArgs e)
     2 {
     3      Menu.SelectItem(0);
     4      Manager.CurrentState = AAGameState.Playing;
     5 }
     6 private void QuitActivated(object sender, EventArgs e)
     7 {
     8      Menu.SelectItem(0);
     9      Manager.CurrentState = AAGameState.MainMenu;
    10 }
                                  现在,我们的暂停状态将总是默认Continue选项,这是大多数游戏玩家期望的。这个逻辑同样可以应用到OptionsGameState。让我们更新DoneActivated事件处理重新设置第一个选项:
    1 private void DoneActivated(object sender, EventArgs e)
    2 {
    3      Menu.SelectItem(0);
    4      Manager.CurrentState = AAGameState.MainMenu;
    5 }

                                   现在我们的菜单已经建立,以更好地满足使用这些玩家的期望。
                                   在这点上我们的游戏已经完成!我们有了菜单,暂停,关卡,co-op游戏,和end-game状态画面,和声音,它接受一个整体但我们完全控制游戏,完成一个小乐趣的游戏。

    结论
                                   现在,我们已经经过一个相当长创建游戏的过程。希望它为你变的很整洁,读者,如果扩展这个例子和其他去创建这个你想制造的游戏。游戏开发不是能让初学者很快掌握的,但是它是一个非常有趣和有意义的。
    《未完,请继续收看》
    源代码:http://www.ziggyware.com/readarticle.php?article_id=170

  • 相关阅读:
    WPF 获取本机所有字体拿到每个字符的宽度和高度
    WPF 自己封装 Skia 差量绘制控件
    WPF 漂亮的现代化控件 新 ModernWPF 界面库
    dotnet 在 UOS 国产系统上使用 MonoDevelop 创建 GTK 全平台带界面应用
    dotnet 在 UOS 国产系统上使用 MonoDevelop 进行拖控件开发 GTK 应用
    dotnet 在 UOS 国产系统上安装 MonoDevelop 开发工具
    通过java采集PC麦克风音频及播放wav音频文件
    nginx-http之ssl(九)
    nginx-http之proxy(八)
    nginx-http之upstream(七)
  • 原文地址:https://www.cnblogs.com/315358525/p/1565733.html
Copyright © 2020-2023  润新知