上节我们已经搭建了有关RPG游戏的游戏状态管理部分的框架,并且绘制了第一个游戏页面,主登陆页面;同时还搭建了输入设备的管理框架。这节主要任务是:
(1)扩充输入设备管理代码,将Xbox游戏手柄控制代码添加进去
(2)定义游戏控件
(3)实现游戏中页面之间跳转
首先扩充输入设备管理代码,也就是InputHandler这个类;对游戏手柄数据的管理同键盘相似,唯一不同的是,计算机上可以连接四个Xbox游戏手柄,所以对于游戏手柄输入信息的管理要用一个数组,具体代码如下:
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,Enum类很关键 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 } }
在XNA中我们不能像Windows编程中那样有现成的GUI控件可以托用,需要自己为游戏创建控件,并用一个控件类来对它们进行管理,具体操作是在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;//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 } }
以上代码都是有关控件的一些公共成员定义;一些具体的操作控件可以继承此基类来实现,同时创建一个控件管理类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;//XNA中用于绘制文字的对象 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 } }
同样在定义了这些基础类后我们要动手创建一些实际控件,第一个要创建的就是可以在游戏页面绘制文字的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;//将tabStop属性设置为false,则Label控件不可以获得聚焦 } #endregion #region Abstract Methods public override void Update(GameTime gameTime) { }
//将text属性绘制在页面上 public override void Draw(SpriteBatch spriteBatch) { spriteBatch.DrawString(SpriteFont, Text, Position, Color); } public override void HandleInput(PlayerIndex playerIndex) { } #endregion } }
接下来定义一个LinkLabel控件,该控件相比Label不在只是单纯的在页面上绘制一些文字,它可以被选中,并激发Selected事件,具体代码如下:
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
//selectedColor属性是指当该控件被选中时的颜色 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); }
//输入数据触发方法,当按下Enter键后触发选中事件 public override void HandleInput(PlayerIndex playerIndex) { if (!HasFocus) return; if (InputHandler.KeyReleased(Keys.Enter) || InputHandler.ButtonReleased(Buttons.A, playerIndex)) base.OnSelected(null); } #endregion } }
在以后的章节中将会增加一些新的控件,但是现在我们可以通过Label控件在游戏页面上绘制文字,同时利用LinkLabel控件可以实现不同页面中间的切换。同时要使用ControlManager,你需要SpriteFont来绘制文字,可以在EyesOfTheDragonContent 中添加一个Fonts文件夹,其中添加一个SpriteFont文件,XNA默认的为其选定Kootenay字体,当然你可以对文件中定义的字体进行自己的设置
在构建了控件类后,需要对上节我们定义的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 } }
好了,到这里我们今天的任务就完成了,现在再创建一个游戏页面,来实现页面之间的跳转,代码如下
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); }
//这个页面很简单,就是跳过来后,按ESC键退出游戏 public override void Draw(GameTime gameTime) { if (InputHandler.KeyReleased(Keys.Escape)) { Game.Exit(); } base.Draw(gameTime); } #endregion #region Game State Method Region #endregion } }
在Game1.cs这个核心类中为该页面添加注册,类中修改部分如下
public StartMenuScreen StartMenuScreen; 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);//将StartMenuScreen对象添加 stateManager.ChangeState(TitleScreen); }
最后就是修改TitleScreen游戏页面,在其中加入LinkLable控件,并触发事件,实现页面跳转
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;//定义LinkLabel控件 #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();
//初始化LinkLable控件 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;
//为其添加Selected事件 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 } }
点击Enter键会跳转到一个蓝屏页面,再按Esc键跳出游戏,OK,我们已经初步实现了游戏中页面跳转模式