Detecting keyboard layout for WASD

I used this trick for PyWeek, so I figured I’d share it.

I’m a dvorak user myself, and the fact that many game developers assume that people use QWERTY can often be a cause of frustration. People simply map their keys to WASD without regard for different keyboard layouts.

Fortunately, many professional games seem to handle it correctly, with even the help messages automatically displaying the right text (saying ‘press E to strafe right’ instead of 'press D to strafe right), which is what prompted me to look into how they do this.

Apparently, the Windows API at least provides a way for developers to map keyboard scan codes to virtual key codes, providing us with a way to know which virtual key is mapped to which physical keyboard key. This allows us to get the correct events to listen to in order for people not to have to reach all the way over their keyboards for basic walk controls. (for dvorak, it’s “,aoe”, for colemak, it’s “wars”, etc.)

Keep in mind that this is rough, mostly untested code.

if sys.platform == 'win32':
        import ctypes
        ctypes = None
        print("ctypes import failed")

def map_vsc(vsc, default):
    """ Map scan code to an event to listen to.  The default argument is what is
    returned if we don't have a way to map the scan code to an event name. """

    if sys.platform == 'win32' and ctypes is not None:
        vk = ctypes.windll.user32.MapVirtualKeyA(vsc, 1) # MAPVK_VSC_TO_VK
        if vk > 0:
            # OK, we've got the vk, now map it to a character.
            char = ctypes.windll.user32.MapVirtualKeyA(vk, 2) # MAPVK_VK_TO_CHAR
            if char > 0:
                return chr(char).lower()

    return default

Now, here’s an example that shows how to use it. The scan codes for WASD are 0x11, 0x1E, 0x1F and 0x20, respectively.

    keybinds = {
        'forward': map_vsc(0x11, default='w'),
        'back':    map_vsc(0x1F, default='s'),
        'lstrafe': map_vsc(0x1E, default='a'),
        'rstrafe': map_vsc(0x20, default='d'),
        'jump': 'space',
    self.accept(keybinds['forward'], self.on_forward)
    ... etc ...

The values for the scan keys are consecutive based on the physical location of the key on the keyboard; ‘Q’ is 16 (0x10), ‘W’ is 17 (0x11), ‘E’ is 18 (0x12) etc.

If I find time to do the equivalent for X11, I’ll post an updated snippet. The current Carbon-based renderer of Panda for OS X actually assumes QWERTY for the mapping, so it’s not an issue there.

In the long run, I’ll look into implementing a way for Panda3D to handle this kind of thing natively and I’ll probably make a blog post about it.

Glad to see the issue I brought up with you has been partially resolved. :smiley: Can’t wait for the native cross-platform solution. While it’s not as difficult to do QWERTY’s WASD on Colemak (W is in the same spot, and the homerow begins with ARSTD, so two shifted keys over from QWERTY) as with Dvorak, it’s still a pain in the butt to stretch fingers across. And I certainly can’t map the set to WARS since that’d send QWERTY users into a madhouse. Automatic detection of the keymap would be an excellent first line of defence.

Of course, users should be able to remap the keys they use, but for small game projects which don’t plan for such user configuration, detection is a must!

Of course; allowing people to remap their keys is always a desirable feature to implement. But this snippet should still be used to determine what the default configuration should be. :wink:

Here’s how to do the same thing on X11:

>>> import ctypes, os
>>> x11 = ctypes.CDLL("")
>>> disp = x11.XOpenDisplay(os.environ.get("DISPLAY", ""))
>>> vk = x11.XKeycodeToKeysym(disp, 27, 0)
>>> print(chr(vk))

In this case, 27 is the scancode for R, which is mapped to P on my dvorak layout. Note that the scan codes are different from the ones used on Windows. Here is a list: … -keycodes/

I should also point out that the fact that you can call chr() on these virtual keycodes is because the vk’s were designed to overlap with ASCII equivalents, but not all virtual keycodes fall within ASCII ranges!

I’ve recently checked in support for Panda for properly handling this thing under the hood. There are now raw-w, raw-a, raw-w-up, etc. events that map directly to the physical locations of the keys as they would be on an ANSI US keyboard.

There is also that returns a mapping between raw keys and virtual keys, which can be used for displaying the key names in help text or determining the default user key mapping.

More on these later.