Typing in XNA & Monogame

For the most part, XNA gets just about everything right. One thing you may eventually get hung up on though is keyboard support for text entry. In my case, I needed it so the player could type in their name for the Online Leaderboard in 48 Chambers. Now, I know you’re thinking “just check the KeyboardState!” It seems like the obvious solution, but upon inspection you’ll realize there’s two major issues with it. The first is that your player may type faster than the framerate (ie. you pushed and released the key before the state snapshot for the next frame was taken). The second issue is that even if you did grab all the keypresses for that frame, there’s no information about the order they were pressed.

So what’s the solution to all this? You need Event-Based key input instead of Poll-Based. So why doesn’t this work out of the box on XNA? Well, it looks like Microsoft simply didn’t implement the message system in the XNA Window. Fortunately, there’s an incredibly easy solution on Windows available here. The Infiniminer guys came up with a solution that hooks into Windows DLLs, and will give you acccess to the events you need.

That was an easy enough problem to solve for Windows, but when I began porting 48 Chambers to Mac with Monogame I ran into an issue: there’s no user32.dll to hook into! I realized that I’d have to hook into the Mac window events (if it even had them unlike the Windows version) if I was ever going to get text input. So what follows is my quick hack to make this work. I’m sure there’s a better solution to it, but for now this is good enough for me.

First, we need to modify the Mac Monogame library. After a little bit of digging, I found that the GameWindow class is the one receiving the keyboard input, and lo and behold it’s event-based! It uses the events from the window to build the KeyboardState for the next frame. What we want to do though is make our own event to attach to. So not only will the KeyboardState be filled, but we can also subscribe for an individual keypress.

We need to create the event delegate and EventArgs, so in MonoGame.Framework.MacOS.GameWindow, add the following code to the top of the file before the GameWindow class:

public delegate void DiscordKeyPressHandler(object source, DiscordKeyEventArgs e);
 
public class DiscordKeyEventArgs : EventArgs
{
	public Keys KeyPressed;
 
	public DiscordKeyEventArgs (Keys key)
	{
		this.KeyPressed = key;
	}
}

Declare our new event in the GameWindow class:

public class GameWindow : MonoMacGameView
{
	//private readonly Rectangle clientBounds;
	private Rectangle clientBounds;
	private Game _game;
	private MacGamePlatform _platform;
	public DiscordKeyPressHandler OnDiscordKeyPress;
...

Now that we’ve got our event you can subscribe to, we just need to trigger it on Keypress. In the same class, scroll down and find the KeyDown method and add our new event:

public override void KeyDown (NSEvent theEvent)
{
	Keys kk = KeyUtil.GetKeys (theEvent); 
 
	if (!_keys.Contains (kk))
		_keys.Add (kk);
 
	if(OnDiscordKeyPress != null)
	{
		OnDiscordKeyPress(this, new DiscordKeyEventArgs(kk));
	}
 
	UpdateKeyboardState ();
}

And that’s it! You can now subscribe to the OnDiscordKeyPress event and get keypresses. I wasn’t sure the best way to get to this class, so I had to go up a level to MacGamePlatform class and make the GameWindow public. So lastly, in my Name Input screen I subscribed to the event and made a quick handler:

...
 
StringBuilder textBuffer = new StringBuilder();
 
public NameInputScreen()
{
     MacGamePlatform plat = (MacGamePlatform)MyGame.Services.GetService(typeof(MacGamePlatform));
     plat._gameWindow.OnDiscordKeyPress += MacKeypressHandler;
}
 
...
 
private void MacKeypressHandler(object sender, DiscordKeyEventArgs e)
{
	string letter = e.KeyPressed.ToString();
 
	if(letter.Length > 1)
	{
		if(letter == "Back")
			textBuffer = textBuffer.Remove(textBuffer.Length - 1, 1);
		else if(letter == "Space")
			textBuffer.Append(" ");
		else if(letter == "OemPeriod")
			textBuffer.Append(".");
		else if(letter.StartsWith("D") && letter.Length == 2)
			textBuffer.Append(letter.Substring(1,1));
 
	}
	else
		textBuffer.Append(letter);
}
 
...
 
public void ExitScreen()
{
     //unsubscribe when you're finished with it
     MacGamePlatform plat = (MacGamePlatform)MyGame.Services.GetService(typeof(MacGamePlatform));
     plat._gameWindow.OnDiscordKeyPress -= MacKeypressHandler;
}