Sunday, May 24, 2009

When KeyPressed / KeyDown just isn't enough. An adventure in GetKeyboardState.

Source Code: http://www.box.net/shared/y7v5jskfd1

A question on StackOverflow this weekend, piqued my interest. It was about some guy that was creating a Tetris game in C# (using Winforms) and was having issues with keyboard input. Basically, he was hooking into the Forms KeyDown event, and if the key was either left down or right, he'd move the tetris piece accordingly. The problem he was running into was if the user would hold down more than one key at a time (which is very common when playing Tetris or any other game for that matter), only one key would register.

Having messed with XNA in the past (I still hope to release a game to the XBOX Community games in this life time :-P ), I had a hunch. In XNA, things work a little different. A little background on how games work in general. All games consist of a "Game Loop". Basically, the entire application runs in one gigantic loop. You have a timer that constantly runs in the background, and every time the timer "ticks" two things happen. Update() is called, and Draw() is called. During the update routine, you update your game logic, ie: move player's position, move enemies position etc.. Then, during the Draw routine, you draw everything to screen.

It is during this Update routine, where you "poll" the keyboard (or gamepad when working on the XBOX 360) to find out what's going on. Here's a small sample:

56 var state = Keyboard.GetState();

57 bool downPressed = state.IsKeyDown(Keys.Down);


As you can see, you "ask" the keyboard if the Down key is pressed at the current time. The cool thing about this method is that you can "ask" for multiple keys in one trip. There's nothing stopping you from doing this:

56 var state = Keyboard.GetState();

57 bool downPressed = state.IsKeyDown(Keys.Down);

58 bool upPressed = state.IsKeyDown(Keys.Up);



In Windows Forms though, if you listen for the KeyDown event, the KeyEventArgs will give you the Keycode, but only of ONE key. So, if more than one key is pressed, what do you do?

After some searching, I found that there's a "GetKeyboardState" function that's part of the Win32 API. We should be able to P/Invoke this function, and poll the keyboard for more than one key. According to the Microsoft documentation, you pass in the int value of the key, and you get back a short.

The return value specifies the status of the specified virtual key, as follows:

  • If the high-order bit is 1, the key is down; otherwise, it is up.
  • If the low-order bit is 1, the key is toggled. A key, such as the CAPS LOCK key, is toggled if it is turned on. The key is off and untoggled if the low-order bit is 0. A toggle key's indicator light (if any) on the keyboard will be on when the key is toggled, and off when the key is untoggled.
So, I found this helpful little class online that wraps this API call nicely.

First, a simple struct to hold the Key's state:



public struct KeyStateInfo
{
private Keys key;
private bool isPressed;
private bool isToggled;

public KeyStateInfo(Keys key, bool ispressed, bool istoggled)
{
this.key = key;
isPressed = ispressed;
isToggled = istoggled;
}

public static KeyStateInfo Default
{
get
{
return new KeyStateInfo(Keys.None, false, false);
}
}

public Keys Key
{
get { return key; }
}

public bool IsPressed
{
get { return isPressed; }
}

public bool IsToggled
{
get { return isToggled; }
}
}

Then, here's the actual class that wraps the P/Invoke:



using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;

public class KeyboardInfo
{
private KeyboardInfo() { }
[DllImport("user32")]
private static extern short GetKeyState(int vKey);

public static KeyStateInfo GetKeyState(Keys key)
{
short keyState = GetKeyState((int)key);
int low = Low(keyState), high = High(keyState);
bool toggled = low == 1;
bool pressed = high == 1;
return new KeyStateInfo(key, pressed, toggled);
}
private static int High(int keyState)
{
return keyState > 0 ? keyState >> 0x10
: (keyState >> 0x10) & 0x1;
}
private static int Low(int keyState)
{
return keyState & 0xffff;
}
}


Simple enough. To prove that this works now with more than one key, I wrote a simple windows app that moves a ball around the form based on the user's pressing of the arrow keys. You'll notice, that if you press two arrows at once, it will move the ball diagonally, proving that it accepts more than one key at a time. Here's the code:

First, I created a simple Ball class:



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;

namespace WindowsFormsApplication1
{
public class Ball
{
private Brush brush;

public float X { get; set; }
public float Y { get; set; }
public float DX { get; set; }
public float DY { get; set; }
public Color Color { get; set; }
public float Size { get; set; }

public void Draw(Graphics g)
{
if (this.brush == null)
{
this.brush = new SolidBrush(this.Color);
}
g.FillEllipse(this.brush, X, Y, Size, Size);
}

public void MoveRight()
{
this.X += DX;
}

public void MoveLeft()
{
this.X -= this.DX;
}

public void MoveUp()
{
this.Y -= this.DY;
}

public void MoveDown()
{
this.Y += this.DY;
}
}

}


This class basically holds the coordinates of the ball, and the "velocity" (DX, and DY), or the speed at which the ball will move each time the key is pressed. It also holds the color and size.

Then, here's the main Form code:



using System;
using System.Drawing;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
private Ball ball;
private Timer timer;
public Form1()
{
InitializeComponent();
this.ball = new Ball
{
X = 10f,
Y = 10f,
DX = 2f,
DY = 2f,
Color = Color.Red,
Size = 10f
};
this.timer = new Timer();
timer.Interval = 20;
timer.Tick += new EventHandler(timer_Tick);
timer.Start();
}

void timer_Tick(object sender, EventArgs e)
{
var left = KeyboardInfo.GetKeyState(Keys.Left);
var right = KeyboardInfo.GetKeyState(Keys.Right);
var up = KeyboardInfo.GetKeyState(Keys.Up);
var down = KeyboardInfo.GetKeyState(Keys.Down);

if (left.IsPressed)
{
ball.MoveLeft();
this.Invalidate();
}

if (right.IsPressed)
{
ball.MoveRight();
this.Invalidate();
}

if (up.IsPressed)
{
ball.MoveUp();
this.Invalidate();
}

if (down.IsPressed)
{
ball.MoveDown();
this.Invalidate();
}


}


protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
if (this.ball != null)
{
this.ball.Draw(e.Graphics);
}
}
}


}

First, I create a red ball that's 10 pixels in size. Then, I create a Timer which will "tick" every 20 milliseconds. This is simple to simulate a game loop. It won't actually "Draw" every tick, it will only ever redraw if one of the arrow keys is pressed. The event handler for the Timer is where the meat of this lies:


void timer_Tick(object sender, EventArgs e)
{
var left = KeyboardInfo.GetKeyState(Keys.Left);
var right = KeyboardInfo.GetKeyState(Keys.Right);
var up = KeyboardInfo.GetKeyState(Keys.Up);
var down = KeyboardInfo.GetKeyState(Keys.Down);

if (left.IsPressed)
{
ball.MoveLeft();
this.Invalidate();
}

if (right.IsPressed)
{
ball.MoveRight();
this.Invalidate();
}

if (up.IsPressed)
{
ball.MoveUp();
this.Invalidate();
}

if (down.IsPressed)
{
ball.MoveDown();
this.Invalidate();
}
}

First, we "poll" the keyboard for the arrows keys. We poll for all four of them, therefore if more than one is pressed, we'll be able to react to all of them. Then, if either key is held down, we move the ball in that direction and call Invalidate() to allow the form to repaint itself.

One final thing I'd like to point out, is that with the standard KeyDown event in Windows Forms, you can get "modifiers" (Shift, Ctrl, Alt etc.) as well as the key that was pressed. So if you're writing an app where you want to have some shortcut like say CTRL + A, you don't need to do this. This is only when you want to get info on more than one standard key................Which is why, you'll probably never need this in a real app outside of games (in which case, you're better off with XNA) but it's still something that's good to know.

1 comment:

nine-o said...

Great explanation, thanks!