zoukankan      html  css  js  c++  java
  • [翻译]XNA外文博客文章精选之sixteen(上)


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

                                                                         外来侵略者
                                                                                    Nick Gravelyn

    前言

    介绍

                                            每一天都有很多很多的人开始游戏开发的旅程。一些是小孩和年轻人通过他们喜欢的视频游戏得到灵感,虽然他们没有计算机编程的经验。还有一些是专业的程序开发者就为了找某些不同的地方。在这两种情况下一个类似的问题通常就产生了。如何学习创建一个游戏的过程呢?
                                            视频游戏从外面看似乎很简单。毕竟与玩家的输入相比他们一点不多于在屏幕上绘制图象。但是游戏制作对许多初学者不是一个马上就容易完成的任务。一些人被详细的代码所困住,例如面向对象的设计,和避免静态数据同时其他努力去明白如何去控制游戏代码的流程。这些或者其他障碍导致一开始的挫败是一个初学者面对的普遍的难题。就是因为这些,许多初学者放弃了制造一个游戏。
                                            这本书目的是帮助那些希望开始创建他们自己的游戏的人。通过这本书,读者将会创进一个完整功能的游戏,我们的游戏显示出一组外星飞船,他们在屏幕上左右移动,并且朝着底部玩家走来很象一款游戏叫做SpaceInvaders(外星入侵者)。我们的游戏将不会配置的任何障碍,但是会代替混合初始的Space Invaders游戏也有快速射击列如Galaxian.我们的游戏,虽然,通过在Galaxian的敌人飞船,不回出现kamikaze dives表演。
                                            在我们继续这本书之前,让我们看下这本书对读者的期望。



    Expectations(期望)
                                         

                                            现在我们在同一页面,让我们开始我们的Alien Aggressor(外来侵略者)的工作

    Getting Started(开始)
    创建项目
                                           象所有的XNA Game Studio工程。外来侵略者开始它的生命在Visual Studio XNA GameStudio 2.0介绍支持所有的2005Visual Studio版本(译者:本人转换成VS2008,XNA3.1也可以运行这个例子)。但是这本书,所有的屏幕截图将采取使用免费版本的Visual C# Express 2005,牢记,如果你使用一个不同的版本,让我们开始把,然后创建一个新的Windows Game project 命名为Alien Aggressors.

    创建一个精灵类

                                           由于外来侵略者基于游戏是个纯粹的精灵。很有意义通过创建一个类去表现一个单一的精灵。所以我们创建一个新的类在我们的游戏工程中叫做Sprite.
                                           每个精灵将会需要少数的数据。首先是一个Texture2D,这样精灵能够被绘制。下一项目是一个位置,这样我们可以左右移动精灵在屏幕上。最后我们需要一个变量去记录精灵的中心位置,这样我们的位置可以引用我们想要的精灵的中心。让我们继续并且添加这些项目到我们的类:
     1 public class Sprite
     2 {
     3      public Vector2 Position = Vector2.Zero;
     4      Vector2 center;
     5      Texture2D texture = null;
     6      public Texture2D Texture
     7      {
     8           get { return texture; }
     9      }
    10 }
                                           你会注意到,我的Sprite类,Position是公有的,虽然不是中心和纹理。这是因为我们的纹理将会被传入通过Sprite结构并且从纹理上,中心将会被计算在这个构造器里。Position,换句话说,将需要被访问和从我们外面的类来更新它。我们同样有一个公开的属性,它允许我们得到精灵实例的外部纹理。
                                            让我们继续通过创建一个基础的结构为我们的Sprite,这样我们可以创建一个实例。
    1 public Sprite(Texture2D spriteTexture)
    2 {
    3    texture = spriteTexture;
    4    center = new Vector2(spriteTexture.Width / 2,spriteTexture.Height / 2);
    5 }
                                            在我们的结构中我们简单的保存这个我们给的纹理,稍后使用并且创建一个新的中心点,使用纹理的宽X值和高Y值的一半。
                                            最后,我们需要去添加一个简单的绘制方法到我们的Sprite中。在我们系统里,我们将会一起把所有的绘制对象放入单一的SpriteBatch.为了完成这个。我们的Draw方法将会接收一个SpriteBatch作为一个参数并且只调用SpriteBatch.Draw:
    1 public void Draw(SpriteBatch spriteBatch)
    2 {
    3      spriteBatch.Draw(texture,new Vector2((int)Position.X, (int)Position.Y),null,Color.White,0f,center,1f,SpriteEffects.None,0);
    4 }
                                            有一件事要注意的是,我们创建一个新的Vector2通过四舍五入我们的位置成整型。这样做确保我们的精灵被绘制在有效的位置,给我们一个整洁的图象。它不需要做这个,所以可以随意进行试验。

                                            现在,让我们添加一个新的图象到我们的Content项目中。我们将会添加玩家1.png图象将会被使用去代表第一个玩家的飞船。为了实现这个。右击在Content子节点上,选择Existing Item从Add菜单中,并且选择这个player1.png文件。这个文件将会复制到你的内容目录上并且添加到项目中。
                                            现在我们有一个图象被加载。我们可以测试我们的新Sprite类。打开Game1.cs文件并且添加一个Sprite实例到你的游戏文件里:
    1 Sprite player1;
                                            接下来,我们必须创建Sprite这样我们可以绘制它到屏幕上。因为我们的Sprite的结构要求一个Texture2D对象,我们必须创建我们的Sprite在LoadContent方法中:
    1 protected override void LoadContent()
    2 {
    3       spriteBatch = new SpriteBatch(GraphicsDevice);   
    4       player1 = new Sprite(Content.Load<Texture2D>("player1"));
    5       player1.Position = new Vector2(100f);
    6 }
                                            现在我们能够绘制我们的精灵到这个屏幕上。添加下面的代码到Draw方法中完成它:
    1 spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
    2 player1.Draw(spriteBatch);
    3 spriteBatch.End();

                                            现在创建运行你的项目,你可以看见一个单一的飞船在蓝色屏幕的中心。让我们继续通过制造背景更多的为我们的游戏的外层空间做一点计算。

    Creating a StarryBackground(创建一个布满星星的背景)
                                            外来侵略者被设置在外太空,所以使用这个默认的CornflowerBlue颜色为这个背景,我们可以简单的改变清除这个颜色改为黑色更好的代表太空,但是这可能仍然是一个无聊的背景。因此要解决这个问题,我们将创建一个类,它来自Texture2D,它为背景将随机产生星星的位置。
                                            首先添加一个新的类到项目中,命名为StarryBackground并且使这个类来自于Texture2D.

    1 public class StarryBackground : Texture2D
    2 {
    3 }
                                            你将会注意如果你试图创建这个项目,你会得到一个如下的错误:
    1 No overload for method 'Texture2D' takes '0' arguments
                                            这个错误表明Texture2D类不能有一个参数-类结构。所以为我们的类来自于它。我们必须创建一个结构,它调用Texture2D之一的结构:我们的结构将会要求一个GraphicsDevice。一个宽和高,和大量的星星布在这个纹理上。这个结构然后调用基类的结构。
    1 public StarryBackground(GraphicsDevice graphics, int width, int height, int numberOfStars): base(graphics, width, height, 0, TextureUsage.None, SurfaceFormat.Color)
    2 {
    3 }

                                             这个构造将会分配一个被要求大小的纹理,使用一些公值为这个多重映像水平,TextureUsage,和SurfaceFormat.虽然这些是common values(公值),一些machines(设计)也许不会接收它们。如果这里抛出一个异常在你的设计中,你也许必须调整一个或多个参数以适应你的图形卡。
                                            接着,让我们添加这些星星。默认的一个空白的纹理是一个大的透明黑色象素的数组。意思是每一个象素的R,G,B和A值都是0,我们的构造器要做的是随机的地方着色象素在这个纹理上,获得一个星空的图象。我们做这个使用一个Random类的实例,并且循环所有的象素直到我们已经添加这些被要求的数量。最后我们调用SetData去设置我们的纹理的象素数据。

     1 if (numberOfStars >= width * height || numberOfStars < 0)
     2       throw new Exception("numberOfStars must be in the range of [0, width * height - 1]");
     3 Random rand = new Random();
     4 Color[] pixels = new Color[width * height];
     5 int starsAdded = 0;
     6 while (starsAdded < numberOfStars)
     7 {
     8        int index = rand.Next(pixels.Length);
     9        if (pixels[index] == Color.TransparentBlack)
    10        { 
    11              float value = (float)rand.NextDouble() + .5f;
    12              pixels[index] = new Color(new Vector4(value));
    13              starsAdded++;
    14        }
    15 }
    16 SetData(pixels);
                                            这是它为StarryBackground类,现在我们可以创建一个实例并且使用它去绘制某些星星为我们的游戏的背景。让我们开始通过添加一个新的StarryBackground变量到主要的Game1类中:
    1 StarryBackground stars;
                                            接下来,我们需要实例这个对象在LoadContent方法中,因为它是结构所需要一个有效的GraphicsDevice reference(参考):
    1 stars = new StarryBackground(GraphicsDevice, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, 200);
                                            最后,我们可以更新我们的Draw方法去绘制这些星星。由于我们的StarryBackground只着色星星,我们必须选择我们的清除颜色为黑色。然后我们可以绘制背景和玩家的飞船:
    1 protected override void Draw(GameTime gameTime)
    2 {
    3       graphics.GraphicsDevice.Clear(Color.Black);
    4       spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
    5       spriteBatch.Draw(stars, Vector2.Zero, Color.White);
    6       player1.Draw(spriteBatch);
    7       spriteBatch.End();
    8       base.Draw(gameTime);
    9 }

                                           如果你现在运行这个游戏,你将会看见我们的飞船漂浮在外太空,和远处的成百的星星一起。同样可以随便改变星星的数量,只要你喜欢。我们选择200颗星星作为开始,但是你可以很容易的改变这个值去添加更多或者更少的星星到我们的游戏中。


    Creating a Ship Class(创建一个飞船类)

                                           由于我们游戏考虑玩家控制一个飞船同时与敌人的飞船交战,我们应该添加一个共同的基类为这两种飞船类型的产生。添加一个新类到你的项目中命名Ship并且使它来自这个Sprite类。同样添加一个基类结构去调用Sprite类的结构和一个虚Update方法为我们的子类型去使用。

    1 public class Ship : Sprite
    2 {
    3       public Ship(Texture2D spriteTexture): base(spriteTexture)
    4       {
    5       }
    6       public virtual void Update(GameTime gameTime)
    7       {
    8       }
    9 }

                                           这是所有我们直到现在将会对于这个Ship类做的。所以让我们添加其他的一些类到我们的项目中命名为PlayerShip.再一次这个类应该来自Ship类并且执行一个基本的构造器。

    1 public class PlayerShip : Ship
    2 {
    3     PlayerIndex index;
    4     public PlayerShip(Texture2D spriteTexture, PlayerIndex playerIndex): base(spriteTexture)
    5     {
    6           index = playerIndex;
    7     }
    8 

                                           注意我们的构造器接收一个PlayerIndex和保存它在我们的类中。这个变量被用来分配一个PlayerShip的实例到一个特别的GamePad索引为了更新。接着让我们添加一个重载到Ship.Update方法中,这样我们的PlayerShip可以左右移动在这个屏幕。

    1 public override void Update(GameTime gameTime)
    2 {
    3        GamePadState gps = GamePad.GetState(index);
    4        Position.X += gps.ThumbSticks.Left.X * 3;
    5        base.Update(gameTime);
    6 }
                                           你将会看见我们得到当前的GamePadState和使用左thumbstick去移动我们的飞船左和右。我们不用担心调整Y轴,因为我们的玩家飞船只能够在屏幕上移动向左或者向右。
                                           现在我们可以测试出这个通过改变我们的玩家1对象成一个PlayerShip.所以在我们的Game1.cs文件中,我们需要去改变声明。
    1 Sprite player1;
                                          为:
    1 PlayerShip player1;

                                          同样我们需要去更新我们的实例代码。

    1 player1 = new Sprite(Content.Load<Texture2D>("player1"));到
    2 player1 = new PlayerShip(Content.Load<Texture2D>("player1"), PlayerIndex.One);

                                          最后,我们需要去添加一个调用player1.Update到我们的游戏的更新方法中:

    1 protected override void Update(GameTime gameTime)
    2 {
    3      if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
    4         this.Exit();
    5      player1.Update(gameTime);
    6      base.Update(gameTime);
    7 }
                                         现在如果我们运行这个游戏我们可以移动我们的玩家来来回回在屏幕的X轴。但是我们的玩家可以很容易的一下子滑动到屏幕的一边,这不是我们想要的。为了解决这个,我们将会添加一个属性到这个Sprite类去我们bounding矩形围绕了这个精灵,然后使用它去跟踪屏幕上的玩家。
                                         首先让我们添加一个新的属性叫做Bounds到Sprite类中。这个属性简单的返回一个新的Rectangle设置到精灵的属性大小。
    1 public Rectangle Bounds
    2 {
    3     get
    4     {
    5         return new Rectangle((int)(Position.X - center.X),(int)(Position.Y - center.Y),texture.Width,texture.Height);
    6     }
    7 }
                                         接着我们添加一些代码在我们的更新方法代码中,去跟踪在屏幕上的这个玩家。为了实现这个我们需要更新Ship.Update方法signature to this:
    1 public virtual void Update(GameTime gameTime, float fieldWidth) 

                                         这个fieldWidth变量将会习惯于告诉Ship,我们玩的地方有多宽。接着让我们更新我们的PlayerShip.Update方法去使用这个参数并且我们新的Bounds属性去保持屏幕上的玩家。

     1 public override void Update(GameTime gameTime, float fieldWidth)
     2 {
     3      GamePadState gps = GamePad.GetState(index);
     4      Position.X += gps.ThumbSticks.Left.X * 3;
     5      if (Bounds.Left < 0)
     6             Position.X = Bounds.Width / 2;
     7      else if (Bounds.Right > fieldWidth)
     8             Position.X = fieldWidth - Bounds.Width / 2;
     9      base.Update(gameTime, fieldWidth);
    10 }
                                         最后,我们需要从这里改变我们的调用player1.Update
    1 player1.Update(gameTime);

                                         到这里

    1 player1.Update(gameTime, GraphicsDevice.Viewport.Width);

                                         现在你的玩家只能在屏幕上移动

    Adding Keyboard Input(添加键盘的输入)
                                         一些开发者和用户喜欢键盘。原因是可以简单不用Xbox360游戏机,或者它可以是他们喜欢的输入设备。幸运的是XNA Game Studio开发者,添加键盘的操作输入不是一个大的挑战。
                                         让我们更新我们的PlayShip类能够接收键盘的输入。我们将会创建一对新的方法:一个为GamePad驱动输入,另一个为键盘驱动输入。我们将选择这两种输入的方式。这里有两个方法我们准备添加到我们的PlayerShip类中:

     1 private void UpdateKeyboard()
     2 {
     3       KeyboardState keyState = Keyboard.GetState();
     4       if (keyState.IsKeyDown(Keys.A) || keyState.IsKeyDown(Keys.Left))
     5                  Position.X -= 3;
     6       if (keyState.IsKeyDown(Keys.D) || keyState.IsKeyDown(Keys.Right))
     7                  Position.X += 3;
     8 }
     9 private void UpdateGamePad(GamePadState gps)
    10 {
    11        Position.X += gps.ThumbSticks.Left.X * 3;
    12 }
                                         我们有UpdateGamePad接收一个GamePadState的参数,因为我们将会得到这个游戏机的状态在Update中,所以我们可以检查这个游戏pad(译者:游戏机设备)是否已经连接。这就是我们将会决定我们使用哪个方法的调用。
                                         现在我们可以更新PlayerShip.Update方法去利用这个新方法。
     1 public override void Update(GameTime gameTime, float fieldWidth)
     2 {
     3       GamePadState gps = GamePad.GetState(index);
     4       if (gps.IsConnected)
     5           UpdateGamePad(gps);
     6       else
     7           UpdateKeyboard();
     8       if (Bounds.Left < 0)
     9           Position.X = Bounds.Width / 2;
    10       else if (Bounds.Right > fieldWidth)
    11           Position.X = fieldWidth - Bounds.Width / 2;
    12       base.Update(gameTime, fieldWidth);
    13 }

                                          现在当我们运行这个游戏,我们可以控制玩家用控制起的左thumbstick,如果控制者没有连接,使用键盘。你的游戏你同样可以选择去产生一个简单操作或者布尔值去设置无论是使用键盘还是使用game pad输入。

    Managing the Flow of Code
    The GameState Class(游戏的状态类)
                                          到目前为止,我们的Game1内的代码提供给我门通过工程模板。这是很快很迅速提炼新的想法或者为非常简单的游戏。但是一旦你超出任何的平凡简单的例子,你将会找到它变成非常难的维持。因此,我们将会执行一个游戏状态去允许我们去分解代码在逻辑块给予游戏的状态。举例来说,我们将会一个状态为这个主菜单,一个为这个操作的菜单,一个为转变在我们游戏中的关卡之间,一个为游戏期间,一个为在一个游戏结束之后。这个方法我们可以保持我们的代码整洁和简明。
                                          为了开始,让我们添加一个新的抽象类命名为GameState到我们的项目中。

    1 public abstract class GameState
    2 {
    3 }

                                           每个游戏状态将会表现出象一个迷你型的游戏类实例。为此,我们需要去揭露一些基础的在类中的数据,为这些来自类使用的数据。我们当前不能有一个GameStateManager类型的定义,但是我们添加它在下一节里,所以不要担心它,Visual Studio不会高亮打印它的名字。

     1 GameStateManager manager;
     2 ContentManager content;
     3 GraphicsDeviceManager graphics;
     4 Game game;
     5 public Game Game
     6 {
     7        get { return game; }
     8 }
     9 public GameStateManager Manager
    10 {
    11        get { return manager; }
    12 }
    13 public ContentManager Content
    14 {
    15        get { return content; }
    16 }
    17 public GraphicsDevice GraphicsDevice
    18 {
    19        get { return graphics.GraphicsDevice; }
    20 }
    21 public GraphicsDeviceManager GraphicsManager
    22 {
    23        get { return graphics; }
    24 }

                                           你将会看见我们揭露一些相当基础的数据类似正象我们在游戏类中或者游戏组件中看到的一样。接着,让我们创建一个构造器去创建一个GameState实例并且重新找回这个数据。

    1 public GameState(Game game)
    2 {
    3       this.game = game;
    4       manager = game.Services.GetService(typeof(GameStateManager)) as GameStateManager;
    5       content = game.Services.GetService(typeof(ContentManager)) as ContentManager;
    6       graphics= game.Services.GetService(typeof(IGraphicsDeviceService)) as GraphicsDeviceManager;
    7 }

                                           我们的构造器简单接收一个Game实例和使用这个Services集合来检索其他三个我们需要的数据。
                                          我们的GameStatel类最后一块是定义方法,它将会被调用通过我们的GameStateManager去更新并且绘制每个状态:

    1 public abstract void Update(GameTime gameTime);
    2 public abstract void Draw(GameTime gameTime);

                                          现在我们有一个简单的GameState类,我们可以起源于它去创建我们的游戏一小部整体代码。

    The GameStateManager Class
                                         现在我们有一个GameState对象,让我们创建我们使用的GameStateManager类型在GameState类中

    1 ublic class GameStateManager : DrawableGameComponent
    2 {
    3       public GameStateManager(Game game): base(game)
    4       {
    5       } 
    6 }

                                         我们现在创建一个非常基础的DrawableGameComponent外壳为我们去保存游戏的状态。为了实现这个,我们首先需要去添加一个枚举到我们的游戏中,我们将调用AAGameState(为外来侵略者游戏状态)。这个枚举将会被使用去设置或者改变当前的状态在GameStateManager.这里是我们的枚举的样子:

    1 public enum AAGameState
    2 {
    3    MainMenu,Options,LevelTransition,Playing,Paused,Win,Lose,
    4 }
                                         你将会看见我们跟踪游戏的每一个方面包含玩家暂停游戏。现在我们可以添加一些数据到我们的manager中。
    1 public class GameStateManager : DrawableGameComponent
    2 {
    3     public Dictionary<AAGameState, GameState> GameStates =new Dictionary<AAGameState, GameState>();
    4     public AAGameState CurrentState = AAGameState.MainMenu;
    5 }
                                        我已经添加一个Dictionary去跟踪所有游戏的状态,使用我们的枚举,这样我们可以很容易访问任何给定的状态。我们同样有一个变量去跟踪当前的状态。接着让我们添加一个简单的Update和Draw方法到update和draw的当前状态中:
     1 public override void Update(GameTime gameTime)
     2 {
     3      GameState state;
     4      if (GameStates.TryGetValue(CurrentState, out state))
     5            state.Update(gameTime);
     6 }
     7 public override void Draw(GameTime gameTime)
     8 {
     9      GameState state;
    10      if (GameStates.TryGetValue(CurrentState, out state))
    11            state.Draw(gameTime);
    12 }

                                        我们使用TryGetValue方法而不是标定指数这个dictionary(字典),这样我们可以避免抛出异常在这种情况下,一个游戏状态可能不会被正确的添加。

    The PlayingGameState Class
                                        下一个事情我们应该开始从游戏类中移出我们的代码,到一个新的游戏状态类中。为了创建一个新类叫做PlayingGameState,它来自于GameState类。

     1 public class PlayingGameState : GameState
     2 {
     3     public PlayingGameState(Game game): base(game)
     4     {
     5     }
     6     public override void Update(GameTime gameTime)
     7     {
     8     }
     9     public override void Draw(GameTime gameTime)
    10     {
    11     }
    12 }
                                        现在让我添加一些fields到这个类中:
    1 SpriteBatch spriteBatch;
    2 PlayerShip player1;
                                        接着,让我们添加代码去创建这些对象。我们的游戏状态将会被创建在Game.Initialize方法中,在base.Initialize确保我们的GraphicsDevice是有效的后面,所以加载内容在这个构造器中是完全可以接受的。
    1 public PlayingGameState(Game game): base(game)
    2 {
    3      spriteBatch = new SpriteBatch(GraphicsDevice);
    4      player1 = new PlayerShip(Content.Load<Texture2D>("player1"), PlayerIndex.One); 
    5      player1.Position = new Vector2(100f);
    6 }
                                       现在让我们添加当前Update和Draw代码去更新和绘制玩家:
     1 public override void Update(GameTime gameTime)
     2 {
     3       player1.Update(gameTime, GraphicsDevice.Viewport.Width);
     4 }
     5 public override void Draw(GameTime gameTime)
     6 {
     7       spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
     8       player1.Draw(spriteBatch);
     9       spriteBatch.End();
    10 }
                                       为了支持新的状态管理系统,我们需要去做一些更新到我们的游戏类。首先让我们删除PlayShip的声明从这个fields并且添加一个新的GameStateManager声明。所以现在我们的游戏类有这些fields在它里面:
    1 GraphicsDeviceManager graphics;
    2 SpriteBatch spriteBatch;
    3 StarryBackground stars;
    4 GameStateManager stateManager;

                                       接着,我们将会更新这个游戏的构造器不仅创建这个GameStateManager,也会去注册它和ContentManager作为服务:

    1 public Game1()
    2 {
    3      graphics = new GraphicsDeviceManager(this);
    4      Content.RootDirectory = "Content";
    5      stateManager = new GameStateManager(this);
    6      Components.Add(stateManager);
    7      Services.AddService(typeof(ContentManager), Content);
    8      Services.AddService(typeof(GameStateManager), stateManager);
    9 }
                                      接着我们继续这个初始化方法,我们将会创建我们的游戏状态在这里面。让我们继续并且添加一个新的PlayingGameState的实例。记住我们必须做这个在base.Initialize之后,为了确保我们的GraphicsDevice是有效的。
    1 protected override void Initialize()
    2 {
    3     base.Initialize();
    4     stateManager.GameStates.Add(AAGameState.Playing, new PlayingGameState(this)); 
    5     stateManager.CurrentState = AAGameState.Playing;
    6 }

                                      接着删除所有的代码,它们处理创建,定位,更新,或者绘制player1.如果你在这点上构建,这个编译器将会指出,这个错误,所有这些行,你正在使用Game1.cs中的玩家1。简单的删除这些行。我们想要留下这些代码为了创建和绘制星星的背景,因为我们将会绘制这些为每一个游戏状态。
                                      如果你再次运行这个游戏,你将会看见我们有相同的功能就象前面一样,但是我们封装它在一个容易使用的状态管理系统里面。

    Making the Game Play
    添加子弹
                                     到目前为止,我们的飞船可以左右移动,但是我们实际上还不能发射,这是游戏的主要部分。我们现在准备添加在这个系统中,为了发射子弹从飞船上并且确保它们正确的更新。让我们从创建这些基础开始:Bullet类。这个类将会包含一个静态的纹理和初始状态为所有的使用的子弹,和一个位置,速度和绘制的颜色。最后Bullet将会有一个Bounds属性去得到一个bounding矩形。

     1 public class Bullet
     2 {
     3      public static Texture2D Texture;
     4      public static Vector2 Origin;
     5      public Vector2 Position;
     6      public Vector2 Velocity;
     7      public Color Color;
     8      public Rectangle Bounds
     9      {
    10             get
    11               {
    12                   return new Rectangle((int)(Position.X - Origin.X),(int)(Position.Y - Origin.Y),Texture.Width,Texture.Height);
    13               }
    14       }
    15 }

                                    接着,让我们添加一个简单的Update方法,它将会添加这个速度到这个位置并且一个Draw方法去绘制子弹使用一个SpriteBatch:

    1 public void Update()
    2 {
    3      Position += Velocity;
    4 }
    5 public void Draw(SpriteBatch spriteBatch)
    6 {
    7      spriteBatch.Draw(Texture,Position,null,Color,0,Origin,1f,SpriteEffects.None,0);
    8 }
                                    现在,我们有一个Bullet类我们可以使用,让我们把这个放入动作中。在我们的PlayingGameState类中,让我们添加一个新的bullets表单为这个玩家:
    1 List<Bullet> playerBullets = new List<Bullet>();
                                    现在我们想要更新这个Ship和PlayerShip类去为我们去处理发射这些子弹。让我们从Ship类开始。我们需要去跟踪elapsed时间,这样我们的飞船只可以发射子弹以给定的速率。为了实现这个我们需要两个值:
    1 float fireTimer = 0f;
    2 float fireRate = .4f;
    3 public float FireRate
    4 {
    5       get { return fireRate; }
    6       set { fireRate = (float)Math.Max(value, .01f); }
    7 }

                                    接着我们需要一些值去了解子弹从哪里发射,给它们的初始速度是多少,它们是什么颜色:

    1 public Color BulletColor = Color.Blue;
    2 public Vector2 BulletVelocity = Vector2.UnitY;
    3 public Vector2 BulletOrigin = Vector2.Zero;
                                    接着我们将会创建一个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         }
    12 }
                                    所以你看见我们首先确保fireTImer小于或者等于0,然后我们使用上面设置的值创建子弹。我们同样设置fireTimer去fireRate,这是我们如何控制发射的速度。接着我们需要去改变Update方法去接受这个子弹列表作为一个参数并且同样去更新fireTimer:
    1 public virtual void Update(GameTime gameTime, List<Bullet> bullets, int fieldWidth)
    2 {
    3            fireTimer -= (float)gameTime.ElapsedGameTime.TotalSeconds;
    4 }

                                    现在我们必须更新这个PlayerShip类去使用这个新的Update方法,让我们发射子弹。让我们首先定位Update方法去声明以配合这个Ship类:

    1 public class PlayerShip : Ship
    2 {
    3       public override void Update(GameTime gameTime, List<Bullet> bullets, int fieldWidth)
    4       { }
    5 }
                                   我们同样需要确保PlayerShip有这个属性值在适当的位置发射子弹。让我们继续设置一些值为这个结构:
    1 BulletColor = Color.Red;
    2 BulletVelocity = new Vector2(0f, -5f); 
    3 BulletOrigin = new Vector2(0-Bounds.Height / 2);
                                   这些值确保我们的子弹上升(使用负Y速率),这个子弹从精灵的上部的中间开始。BulletOrigin是一个我们精灵中心到我们发射子弹位置的偏移量。因为这个位置已经是精灵的中心了,通过精灵高度的一般,我们只能向上移动。我同样设置子弹为红色代替默认的蓝色。现在我们可以更新我们的两个输入处理方法在PlayerShip中去调用Fire方法,并且让我们发射子弹:
     1 private void UpdateKeyboard(List<Bullet> bullets)
     2 {
     3      KeyboardState keyState = Keyboard.GetState();
     4      if (keyState.IsKeyDown(Keys.A) || keyState.IsKeyDown(Keys.Left))
     5            Position.X -= 3;
     6      if (keyState.IsKeyDown(Keys.D) || keyState.IsKeyDown(Keys.Right))
     7            Position.X += 3;
     8      if (keyState.IsKeyDown(Keys.Space))
     9            Fire(bullets);
    10 }
    11 private void UpdateGamePad(GamePadState gps, List<Bullet> bullets)
    12 {
    13      Position.X += gps.ThumbSticks.Left.X * 3;
    14      if (gps.Buttons.A == ButtonState.Pressed || gps.Triggers.Right > .3f)
    15           Fire(bullets);
    16 }
                                    最后我们确保把子弹表单放入我们调用的Update方法中:
     1 public override void Update(GameTime gameTime, List<Bullet> bullets, int fieldWidth)
     2 {
     3         GamePadState gps = GamePad.GetState(index);
     4         if (gps.IsConnected)
     5                 UpdateGamePad(gps, bullets);
     6         else
     7                 UpdateKeyboard(bullets);
     8         if (Bounds.Left < 0)
     9                 Position.X = Bounds.Width / 2;
    10         else if (Bounds.Right > fieldWidth)
    11                 Position.X = fieldWidth - Bounds.Width / 2;
    12         base.Update(gameTime, bullets, fieldWidth);
    13 }
                                   现在我们的PlayerShip可以发射子弹了,但是我们还没有更新或者绘制它们。让我们回到这个PlayingGameState类并且生成一些改变在这里。首先我们将会重新配置我们的精灵在屏幕的底部,我们想要它开始时在那:
    1 player1.Position = new Vector2(GraphicsDevice.Viewport.Width / 2 - player1.Bounds.Width / 2, GraphicsDevice.Viewport.Height - player1.Bounds.Height);
                                   这段代码将会布置我们的精灵在屏幕底部的正中心。接着我们需要确保我们加载子弹纹理并且设置子弹的初始绘制位置。让我们添加这两行到结构中:
    1 Bullet.Texture = Content.Load<Texture2D>("bullet");
    2 Bullet.Origin = new Vector2(Bullet.Texture.Width / 2, Bullet.Texture.Height / 2);
                                   现在让我们继续到Update方法并且确保我们传递给我们的玩家子弹列表和我们更新子弹。我们将会执行一个简单的系统为删除留在屏幕上的子弹:
     1 public override void Update(GameTime gameTime)
     2 {
     3      player1.Update(gameTime, playerBullets, GraphicsDevice.Viewport.Width);
     4      Rectangle screenRect = new Rectangle(00, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height);
     5      List<Bullet> bulletsToRemove = new List<Bullet>();
     6      foreach (Bullet b in playerBullets)
     7      {
     8           b.Update();
     9           if (!screenRect.Intersects(b.Bounds))
    10                 bulletsToRemove.Add(b);
    11      }
    12      foreach (Bullet b in bulletsToRemove)
    13      playerBullets.Remove(b);
    14 }
                                   我们使用一个第二表单的原因是因为你不能删除一个表单中的元素在一个foreach循环中。所以我们产生一个第二子弹表单,我们想要删除它。当我们的foreach循环在playerBullets列表的结尾,我们通过子弹列表的循环去删除并且从playerBullets列表里删除他们。
    1 public override void Draw(GameTime gameTime)
    2 {
    3      spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
    4      foreach (Bullet b in playerBullets)
    5          b.Draw(spriteBatch);
    6      player1.Draw(spriteBatch);
    7      spriteBatch.End();
    8 }

                                   现在当我们运行游戏,你可能会从一边到另一边的移动,并且使用空格键发射子弹。你可以调整这个fireRate变量在Ship类中去声明你的飞船能射击的有多快。数值越小,发射的越快。


    A Lone Aggressor(一个孤独的侵略者)

                                   现在我们有了子弹左右飞。让我们开始添加射击的对象。我们将会创建一组外星飞船,他们从一边移动到另一边,每一次撞到屏幕的一边时,向下移动一行。为了开始这项工作,我们首先需要创建AlienShip类来自我们的Ship类:

    1 public class AlienShip : Ship
    2 {
    3       public AlienShip(Texture2D spriteTexture): base(spriteTexture)
    4       {
    5       }
    6 }

                                    首先我们需要设置固定的子弹值在我们的构造器中:

    1 public AlienShip(Texture2D spriteTexture): base(spriteTexture)
    2 {
    3        BulletOrigin = new Vector2(016);
    4        BulletVelocity = new Vector2(05);
    5        BulletColor = Color.Blue;
    6 }
                                   接下来我们添加到我们的AlienShip类一个静态的Random实例为产生随机数为了检测,当发射以及一个值表明偶然发生的发射。我们的ChanceToFile代表休息时间的数量是1000毫秒(译者:子弹之间的间隔时间),这个外星飞船可以发射一个子弹。一个小数目通常是足够的,但是你可以使它更加困难。
    1 static Random rand = new Random();
    2 public int ChanceToFire = 2;
                                   最后我们需要重载这个Update方法去随机从这个飞船上发射子弹:
    1 public override void Update(GameTime gameTime, List<Bullet> bullets, int fieldWidth)
    2 {
    3      if (rand.Next(1000< ChanceToFire)
    4            Fire(bullets);
    5      base.Update(gameTime, bullets, fieldWidth);
    6 }
                                   现在我们已经完成了AlienShip,让我们把一个单独的飞船放在屏幕上,看看它是否朝着玩家移动。首先的事情是确保你添加alien.png文件到你的内容项目中。
                                   一旦这个做了,让我们回到PlayingGameState类并且添加一个行的子弹表单和外星飞船:
    1 List<Bullet> alienBullets = new List<Bullet>();
    2 AlienShip alienShip;
                                  从玩家的子弹中,我门想要保留外星子弹在一个分离的表单中,稍后我们可以很容易告诉什么是什么当做碰撞检测的时候。
                                 接着,让我们创建一个外星的飞船在我们的结构中:
    1 alienShip = new AlienShip(Content.Load<Texture2D>("alien"));
    2 alienShip.Position = new Vector2(GraphicsDevice.Viewport.Width / 2 - alienShip.Bounds.Width / 2,alienShip.Bounds.Height);
                                 这将创建外星飞船在屏幕的上部的中央的地方。接着让我们修改下Update方法去更新外星飞船以及外星子弹。
     1 public override void Update(GameTime gameTime)
     2 {
     3       player1.Update(gameTime, playerBullets, GraphicsDevice.Viewport.Width);
     4       alienShip.Update(gameTime, alienBullets, GraphicsDevice.Viewport.Width);
     5       Rectangle screenRect = new Rectangle(00, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height);
     6       List<Bullet> bulletsToRemove = new List<Bullet>();
     7       foreach (Bullet b in playerBullets)
     8       {
     9           b.Update();
    10           if (!screenRect.Intersects(b.Bounds))
    11                bulletsToRemove.Add(b);
    12       }
    13       foreach (Bullet b in bulletsToRemove)
    14              playerBullets.Remove(b);
    15       bulletsToRemove.Clear();
    16       foreach (Bullet b in alienBullets)
    17       {
    18            b.Update();
    19            if (!screenRect.Intersects(b.Bounds))
    20            bulletsToRemove.Add(b);
    21       }
    22       foreach (Bullet b in bulletsToRemove)
    23            alienBullets.Remove(b);
    24 }
                                你可以看见我们基本的复制使用的代码为玩家,为了更新外星飞船和它的子弹。现在让我们更新Draw方法去绘制新的外星飞船和子弹。
     1 public override void Draw(GameTime gameTime)
     2 {
     3        spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
     4        foreach (Bullet b in playerBullets)
     5            b.Draw(spriteBatch);
     6        foreach (Bullet b in alienBullets)
     7            b.Draw(spriteBatch);
     8        player1.Draw(spriteBatch);
     9        alienShip.Draw(spriteBatch);
    10        spriteBatch.End();
    11 }

                                现在运行这个游戏,这个外星飞船应该出现在屏幕的顶部,并且玩家周期的发射。再一次你可以调整ChangeToFire变量去使外星发射子弹的频率高或低。

    Enemy Fleet Approaching(敌人快速的接近)

                                我们现在有一个单独的外星飞船,它能够向玩家发射子弹,但是这是一对一的游戏风格,这几乎没有什么乐趣。现在我们需要做的是创建一个结构能够保存一组外星飞船的能力。如果我们回忆起Space Invaders(太空入侵者),你将会记起游戏有一组外星飞船来回的在屏幕上移动。每一次这组外星飞船撞击到了屏幕的边缘,这整个一组向下移动一行。我们就要完成这种效果。让我们通过添加一个新的类到我们的项目中开始,命名它为AlienGrid,这个类将会来自<<AlienSprite>>的表单中:
    1 public class AlienGrid : List<List<AlienShip>>
    2 {
    3 
                                我们使用一个Aliens表单的列表的原因是创建一组外星飞船。让我们使用这个初始的Space Invaders(太空入侵者)图表去可视化这些数据结构:

                                在这个图表中,我们可以看做一个单一的内部,红色框作为一个<AlienShip>列表。它是一个单一的外星飞船列。外部的,蓝色框是<List<AlienShip>>列表(AlienGrid类它本身),因为它包含多个外星飞船列。
                                现在,我们有一个基础AlienGrid结构的了解。让我们开始这个过程。首先我们需要添加一对数据到这个类中:
    1 public const float MaxSpeed = 10f;
    2 public float Velocity = 1f; 
                                我们定义一个MaxSpeed作为一个常数去限制外星飞船可以从一边移动到另一边有多快。我们同样包含当前的组的Velocity。这些都定义为每帧像素。
                                接着,让我们添加实体到这个类中:Update方法。这里完整的代码,我们将会把它切成小块分析:
     1 public bool Update(GameTime gameTime,List<Bullet> bullets,int fieldWidth,int playerLine)
     2 {
     3      if (this.Count == 0)
     4           return false;
     5      AlienShip leftMost = this[0][0];
     6      AlienShip rightMost = this[this.Count - 1][0];
     7      if ((Velocity > 0 && rightMost.Bounds.Right >= fieldWidth) ||(Velocity < 0 && leftMost.Bounds.Left <= p))
     8      {
     9            float velocityChange = 1.01f;
    10            Velocity = MathHelper.Clamp(Velocity * -velocityChange, -MaxSpeed, MaxSpeed); 
    11            foreach (List<AlienShip> column in this)
    12            {
    13                  foreach (Sprite enemy in column)
    14                  {
    15                         enemy.Position.Y += enemy.Bounds.Height / 2;
    16                         if (enemy.Bounds.Bottom > playerLine)
    17                              return true
    18                  }
    19            }
    20       }
    21       foreach (List<AlienShip> column in this)
    22       {
    23              foreach (AlienShip enemy in column)
    24              {
    25                     enemy.Position.X += Velocity;
    26                     enemy.Update(gameTime, bullets, fieldWidth);
    27              }
    28       }
    29       return false;
    30 }
                                   这是一个相当多的代码,所以让我们一段段来,确保我们理解它们所有。让我们开始这个方法自身的声明:
    1 public bool Update(GameTime gameTime,List<Bullet> bullets,int fieldWidth,int playerLine)
                                   这个方法接收一个GameTime,子弹的列表,field宽度很象Ship类的Update方法。我们同样可以接收一个playLine值。这个值是在玩家bounding盒子的上部,并且通过达到玩家的水平面,被使用来定义外星飞船是否赢了这个游戏。这个方法同样返回一个布尔值。这个值表明是否是外星人赢了。例如如果我们返回true到这个游戏,将会假定外星人已经达到了窗口的底部,并且导致游戏结束,玩家输了。
    1 if (this.Count == 0)
    2       return false;
                                   代码的开头简单确保了,我们至少有一列外星飞船在这个grid(组)里。如果这个组没有列了(因此是空的),我们只能返回false到方法的结尾。
    1 AlienShip leftMost = this[0][0];
    2 AlienShip rightMost = this[this.Count - 1][0];
                                   接下来的两行找出外星飞船,离屏幕的左边最远有多远,离屏幕的右边最远有多远,是通过抓取组中第一列和最后一列,并且把第一个飞船考虑在其中。
    1 if ((Velocity > 0 && rightMost.Bounds.Right >= fieldWidth) ||(Velocity < 0 && leftMost.Bounds.Left <= 0))
    2 {}
                                   if语句检查去看看,这个组是否已经撞击到屏幕的边框了。我们测试这个速度以及他们的bounds去确保我们会受骗在屏幕的边缘上不断的reversing这个速率。
    1 float velocityChange = 1.01f;
    2 Velocity = MathHelper.Clamp(Velocity * -velocityChange, -MaxSpeed, MaxSpeed); 
                                   这两行被用来reverse这个组的速率。首先我们选择速率改变成一个可能的值。当前我们设置这个为1.01f,它将导致飞船每一次它们反向时轻微的加速。第二行应用这个变化作为一个负的速率的乘法。调用MathHelper.Clamp去确保我们的速率在指定的最大速度之间。
     1 foreach (List<AlienShip> column in this)
     2 {
     3          foreach (Sprite enemy in column)
     4          {
     5                 enemy.Position.Y += enemy.Bounds.Height / 2;
     6                 if (enemy.Bounds.Bottom > playerLine)
     7                      return true;
     8          }
     9 
    10 
    11 return false;
                                    这个循环被用来下移所有的单独的外星飞船一个值,这个值是他们其中的一个的高度的一半。在这个循环内部我们同样可以检查去看看,敌人是否移动到玩家所在的行,并且返回true,如果是这种情况的话。最后我们返回false去表示没有外星飞船到达玩家那一行。
                                    这里面的Update方法,让我们创建一个初始方法,它将会很快的产生一组我们选择大小的外星飞船。
     1 public void Initialize(Texture2D alienTexture, int columns, int rows)
     2 {
     3          this.Clear();
     4          for (int x = 0; x < columns; x++)
     5          {
     6               List<AlienShip> column = new List<AlienShip>();
     7               for (int y = 0; y < rows; y++)
     8               {
     9                   AlienShip alien = new AlienShip(alienTexture);
    10                   Vector2 alienSpacing = new Vector2(alien.Bounds.Width,alien.Bounds.Height) * 1.25f;
    11                   alien.Position = new Vector2((alien.Bounds.Width / 2+ (alienSpacing.X * x),(alien.Bounds.Height / 2+ (alienSpacing.Y * y));
    12                   column.Add(alien);
    13               }
    14               this.Add(column);
    15          }
    16 }
                                     首先的事情我们要做的是初始化清除这个组。接着我们循环列数创建一个新的Lis<AlienShip>为每一列。接着我们循环所有的行数添加一个新的外星人到这列的每一行。我们产生每个外星飞船的位置,是基于飞船的边框加上一个间距数量和飞船在组中的这个(X,Y)位置。
                                     现在我们正准备去添加一个Draw方法去允许我们很快的绘制整个外星组:
    1 public void Draw(SpriteBatch spriteBatch)
    2 {
    3      foreach (List<AlienShip> column in this)
    4            foreach (AlienShip s in column)
    5                 s.Draw(spriteBatch);
    6 }
                                     我们的组类已经完成。让我们继续并且添加一个grid(组)到我们的PlayingGameState中。首先删除所有的之前我们添加的涉及到单独的AlienShip。留下List<Bullet>作为我们需要的。但是删除单独的飞船从这个游戏中。接下来添加一个新的grid(组)到我们的游戏中:
    1 AlienGrid alienGrid = new AlienGrid();
                                     现在让我们初始化这个grid(组)在我们的结构中。我们开始通过创建一个grid(组)在10列和5行中:
    1 alienGrid.Initialize(Content.Load<Texture2D>("alien"), 105);
                                     接下来,让我们更新这个外星组在Update方法中。到现在为止我们将会忽略方法返回的值。
    1 alienGrid.Update(gameTime, alienBullets, GraphicsDevice.Viewport.Width,player1.Bounds.Top);
                                     最后我们需要添加这个调用到外星grid(组)的Draw方法在我们的PlayingGameState的Draw方法中:
    1 alienGrid.Draw(spriteBatch);

                                     现在当你运行这个游戏,你可以有一个整体的外星飞船舰队来回移动在屏幕上,大量下降的子弹飞向玩家。你将会注意到整个单一的飞船不会发射这么多,屏幕上有很多飞船,它仍然有大量的子弹。在这点上,在游戏里几乎是同时。接着我们将会添加一个分数和生命系统到这个游戏里,同时有赢或输的能力。


    Taking Damage

                                     现在,我们有一些子弹和敌人,让我们开始添加所需要的数据去使我们的游戏有更多挑战。为了实现这个,我们需要允许玩家失败。让我们开始并添加一些数据到PlayerShip类中。我们将会添加大量额外的生命到这个玩家,有一个属性去检测玩家是否还活着。所以添加这些代码到你的PlayerShip类中:

    1 public int ExtraLives = 3;
    2 public bool IsAlive
    3 {
    4       get { return ExtraLives >= 0; }
    5 }
                                     你将会看见我们没有设置总共的生命数量给玩家,但是这个extra lives的数量。意思玩家活的很长只要ExtraLives是大于或者等于0。一旦它小于0,我们知道玩家死亡了。
                                     现在,适当的数据在适当的位置,我们需要开始看下,所有的这些子弹实际上是否撞到什么东西没。让我们添加一个新的方法到我们的Ship类去告诉,是否子弹已经撞击到了飞船:
    1 public bool CollideBullet(Bullet b)
    2 {
    3         return Bounds.Contains((int)b.Position.X, (int)b.Position.Y);
    4 }
                                     所以我们将会看看,这个飞船的边框是否包含子弹的位置。这告诉我们子弹是否与飞船发生碰撞。接着让我们更新我们的AlienGrid类能够检查碰撞所有的内部外星飞船。
     1 public bool CollideBullet(Bullet b)
     2 
     3     if (this.Count == 0)
     4     return false;
     5     AlienShip collider = null;
     6     foreach (List<AlienShip> column in this)
     7     {
     8          foreach (AlienShip enemy in column)
     9          {
    10                 if (enemy.CollideBullet(b))
    11                 {
    12                      collider = enemy;
    13                      break;
    14                 }
    15          }
    16          if (collider != null)
    17          {
    18               column.Remove(collider);
    19               break;
    20          }
    21     }
    22     for (int i = this.Count - 1; i >= 0; i--)
    23          if (this[i].Count == 0)
    24              this.RemoveAt(i);
    25          return (collider != null);
    26 }
                                       让我们运行它并且确保我们理解它在这里发生了什么。首先我们创建一个AlienShip变量去保存这个飞船,它被子弹撞击。接下来我们循环这个飞船grid(组)并且使用这个CollideBullet方法去看看是否这个子弹撞击了飞船。如果它撞了,我们保存这个飞船作为碰撞者并且摆脱内循环。然后我们检查去看看这个碰撞者是否为空,如果它有一个值,我们删除这个飞船从这个列中并且摆脱外部循环。在这个之后我们运行一个循环去看看其他的列是否是空的。我们做这个是一个循环,它倒记数,这样我们可以使用RemoveAt方法并且删除任何空的列。最后我们返回如果我们的碰撞者不为空,告诉我们的游戏我们撞击了一个外星飞船。
                                       现在我们可以改变在我们的PlayingGameState的Update方法中的代码去允许我们杀死敌人的飞船。所以我们需要做的是找到foreach循环,它迭代playerBullets列表并且更新它去调用这个AlienGrid的CollideBullet方法:
    1 foreach (Bullet b in playerBullets)
    2 {
    3     b.Update();
    4     if (!screenRect.Intersects(b.Bounds))
    5            bulletsToRemove.Add(b);
    6     else if (alienGrid.CollideBullet(b))
    7            bulletsToRemove.Add(b);
    8 }
                                        现在我们的游戏检查看看,一个子弹是否与其他的外星飞船发生碰撞,以及标记为删除。运行游戏现在,你会看见,你可能射击敌人的飞船并且使它们消失从这个游戏中。同样注意,你可以消除整个列从游戏的结束状态和飞船确保到它们所支持的边界(译者:飞船到了屏幕最下面)
                                        因为我们的外星飞船现在容易受到子弹的攻击。让我们为玩家做同样的事情。显然,我们不想让它在第一次击中后消息,但是我们想要表示它被射中了。然后我们要做的是重新设置它到屏幕的左边。让我们现在更新下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      }
    12 }

                                       你将会看见,玩家是否被子弹击中,我们从这个表中删除它。然后我们减去一个玩家额外的生命值,并且设置玩家的X位置是它的边框宽度的一半,移动它至始至终在屏幕的左部。
                                       我们同样想要改变我们如何调用player1.Update去确保玩家在更新它之前是活的。这将会在稍后使用,当我们添加这个co-op:

    1 if (player1.IsAlive)
    2 player1.Update(gameTime, playerBullets, GraphicsDevice.Viewport.Width);
                                      
    只有一个可以赢
                                        在此时,外星飞船可以射击玩家,玩家获得重设置并且玩家可以射击外星飞船直到他们都被消灭。但是这仍然不能算赢。它只留下玩家一个人在太空中,没有什么事干。让我们继续去创建一个新的游戏状态叫做EndPlayingGameState:
     1 public class EndPlayingGameState: GameState
     2 {
     3      public EndPlayingGameState(Game game): base(game)
     4      {
     5      }
     6      public override void Update(GameTime gameTime)
     7      {
     8      }
     9      public override void Draw(GameTime gameTime)
    10      {
    11      }
    12 }
                                        首先的事情我们需要一个SpriteBatch和SpriteFont去显示游戏的结果。
    1 SpriteBatch spriteBatch;
    2 SpriteFont spriteFont;
                                        在结构中,我们将会创建SpriteBatch和加载这个SpriteFont:
    1 spriteBatch = new SpriteBatch(GraphicsDevice);
    2 spriteFont = Content.Load<SpriteFont>("Courier New");
                                        你将会注意到,我们加载一个叫做Courier New的文件。我们可以创建这个文件通过右击在你的Content项目上,并且选择Add New Item.当这个窗口打开,选择Sprite Font并且输入Courier New为这个名字。这可以创建一个新的字体文件在你的内容项目中,使用Courier New为这个字体。

                                        接下来,我们想要修改一下字体。打开Courier New.spritefont文件,并且让我们看一下。它是完全是一个XML,所以它非常容易被编辑。这个模板同样可以包含一些注释来帮助你理解。我们将会开始改变字体的大小为20和设置风格为Bold。
                                        现在让我们填写EndPlayingGameState的draw方法:
    1 public override void Draw(GameTime gameTime)
    2 {
    3      Vector2 centerScreen = new Vector2(GraphicsDevice.Viewport.Width / 2, GraphicsDevice.Viewport.Height / 2);
    4      string result = (Manager.CurrentState == AAGameState.Win)? "WIN!""FAIL!";
    5      Vector2 halfStringSize = spriteFont.MeasureString(result) / 2;
    6      spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
    7      spriteBatch.DrawString(spriteFont,result,centerScreen - halfStringSize,Color.White);
    8      spriteBatch.End();
    9 }
                                        首先我们计算这个屏幕的中心。接着我们测试文本的显示。因为我们使用相同的画面为输和赢。我们检查去看看我们是否赢了并使用它来显示合适的字符串。接下来,我们指出文本的一半大小。然后我们绘制文本到屏幕的中心,使用我们创建的字体。
                                        接着,让我们添加代码到我们的PlayingGameState去引发新的状态,当玩家赢了或输了,我们可以做这个在我们的update方法的内部。首先我们想要调整我们的调用到alienGrid.Update去看看外星飞船是否已经达到了玩家的行:
    1 if (alienGrid.Update(gameTime, alienBullets, GraphicsDevice.Viewport.Width, player1.Bounds.Top))
    2         Manager.CurrentState = AAGameState.Lose;
                                        接着,我们添加这个逻辑到我们的子弹循环中去看看,玩家是否已经死亡或者是否所有的外星飞船死了。让我们考虑下这种情况,所有的外星飞船都死了。我们通过更新我们的foreach循环迭代playerBullets表单来实现这个:
     1 foreach (Bullet b in playerBullets)
     2 {
     3     b.Update();
     4     if (!screenRect.Intersects(b.Bounds))
     5            bulletsToRemove.Add(b);
     6     else if (alienGrid.CollideBullet(b))
     7     {
     8            bulletsToRemove.Add(b);
     9            if (alienGrid.Count == 0)
    10                 Manager.CurrentState = AAGameState.Win;
    11     }
    12 }
                                        这里的关键部分是我们检查看看,alienGrid.Count是否是0。如果他达到了0,我们知道这里没有更多的外星飞船,所以我们选择Manager的CurrentState为Win.现在让我们做一个类似的事情在这个foreach循环迭代alienBullet表单里:
     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            if (!player1.IsAlive)
    12                Manager.CurrentState = AAGameState.Lose;
    13      }
    14 }

                                         你将会看见在这个循环中,我们检查去看看,玩家是否还活着并且选择Lose gamestate,如果是这种情况。
                                        现在让我们添加新的状态在我们的Game1类中,为它代表的两种游戏的状态。这可以放到我们的Initialize方法旁边的地方,这里我们创建了PlayingGameState:

    1 EndPlayingGameState epgs = new EndPlayingGameState(this);
    2 stateManager.GameStates.Add(AAGameState.Win, epgs);
    3 stateManager.GameStates.Add(AAGameState.Lose, epgs);
                                        如果你运行游戏,你会发现,你可以赢或者输全靠你是否丢失你的生命,杀完所有的外星飞船,或者让这些飞船达到屏幕的底部。此时,我们有了基础完整的game-play为Alien Aggressors(外星侵略者).在下面几节里,我们将会覆盖添加一些游戏的惩罚措施。


    (未完,请看下一集)
    源代码:http://www.ziggyware.com/readarticle.php?article_id=170

  • 相关阅读:
    彻底弄懂三段式状态机
    关于verilog的有符号数与无符号数的转换
    关于win10系统安装vivado 2017.1 .2 .3 报runtime error 问题解决办法 亲测有效
    python的列表拷贝
    PYTHON
    保存linux下当前目录下所有文件的相对路径
    Git 命令及使用经验
    讯飞SDK的使用
    DE4加DVI子板实现静态图片显示
    HDL代码风格建议(2)乘法器和DSP推断
  • 原文地址:https://www.cnblogs.com/315358525/p/1564668.html
Copyright © 2011-2022 走看看