您的位置:首页 > 其它

XNA4.0 RPG游戏开发教程(二)

2016-03-02 17:48 447 查看
这个系列教程适合有一定C#和XNA4.0基础的朋友,如果没有学习过C#,那么《C#高级编程(第7版)》是一本不错的入门书籍,在http://pan.baidu.com/s/1sko1FnJ可以找到PDF版本,如果你从未接触过XNA4.0,那么《XNA4.0学习指南》是一本不错的入门书籍,在http://pan.baidu.com/s/1baJbWE可以找到PDF版本和随书源代码。

翻译国外的系列教程,一步步讲述如何用XNA4.0开发RPG游戏,是XNA4.0游戏开发为数不多的具有实战意义的教程。

原文地址:http://xnagpa.net/xna4rpg.php

作者非常耐心,每一步操作都讲的很详细,每一课都附有源代码。

我在翻译的过程中,根据作者的教程,又重新把每一课的源代码写一遍,目的是为了验证作者的源代码。

我重写的每一课源代码下载:http://pan.baidu.com/s/1c1tB2Sw

在这个系列教程的前几课,为了照顾初学者,我会加入操作截图,后续的课程我将假定你使用XNA4.0框架已经比较熟练了,所以不会再添加截图。

二、更多游戏组件

我正在写一些XNA4.0框架的教程,这些教程如果按照顺序阅读将会起到更好的效果。

这是这个系列教程(XNA4.0 RPG游戏开发)的第二部分,在这一课我将添加更多的游戏组件,打开我们上一课未完成的解决方案。

首先我们要更新InputHandler.cs,添加对游戏手柄的支持。完整的代码如下

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
namespace XRpgLibrary
{
public class InputHandler : Microsoft.Xna.Framework.GameComponent
{
#region Keyboard Field Region
static KeyboardState keyboardState;
static KeyboardState lastKeyboardState;
#endregion
#region Game Pad Field Region
static GamePadState[] gamePadStates;
static GamePadState[] lastGamePadStates;

#endregion
#region Keyboard Property Region
public static KeyboardState KeyboardState
{
get { return keyboardState; }
}
public static KeyboardState LastKeyboardState
{
get { return lastKeyboardState; }
}
#endregion
#region Game Pad Property Region
public static GamePadState[] GamePadStates
{
get { return gamePadStates; }
}
public static GamePadState[] LastGamePadStates
{
get { return lastGamePadStates; }
}
#endregion
#region Constructor Region
public InputHandler(Game game)
: base(game)
{
keyboardState = Keyboard.GetState();
gamePadStates = new GamePadState[Enum.GetValues(typeof(PlayerIndex)).Length];
foreach (PlayerIndex index in Enum.GetValues(typeof(PlayerIndex)))
gamePadStates[(int)index] = GamePad.GetState(index);
}
#endregion
#region XNA methods
public override void Initialize()
{
base.Initialize();
}
public override void Update(GameTime gameTime)
{
lastKeyboardState = keyboardState;
keyboardState = Keyboard.GetState();
lastGamePadStates = (GamePadState[])gamePadStates.Clone();
foreach (PlayerIndex index in Enum.GetValues(typeof(PlayerIndex)))
gamePadStates[(int)index] = GamePad.GetState(index);
base.Update(gameTime);
}
#endregion
#region General Method Region
public static void Flush()
{
lastKeyboardState = keyboardState;
}
#endregion
#region Keyboard Region
public static bool KeyReleased(Keys key)
{
return keyboardState.IsKeyUp(key) && lastKeyboardState.IsKeyDown(key);
}
public static bool KeyPressed(Keys key)
{
return keyboardState.IsKeyDown(key) &&
lastKeyboardState.IsKeyUp(key);
}
public static bool KeyDown(Keys key)
{
return keyboardState.IsKeyDown(key);
}
#endregion
#region Game Pad Region
public static bool ButtonReleased(Buttons button, PlayerIndex index)
{
return gamePadStates[(int)index].IsButtonUp(button) &&
lastGamePadStates[(int)index].IsButtonDown(button);
}
public static bool ButtonPressed(Buttons button, PlayerIndex index)
{
return gamePadStates[(int)index].IsButtonDown(button) &&
lastGamePadStates[(int)index].IsButtonUp(button);
}
public static bool ButtonDown(Buttons button, PlayerIndex index)
{
return gamePadStates[(int)index].IsButtonDown(button);
}
#endregion
}
}
对于XBOX360来说,可能有4个游戏手柄,所以我使用数组来保存每个手柄的输入,gamePadStates当前帧手柄的输入,lastGamePadStates上一帧手柄的输入,这是与键盘最大的区别,因为键盘通常只有一个,其他的代码都与键盘输入处理类似,他们也都是静态字段、属性、方法。

接下来我要添加一些GUI控件,以及一个管理这些控件的类。游戏开发与winform开发有很大的不同,winform开发有很多现成的控件使用,而游戏开发没有任何现成的控件,所有的控件都要自己编写。用一个类来管理每一个游戏状态里所有的控件是很有必要的,首先创建一个所有控件的基类,在“解决方案资源管理器”中右键点击“XRpgLibrary”项目,选择“添加”——“新建文件夹”,给这个文件夹命名为“Controls”,右键点击这个文件夹,选择“添加”——“新建项”——“类”,名字输入“Control”。这个类的代码如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace XRpgLibrary.Controls
{
public abstract class Control
{
#region Field Region
protected string name;
protected string text;
protected Vector2 size;
protected Vector2 position;
protected object value;
protected bool hasFocus;
protected bool enabled;
protected bool visible;
protected bool tabStop;
protected SpriteFont spriteFont;
protected Color color;
protected string type;
#endregion
#region Event Region
public event EventHandler Selected;
#endregion
#region Property Region
public string Name
{
get { return name; }
set { name = value; }
}
public string Text
{
get { return text; }
set { text = value; }
}
public Vector2 Size
{
get { return size; }
set { size = value; }
}
public Vector2 Position
{
get { return position; }
set
{
position = value;
position.Y = (int)position.Y;
}
}
public object Value
{
get { return value; }
set { this.value = value; }
}
public bool HasFocus
{
get { return hasFocus; }
set { hasFocus = value; }
}
public bool Enabled
{
get { return enabled; }
set { enabled = value; }
}
public bool Visible
{
get { return visible; }
set { visible = value; }
}
public bool TabStop
{
get { return tabStop; }
set { tabStop = value; }
}
public SpriteFont SpriteFont
{
get { return spriteFont; }
set { spriteFont = value; }
}
public Color Color
{
get { return color; }
set { color = value; }
}
public string Type
{
get { return type; }
set { type = value; }
}
#endregion
#region Constructor Region
public Control()
{
Color = Color.White;
Enabled = true;
Visible = true;
SpriteFont = ControlManager.SpriteFont;
}
#endregion
#region Abstract Methods
public abstract void Update(GameTime gameTime);
public abstract void Draw(SpriteBatch spriteBatch);
public abstract void HandleInput(PlayerIndex playerIndex);
#endregion
#region Virtual Methods
protected virtual void OnSelected(EventArgs e)
{
if (Selected != null)
{
Selected(this, e);
}
}
#endregion
}
}
这个类有很多保护字段,他们对所有的控件是通用的,通过对应的属性将这些字段公开,所以他们在继承类里可以被重写。OnSelected方法用来触发Selected事件。有3个虚方法在继承类里必须要实现:Update,更新控件;Draw,绘图;HandleInput,处理控件的输入。我们定义的这个控件与winform编程常用的控件有很多相似的字段、属性,比如Name、Text、Value、HasFocus、Enabled、Visible、TabStop等,他们的作用也类似。

需要提到的是,在XNA游戏框架内,用SpriteFont来表示游戏内显示文字内容的字体,Color表示文字内容的颜色。关于SpriteFont的详细解释请参考《XNA4.0学习指南》。

ControlManager是接下来我们要添加的类,右键点击“Controls”文件夹,选择“添加”——“新建项”——“类”,名字输入“ControlManager”。这个类的代码如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace XRpgLibrary.Controls
{
public class ControlManager : List<Control>
{
#region Fields and Properties
int selectedControl = 0;
static SpriteFont spriteFont;
public static SpriteFont SpriteFont
{
get { return spriteFont; }
}
#endregion
#region Constructors
public ControlManager(SpriteFont spriteFont)
: base()
{
ControlManager.spriteFont = spriteFont;
}
public ControlManager(SpriteFont spriteFont, int capacity)
: base(capacity)
{
ControlManager.spriteFont = spriteFont;
}
public ControlManager(SpriteFont spriteFont, IEnumerable<Control> collection) :
base(collection)
{
ControlManager.spriteFont = spriteFont;
}
#endregion
#region Methods
public void Update(GameTime gameTime, PlayerIndex playerIndex)
{
if (Count == 0)
return;
foreach (Control c in this)
{
if (c.Enabled)
c.Update(gameTime);
if (c.HasFocus)
c.HandleInput(playerIndex);
}
if (InputHandler.ButtonPressed(Buttons.LeftThumbstickUp, playerIndex) ||
InputHandler.ButtonPressed(Buttons.DPadUp, playerIndex) ||
InputHandler.KeyPressed(Keys.Up))
PreviousControl();
if (InputHandler.ButtonPressed(Buttons.LeftThumbstickDown, playerIndex) ||
InputHandler.ButtonPressed(Buttons.DPadDown, playerIndex) ||
InputHandler.KeyPressed(Keys.Down))
NextControl();
}
public void Draw(SpriteBatch spriteBatch)
{
foreach (Control c in this)
{
if (c.Visible)
c.Draw(spriteBatch);
}
}
public void NextControl()
{
if (Count == 0)
return;
int currentControl = selectedControl;
this[selectedControl].HasFocus = false;
do
{
selectedControl++;
if (selectedControl == Count)
selectedControl = 0;
if (this[selectedControl].TabStop && this[selectedControl].Enabled)
break;
} while (currentControl != selectedControl);
this[selectedControl].HasFocus = true;
}
public void PreviousControl()
{
if (Count == 0)
return;
int currentControl = selectedControl;
this[selectedControl].HasFocus = false;
do
{
selectedControl--;
if (selectedControl < 0)
selectedControl = Count - 1;
if (this[selectedControl].TabStop && this[selectedControl].Enabled)
break;
} while (currentControl != selectedControl);
this[selectedControl].HasFocus = true;
}
#endregion
}
}
这个类是继承于List<T>,它将具有List<T>所有的字段、属性、方法,这样在添加、删除控件的时候使用List<T>自带的方法即可。

List<T>类具有3个构造函数,所以在这个类里我也写了3个具有类似参数的构造函数,selectedIndex字段保存当前选中的控件的序号。

Update方法用来更新控件和处理输入,Draw方法用来绘制控件,NextControl和PreviousControl方法用来在控件之间移动。

NextControl和PreviousControl方法目前还存在一些问题可能导致异常,后续我将更新他们。

译者注:break用于跳出当前循环。

接下来我们要添加具体的控件了,首先添加一个Label控件,用于在游戏里显示文字,右键点击“Controls”文件夹,选择“添加”——“新建项”——“类”,名字输入“Label”,代码如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace XRpgLibrary.Controls
{
public class Label : Control
{
#region Constructor Region
public Label()
{
tabStop = false;
}
#endregion
#region Abstract Methods
public override void Update(GameTime gameTime)
{
}
public override void Draw(SpriteBatch spriteBatch)
{
spriteBatch.DrawString(SpriteFont, Text, Position, Color);
}
public override void HandleInput(PlayerIndex playerIndex)
{
}
#endregion
}
}
这个类比较简单,在构造函数里,将tabStop字段默认设置为false,这样Label控件默认无法被选中。在Draw方法中调用spriteBatch.DrawString方法绘制文字。

接下来再添加一个LinkLabel控件,这个控件与Label很相似,但是它可以被选中。选择“添加”——“新建项”——“类”,名字输入“LinkLabel”,代码如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace XRpgLibrary.Controls
{
public class LinkLabel : Control
{
#region Fields and Properties
Color selectedColor = Color.Red;
public Color SelectedColor
{
get { return selectedColor; }
set { selectedColor = value; }
}
#endregion
#region Constructor Region
public LinkLabel()
{
TabStop = true;
HasFocus = false;
Position = Vector2.Zero;
}
#endregion
#region Abstract Methods
public override void Update(GameTime gameTime)
{
}
public override void Draw(SpriteBatch spriteBatch)
{
if (hasFocus)
spriteBatch.DrawString(SpriteFont, Text, Position, selectedColor);
else
spriteBatch.DrawString(SpriteFont, Text, Position, Color);
}
public override void HandleInput(PlayerIndex playerIndex)
{
if (!HasFocus)
return;
if (InputHandler.KeyReleased(Keys.Enter) ||
InputHandler.ButtonReleased(Buttons.A, playerIndex))
base.OnSelected(null);
}
#endregion
}
}
这个类也不算太复杂,增加了一个字段selectedColor,表示LinkLabel被选中时的文字颜色。

在构造函数里,TabStop属性被默认设置为true,这样LinkLabel控件默认可以被选中;HasFocus属性默认为false,即未选中状态。在Draw方法里,如果未选中按通常颜色绘制,如果被选中则按指定的selectedColor颜色绘制。HandleInput方法里,如果未被选中,立即返回,不执行任何操作。如果被选择并且按下键盘Enter键或者游戏手柄A键,则触发OnSelected事件。

在后续的课程中我还会添加其他控件,但是对于本课程来说,这2个控件已经足够我们实现在游戏状态之间切换。接下来我们要做的是添加游戏状态(GameState),一个游戏开始时显示的菜单,这个菜单有不同的选项,玩家可以选择以何种方式开始游戏。

要使用ControlManager,需要一个SpriteFont,右键点击“EyesOfTheDragonContent”项目,选择“添加”——“新建文件夹”,给这个文件夹命名为“Fonts”,右键点击“Fonts”文件夹,选择“添加”——“新建项”——“Sprite Font”,名字输入“ControlFont“,然后将Size修改为20。



我还要小小的修改一下BaseGameState类,修改之后的代码如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using XRpgLibrary;
using XRpgLibrary.Controls;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

namespace EyesOfTheDragon.GameScreens
{
public abstract partial class BaseGameState : GameState
{
#region Fields region
protected Game1 GameRef;
protected ControlManager ControlManager;
protected PlayerIndex playerIndexInControl;
#endregion
#region Properties region
#endregion
#region Constructor Region
public BaseGameState(Game game, GameStateManager manager)
: base(game, manager)
{
GameRef = (Game1)game;
playerIndexInControl = PlayerIndex.One;
}
#endregion
#region XNA Method Region
protected override void LoadContent()
{
ContentManager Content = Game.Content;
SpriteFont menuFont = Content.Load<SpriteFont>(@"Fonts\ControlFont");
ControlManager = new ControlManager(menuFont);
base.LoadContent();
}
public override void Update(GameTime gameTime)
{
base.Update(gameTime);
}
public override void Draw(GameTime gameTime)
{
base.Draw(gameTime);
}
#endregion
}
}
添加了ControlManager、playerIndexInControl字段,在LoadContent方法中,加载了SpriteFont,并用这种字体实例化了ControlManager对象。

在示范ControlManager的用法之前,我要添加一个screen。右键点击GameScreens文件夹,选择“添加”——“新建项”——“类”,名字输入“StartMenuScreen”,这个类的代码如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using XRpgLibrary;

namespace EyesOfTheDragon.GameScreens
{
public class StartMenuScreen : BaseGameState
{
#region Field region
#endregion
#region Property Region
#endregion
#region Constructor Region
public StartMenuScreen(Game game, GameStateManager manager)
: base(game, manager)
{
}
#endregion
#region XNA Method Region
public override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
base.LoadContent();
}
public override void Update(GameTime gameTime)
{
base.Update(gameTime);
}
public override void Draw(GameTime gameTime)
{
if (InputHandler.KeyReleased(Keys.Escape))
{
Game.Exit();
}
base.Draw(gameTime);
}
#endregion
#region Game State Method Region
#endregion
}
}
目前这个类只是一个游戏状态的框架,在下一课我将添加更多内容。我现在只是用它来展示如何在游戏状态之间切换。在Draw方法内,加入了键盘输入检查,如果按下ESC键,则退出游戏。这只是为了展示键盘输入的处理,并不是一个合理的游戏逻辑,在下一课我将更新它。打开Game1.cs,在TitleScreen字段下添加这个screen

public StartMenuScreen StartMenuScreen;
在Game1的构造函数里初始化它,Game1的构造函数应该是这样的

public Game1()
{
graphics = new GraphicsDeviceManager(this);
graphics.PreferredBackBufferWidth = screenWidth;
graphics.PreferredBackBufferHeight = screenHeight;
ScreenRectangle = new Rectangle(
0,
0,
screenWidth,
screenHeight);
Content.RootDirectory = "Content";

Components.Add(new InputHandler(this));
stateManager = new GameStateManager(this);
Components.Add(stateManager);
TitleScreen = new TitleScreen(this, stateManager);
StartMenuScreen = new GameScreens.StartMenuScreen(this, stateManager);
stateManager.ChangeState(TitleScreen);
}


在构造函数的末尾,通过调用stateManager的ChangeState方法直接切换到TitleScreen,成为游戏启动后的第一个显示。

然后打开TitleScreen.cs,由于改动有点多,我直接把代码贴出来

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using XRpgLibrary;
using XRpgLibrary.Controls;

namespace EyesOfTheDragon.GameScreens
{
public class TitleScreen : BaseGameState
{
#region Field region
Texture2D backgroundImage;
LinkLabel startLabel;
#endregion
#region Constructor region
public TitleScreen(Game game, GameStateManager manager)
: base(game, manager)
{
}
#endregion
#region XNA Method region
protected override void LoadContent()
{
ContentManager Content = GameRef.Content;
backgroundImage = Content.Load<Texture2D>(@"Backgrounds\titlescreen");
base.LoadContent();
startLabel = new LinkLabel();
startLabel.Position = new Vector2(350, 600);
startLabel.Text = "Press ENTER to begin";
startLabel.Color = Color.White;
startLabel.TabStop = true;
startLabel.HasFocus = true;
startLabel.Selected += new EventHandler(startLabel_Selected);
ControlManager.Add(startLabel);
}
public override void Update(GameTime gameTime)
{
ControlManager.Update(gameTime, PlayerIndex.One);
base.Update(gameTime);
}
public override void Draw(GameTime gameTime)
{
GameRef.SpriteBatch.Begin();
base.Draw(gameTime);
GameRef.SpriteBatch.Draw(
backgroundImage,
GameRef.ScreenRectangle,
Color.White);
ControlManager.Draw(GameRef.SpriteBatch);
GameRef.SpriteBatch.End();
}
#endregion
#region Title Screen Methods
private void startLabel_Selected(object sender, EventArgs e)
{
StateManager.PushState(GameRef.StartMenuScreen);
}
#endregion
}
}
第一个改动是添加了一个LinkLabel控件,在LoadContent方法内,初始化这个控件,例如位置、文字内容、文字颜色等,在这个screen中,startLabel的HasFocus属性为true,所以它是默认选中状态,并且将它的Selected事件绑定到对应的事件处理函数startLabel_Selected,然后把它添加到ControlManager。对于游戏screen来说,在调用base.LoadContent之后初始化控件很重要,这是因为ControlManager在base.LoadContent之后才存在。

在Update方法中,调用了ControlManager的Update方法,在Draw方法中也调用了ControlManager的Draw方法。

在事件处理函数startLabel_Selected内,调用了StateManager的PushState方法,这样当我们按下ENTER键或者手柄A键的时候,StartMenuScreen就会被压入StateManager的栈中,成为最顶端而显示在游戏中。

本课教程就到这里吧,我尽量让每课教程保持一个合理的长度,这样不会让你一下子面对太多内容。

祝你在游戏开发之旅好运。

Jamie McMahon

翻译:阿斌
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  XNA