Input类
Input类有许多轻松获得所有最常用的键盘,鼠标和手柄键状态(见图10-1 )的属性。此类还提供了一些辅助方法来管理键盘输入,文字输入,但你可能不会在第一个游戏中使用的Input类的所有代码,但当你的游戏发展并使用更多的用户界面代码时,就会觉得它们的用处了。此类最重要的方法是Update方法,该方法每一帧都被调用(在BaseGame类的Update方法中被自动调用)。
请注意,Xbox 360中不存在鼠标功能,当您编译为Xbox 360时无法获得鼠标状态(没有这个类)。所以,你必须注释掉所有鼠标代码才能使游戏运行在Xbox 360。这并不轻松,尤其是在项目中有数以百计的鼠标调用时。一个较好的解决办法是完全隐藏鼠标状态(把它变成私有),只通过Input类的属性获取鼠标参数。正如你在图10-1中看得的那样,如果你要检查是否有另一个键或按钮被按下,你可以直接操作手柄和键盘状态,但对于鼠标,您只能通过Input类的属性。现在,如果运行在PC上,会返回所有属性,但在Xbox 360上,只返回false或虚拟的鼠标位置,您可以像在PC上一样设置并获得鼠标位置,但用户不能直接控制。
现在,同样的代码可用于两个平台。XNA以同样方式的98%的代码实现了目标,行为你的引擎可达到100%的。
例如,Xbox 360 手柄的A键或键盘的方向键使用非常频繁。使用以下的代码,通过向上/向下键键盘浏览菜单条目非常容易。此外也支持手柄向上/向下控制光标,通过辅助属性使用左摇杆控制菜单更容易些。如果你每次都编写所有状态检查去判断一个键或一个按钮是否被按下或没按下,您可能一会儿就会发疯了。
// Handle GamePad input, and also allow keyboard input if (Input.GamePadUpJustPressed || Input.KeyboardUpJustPressed) { Sound.Play(Sound.Sounds.Highlight); selectedButton = (selectedButton + NumberOfButtons - 1) % NumberOfButtons; } // if (BaseGame.GamePadLeftNowPressed) else if (Input.GamePadDownJustPressed || Input.KeyboardDownJustPressed) { Sound.Play(Sound.Sounds.Highlight); selectedButton = (selectedButton + 1) % NumberOfButtons; } // else if
GamePadUpJustPressed 方法代码如下:
/// <summary> /// Game pad up just pressed /// </summary> /// <returns>Bool</returns> public static bool GamePadUpJustPressed { get { return (gamePadState.DPad.Up == ButtonState.Pressed && gamePadStateLastFrame.DPad.Up == ButtonState.Released) || (gamePadState.ThumbSticks.Left.Y > 0.75f && gamePadStateLastFrame.ThumbSticks.Left.Y < 0.75f); } // get } // GamePadUpJustPressed
该代码使用目前的手柄状态(这是每帧开始时分配的)和上一帧的手柄状态以按键状态是否已经发生变化。如你所见,如果您每一次重复编写此代码去检查手柄按钮是否被按下,你会发疯的。复制与粘贴第一次或第二次不错,但如果你重复使用相同的代码,最好还是写一个辅助方法或属性,让它为您执行这项工作。以后当你写更多的用户界面代码时,它将更容易检查所有输入状态,处理许多不同的输入设备。
Input类中的Update方法
Update方法并没有什么复杂,但它是Input类中最重要的部分,因为上一帧的所有状态被复制,然后你获得每一个键盘,鼠标和手柄输入的新状态。这里有一些额外的代码处理相对鼠标移动。鼠标的相对移动在XNA中不支持,不同于DirectX中的DirectInput类,它能让您获得相对鼠标位置去替代绝对鼠标位置,比如用相对坐标(-3.5,7.0)替代绝对坐标(350,802)。
/// <summary> /// Update, called from BaseGame.Update(). /// Will catch all new states for keyboard, mouse and the gamepad. /// </summary> internal static void Update() { #if XBOX360 // No mouse support on the XBox360 yet :( mouseDetected = false; #else // Handle mouse input variables mouseStateLastFrame = mouseState; mouseState = Microsoft.Xna.Framework.Input.Mouse.GetState(); // Update mouseXMovement and mouseYMovement lastMouseXMovement += mouseState.X - mouseStateLastFrame.X; lastMouseYMovement += mouseState.Y - mouseStateLastFrame.Y; mouseXMovement = lastMouseXMovement / 2.0f; mouseYMovement = lastMouseYMovement / 2.0f; lastMouseXMovement -= lastMouseXMovement / 2.0f; lastMouseYMovement -= lastMouseYMovement / 2.0f; if (MouseLeftButtonPressed == false) startDraggingPos = MousePos; mouseWheelDelta = mouseState.ScrollWheelValue - mouseWheelValue; mouseWheelValue = mouseState.ScrollWheelValue; // Check if mouse was moved this frame if it is not detected yet. // This allows us to ignore the mouse even when it is captured // on a windows machine if just the gamepad or keyboard is used. if (mouseDetected == false) mouseDetected = mouseState.X != mouseStateLastFrame.X || mouseState.Y != mouseStateLastFrame.Y || mouseState.LeftButton != mouseStateLastFrame.LeftButton; #endif // Handle keyboard input keysPressedLastFrame = new List<Keys>(keyboardState.GetPressedKeys()); keyboardState = Microsoft.Xna.Framework.Input.Keyboard.GetState(); // And finally catch the XBox Controller input (only use 1 player) gamePadStateLastFrame = gamePadState; gamePadState = Microsoft.Xna.Framework.Input.GamePad.GetState( PlayerIndex.One); // Handle rumbling if (leftRumble > 0 || rightRumble > 0) { if (leftRumble > 0) leftRumble -= 1.5f * BaseGame.MoveFactorPerSecond; if (rightRumble > 0) rightRumble -= 1.5f * BaseGame.MoveFactorPerSecond; Microsoft.Xna.Framework.Input.GamePad.SetVibration( PlayerIndex.One, leftRumble, rightRumble); } // if (leftRumble) } // Update()
您可能会注意到Update方法最后的手柄振动代码,可以使用下列方法让手柄振动。,如果下一帧不调用GamePadRumble,Update方法将自动减少振动强度。您还可以为小事件使用较弱的振动效果。启用振动效果玩Shooter会更有乐趣。
/// <summary> /// Game pad rumble /// </summary> /// <param name="setLeftRumble">Set left rumble</param> /// <param name="setRightRumble">Set right rumble</param> public static void GamePadRumble( float setLeftRumble, float setRightRumble) { leftRumble = setLeftRumble; rightRumble = setRightRumble; } // GamePadRumble(setLeftRumble, setRightRumble)
鼠标矩形
Input类也有一些方法帮助您检测鼠标是否处在一个用户界面元素(例如一个菜单按钮)上。使用下列方法来检查鼠标悬停在用户界面元素上。如果您进入这个矩形该方法也将自动播放声音。
/// <summary> /// Mouse in box /// </summary> /// <param name="rect">Rectangle</param> /// <returns>Bool</returns> public static bool MouseInBox(Rectangle rect) { #if XBOX360 return false; #else bool ret = mouseState.X >= rect.X && mouseState.Y >= rect.Y && mouseState.X < rect.Right && mouseState.Y < rect.Bottom; bool lastRet = mouseStateLastFrame.X >= rect.X && mouseStateLastFrame.Y >= rect.Y && mouseStateLastFrame.X < rect.Right && mouseStateLastFrame.Y < rect.Bottom; // Highlight happened? if (ret && lastRet == false) Sound.Play(Sound.Sounds.Highlight); return ret; #endif } // MouseInBox(rect)
例如:你可以使用这个代码让您选择主菜单元素,如图10-2。如我以前所说,尽量保持简单。菜单应显示游戏的所有重要部分。背景纹理也应该选用一张漂亮的图片——由你的图形艺术家制作(如果你有的话)。
要能工作你可以使用下面的代码片断:
//Render background game.RenderMenuBackground(); //Show all buttons int buttonNum =0; MenuButton [] menuButtons ==new MenuButton [] { MenuButton.Missions, MenuButton.Highscore, MenuButton.Credits, MenuButton.Exit, MenuButton.Back, }; foreach (MenuButton button in menuButtons) // Don’t render the back button if (button !=MenuButton.Back) { if (game.RenderMenuButton(button,buttonLocations [buttonNum ])) { if (button ==MenuButton.Missions) game.AddGameScreen(new Mission()); else if (button ==MenuButton.Highscore) game.AddGameScreen(new Highscores()); else if (button ==MenuButton.Credits) game.AddGameScreen(new Credits()); else if (button ==MenuButton.Exit) quit =true; }//if buttonNum++; if (buttonNum >=buttonLocations.Length) break; }//if //Hotkeys,M=Mission,H=Highscores,C=Credits,Esc=Quit if (Input.KeyboardKeyJustPressed(Keys.M)) game.AddGameScreen(new Mission()); else if (Input.KeyboardKeyJustPressed(Keys.H)) game.AddGameScreen(new Highscores()); else if (Input.KeyboardKeyJustPressed(Keys.C)) game.AddGameScreen(new Credits()); else if (Input.KeyboardEscapeJustPressed) quit = true; //If pressing XBox controller up/down change selection if (Input.GamePadDownJustPressed) { xInputMenuSelection = (xInputMenuSelection + 1) % buttonLocations.Length; SelectMenuItemForXInput(); }//if (Input.GamePad) else if (Input.GamePadUpJustPressed) { if (xInputMenuSelection <= 0) xInputMenuSelection = buttonLocations.Length; xInputMenuSelection -; SelectMenuItemForXInput(); }//if (Input.GamePad)
如果使用更多的控制元素,比如复选框,处理滚动条或编辑框,代码会更加复杂,在你重复地复制和粘贴相同的代码前,你应该引进UI类去处理所有的用户界面代码。如果你只需要使用一个编辑框,您也可以直接把代码写入Option类中,就像在Rocket Commander中做的那样,但是如果你使用多次,代码应该被重构。处理文本框在XNA中更复杂,因为你必须实现自己的键盘输入方法。
在XNA中输入文字
您可能还注意到,在Input类中没有keyboardStateLastFrame变量,因为与MouseState和GamePadState不同,下一帧您无法再次使用KeyboardState结构,数据不再有效。之所以发生这种情况是因为KeyboardState为所有键值使用内部List,会被下一帧再次使用,当您调用Keyboard.GetState时会覆盖所有的键盘状态。如果要修正这个问题你需维持自己的键值列表,每次从上一帧的键值克隆。使用通用List类构造函数会自动为你复制所有内容。
使用此list与直接使用键盘状态的工作方式相同。看一下Input类中的KeyboardSpaceJustPressed的属性。
/// <summary> /// Keyboard space just pressed? /// </summary> /// <returns>Bool</returns> public static bool KeyboardSpaceJustPressed { get { return keyboardState.IsKeyDown(Keys.Space) && keysPressedLastFrame.Contains(Keys.Space) == false; } // get } // KeyboardSpaceJustPressed
Input类的其余部分与我刚才提到的键盘输入文字的方法很类似,为文本框获取键盘输入与一个普通的Windows文本框类似,下面是单元测试:
/// <summary> /// Test keyboard chat input /// </summary> public static void TestKeyboardChatInput() { // Better version with help of Input.HandleKeyboardInput! string chatText = ""; TestGame.Start("TestKeyboardChatInput", null, delegate { TextureFont.WriteText(100, 100, "Your chat text: " + chatText + // Add blinking | ((int)(BaseGame.TotalTime / 0.35f) % 2 == 0 ? "|" : "")); Input.HandleKeyboardInput(ref chatText); }, null); } // TestKeyboardChatInput()
该单元测试显示chatText字符串和后面的一个闪烁|标志。然后HandleKeyboardInput方法用来让使用者输入新的文字。看看HandleKeyboardInput方法:
/// <<summary> /// Handle keyboard input helper method to catch keyboard input /// for an input text. Only used to enter the player name in the /// game. /// </summary> /// <param name="inputText">Input text</param> public static void HandleKeyboardInput(ref string inputText) { // Is a shift key pressed (we have to check both, left and right) bool isShiftPressed = keyboardState.IsKeyDown(Keys.LeftShift) || keyboardState.IsKeyDown(Keys.RightShift); // Go through all pressed keys foreach (Keys pressedKey in keyboardState.GetPressedKeys()) // Only process if it was not pressed last frame if (keysPressedLastFrame.Contains(pressedKey) == false) { // No special key? if (IsSpecialKey(pressedKey) == false && // Max. allow 32 chars inputText.Length < 32) { // Then add the letter to our inputText. // Check also the shift state! inputText += KeyToChar(pressedKey, isShiftPressed); } // if (IsSpecialKey) else if (pressedKey == Keys.Back && inputText.Length > 0) { // Remove 1 character at end inputText = inputText.Substring(0, inputText.Length - 1); } // else if } // foreach if (WasKeyPressedLastFrame) } // HandleKeyboardInput(inputText)
该方法使用了Input类中的两个辅助方法。首先,IsSpecialKey用来判断键入的是文字还是如F1,方向键,Delete,Enter之类的其他键,这些其他键不会直接加入到文字中去。然后,你处理Backspace键的特殊情况。您还可以扩展代码来处理Enter或Delete键。
下一个问题是,你不是总能将键值添加到您的输入文字中。首先无论用户是否按下Shift键,按“A”键总是在文本框中添加A。而一些特殊键 如+,-,{等等,将显示为“Plus”,“Minus”,“OemOpenBrackets”。为解决这个问题,如果XNA会为您提供键的真正意义应该不错,但它没有,你必须通过Input类中的KeyToChar辅助方法自己实现:
/// <summary> /// Keys to char helper conversion method. /// Note: If the keys are mapped other than on a default QWERTY /// keyboard, this method will not work properly. Most keyboards /// will return the same for A-Z and 0-9, but the special keys /// might be different. Sorry, no easy way to fix this with XNA ... /// For a game with chat (windows) you should implement the /// Windows events for catching keyboard input, which are much better! /// </summary> /// <param name="key">Keys</param> /// <returns>Char</returns> public static char KeyToChar(Keys key, bool shiftPressed) { // If key will not be found, just return space char ret = ' '; int keyNum = (int)key; if (keyNum >= (int)Keys.A && keyNum <= (int)Keys.Z) { if (shiftPressed) ret = key.ToString()[0]; else ret = key.ToString().ToLower()[0]; } // if (keyNum) else if (keyNum >= (int)Keys.D0 && keyNum <= (int)Keys.D9 && shiftPressed == false) { ret = (char)((int)'0' + (keyNum - Keys.D0)); } // else if else if (key == Keys.D1 && shiftPressed) ret = '!'; else if (key == Keys.D2 && shiftPressed) ret = '@'; [etc. about 20 more special key checks] // Return result return ret; } // KeyToChar(key)
通过这种方法,HandleKeyboardInput方法现在以您所期望的方式工作了,单元测试可以进行了。Input类中有更多的单元测试,研究一下可以了解更多有用的的属性和方法。