最近有些时间,想把C#,XNA,kinect等这些最近学的东西用个RPG游戏来总结下,在网上找到一份国外的开发教程,可惜是英文版的,详细的介绍了一个基于XNA的RPG游戏引擎的设计,从今天开始,我就边翻译边学习下引擎设计,有不到位的地方,还请谅解
首先打开Visual Studio 2010 (我是用C#来开发,当然C++也是可以的),新建-》项目-》点击XNA Game Studio(4.0),选择Windows Game(4.0),创建工程,命名为EyesOfTheDragon。打开工程文件可以看到平台自动为我们创建了两个项目,一个是游戏项目EyesOfTheDragon,另一个是游戏内容项目EyesOfTheDragonContent,专门用来存放一些游戏创建中有关的图片,音频等文件
对于XNA4.0有两种Graphics profiles可供选择,HiDef和Reach 配置文件,我们这里要用的是Reach这种配置文件。具体操作是右击工程下的EyesOfTheDragonContent项目文件,选择属性,可以看到有对Graphics profiles的选择,选择Reach配置文件。
配置好这些后就开始我们的模块构建。首先,我想在工程中添加两个类库文件,一个是标准类库,保存着一些可以在其他项目中公用的类代码;一个是XNA游戏类库,这个类可以保存在其他XNA游戏项目中公用的类代码;右击工程文件,添加新建项目,选择XNA Game Studio(4.0),添加一个Windows Game Library (4.0)类库,命名为XRpgLibrary;再右击工程文件,添加新建项目,选择Visual C#,添加一个标准类库,命名为RpgLibrary。将添加的这两个类库中的自动生成类Class1.cs删去,因为我们以后不会用到它们。创建这两个类库,就是为了将游戏代码的模块更加清晰化,提高代码的重用性。接下来就是要将这两个类库的引用添加到游戏程序中去,这样当游戏运行时才能找得到类库,具体操作右击EyesOfTheDragon项目文件,添加引用,在项目中找到这两个类库文件,点击添加。
做完了这些终于到了我们敲代码的时候了...我们知道一个RPG游戏往往是一个比较大型的游戏,所以我们在开发前需要对其有一个整体的把握。其中首先考虑的就是游戏中的输入项目。用一个游戏组件来管理控制游戏中所有可能的输入将会是一个很好的选择。当我们创建一个一个游戏组件,并将其加入游戏组件列表中后,游戏运行时将会自动调用组件中的Update和Draw方法,当然前提是我们的游戏组件继承的是drawablegame component类,同时,对于游戏组件,它们可以通过设置Enabled属性来确定是否执行Update方法,对于可绘制组件,它们可以设置Visible属性来确定是否执行Draw方法
我们首先在XRpgLibrary中添加一个游戏组件(一个公用类)来管理游戏中的输入操作。在本节中我们只介绍有关键盘输入操作的管理,鼠标和Xbox 游戏杆的输入在下节介绍。具体操作,右击XRpgLibrary,添加新项目,选择Game Componet,将其命名为InputHandler,其中添加代码如下
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 Field Region static KeyboardState keyboardState;//当前鼠标状态 static KeyboardState lastKeyboardState;//上一帧鼠标状态 #endregion #region Property Region 公共属性,提供对外属性接口 public static KeyboardState KeyboardState { get { return keyboardState; } } public static KeyboardState LastKeyboardState { get { return lastKeyboardState; } }
#endregion #region Constructor Region 构造函数,获取当前帧中键盘状态 public InputHandler(Game game) : base(game) { keyboardState = Keyboard.GetState(); } #endregion #region XNA methods 初始化和Update函数 public override void Initialize() { base.Initialize(); }
//更新键盘状态信息 public override void Update(GameTime gameTime) { lastKeyboardState = keyboardState; keyboardState = Keyboard.GetState(); 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 } }
该类中主要包含的是键盘状态的属性信息和获取键盘状态属性的方法;同时XNA给出了Initialize和Update两个方法,其中在Update中对两个键盘状态做出改变;最后是给出了对按键状态进行判断的方法,前两个都是对一次按键的按起和释放的判断,第三个是对按键按下状态的判断
接着就是要把这个游戏组件添加到游戏组件列表中,游戏组件列表定义在Game1.cs这个类中,这个类是游戏的核心类,整个游戏的主要逻辑都在这个类中。添加代码如下
using XRpgLibrary; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; //添加组件 Components.Add(new InputHandler(this)); }
在大型的游戏开发中,对游戏状态的管理往往很重要,很多人在编写一段时间代码后才发现要加入游戏状态,这时就会比较麻烦。我们先定义一个游戏状态类,随后定义一个游戏状态管理类,来统统筹管理游戏中的状态变化,具体代码如下
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 abstract partial class GameState : DrawableGameComponent { #region Fields and Properties
//第一区域是GameState的属性定义 List<GameComponent> childComponents;//游戏屏幕中组件对象集 public List<GameComponent> Components { get { return childComponents; } } GameState tag;//游戏状态 public GameState Tag { get { return tag; } } protected GameStateManager StateManager;//游戏状态管理对象 #endregion
#region Constructor Region构造函数 public GameState(Game game, GameStateManager manager) : base(game) { StateManager = manager; childComponents = new List<GameComponent>(); tag = this; }
#endregion #region XNA Drawable Game Component Methods对继承类的方法重写 public override void Initialize() { base.Initialize(); } public override void Update(GameTime gameTime) { foreach (GameComponent component in childComponents) { if (component.Enabled) component.Update(gameTime); } base.Update(gameTime); } public override void Draw(GameTime gameTime) { DrawableGameComponent drawComponent; foreach (GameComponent component in childComponents) { if (component is DrawableGameComponent) { drawComponent = component as DrawableGameComponent; if (drawComponent.Visible) drawComponent.Draw(gameTime); } } base.Draw(gameTime); } #endregion #region GameState Method Region状态改变所触发事件的处理方法 internal protected virtual void StateChange(object sender, EventArgs e) { if (StateManager.CurrentState == Tag) Show(); else Hide(); } protected virtual void Show() { Visible = true; Enabled = true; foreach (GameComponent component in childComponents) { component.Enabled = true; if (component is DrawableGameComponent) ((DrawableGameComponent)component).Visible = true; } } protected virtual void Hide() { Visible = false; Enabled = false; foreach (GameComponent component in childComponents) { component.Enabled = false; if (component is DrawableGameComponent) ((DrawableGameComponent)component).Visible = false; } } #endregion } }
状态管理类的代码:
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 GameStateManager : GameComponent { #region Event Region public event EventHandler OnStateChange; #endregion #region Fields and Properties Region Stack<GameState> gameStates = new Stack<GameState>(); const int startDrawOrder = 5000; const int drawOrderInc = 100; int drawOrder; public GameState CurrentState { get { return gameStates.Peek(); } } #endregion #region Constructor Region public GameStateManager(Game game) : base(game) { drawOrder = startDrawOrder; } #endregion #region XNA Method Region public override void Initialize() { base.Initialize(); } public override void Update(GameTime gameTime) { base.Update(gameTime); } #endregion #region Methods Region public void PopState() { if (gameStates.Count > 0) { RemoveState(); drawOrder -= drawOrderInc; if (OnStateChange != null) OnStateChange(this, null); } } private void RemoveState() { GameState State = gameStates.Peek(); OnStateChange -= State.StateChange; Game.Components.Remove(State); gameStates.Pop(); } public void PushState(GameState newState) { drawOrder += drawOrderInc; newState.DrawOrder = drawOrder; AddState(newState); if (OnStateChange != null) OnStateChange(this, null); } private void AddState(GameState newState) { gameStates.Push(newState); Game.Components.Add(newState); OnStateChange += newState.StateChange; } public void ChangeState(GameState newState) { while (gameStates.Count > 0) RemoveState(); newState.DrawOrder = startDrawOrder; drawOrder = startDrawOrder; AddState(newState); if (OnStateChange != null) OnStateChange(this, null); } #endregion } }
游戏状态类和状态管理类都是比较抽象的类。用个通俗的例子来解释,在我们游戏中通常会有不同的页面,初始页面,等级页面,不同游戏场景页面等等,每种页面都对应着一种游戏状态,对它们的加载,更新等管理就在游戏状态管理类中。以上代码,事件区域的代码:当有状态或者屏幕中的状态发生改变时触发的事件 ;在GameState类中事件的拥有者是StateChange方法。为了管理游戏状态用一个栈结Stack<GameState>,它的特性在于先进后出;有三个整数成员变量,这些成员变量是用来标示游戏页面绘制顺序的。继承基类DrawableGameComponent有一个属性DrawOrder,用这个属性来标示游戏组件绘制的顺序,DrawOrder值越高的组件,其越晚被绘制。我们选择5000作为这个属性的起始赋值点,也就是startDrawOrder。当一个新的组件加载到Stack中,我们将它的DrawOrder属性赋一个比前面组件更高的一个值。drawOrderInc是当组件被加载或者移除时,组件的DrawOrder属性加上或者减去多少值,drawOrder保存着当前栈顶部组件的属性值。CurrentScreen属性返回的是当前处于Stack顶部的组件对象。在类的构造函数中,将startDrawOrder赋值给drawOrder,选择5000作为起始点,100作为每次加上或减去的值,那么在栈中就可以存储相对多的组件。在方法区域,有三个公共方法和两个私有方法,公共方法是PopState,PushState,ChangeState,私有方法是RemoveState,AddState,PopState方法是当你想移除当前组件,回到上一个组件时调用;PushState方法是当你想转移到另一个组件,同时保存当前组件;ChangeState方法是当你想移除所有栈中的组件状态。AddState方法是在状态管理栈中添加一个新的组件状态,并将其添加到游戏组件列表中;
接下来就是将游戏状态管理类添加到游戏组件列表中,在Game1.cs中添加如下代码
GameStateManager stateManager; public Game1() { graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
Components.Add(new InputHandler(this));
stateManager = new GameStateManager(this);
Components.Add(stateManager);
}
到目前为止,虽然我们敲了很多代码,但是运行后得到的依然是一个蓝色的窗体;不要着急,前面所做的都是为了能让后面的代码写起来更加清晰,磨刀不误砍柴工嘛,接下来我们就要在EyesOfTheDragon中添加两个基础类,来实现图片的加载。在EyesOfTheDragon中添加一个新建文件夹,命名为GameScreens,在其中添加两个类:BaseGameState.cs和TitleScreen.cs。其中BaseGameState.cs是游戏页面的基础类,包含的都是公共成员;TitleScreen.cs是游戏登陆主页面类,是我们第一个呈现的页面,代码如下
using System; using System.Collections.Generic; using System.Linq; using System.Text; using XRpgLibrary; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; namespace EyesOfTheDragon.GameScreens {
//定义为可继承的抽象类 public abstract partial class BaseGameState : GameState { #region Fields region protected Game1 GameRef; #endregion #region Properties region #endregion #region Constructor Region public BaseGameState(Game game, GameStateManager manager) : base(game, manager) { GameRef = (Game1)game; } #endregion } }
该类继承自GameState类,所以可以在GameStateManager类中调用。
为了在TitleScreen.CS中加载图片,需要先将图片加载到游戏内容项目中,在EyesOfTheDragonContent下添加一个新建文件夹,将背景图片TitleScreen.png加载进来。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; namespace EyesOfTheDragon.GameScreens { public class TitleScreen : BaseGameState { #region Field region//Texture2D为图片加载对象 Texture2D backgroundImage; #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(); } public override void Update(GameTime gameTime) { base.Update(gameTime); } public override void Draw(GameTime gameTime) {
//绘制背景图片,注意绘制操作都是在Begin()和End()操作之间 GameRef.SpriteBatch.Begin(); base.Draw(gameTime); GameRef.SpriteBatch.Draw( backgroundImage, GameRef.ScreenRectangle, Color.White); GameRef.SpriteBatch.End(); } #endregion } }
做完这些后,就是要在游戏核心类Game1.cs中添加相关引用了,Game1.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; using XRpgLibrary; using EyesOfTheDragon.GameScreens; namespace EyesOfTheDragon { public class Game1 : Microsoft.Xna.Framework.Game { #region XNA Field Region//定义图像管理和图像绘制对象 GraphicsDeviceManager graphics; public SpriteBatch SpriteBatch; #endregion #region Game State Region GameStateManager stateManager;//游戏状态管理对象 public TitleScreen TitleScreen;//登陆页对象 #endregion #region Screen Field Region//设定游戏窗体的属性 const int screenWidth = 1024; const int screenHeight = 768; public readonly Rectangle ScreenRectangle; #endregion public Game1() { graphics = new GraphicsDeviceManager(this); graphics.PreferredBackBufferWidth = screenWidth; graphics.PreferredBackBufferHeight = screenHeight; ScreenRectangle = new Rectangle( 0, 0, screenWidth, screenHeight); Content.RootDirectory = "Content";
//游戏类中添加InputHandle,stateManager,TitleScreen组件 Components.Add(new InputHandler(this)); stateManager = new GameStateManager(this); Components.Add(stateManager); TitleScreen = new TitleScreen(this, stateManager); stateManager.ChangeState(TitleScreen); } protected override void Initialize() { base.Initialize(); } protected override void LoadContent() { SpriteBatch = new SpriteBatch(GraphicsDevice); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); base.Draw(gameTime); } } }
通过以上代码的搭建,我们已经完成了游戏中对不同页面管理的机制。并且实例化了登陆页,并呈现。
呈现图: