[翻译]XNA外文博客文章精选之sixteen(中)
PS:自己翻译的,转载请著明出处
<接上一篇>
外来侵略者
Nick Gravelyn
There's More to a Game than Just the Game
Decisions, Decisions
大多数游戏一般都不会直接进入游戏。所以有一个游戏包含主要的菜单是一个大的步骤正确的趋势。为了完成这个,让我们开始创建一个主要的菜单从头开始我们的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 }
接着,让我们添加一些方法,我们的Menu类可以用来触发这些我们创建的事件:
2 {
3 if (Activate != null)
4 Activate(this, null);
5 }
6 public void PerformOptionLeft()
7 {
8 if (OptionLeft != null)
9 OptionLeft(this, null);
10 }
11 public void PerformOptionRight()
12 {
13 if (OptionRight != null)
14 OptionRight(this, null);
15 }
现在,让我们继续去创建实际的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 }
接着,让我们添加一些不同的选项方法。
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 }
最后,这个菜单创建一个绘制它的方法。
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类,它将使它非常容易为我们去处理输入在我们的菜单上。让我们开始它们的基础的外壳:
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)。这就是我们如何跟踪当前的祯的输入状态,和之前的祯的输入状态。接着,让我们开始通过创建一个方法去更新所有的我们的输入状态。
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 }
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 }
2 {
3 return (KeyboardState.IsKeyDown(key) && LastKeyboardState.IsKeyUp(key));
4 }
2 {Left,Right}
3 public enum ThumbstickAxis
4 {X,Y}
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 }
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 }
2 return (lastValue > threshold && newValue < threshold);
3 else
4 return (lastValue < threshold && newValue > threshold);
最后我们计算出threshold是否已经被算出,如果threshold是在lastValue和newValue之间,并且完成InputHelper类。
Menu Base
因为我们的游戏将会有两个画面,有菜单在它们上面(主菜单和选项),它的意义是创建一个基础的类去在这两个之间共享。这将阻止我们为每个写同样的代码两次类。让我们创建一个新的类叫做MenuBaseGameState:
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 }
现在我们将会跳到Update方法中,它将会大量使用这个InputHelper我们之前写的去处理所有的菜单控制:
2 {
3 }
2 {
3 if (Menu.SelectedItem != null)
4 Menu.SelectedItem.PerformActivate();
5 }
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 }
最后,我们需要去添加逻辑到实际滚动菜单项。同样我们将会使用任何游戏pad的thumbstick或者DPad以及使用Up/Down箭头和W/S键盘键去允许玩家一个最大数量的输入选择。
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 }
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.我们同样将会填满这个结构去为我们创建所有的菜单选择。
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 }
你将会看见我们创建所有我们的四个菜单选项和挂起事件处理到某些方法中。接着让我们添加所有的处理这些选项的方法:
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里找。
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 }
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 }
2 {
3 InputHelper.Update();
4 base.Update(gameTime);
5 }
现在你可以运行你的游戏了,可以看看你辛苦工作的成果。你将会看见我们的主菜单,你可以使用游戏pads以及键盘来浏览。你可以选择Quit选项和激活它去退出这个游戏。接着让我们配合我们的MainMenuGameState在我们的PlayingGameState,这样我们可以开始我们的新游戏。
Starting Something
现在我们有一个菜单,我们需要允许它开始我们的游戏。为了开始这个,首先我们首先需要添加重置PlayingGameState的能力.这将允许我们开始多个游戏,不用退出游戏。让我们开始通过分解一些PlayingGameState的结构成一个新的方法叫做Initialize:
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"), 10, 5);
14 }
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方法中:
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 }
2 Vector2 halfInfoSize = spriteFont.MeasureString(info) / 2;
3 Vector2 infoPos = centerScreen - halfInfoSize + new Vector2(0f, 100f);
我们添加一些新变量包括信息字符串并且布置它在屏幕上,在我们先前绘制的结果字符串的下面一点。现在你可以玩这个游戏,你想玩多少次就玩多少次,每次不需要重新开始整个程序。
Fleshing out the Rest of the Game(充实剩下的游戏代码)
Bring a Friend
接着,让我们添加第二个玩家。这是全部一个非常简单的过程,但是将允许我们有更多的乐趣在我们的游戏中。首先确保你已经添加了player2.png精灵到你的游戏的内容项目中,然后让我们head over到PlayingGameState并且添加另外的玩家到我们的类的数据中:
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"), 10, 5);
11 }
2 player2.Update(gameTime, playerBullets, GraphicsDevice.Viewport.Width);
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 }
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 }
接着,我们将会更新draw代码为我们的玩家去确保他们不能绘制,当死了并确保玩家2被绘制:
2 player1.Draw(spriteBatch);
3 if (coOp && player2.IsAlive)
4 player2.Draw(spriteBatch);
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为了绘制它。
2 SpriteFont spriteFont;
2 {
3 //
4 if (newGame)
5 {
6 score = 0;
7 }
8 }
2 {
3 //
4 else if (alienGrid.CollideBullet(b))
5 {
6 //
7 score += 100;
8 }
9 }
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 Vector2 halfScoreSize = spriteFont.MeasureString(score) / 2;
3 Vector2 scorePos = centerScreen - halfScoreSize - new Vector2(0f, 100f);
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 }
2 {
3 GameState loseState = Manager.GameStates[AAGameState.Lose];
4 (loseState as EndPlayingGameState).FinalScore = score;
5 }
完成后我们现在看看在游戏完成后,我们的游戏做的如何。
Waves of Fleets
我们的游戏已经接近完成。所有我们需要添加的是支持多关卡在这个游戏中。毕竟它只有一个波浪的敌人去射击是相当乏味的。让我们创建一个系统,它给我们的游戏多个关卡,你几乎任何的限制。现在我们将会限制我们10。为了开始让我们转倒PlayingGameState类并且添加这两个新的变量:
2 const int maxLevel = 10;
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:
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 }
我们的游戏现在有10关,然后它是非常生硬的在他们之间转换。我们可以修正这个通过添加一个新的类来作为过渡在这些关之间。让我们添加一个TransitionGameState到我们的游戏项目中:
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 }
现在让我们创建Update方法去处理我们的记时器逻辑:
2 {
3 transitionTimer += (float)gameTime.ElapsedGameTime.TotalSeconds;
4 if (transitionTimer >= transitionLength)
5 {
6 transitionTimer = 0f;
7 Manager.CurrentState = AAGameState.Playing;
8 }
9 }
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 }
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 }
接着我们需要更新我们的PlayingGameState去使用这个过渡在关卡之间。这将使游戏非常容易理解,通过允许玩家级时去准备为一个新的一波敌人。让我们转到我们更新我们的关卡并且计划它去利用这个过渡游戏状态:
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里。
Make Some Noise
我们的游戏接近完成,但是我们失去了经历的大部分,它来自游戏:声音。我们当前有一个十分安静的太空战争,而可能是准确的科学,难道不是十分有趣吗。现在让我们添加些声音到这里面。作为说明,本节将涵盖如何去处理我们在游戏代码中的声音。有关创建声音的效果和设置XACT项目。请看Appendix B。
第一步添加音频去确保我们有必须做这个的文件。我们需要开始通过添加所有文件从Audio文件夹找到AlienAggressorsContent.zip文件夹到我们的Content文件夹。这是文件夹包含我们的WAV文件和XACT项目文件需要通过我们的游戏。在这点上,你的内容目录应该包含所有的这些文件。
接着进入你的游戏中的Content项目,右击,并选择Add Existing选项并且添加XACT项目文件到内容中。你需要做这个,或者想要,去添加他们的WAV文件;他们引用XACT项目,这样,我们需要他们在这个项目的目录中,我们不想要他们在游戏的项目中。我们不想要它们在游戏项目中。
现在,我们有声音内容块,让我们开始在我们的支持的声音里计划下。我们将会实现这个通过创造一个SoundManager类,它将包含并控制所有的声音数据为我们的游戏:
2 {
3 static AudioEngine engine;
4 static WaveBank waveBank;
5 static SoundBank soundBank;
6 static Cue music;
7 }
我们的管理只包含三段音频系统(一个AuidoEngine,WaveBank,和SoundBank)以及一个Cue为我们的背景音乐。
接着,让我们创建一个Initialize方法,它将加载所有的这些文件为我们:
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中,这样我们可以得到游戏的内容根目录。这将允许我们去改变内容项目的根目录,每一次不用必须去改变所有三个文件路径。
现在,让我们添加一个方法去播放我们其中之一个的声音。
2 {
3 soundBank.PlayCue(name);
4 }
它非常的简单正如你看见的,我们刚才调用了PlayCue在soundBand并且传入了相同的声音,我们想要播放的。接着让我们添加一些方法去控制我们的背景音乐:
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在我们停止它之前并且设置它为空。
现在让我们添加一对方法去控制我们的声音的音量。
2 {
3 engine.GetCategory("SoundFx").SetVolume(volume);
4 }
5 public static void SetMusicVolume(float volume)
6 {
7 engine.GetCategory("Music").SetVolume(volume);
8 }
最后的方法我们需要写的是一个Update方法,它将更新声音系统和确保我们的声音能正确的播放:
2 {
3 engine.Update();
4 }
为了把它溶入我们的游戏中,我们首先需要确保我们调用SoundManager.Initialize在我们的Game1.Initialize方法中:
2 {
3 InputHelper.Update();
4 SoundManager.Update();
5 base.Update(gameTime);
6 }
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 }
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 }
最后,让我们播放背景音乐为这个游戏。让我们转到我们的Game1.Initialize语句中,并且添加这个调用到SoundManager.PlayMusic:
现在如果我们运行这个游戏我们发现所有的行为创建声音的反馈和我们有一些fase-paced背景音乐。
Everyone Loves Options
现在,我们有了音频在我们的游戏中,让我们添加在选项中为这个游戏,这样玩家可以选择他们想要音频如何大声。并且同样他们是否要全屏幕。让我们开始通过添加一个新的类叫做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 }
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);
2 {
3 soundFxVolume = (int)Math.Max(soundFxVolume - 10, 0);
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 + 10, 100);
10 soundFxItem.Name = string.Format("Sound FX Volume: {0}%", soundFxVolume);
11 SoundManager.SetSoundFXVolume(soundFxVolume / 100f);
12 }
接着,我们做这个音乐音量在某种意义上:
2 {
3 musicVolume = (int)Math.Max(musicVolume - 10, 0);
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 + 10, 100);
10 musicItem.Name = string.Format("Music Volume: {0}%", musicVolume);
11 SoundManager.SetMusicVolume((float)musicVolume / 100f);
12 }
2 {
3 GraphicsManager.ToggleFullScreen();
4 }
2 {
3 Manager.CurrentState = AAGameState.MainMenu;
4 }
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(null, null);
6 }
7 base.Update(gameTime);
8 }
现在我们的类被设立,让我们添加代码在Game1.Initialize去为我们创建新的状态:
2 {
3 Manager.CurrentState = AAGameState.Options;
4 }
Take a Break
最后一项,我们想做的是允许我们的游戏可以被暂停。这是一个很好的方式实现这个,当你正在游戏的中间时(译者:打游戏时,想上厕所),电话响了后者可能你的晚饭开始了。为了实现暂停功能系统,我们将创建一个新的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 }
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 }
2 {
3 Manager.CurrentState = AAGameState.Paused;
4 return;
5 }
这段代码不仅设置我们的状态到暂停,而且它退出update方法,确保这个游戏在没有更新其余的PlayingGameState下很快的暂停。
Meeting Expectations
在这点上,我们现在可以暂停我们的游戏并且选择继续它。我们有的一个问题是,我们的暂停菜单不能重新设置选择的项目。所以如果你退出这个主菜单,开始一个新的游戏,点击暂停,你将会看见,Quit To Main Menu对象被选择。这不是大多数游戏期望的,所以我们将会添加一个新的方法到Menu类,让我们设置选择我们想要的选项:
2 {
3 if (index >= 0 && index < Items.Count)
4 {
5 currentItem = index + 1;
6 SelectPrevious();
7 }
8 }
现在,我们只必须更新我们的PausedGameState事件处理重新设置菜单选项在改变状态之前:
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 }
2 {
3 Menu.SelectItem(0);
4 Manager.CurrentState = AAGameState.MainMenu;
5 }
现在我们的菜单已经建立,以更好地满足使用这些玩家的期望。
在这点上我们的游戏已经完成!我们有了菜单,暂停,关卡,co-op游戏,和end-game状态画面,和声音,它接受一个整体但我们完全控制游戏,完成一个小乐趣的游戏。
结论
现在,我们已经经过一个相当长创建游戏的过程。希望它为你变的很整洁,读者,如果扩展这个例子和其他去创建这个你想制造的游戏。游戏开发不是能让初学者很快掌握的,但是它是一个非常有趣和有意义的。
《未完,请继续收看》
源代码:http://www.ziggyware.com/readarticle.php?article_id=170