keyboard mapping snippet

Hi, I wanted a powerful but simple to use key mapping system for my game, so I designed this on top of pandas messenging system.

It uses an xml file (i have it set up to use 2, one as a default, allowing the user to reset if they mess things up, the game will reset automatically if the normal file disappears)

Its fairly resillient, i dont think it will crash much except where i raise an exception deliberatly. So meddling users shouldnt matter too much if they break the file, you can give them nice clear error messeges.

It uses an xml schema to define the format of the xml files, which means i dont have to do any fancy parsing, and your technically capable users will be able to understand the format.

One issue is that you need to use the lxml library (the standard library doesnt allow xmlschema parsing for some stupid reason)

adding “-on” or “-off” to the end of a control(a control is what i called the psuedo-keys that the real keys get mapped to, things like “fire_gun”) will send a boolean arguement along with the message, meaning you can design your game to cope with all commands being held or toggled, giving the user as much choice as possible.

Heres the code:

from lxml import etree
from direct.stdpy.file import *
import shutil
from pandac.PandaModules import Filename
from direct.showbase.DirectObject import DirectObject
'''
Created on 5 Jul 2009

@author: Finn Bryant
'''

NS = "{http://www.quantusgame.org}"

class KeymapController(DirectObject):
	'''
	Create instance of this and it will read key/mouse button presses 
	and spew out in-game controls using the messanger system.
	The controls given depend on which branch of the control tree is currently
	on, e.g. if the player is in a tank then the general.unit.vehicle.ground branch
	so anything anywhere on that branch will be read and sent.
	If a key is defined twice on a branch, the most specific definition is used.
	(general.unit.vehicle beats general.unit).
	
	reads mapping data from main.keymap using the keymap.xsd schema.
	if main.keymap is missing or empty it will copy the data from default.keymap
	
	main.keymap may be changed by the user, keymap.xsd and default.keymap 
	should never be modified by the user.
	(but no checks will be made, so go right ahead if you want...)
	'''
	currentBranch = "general"
	defaultFN = Filename("default.keymap")
	mainFN = Filename("main.keymap")
	schemaFN = Filename("keymap.xsd")
	
	def __init__(self,startingBranch = currentBranch):
		self.currentBranch = startingBranch
		
		if self.schemaFN.exists() == False:
			raise NameError('The File "keymap.xsd" does not appear to exist')
		if self.defaultFN.exists() == False:
			raise NameError('The File "default.keymap" does not appear to exist')
		if self.mainFN.exists() == False:
			shutil.copy(self.defaultFN.toOsSpecific(), self.mainFN.toOsSpecific())
		
		schemaRoot = etree.parse(self.schemaFN.toOsSpecific())
		self.schema = etree.XMLSchema(schemaRoot)
		
		parser = etree.XMLParser(schema = self.schema)
		
		try:
			self.default = etree.parse(self.defaultFN.toOsSpecific(),parser)
		except etree.XMLSyntaxError:
			raise NameError('The File "default.keymap" failed validation.')
		
		try:
			self.main = etree.parse(self.mainFN.toOsSpecific(),parser)
		except etree.XMLSyntaxError, detail:
			if detail.message == "Document is empty, line 1, column 1":
				shutil.copy(self.defaultFN.toOsSpecific(), self.mainFN.toOsSpecific())
				self.main = etree.parse(self.mainFN.toOsSpecific(),parser)
			else:
				raise
		except etree.XMLSyntaxError:
			raise NameError('The File "main.keymap" failed validation.')
		
		branchList = self.getBranchList(self.main.getroot())
		self.mappingList = self.getMappingList(branchList)
		self.setupMappings()
	
	def getBranchList(self, root):
		result = []
		children = root.findall(NS + "branch")
		result.extend(children)
		
		for branch in children:
			result.extend(self.getBranchList(branch))
		
		return result
	
	def getMappingList(self, branchList):
		result = []
		for branch in branchList:
			branchMappings = branch.findall(NS + "mapping")
			element = branch.getparent()
			branchName = branch.get("name")
			while element.tag == NS + "branch":
				branchName = element.get("name") + "." + branchName
				element = element.getparent()
			for mapping in branchMappings:
				result.append((mapping,branchName))
		return result
	
	def setupMappings(self):
		inputsByKey = {}
		for mapping in self.mappingList:
			input = mapping[0].find(NS + "input").text
			controlText = mapping[0].find(NS + "control").text
			
			active = None
			if controlText.endswith("-on"):
				active = True
			elif controlText.endswith("-off"):
				active = False
			
			controlText = controlText.split("-")[0]
			
			if not inputsByKey.has_key(input):
				inputsByKey[input] = []
			
			if active != None:
				inputsByKey[input].append( (mapping[1], controlText, [active]) )
			else:
				inputsByKey[input].append( (mapping[1], controlText, [      ]) )
		
		for input in inputsByKey:
			self.accept(input, self.translate, [inputsByKey[input]])
		
	
	def setCurrentBranch(self,branchName):
		self.currentBranch = branchName
		print branchName
		self.setupMappings()
	
	def translate(self, controls):
		selectedControl = None
		branchLength = 0
		for control in controls:
			if self.currentBranch.startswith(control[0]):
				if len(control[0]) > branchLength:
					selectedControl = control
					branchLength = len(control[0])
		if selectedControl != None:
			messenger.send(selectedControl[1],selectedControl[2])
			print selectedControl[0] + "." + selectedControl[1], selectedControl[2]

and heres the xml schema (“keymap.xsd”):

<?xml version="1.0"?>

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.quantusgame.org"
xmlns="http://www.quantusgame.org"
elementFormDefault="qualified">
	
	<xs:element name="keymap" type="Root"/>
	
	
	<xs:complexType name="Root">
		<xs:sequence>
			<xs:element name="branch" type="BranchType"/>
		</xs:sequence>
	</xs:complexType>
	
	<xs:complexType name="BranchType">
		<xs:sequence>
			<xs:element name="mapping" type="MappingType"
					minOccurs="0" maxOccurs="unbounded"/>
			<xs:element name="branch" type="BranchType"
					minOccurs="0" maxOccurs="unbounded"/>
		</xs:sequence>
		<xs:attribute name="name" type="xs:string" use="required"/>
	</xs:complexType>
	
	<xs:complexType name="MappingType">
		<xs:sequence>
			<xs:element name="input" type="InputType"/>
			<xs:element name="control" type="ControlType"/>
		</xs:sequence>
	</xs:complexType>
	
	<xs:simpleType name="InputType">
		<xs:restriction base="xs:string">
			<xs:pattern value=
			"(shift-)?(ctrl-)?(alt-)?([^A-Z]|[a-z][a-z0-9_]{0,15})(-up|-repeat)?"/>
		</xs:restriction>
	</xs:simpleType>
	
	<xs:simpleType name="ControlType">
		<xs:restriction base="xs:string">
			<xs:pattern value="[a-z0-9_]{0,20}(-off|-on)?"/>
		</xs:restriction>
	</xs:simpleType>
	
</xs:schema>

i recomend you replace the namespace name… (find any instances of “http://www.quantusgame.org” and replace with the name of your game/its website)

oh, and heres an example of a keymap file:

<?xml version="1.0" ?>

<keymap
xmlns="http://www.quantusgame.org"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="keymap.xsd">
	<branch name="general">
		<mapping>
			<input>g</input>
			<control>god_mode</control>
		</mapping>
		<mapping>
			<input>p</input>
			<control>per_pixel_lighting</control>
		</mapping>
		<mapping>
			<input>escape</input>
			<control>menu</control>
		</mapping>
		<mapping>
			<input>w</input>
			<control>move_forward-on</control>
		</mapping>
		<mapping>
			<input>w-up</input>
			<control>move_forward-off</control>
		</mapping>
		
		<mapping>
			<input>s</input>
			<control>move_backward-on</control>
		</mapping>
		<mapping>
			<input>s-up</input>
			<control>move_backward-off</control>
		</mapping>
		
		<mapping>
			<input>a</input>
			<control>move_left-on</control>
		</mapping>
		<mapping>
			<input>a-up</input>
			<control>move_left-off</control>
		</mapping>
		
		<mapping>
			<input>d</input>
			<control>move_right-on</control>
		</mapping>
		<mapping>
			<input>d-up</input>
			<control>move_right-off</control>
		</mapping>
		
		<branch name="god_mode">
			<mapping>
				<input>wheel_up</input>
				<control>speed_up</control>
			</mapping>
			<mapping>
				<input>wheel_down</input>
				<control>speed_down</control>
			</mapping>
			<mapping>
				<input>c</input>
				<control>switch_sector</control>
			</mapping>
			<mapping>
				<input>r</input>
				<control>move_up-on</control>
			</mapping>
			<mapping>
				<input>r-up</input>
				<control>move_up-off</control>
			</mapping>
			<mapping>
				<input>f</input>
				<control>move_down-on</control>
			</mapping>
			<mapping>
				<input>f-up</input>
				<control>move_down-off</control>
			</mapping>
		</branch>
		
		<branch name="unit">
			<mapping>
				<input>mouse1</input>
				<control>primary_fire-on</control>
			</mapping>
			<mapping>
				<input>mouse1-up</input>
				<control>primary_fire-off</control>
			</mapping>
			<mapping>
				<input>mouse3</input>
				<control>secondary_fire-on</control>
			</mapping>
			<mapping>
				<input>mouse3-up</input>
				<control>secondary_fire-off</control>
			</mapping>
			
			<branch name="infantry">
				
			</branch>
			<branch name="vehicle">
				<branch name="aircraft">
					<mapping>
						<input>r</input>
						<control>move_up-on</control>
					</mapping>
					<mapping>
						<input>r-up</input>
						<control>move_up-off</control>
					</mapping>
					<mapping>
						<input>f</input>
						<control>move_down-on</control>
					</mapping>
					<mapping>
						<input>f-up</input>
						<control>move_down-off</control>
					</mapping>
				</branch>
				<branch name="ground">
					
				</branch>
			</branch>
		</branch>
		<branch name="map">
			
		</branch>
	</branch>
</keymap>

remember that the namespace must be the same as the namespace in keymap.xsd

example usage:

		self.input = KeymapController()
		self.input.setCurrentBranch("general.god_mode")
		self.accept("control_name_here",self.functionName)

watch the command line while you press keys, you can see it all working.

did i just see XML!
/me stabs eyes.

dirtsimple.org/2004/12/python-is-not-java.html

Seems a little harsh. XML’s not so bad.

David

huh? its a very simple problem, that uses a tree structure, what is a better choice? (and can this better choice do automatic parsing?)

and when using the etree interface, it seems ok, easier than direct text file manipulation…

but good link nonetheless, i hadnt ever thought about a few of the things mentioned in that article, i shall now endever to write better python code.

EDIT: if there really is an option better than xml, i would love to here it, ive been under the impression that you either have plain text (hard to parse) binary (user impenitrable, confusing), code (a posibility, but seems overkill as a data storage format, and if i want some of my data to be downloaded from a server, this would be unsecure) or finally xml (is there another data storage format that i have just never heard of?)

JSON json.org/
YAML yaml.org/
python config files: docs.python.org/library/configparser.htm
python code as data format - why write a python program that scans ugly xml file and does key binding and instead use python interpreter to scan nice python file that does key binding?
S-Expressions en.wikipedia.org/wiki/S-expression

i like the look of yaml. I can see why its better than xml, lots of cool features, and quite compact.

ive done a bit of research into why xml might be considered bad, and tbh it looks like a “meh” issue, but ill keep an eye on PyYAML, since a new standard just appeared, ill wait for it to catch up.

YAML is like 8 years old. More cool projects are using it like RoR and Google’s AppEngine.
I feel it has a stab at replacing XML in the next 20 years.

But why not just use python for this? XML is text and python is also text.

because python is an insecure language, and i would prefer to use the same format for all my data files (some of them will be passed to the client from a potentially unsafe server), plus i want a way to check the clients for modifications, and if i have a python file that the user can modify that opens an unclosable can of worms.

we had a long discussion about the format of config files recently in our team. here’s the outcome:

XML
[color=green]pros:

  • official, well known format
  • easy to work with through code
  • doesn’t require any extra libraries
  • supports data trees and attributes

[color=darkred]cons:

  • hard to edit by hand
  • everything needs validation
  • parser needed (there’s only rough code in the python xml module)
  • all schema languages require non-standard libraries -> not cross-plattform anymore

Panda’s PRC
[color=green]pros:

  • doesn’t need any external libraries, not even any read methods - everything is there already
  • very easy to use
  • value/type checking included
  • best format to edit by hand

[color=darkred]cons:

  • not an official format
  • flat -> no nesting
  • not extendible (because written in C or C++)
  • generating such files may be dirty
  • all options and values are thrown together -> possible unwanted overriding because of panda’s preserved options when loading before ShowBase()

the result of that discussion for us was that we use prc where nesting is not needed. as for keybindings, xml is way better: you can have multiple categories and it’s easier to manipulate such files from within code.

EDIT: other libraries don’t come in question because of portability. we want our code to run on all OSes without having to compile or install anything first (except panda)

why not:
JSON docs.python.org/library/json.html

pros:

  • official, well known format
  • easy to work with through code
  • doesn’t require any extra libraries (in python std lib)
  • supports list, dicts and others that map nicely to python data types
  • very easy to use
  • one of the best formats to edit by hand

cons:

  • does not use tabs and has Cish syntax

google.com/trends?q=python+X … all&sort=0

YAML
pors:
same as json

cons:
not as used
not as standard, but has pure python lib (which you just drop into your game folder)

JSON

{
    'general': {
         'g':'godmode',
         'p':'per_pixel_lighting',
         'escape':'menu',
          ...
         'god_mode':{
            'wheel_up':'speed_up'
             ...    
         }
    }
}

YAML

genral:
    g: godmode
    p: per_pixel_lighting
    escape: menu   
   ...
    god_mode:
        wheel_up: speed_up
        ...

JSON is included in Python 2.6: http://docs.python.org/library/json.html
Sometimes it is not secure to send it via internet, but there is a regular expression that checks json content for illegal stuff, very simple is easy to use. You can find it somewhere on official json homepage.