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.