最近有些時間,想把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); } } }
通過以上代碼的搭建,我們已經完成了游戲中對不同頁面管理的機制。並且實例化了登陸頁,並呈現。
呈現圖: