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

来源:互联网 发布:小米4电信3g 网络 编辑:程序博客网 时间:2024/05/29 10:58

这个系列教程适合有一定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游戏开发)的第一部分,我之前写过类似的系列教程,不过是在XNA3.0和XNA3.1框架。

首先我们要创建一个XNA 4.0 Windows game,打开VS2010(或者VS2010 Express),这个系列教程所使用的开发语言为C#。

“文件”——“新建”——“项目”,在“新建项目”对话框中选择“已安装的模板”——“Visual C#”——“XNA Game Studio 4.0”——“Windows Game (4.0) ”,游戏的名字输入“EyesOfTheDragon”,Visual C#会为你创建一个基础的解决方案,在“解决方案资源管理器”中会有2个项目,第一个是你的游戏“EyesOfTheDragon”,第二个是你的游戏要用到的资源“EyesOfTheDragonContent”,比如声音,图片。



在XNA4.0中你可以使用2种graphics profile,“HiDef profile”使用Shader Model 3.0或更高,“Reach profile”使用Shader Model 1.1或更高,右键点击“EyesOfTheDragon”项目,选择属性,会出现游戏的属性设置,在“XNA Game Studio”选项卡中选择“Reach profile”,在这个系列教程中我将使用“Reach profile”。如果你确定你的显卡支持“HiDef profile”那么尽量使用“HiDef profile”。


我要添加2个类库,第一个是标准类库,里面放置可以被其他项目使用的通用类。另一个是XNA游戏类库,里面放置可以被XNA项目共用的类。在“解决方案资源管理器”中右键点击你的解决方案(不是项目),选择“添加”——“新建项目”,在“新建项目”对话框中选择“已安装的模板”——“Visual C#”——“XNA Game Studio 4.0”——“Windows Game Library (4.0)”,库的名字输入“XRpgLibrary”。在“解决方案资源管理器”中右键点击你的解决方案(不是项目),选择“添加”——“新建项目”,在“新建项目”对话框中选择“已安装的模板”——“Visual C#”——“类库”,库的名字输入“RpgLibrary”。将这2个类库中的Class1.cs删除(右键点击Class1.cs),因为后续我们不会使用它们。



为了能够使用这2个类库,还需要做最后一件事情,将它们引入到你的游戏里。右键点击项目“EyesOfTheDragon”,选择“添加引用”,在弹出的对话框中选择“项目”选项卡,你可以看到这2个类库,按住“ctrl”键将这2个类库选中,点击“确定”。


现在开始真正的写代码了,在大型游戏里,比如RPG游戏,在全局层面处理一些事情是个不错的主意,比如游戏的输入。用“Game Component”来处理游戏输入非常完美。当你创建一个“Game Component”并把它添加到你的游戏组件列表之后,XNA会自动调用它的“Update”方法,如果它是一个“drawable game component”,还会自动调用它的“Draw”方法。“Game Component”具有“Enable”属性,通过设置“Enable”属性为true或false来决定该组件的“Update”方法是否被自动调用。“drawable game component”还具有“Visable”属性,通过设置“Visable”属性为true或false来决定该组件的“Draw”方法是否被自动调用。

我将在“XRpgLibrary”库中添加一个“Game Component”来管理所有的游戏输入,在这个教程里主要处理键盘输入,在后续的教程中会添加鼠标和游戏手柄输入的管理。在“解决方案资源管理器”中右键点击“XRpgLibrary”库,选择“添加”——“新建项”,在“新建项目”对话框中选择“已安装的模板”——“Visual C#”——“XNA Game Studio 4.0”——“Game Component”,名字输入“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        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    }}
这个类的Field Region区,有2个字段,keyboardState,游戏当前帧的键盘输入,lastKeyboardState,游戏上一帧的键盘输入。这2个都是静态字段,这意味着他们被InputHandler类的所有实例共用,静态字段是通过类名的方式使用而不是实例名。

游戏每运行一次Update和Draw方法,称之为一帧。我们需要比较游戏里上一帧的键盘输入与当前帧的键盘输入,来监听一次很短的键盘按键(按下,松开)。

接下来是Property Region区,这个区放置属性,通过属性来公开类的字段,LastKeyboardState和KeyboardState属性返回对应的lastKeyboardState和keyboardState字段。

在Constructor Region区是构造函数,在创建Game Component的时候自动创建的。在这个区,我通过Keyboard类的GetState()方法获得了当前帧的键盘输入,并保存到keyboardState字段。

在Game Component Method Region区,XNA也自动创建了2个方法,Initialize和Update。Initialize方法中我没有添加代码,在Update方法中,我将keyboardState保存到lastKeyboardState,并把当前帧的键盘输入保存到keyboardState。这样做的目的是为了确保游戏中上一帧和当前帧的键盘输入都能够保存。
在General Method Region区,只有一个Flush方法,他的作用是让KeyPressed和KeyReleased方法返回false,通过调用这个方法,你可以在你需要的时候重置KeyPressed和KeyReleased方法的返回值为false。注意Flush方法是静态方法,静态方法也是通过类名使用而不是实例对象。

最后是Keyboard Region区,所有与键盘相关的方法都放在这个区。这里有3个方法,KeyPressed(松开,按下),KeyReleased(按下,松开),KeyDown(按下)。这三个方法也是静态方法。

译者注:加入KeyPressed和KeyReleased方法是很有必要的,keyboardState类提供了IsKeyDown()和IsKeyUp()用于分别检查按键的按下和松开,如果你在Update中用IsKeyDown()和IsKeyUp()来检查键盘单击,比如:

            if (keyboardState.IsKeyDown(Keys.Up))            {i++;            }
你会发现无论你用多快的速度点击UP键,i的值根本不止加1,而是加了3或者4,因为游戏循环的速度太快了(默认1/60秒一次),而通过KeyPressed或KeyReleased方法则可以准确的捕捉一次按键。

现在我们把这个Game Component添加到游戏的组件列表里,在Game1.cs里,这是游戏的主类。第一步,添加using语句引入XRpgLibrary类库的命名空间,这样你就可以直接使用类库中的类而不需要输入完整的名称。例如加入了using XRpgLibrary之后,你就可以直接使用InputHandler类,而不需要通过XRpgLibrary.InputHandler的方式使用InputHandler类。然后在Game1的构造函数里,向游戏组件列表添加一个InputHandler类的实例。

using XRpgLibrary;public Game1(){ graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content";  Components.Add(new InputHandler(this));}

创建大型游戏常见的错误是在开始的时候没有考虑游戏状态(game state)的处理,这样做的结果就是当你回过头来处理游戏状态(game state)的时候,这将是一件很痛苦的事情。我将用Game Component来管理游戏状态。我需要一个基础状态类,右键点击“XRpgLibrary”类库,选择“添加”——“新建项”——“XNA Game Studio 4.0”——“Game Component”,名字输入“GameState”。“GameState”类的代码如下

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        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    }}

当你创建Game Component时,它们默认继承于GameComponent类,我们的GameState组件需要处理绘图,所以需要继承于DrawableGameComponent类。childComponents字段,屏幕上所有Game Component的列表;tag,存储游戏状态的字段;StateManager,管理游戏状态的类,我们现在还没有添加。StateManager是受保护的字段,只对所有继承于(直接或间接)GameState的类可见。Components,只读属性,返回childComponents字段;Tag,只读属性,返回tag字段。

GameState的构造函数有2个参数,第一个参数是Game类的对象game。第二个参数是GameStateManager类的对象manager,传入的对象manager被保存到StateManager字段。构造函数接下来初始化childComponents字段,将tag字段指向this,GameState类的当前实例。

在Update方法中,遍历childComponents中所有的Game Component,检查它们的Enabled属性,如果Enabled为true,则调用它们的Update方法。类似的,Draw方法遍历childComponents中所有的Game Component,检查它们是否是DrawableGameComponent,如果是,继续检查它们的Visible属性,如果Visible属性为true,则调用它们的Draw方法。

接下来是游戏状态发生变化时的事件处理,StateChange方法。所有活动的screen都会订阅GameStateManager类里面的OnStateChange事件,当游戏状态发生改变时,所有订阅这个事件的GameState类对象都将收到一个游戏状态发生改变的消息。在StateChange方法里,如果StateManager.CurrentState等于当前GameState类的实例对象,则调用Show方法,否则调用Hide方法。

在Show方法里面,首先将当前GameState类的实例对象的Visible和Enabled属性设置为true,然后遍历childComponents中所有的Game Component,将它们的Enabled属性设置为true,如果是DrawableGameComponent,将它们的Visible属性设置为true。而Hide方法则正好相反。

接下来我们要添加GameStateManager类,右键点击“XRpgLibrary”类库,选择“添加”——“新建项”——“XNA Game Studio 4.0”——“Game Component”,名字输入“GameStateManager”。“GameStateManager”类的代码如下

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    }}
在Event Region区,我定义了一个OnStateChange事件,在游戏状态发生改变时它将被触发。GameState类的StateChange方法是这个事件的处理函数。

为了管理游戏状态,我用了一个栈Stack<GameState>,关于栈的概念,请自行参考相关资料。

译者注:这个系列教程的作者,可能希望没有编程基础的人也能看懂,所以解释了一些基础概念,其出发点是好的。但是客观的说,看懂这样一个RPG游戏的源代码而没有一点编程基础几乎是不可能的,所以我并不打算翻译一些基础概念性的东西,因为短短的几句话并不能解释清楚,所以我更希望读者先学习一定的编程基础。

有3个整数字段:startDrawOrder(drawOrder初始值),drawOrderInc(drawOrder增量)和drawOrder(绘制顺序),他们决定游戏内screen的绘制顺序。DrawableGameComponent有一个DrawOrder属性,所有的Game Component都会根据它们DrawOrder属性值按照升序(由小到大)被绘制。drawOrder的初始值被设置为5000,增量被设置为100,每次添加或移除screen的时候,drawOrder都会按照这个增量增加或减少。CurrentScreen属性返回栈gameStates的最顶端的screen。

在Constructor Region区,将drawOrder字段的值设置为startDrawOrder字段的值。

在Methods Region区,有3个公开的方法和2个私有的方法,公开的方法是PopState,PushState和ChangeState,私有的方法是RemoveState和AddState。

调用PopState方法将移除栈顶端的screen,并返回到上一个screen,调用PushState方法将压入一个新的screen到栈里,调用ChangeState方法将会移除栈内所有其他的screen,并压入一个新的screen到栈里。

译者注:值得注意的是Game Component类有一个Game属性,这个属性返回Game Component对象所关联的Game类对象,对于这个游戏来说,就是Game1。在RemoveState和AddState方法中,通过Game.Components的Add和Remove方法将GameState对象注册或取消注册到Game1,这很重要,因为只有注册到Game1的游戏组件才会在Game1运行的时候自动调用该组件的Update和Draw方法。

现在添加一个GameStateManager对象到游戏里,打开Game1.cs,在SpriteBatch字段下添加一个字段,GameStateManager对象,在Game1的构造函数里面把它初始化,并添加到游戏组件列表。代码如下

        GameStateManager stateManager;        public Game1()        {            graphics = new GraphicsDeviceManager(this);            Content.RootDirectory = "Content";                        Components.Add(new InputHandler(this));            stateManager = new GameStateManager(this);            Components.Add(stateManager);        }

特别感谢Tuckbone,我系列教程的读者,帮我制作游戏图片。在http://xnagpa.net/xna4/downloads/titlescreen.zip可以下载到我们将要用到的第一张图片。

对游戏资源进行分类管理很有必要,右键点击“EyesOfTheDragonContent”项目,选择“添加”——“新建文件夹”,给这个文件夹命名为“Backgrounds”。将你刚才下载并解压的图片拷贝到“Backgrounds”文件夹,右键点击“Backgrounds”文件夹,选择“添加”——“现有项”,选择“Backgrounds”文件夹下的titlescreen.png并点击添加。游戏用到的图片资源最好是无压缩格式的,比如png,bmp,tga。png和tga格式的图片支持alpha通道(透明)。

我们添加了很多代码但是似乎什么都没有改变,当你运行游戏的时候仍然是一个蓝色的窗口。在“解决方案资源管理器”中右键点击“EyesOfTheDragon”项目,选择“添加”——“新建文件夹”,给这个文件夹命名为“GameScreens”。右键点击“GameScreens”文件夹,选择“添加”——“新建项”——“类”,名字输入“BaseGameState”,代码如下


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    }}
BaseGameState类是一个非常基础的类,从GameState类继承,所以能够被GameStateManager使用。GameRef字段保存Game对象,在构造函数中将当前游戏对象赋值给GameRef字段。

接下来我们要添加一个screen,右键点击“GameScreens”文件夹,选择“添加”——“新建项”——“类”,名字输入“TitleScreen”,代码如下

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 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)        {            GameRef.SpriteBatch.Begin();            base.Draw(gameTime);            GameRef.SpriteBatch.Draw(            backgroundImage, GameRef.ScreenRectangle,            Color.White);            GameRef.SpriteBatch.End();        }        #endregion    }}
通过一些using语句引入了Xna.Framework,Xna.Framework.Content(游戏资源管理),Xna.Framework.Graphics(绘图)命名空间,以及我们的XRpgLibrary库。

这个类继承于BaseGameState类。这个类目前只有一个字段,Texture2D的对象,我们的背景图片。在LoadContent方法内,加载了背景图片,在Draw方法内将这个图片绘制到了游戏窗口。关于如何使用SpriteBatch绘制2D图片,参考《XNA4.0学习指南》,讲的非常详细。

这个类代码编辑器会提示一些错误,因为在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";                        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);        }    }}
第一个改动是添加了分区,其次我把SpriteBatch字段的名字首字母改成了大写,并公布了这个字段。

添加了一个Game State Region区,放置所有与GameState相关的字段和属性。

添加了一个Screen Field Region区,放置所有与游戏窗口设置相关的字段和属性,比如游戏窗口的高、宽。

构造函数也有一些改变,设置了游戏窗口的高、宽,初始化了TitleScreen对象,然后调用stateManager的ChangeState方法切换到指定游戏状态。



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

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

Jamie McMahon

翻译:阿斌

0 0
原创粉丝点击