[翻译]XNA外文博客文章精选之sixteen(上)
PS:自己翻译的,转载请著明出处
外来侵略者
Nick Gravelyn
前言
介绍
每一天都有很多很多的人开始游戏开发的旅程。一些是小孩和年轻人通过他们喜欢的视频游戏得到灵感,虽然他们没有计算机编程的经验。还有一些是专业的程序开发者就为了找某些不同的地方。在这两种情况下一个类似的问题通常就产生了。如何学习创建一个游戏的过程呢?
视频游戏从外面看似乎很简单。毕竟与玩家的输入相比他们一点不多于在屏幕上绘制图象。但是游戏制作对许多初学者不是一个马上就容易完成的任务。一些人被详细的代码所困住,例如面向对象的设计,和避免静态数据同时其他努力去明白如何去控制游戏代码的流程。这些或者其他障碍导致一开始的挫败是一个初学者面对的普遍的难题。就是因为这些,许多初学者放弃了制造一个游戏。
这本书目的是帮助那些希望开始创建他们自己的游戏的人。通过这本书,读者将会创进一个完整功能的游戏,我们的游戏显示出一组外星飞船,他们在屏幕上左右移动,并且朝着底部玩家走来很象一款游戏叫做SpaceInvaders(外星入侵者)。我们的游戏将不会配置的任何障碍,但是会代替混合初始的Space Invaders游戏也有快速射击列如Galaxian.我们的游戏,虽然,通过在Galaxian的敌人飞船,不回出现kamikaze dives表演。
在我们继续这本书之前,让我们看下这本书对读者的期望。
Expectations(期望)
略
现在我们在同一页面,让我们开始我们的Alien Aggressor(外来侵略者)的工作
创建项目
象所有的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,这样精灵能够被绘制。下一项目是一个位置,这样我们可以左右移动精灵在屏幕上。最后我们需要一个变量去记录精灵的中心位置,这样我们的位置可以引用我们想要的精灵的中心。让我们继续并且添加这些项目到我们的类:
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,这样我们可以创建一个实例。
2 {
3 texture = spriteTexture;
4 center = new Vector2(spriteTexture.Width / 2,spriteTexture.Height / 2);
5 }
最后,我们需要去添加一个简单的绘制方法到我们的Sprite中。在我们系统里,我们将会一起把所有的绘制对象放入单一的SpriteBatch.为了完成这个。我们的Draw方法将会接收一个SpriteBatch作为一个参数并且只调用SpriteBatch.Draw:
2 {
3 spriteBatch.Draw(texture,new Vector2((int)Position.X, (int)Position.Y),null,Color.White,0f,center,1f,SpriteEffects.None,0);
4 }
现在,让我们添加一个新的图象到我们的Content项目中。我们将会添加玩家1.png图象将会被使用去代表第一个玩家的飞船。为了实现这个。右击在Content子节点上,选择Existing Item从Add菜单中,并且选择这个player1.png文件。这个文件将会复制到你的内容目录上并且添加到项目中。
现在我们有一个图象被加载。我们可以测试我们的新Sprite类。打开Game1.cs文件并且添加一个Sprite实例到你的游戏文件里:
2 {
3 spriteBatch = new SpriteBatch(GraphicsDevice);
4 player1 = new Sprite(Content.Load<Texture2D>("player1"));
5 player1.Position = new Vector2(100f);
6 }
2 player1.Draw(spriteBatch);
3 spriteBatch.End();
现在创建运行你的项目,你可以看见一个单一的飞船在蓝色屏幕的中心。让我们继续通过制造背景更多的为我们的游戏的外层空间做一点计算。
Creating a StarryBackground(创建一个布满星星的背景)
外来侵略者被设置在外太空,所以使用这个默认的CornflowerBlue颜色为这个背景,我们可以简单的改变清除这个颜色改为黑色更好的代表太空,但是这可能仍然是一个无聊的背景。因此要解决这个问题,我们将创建一个类,它来自Texture2D,它为背景将随机产生星星的位置。
首先添加一个新的类到项目中,命名为StarryBackground并且使这个类来自于Texture2D.
2 {
3 }
2 {
3 }
这个构造将会分配一个被要求大小的纹理,使用一些公值为这个多重映像水平,TextureUsage,和SurfaceFormat.虽然这些是common values(公值),一些machines(设计)也许不会接收它们。如果这里抛出一个异常在你的设计中,你也许必须调整一个或多个参数以适应你的图形卡。
接着,让我们添加这些星星。默认的一个空白的纹理是一个大的透明黑色象素的数组。意思是每一个象素的R,G,B和A值都是0,我们的构造器要做的是随机的地方着色象素在这个纹理上,获得一个星空的图象。我们做这个使用一个Random类的实例,并且循环所有的象素直到我们已经添加这些被要求的数量。最后我们调用SetData去设置我们的纹理的象素数据。
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);
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方法为我们的子类型去使用。
2 {
3 public Ship(Texture2D spriteTexture): base(spriteTexture)
4 {
5 }
6 public virtual void Update(GameTime gameTime)
7 {
8 }
9 }
这是所有我们直到现在将会对于这个Ship类做的。所以让我们添加其他的一些类到我们的项目中命名为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可以左右移动在这个屏幕。
2 {
3 GamePadState gps = GamePad.GetState(index);
4 Position.X += gps.ThumbSticks.Left.X * 3;
5 base.Update(gameTime);
6 }
现在我们可以测试出这个通过改变我们的玩家1对象成一个PlayerShip.所以在我们的Game1.cs文件中,我们需要去改变声明。
同样我们需要去更新我们的实例代码。
2 player1 = new PlayerShip(Content.Load<Texture2D>("player1"), PlayerIndex.One);
最后,我们需要去添加一个调用player1.Update到我们的游戏的更新方法中:
2 {
3 if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
4 this.Exit();
5 player1.Update(gameTime);
6 base.Update(gameTime);
7 }
首先让我们添加一个新的属性叫做Bounds到Sprite类中。这个属性简单的返回一个新的Rectangle设置到精灵的属性大小。
2 {
3 get
4 {
5 return new Rectangle((int)(Position.X - center.X),(int)(Position.Y - center.Y),texture.Width,texture.Height);
6 }
7 }
这个fieldWidth变量将会习惯于告诉Ship,我们玩的地方有多宽。接着让我们更新我们的PlayerShip.Update方法去使用这个参数并且我们新的Bounds属性去保持屏幕上的玩家。
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 }
到这里
现在你的玩家只能在屏幕上移动
Adding Keyboard Input(添加键盘的输入)
一些开发者和用户喜欢键盘。原因是可以简单不用Xbox360游戏机,或者它可以是他们喜欢的输入设备。幸运的是XNA Game Studio开发者,添加键盘的操作输入不是一个大的挑战。
让我们更新我们的PlayShip类能够接收键盘的输入。我们将会创建一对新的方法:一个为GamePad驱动输入,另一个为键盘驱动输入。我们将选择这两种输入的方式。这里有两个方法我们准备添加到我们的PlayerShip类中:
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 }
现在我们可以更新PlayerShip.Update方法去利用这个新方法。
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到我们的项目中。
2 {
3 }
每个游戏状态将会表现出象一个迷你型的游戏类实例。为此,我们需要去揭露一些基础的在类中的数据,为这些来自类使用的数据。我们当前不能有一个GameStateManager类型的定义,但是我们添加它在下一节里,所以不要担心它,Visual Studio不会高亮打印它的名字。
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实例并且重新找回这个数据。
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去更新并且绘制每个状态:
2 public abstract void Draw(GameTime gameTime);
现在我们有一个简单的GameState类,我们可以起源于它去创建我们的游戏一小部整体代码。
The GameStateManager Class
现在我们有一个GameState对象,让我们创建我们使用的GameStateManager类型在GameState类中
2 {
3 public GameStateManager(Game game): base(game)
4 {
5 }
6 }
我们现在创建一个非常基础的DrawableGameComponent外壳为我们去保存游戏的状态。为了实现这个,我们首先需要去添加一个枚举到我们的游戏中,我们将调用AAGameState(为外来侵略者游戏状态)。这个枚举将会被使用去设置或者改变当前的状态在GameStateManager.这里是我们的枚举的样子:
2 {
3 MainMenu,Options,LevelTransition,Playing,Paused,Win,Lose,
4 }
2 {
3 public Dictionary<AAGameState, GameState> GameStates =new Dictionary<AAGameState, GameState>();
4 public AAGameState CurrentState = AAGameState.MainMenu;
5 }
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类。
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 }
2 PlayerShip player1;
2 {
3 spriteBatch = new SpriteBatch(GraphicsDevice);
4 player1 = new PlayerShip(Content.Load<Texture2D>("player1"), PlayerIndex.One);
5 player1.Position = new Vector2(100f);
6 }
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 }
2 SpriteBatch spriteBatch;
3 StarryBackground stars;
4 GameStateManager stateManager;
接着,我们将会更新这个游戏的构造器不仅创建这个GameStateManager,也会去注册它和ContentManager作为服务:
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 }
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矩形。
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:
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 }
2 float fireRate = .4f;
3 public float FireRate
4 {
5 get { return fireRate; }
6 set { fireRate = (float)Math.Max(value, .01f); }
7 }
接着我们需要一些值去了解子弹从哪里发射,给它们的初始速度是多少,它们是什么颜色:
2 public Vector2 BulletVelocity = Vector2.UnitY;
3 public Vector2 BulletOrigin = Vector2.Zero;
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 }
2 {
3 fireTimer -= (float)gameTime.ElapsedGameTime.TotalSeconds;
4 }
现在我们必须更新这个PlayerShip类去使用这个新的Update方法,让我们发射子弹。让我们首先定位Update方法去声明以配合这个Ship类:
2 {
3 public override void Update(GameTime gameTime, List<Bullet> bullets, int fieldWidth)
4 { }
5 }
2 BulletVelocity = new Vector2(0f, -5f);
3 BulletOrigin = new Vector2(0, -Bounds.Height / 2);
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 }
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 }
2 Bullet.Origin = new Vector2(Bullet.Texture.Width / 2, Bullet.Texture.Height / 2);
2 {
3 player1.Update(gameTime, playerBullets, GraphicsDevice.Viewport.Width);
4 Rectangle screenRect = new Rectangle(0, 0, 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 }
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类:
2 {
3 public AlienShip(Texture2D spriteTexture): base(spriteTexture)
4 {
5 }
6 }
首先我们需要设置固定的子弹值在我们的构造器中:
2 {
3 BulletOrigin = new Vector2(0, 16);
4 BulletVelocity = new Vector2(0, 5);
5 BulletColor = Color.Blue;
6 }
2 public int ChanceToFire = 2;
2 {
3 if (rand.Next(1000) < ChanceToFire)
4 Fire(bullets);
5 base.Update(gameTime, bullets, fieldWidth);
6 }
一旦这个做了,让我们回到PlayingGameState类并且添加一个行的子弹表单和外星飞船:
2 AlienShip alienShip;
接着,让我们创建一个外星的飞船在我们的结构中:
2 alienShip.Position = new Vector2(GraphicsDevice.Viewport.Width / 2 - alienShip.Bounds.Width / 2,alienShip.Bounds.Height);
2 {
3 player1.Update(gameTime, playerBullets, GraphicsDevice.Viewport.Width);
4 alienShip.Update(gameTime, alienBullets, GraphicsDevice.Viewport.Width);
5 Rectangle screenRect = new Rectangle(0, 0, 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 }
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>>的表单中:2 {
3 }
在这个图表中,我们可以看做一个单一的内部,红色框作为一个<AlienShip>列表。它是一个单一的外星飞船列。外部的,蓝色框是<List<AlienShip>>列表(AlienGrid类它本身),因为它包含多个外星飞船列。
现在,我们有一个基础AlienGrid结构的了解。让我们开始这个过程。首先我们需要添加一对数据到这个类中:
2 public float Velocity = 1f;
接着,让我们添加实体到这个类中:Update方法。这里完整的代码,我们将会把它切成小块分析:
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 }
2 return false;
2 AlienShip rightMost = this[this.Count - 1][0];
2 {}
2 Velocity = MathHelper.Clamp(Velocity * -velocityChange, -MaxSpeed, MaxSpeed);
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;
这里面的Update方法,让我们创建一个初始方法,它将会很快的产生一组我们选择大小的外星飞船。
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 }
现在我们正准备去添加一个Draw方法去允许我们很快的绘制整个外星组:
2 {
3 foreach (List<AlienShip> column in this)
4 foreach (AlienShip s in column)
5 s.Draw(spriteBatch);
6 }
现在当你运行这个游戏,你可以有一个整体的外星飞船舰队来回移动在屏幕上,大量下降的子弹飞向玩家。你将会注意到整个单一的飞船不会发射这么多,屏幕上有很多飞船,它仍然有大量的子弹。在这点上,在游戏里几乎是同时。接着我们将会添加一个分数和生命系统到这个游戏里,同时有赢或输的能力。
Taking Damage
现在,我们有一些子弹和敌人,让我们开始添加所需要的数据去使我们的游戏有更多挑战。为了实现这个,我们需要允许玩家失败。让我们开始并添加一些数据到PlayerShip类中。我们将会添加大量额外的生命到这个玩家,有一个属性去检测玩家是否还活着。所以添加这些代码到你的PlayerShip类中:
2 public bool IsAlive
3 {
4 get { return ExtraLives >= 0; }
5 }
现在,适当的数据在适当的位置,我们需要开始看下,所有的这些子弹实际上是否撞到什么东西没。让我们添加一个新的方法到我们的Ship类去告诉,是否子弹已经撞击到了飞船:
2 {
3 return Bounds.Contains((int)b.Position.X, (int)b.Position.Y);
4 }
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 }
现在我们可以改变在我们的PlayingGameState的Update方法中的代码去允许我们杀死敌人的飞船。所以我们需要做的是找到foreach循环,它迭代playerBullets列表并且更新它去调用这个AlienGrid的CollideBullet方法:
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表,去响应玩家被击中:
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:
2 player1.Update(gameTime, playerBullets, GraphicsDevice.Viewport.Width);
只有一个可以赢
在此时,外星飞船可以射击玩家,玩家获得重设置并且玩家可以射击外星飞船直到他们都被消灭。但是这仍然不能算赢。它只留下玩家一个人在太空中,没有什么事干。让我们继续去创建一个新的游戏状态叫做EndPlayingGameState:
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 }
2 SpriteFont spriteFont;
2 spriteFont = Content.Load<SpriteFont>("Courier New");
接下来,我们想要修改一下字体。打开Courier New.spritefont文件,并且让我们看一下。它是完全是一个XML,所以它非常容易被编辑。这个模板同样可以包含一些注释来帮助你理解。我们将会开始改变字体的大小为20和设置风格为Bold。
现在让我们填写EndPlayingGameState的draw方法:
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去看看外星飞船是否已经达到了玩家的行:
2 Manager.CurrentState = AAGameState.Lose;
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 }
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:
2 stateManager.GameStates.Add(AAGameState.Win, epgs);
3 stateManager.GameStates.Add(AAGameState.Lose, epgs);
(未完,请看下一集)
源代码:http://www.ziggyware.com/readarticle.php?article_id=170