Developers following my twitter feed may already know that in the past few days, I’ve been working on a new component for the Nuclex Framework: Nuclex.Input. This component aims to solve all problems I ever had with input devices in XNA :)
It’s a very simple library that provides input device classes
similar to the ones in XNA (Keyboard,
Mouse,
GamePad),
but instead of 4 game pads, there are 8 (with indexes 5-8 for
DirectInput-based controllers). All input devices
provide events (like KeyPressed
and
CharacterEntered
on the keyboard or
MouseWheelRotated
on the mouse, for example).
Here’s a quick run down of the features:
-
Well-behaving keyboard text input
- Honors keyboard layout and system locale
- Supports XBox 360 chat pads
- Very easy to use: just subscribe to an event
-
Support for standard PC game controllers
- Works with any DirectInput-compatible controller
-
Mouse movement with sub-pixel accuracy(postponed)Finally put those expensive high-dpi mice to use ;-)
-
Allows event-based input handling
- Fully type-safe: events instead of message objects
- Only compares states if events have subscribers
- Mouse and keyboard don’t have to compare states at all
-
Zero garbage: doesn’t feed the garbage collector
- During usage, the library produces zero garbage
Curious? Click on “Read More” to view some code samples!
Design Overview
If you understand UML, here’s an overview of the library’s design:
Specific usage examples follow below!
Initialization
To use Nuclex.Input, you simply create an instance of its
InputManager
and add it to the Game.Components
collection.
Advanced users can also specify a custom window handle and make use of the component without having it register itself in the Game.Services container. But let’s keep it simple for now:
// Highlight 3,12,16,20
using Microsoft.Xna.Framework;
using Nuclex.Input;
namespace TestGame {
/// <summary>This is the main type for your game</summary>
public class Game1 : Microsoft.Xna.Framework.Game {
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
InputManager input;
public Game1() {
graphics = new GraphicsDeviceManager(this);
input = new InputManager(Services);
Content.RootDirectory = "Content";
Components.Add(input);
}
// ...
}
} // namespace TestGame
Getting the Game Pad State
This is as simple as it was before. Instead of using the GetState()
on XNA’s Keyboard
, Mouse
or GamePad
classes, you ask the input manager for the device and then for its state:
/// <summary>
/// Allows the game to run logic such as updating the world,
/// checking for collisions, gathering input, and playing audio.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Update(GameTime gameTime) {
/* old code:
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
*/
// Allows the game to exit
if (input.GetGamePad(PlayerIndex.One).GetState().Buttons.Back == ButtonState.Pressed)
this.Exit();
// TODO: Add your update logic here
base.Update(gameTime);
}
As you can see, instead of calling GamePad.GetState(PlayerIndex.One)
the above snippet calls input.GetGamePad(PlayerIndex.One).GetState()
.
That basically all there is to it.
In addition to the 4 XBox 360 controllers that XNA’s GamePad
class
allows you to access, the InputManager
provides 4 additional game pads
that represent any DirectInput-compatible joysticks or game pads attached to
the system. So game pads 1-4 are XNA’s XBox 360 game pads and game pads 5-7 are
standard DirectInput-compatible game pads. You can query them like so:
// Allows the game to exit
if (input.GetGamePad(ExtendedPlayerIndex.Five).GetState().Buttons.Back == ButtonState.Pressed)
this.Exit();
Note the ExtendedPlayerIndex
enumeration. Also important is that
there are always 8 game pads you can query. This is identical
in behavior to XNA’s GamePad
class which allows you to query non-existent
game pads. If there are less than 4 DirectInput-compatible controllers in a system,
the remaining slots will be filled with dummies that simply do nothing and
return a state with all buttons released and all sticks in neutral positions.
In other words, you can safely call GetGamePad(ExtendedPlayerIndex.Five)
and it will always work. If a DirectInput-compatible game pad is attached, pressing
the back button on it will exit the game. If no DirectInput-compatible game pad
is attached, the game pad will simply report that the button is not pressed all
the time. This is very fast.
Event-based Input
Sometimes, you’ll want to perform an action only when a button is pressed down or when a certain key is pressed. This requires you to keep the previous state of an input device and check whether the state of a button has changed from the last time the controller’s state was queried:
previous = current;
current = GamePad.GetState(PlayerIndex.One);
bool pressedInThisFrame =
(previous.Buttons.A == ButtonState.Released) &&
(current.Buttons.A == ButtonState.Pressed);
if(pressedInThisFrame) {
doSomething();
}
With event-based input, this has become a lot simpler:
IGamePad gamePad = input.GetGamePad(PlayerIndex.One);
gamePad.ButtonPressed += delegate(Buttons buttons) {
if((buttons & Buttons.A) != 0) {
doSomething();
}
};
There are various ways in which event-based input can be designed, the most
popular one probably being message objects that are passed around.
I decided against this design because it tends to create unsightly code
(long methods with dozens of switch cases), loses type safety and
requires additional user interaction to allow for an important performance
optimization: only comparing the states of those devices in whose events
the user is actually interested. This comes free with events because each
device can simply look if its ButtonPressed
etc. event has
any subscribers before looking for state changes.
Event-based input is ideal for GUI libraries because GUIs need to route input notifications down the control tree. For example, if you press space with a text box in focus, a GUI would add a space character to the text box, but if you press space with a button in focus, key press is routed to the button instead of the text box and might cause it to be pushed down.
Text Entry
This is another often-needed feature.
You can certainly write a text input system that uses XNA’s
Keyboard
class, but that would
- force you to check all 256 keys for state changes during each update cycle
- require hand-coding the handling of shift, caps lock, num lock and other keys
- still not respect the keyboard layout the user has selected. The top row of french keyboards, for example, reads “azerty”, not “qwerty”. Then there is an accent key, a special shift key called “alt gr”, …
The way normal windows applications receive their text input is via a special
window message called WM_CHAR
. Nuclex.Input subclasses XNA’s
main window and intercepts this message so that its keyboard device can
provide you with plain char
s that you can simply append to
a string
, no matter whether you game is being played on a
French, German, Chinese or Russian keyboard.
All you have to do is subscribe to the Keyboard
s
CharacterEntered
event:
/// <summary>
/// Allows the game to perform any initialization it needs to before starting to run.
/// This is where it can query for any required services and load any non-graphic
/// related content. Calling base.Initialize will enumerate through any components
/// and initialize them as well.
/// </summary>
protected override void Initialize() {
base.Initialize();
// TODO: Add your initialization logic here
IKeyboard keyboard = input.GetKeyboard();
keyboard.CharacterEntered += characterEntered;
}
void characterEntered(char character) {
Trace.WriteLine("User has typed the character '" + character + "'");
}
How easy is that!
Of course this still leaves you to implement any luxury features such as backspace, movable caret or text selection :)