浏览代码

samples: fixes and improvements to gamepad and mappingGUI sample

rdb 7 年之前
父节点
当前提交
c4b3b558c9
共有 4 个文件被更改,包括 713 次插入208 次删除
  1. 129 65
      samples/gamepad/gamepad.py
  2. 200 143
      samples/gamepad/mappingGUI.py
  3. 384 0
      samples/gamepad/models/xbone-icons.egg
  4. 二进制
      samples/gamepad/models/xbone-icons.png

+ 129 - 65
samples/gamepad/gamepad.py

@@ -9,21 +9,51 @@ move the camera where the right stick will rotate the camera.
 
 from direct.showbase.ShowBase import ShowBase
 from panda3d.core import TextNode, InputDevice, loadPrcFileData, Vec3
+from panda3d.core import TextPropertiesManager
 from direct.gui.OnscreenText import OnscreenText
 
-loadPrcFileData("", "notify-level-device debug")
+loadPrcFileData("", """
+    default-fov 60
+    notify-level-device debug
+""")
+
+# Informational text in the bottom-left corner.  We use the special \5
+# character to embed an image representing the gamepad buttons.
+INFO_TEXT = """Move \5lstick\5 to strafe, \5rstick\5 to turn
+Press \5ltrigger\5 and \5rtrigger\5 to go down/up
+Press \5face_x\5 to reset camera"""
+
 
 class App(ShowBase):
     def __init__(self):
         ShowBase.__init__(self)
-        # print all events sent through the messenger
-        self.messenger.toggleVerbose()
+        # Print all events sent through the messenger
+        #self.messenger.toggleVerbose()
+
+        # Load the graphics for the gamepad buttons and register them, so that
+        # we can embed them in our information text.
+        graphics = loader.loadModel("models/xbone-icons.egg")
+        mgr = TextPropertiesManager.getGlobalPtr()
+        for name in ["face_a", "face_b", "face_x", "face_y", "ltrigger", "rtrigger", "lstick", "rstick"]:
+            graphic = graphics.find("**/" + name)
+            graphic.setScale(1.5)
+            mgr.setGraphic(name, graphic)
+            graphic.setZ(-0.5)
+
+        # Show the informational text in the corner.
+        self.lblInfo = OnscreenText(
+            parent = self.a2dBottomLeft,
+            pos = (0.1, 0.3),
+            fg = (1, 1, 1, 1),
+            bg = (0.2, 0.2, 0.2, 0.9),
+            align = TextNode.A_left,
+            text = INFO_TEXT)
+        self.lblInfo.textNode.setCardAsMargin(0.5, 0.5, 0.5, 0.2)
 
         self.lblWarning = OnscreenText(
             text = "No devices found",
             fg=(1,0,0,1),
             scale = .25)
-        self.lblWarning.hide()
 
         self.lblAction = OnscreenText(
             text = "Action",
@@ -31,99 +61,133 @@ class App(ShowBase):
             scale = .15)
         self.lblAction.hide()
 
-        self.checkDevices()
+        # Is there a gamepad connected?
+        self.gamepad = None
+        devices = self.devices.getDevices(InputDevice.DC_gamepad)
+        if devices:
+            self.connect(devices[0])
 
         # Accept device dis-/connection events
-        # NOTE: catching the events here will overwrite the accept in showbase, hence
-        #       we need to forward the event in the functions we set here!
         self.accept("connect-device", self.connect)
         self.accept("disconnect-device", self.disconnect)
 
         self.accept("escape", exit)
-        self.accept("gamepad0-start", exit)
-        self.accept("flight_stick0-start", exit)
 
         # Accept button events of the first connected gamepad
-        self.accept("gamepad0-action_a", self.doAction, extraArgs=[True, "Action"])
-        self.accept("gamepad0-action_a-up", self.doAction, extraArgs=[False, "Release"])
-        self.accept("gamepad0-action_b", self.doAction, extraArgs=[True, "Action 2"])
-        self.accept("gamepad0-action_b-up", self.doAction, extraArgs=[False, "Release"])
+        self.accept("gamepad-back", exit)
+        self.accept("gamepad-start", exit)
+        self.accept("gamepad-face_x", self.reset)
+        self.accept("gamepad-face_a", self.action, extraArgs=["face_a"])
+        self.accept("gamepad-face_a-up", self.actionUp)
+        self.accept("gamepad-face_b", self.action, extraArgs=["face_b"])
+        self.accept("gamepad-face_b-up", self.actionUp)
+        self.accept("gamepad-face_y", self.action, extraArgs=["face_y"])
+        self.accept("gamepad-face_y-up", self.actionUp)
 
         self.environment = loader.loadModel("environment")
         self.environment.reparentTo(render)
 
-        # disable pandas default mouse-camera controls so we can handle the camera
-        # movements by ourself
+        # Disable the default mouse-camera controls since we need to handle
+        # our own camera controls.
         self.disableMouse()
+        self.reset()
 
-        # list of connected gamepad devices
-        gamepads = base.devices.getDevices(InputDevice.DC_gamepad)
+        self.taskMgr.add(self.moveTask, "movement update task")
 
-        # set the center position of the control sticks
-        # NOTE: here we assume, that the wheel is centered when the application get started.
-        #       In real world applications, you should notice the user and give him enough time
-        #       to center the wheel until you store the center position of the controler!
-        self.lxcenter = gamepads[0].findControl(InputDevice.C_left_x).state
-        self.lycenter = gamepads[0].findControl(InputDevice.C_left_y).state
-        self.rxcenter = gamepads[0].findControl(InputDevice.C_right_x).state
-        self.rycenter = gamepads[0].findControl(InputDevice.C_right_y).state
+    def connect(self, device):
+        """Event handler that is called when a device is discovered."""
 
+        # We're only interested if this is a gamepad and we don't have a
+        # gamepad yet.
+        if device.device_class == InputDevice.DC_gamepad and not self.gamepad:
+            print("Found %s" % (device))
+            self.gamepad = device
 
-        self.taskMgr.add(self.moveTask, "movement update task")
+            # Enable this device to ShowBase so that we can receive events.
+            # We set up the events with a prefix of "gamepad-".
+            self.attachInputDevice(device, prefix="gamepad")
 
-    def connect(self, device):
-        # we need to forward the event to the connectDevice function of showbase
-        self.connectDevice(device)
-        # Now we can check for ourself
-        self.checkDevices()
+            # Hide the warning that we have no devices.
+            self.lblWarning.hide()
 
     def disconnect(self, device):
-        # we need to forward the event to the disconnectDevice function of showbase
-        self.disconnectDevice(device)
-        # Now we can check for ourself
-        self.checkDevices()
-
-    def checkDevices(self):
-        # check if we have gamepad devices connected
-        if self.devices.get_devices(InputDevice.DC_gamepad):
-            # we have at least one gamepad device
-            self.lblWarning.hide()
+        """Event handler that is called when a device is removed."""
+
+        if self.gamepad != device:
+            # We don't care since it's not our gamepad.
+            return
+
+        # Tell ShowBase that the device is no longer needed.
+        print("Disconnected %s" % (device))
+        self.detachInputDevice(device)
+        self.gamepad = None
+
+        # Do we have any other gamepads?  Attach the first other gamepad.
+        devices = self.devices.getDevices(InputDevice.DC_gamepad)
+        if devices:
+            self.connect(devices[0])
         else:
-            # no devices connected
+            # No devices.  Show the warning.
             self.lblWarning.show()
 
-    def doAction(self, showText, text):
-        if showText and self.lblAction.isHidden():
-            self.lblAction.show()
-        else:
-            self.lblAction.hide()
+    def reset(self):
+        """Reset the camera to the initial position."""
+
+        self.camera.setPosHpr(0, -200, 10, 0, 0, 0)
+
+    def action(self, button):
+        # Just show which button has been pressed.
+        self.lblAction.text = "Pressed \5%s\5" % button
+        self.lblAction.show()
+
+    def actionUp(self):
+        # Hide the label showing which button is pressed.
+        self.lblAction.hide()
 
     def moveTask(self, task):
         dt = globalClock.getDt()
-        movementVec = Vec3()
 
-        gamepads = base.devices.getDevices(InputDevice.DC_gamepad)
-        if len(gamepads) == 0:
-            # savety check
+        if not self.gamepad:
             return task.cont
 
+        strafe_speed = 85
+        vert_speed = 50
+        turn_speed = 100
+
+        # If the left stick is pressed, we will go faster.
+        lstick = self.gamepad.findButton("lstick")
+        if lstick.pressed:
+            strafe_speed *= 2.0
+
         # we will use the first found gamepad
         # Move the camera left/right
-        left_x = gamepads[0].findControl(InputDevice.C_left_x)
-        movementVec.setX(left_x.state - self.lxcenter)
+        strafe = Vec3(0)
+        left_x = self.gamepad.findAxis(InputDevice.Axis.left_x)
+        left_y = self.gamepad.findAxis(InputDevice.Axis.left_y)
+        strafe.x = left_x.value
+        strafe.y = left_y.value
+
+        # Apply some deadzone, since the sticks don't center exactly at 0
+        if strafe.lengthSquared() >= 0.01:
+            self.camera.setPos(self.camera, strafe * strafe_speed * dt)
+
+        # Use the triggers for the vertical position.
+        trigger_l = self.gamepad.findAxis(InputDevice.Axis.left_trigger)
+        trigger_r = self.gamepad.findAxis(InputDevice.Axis.right_trigger)
+        lift = trigger_r.value - trigger_l.value
+        self.camera.setZ(self.camera.getZ() + (lift * vert_speed * dt))
+
         # Move the camera forward/backward
-        left_y = gamepads[0].findControl(InputDevice.C_left_y)
-        movementVec.setY(left_y.state - self.lycenter)
-        # Control the cameras heading
-        right_x = gamepads[0].findControl(InputDevice.C_right_x)
-        base.camera.setH(base.camera, 100 * dt * (right_x.state - self.rxcenter))
-        # Control the cameras pitch
-        right_y = gamepads[0].findControl(InputDevice.C_right_y)
-        base.camera.setP(base.camera, 100 * dt * (right_y.state - self.rycenter))
-
-        # calculate movement
-        base.camera.setX(base.camera, 100 * dt * movementVec.getX())
-        base.camera.setY(base.camera, 100 * dt * movementVec.getY())
+        right_x = self.gamepad.findAxis(InputDevice.Axis.right_x)
+        right_y = self.gamepad.findAxis(InputDevice.Axis.right_y)
+
+        # Again, some deadzone
+        if abs(right_x.value) >= 0.1 or abs(right_y.value) >= 0.1:
+            self.camera.setH(self.camera, turn_speed * dt * -right_x.value)
+            self.camera.setP(self.camera, turn_speed * dt * right_y.value)
+
+            # Reset the roll so that the camera remains upright.
+            self.camera.setR(0)
 
         return task.cont
 

+ 200 - 143
samples/gamepad/mappingGUI.py

@@ -16,62 +16,75 @@ from panda3d.core import (
     TextNode,
     Vec2,
     InputDevice,
-    loadPrcFileData)
+    loadPrcFileData,
+    GamepadButton,
+    KeyboardButton)
 
 # Make sure the textures look crisp on every device that supports
 # non-power-2 textures
 loadPrcFileData("", "textures-auto-power-2 #t")
 
-class App(ShowBase):
+# How much an axis should have moved for it to register as a movement.
+DEAD_ZONE = 0.33
+
+
+class InputMapping(object):
+    """A container class for storing a mapping from a string action to either
+    an axis or a button.  You could extend this with additional methods to load
+    the default mappings from a configuration file. """
+
+    # Define all the possible actions.
+    actions = (
+        "Move forward", "Move backward", "Move left", "Move right", "Jump",
+        "Buy", "Use", "Break", "Fix", "Trash", "Change", "Mail", "Upgrade",
+    )
+
     def __init__(self):
-        ShowBase.__init__(self)
+        self.__map = dict.fromkeys(self.actions)
 
-        self.setBackgroundColor(0, 0, 0)
-        # make the font look nice at a big scale
-        DGG.getDefaultFont().setPixelsPerUnit(100)
+    def mapButton(self, action, button):
+        self.__map[action] = ("button", str(button))
 
-        # a dict of actions and button/axis events
-        self.gamepadMapping = {
-            "Move forward":"Left Stick Y",
-            "Move backward":"Left Stick Y",
-            "Move left":"Left Stick X",
-            "Move right":"Left Stick X",
-            "Jump":"a",
-            "Action":"b",
-            "Sprint":"x",
-            "Map":"y",
-            "action-1":"c",
-            "action-2":"d",
-            "action-3":"e",
-            "action-4":"f",
-            "action-5":"g",
-            "action-6":"h",
-            "action-7":"i",
-            "action-8":"j",
-            "action-9":"k",
-            "action-10":"l",
-            "action-11":"m",
-        }
-        # this will store the action that we want to remap
-        self.actionToMap = ""
-        # this will store the key/axis that we want to asign to an action
-        self.newActionKey = ""
-        # this will store the label that needs to be actualized in the list
-        self.actualizeLabel = None
+    def mapAxis(self, action, axis):
+        self.__map[action] = ("axis", axis.name)
 
-        # The geometry for our basic buttons
-        maps = loader.loadModel("models/button_map")
-        self.buttonGeom = (
-            maps.find("**/ready"),
-            maps.find("**/click"),
-            maps.find("**/hover"),
-            maps.find("**/disabled"))
+    def unmap(self):
+        self.__map[action] = None
 
-        # Create the dialog that asks the user for input on a given
-        # action to map a key to.
-        DGG.setDefaultDialogGeom("models/dialog.png")
-        # setup a dialog to ask for device input
-        self.dlgInput = OkCancelDialog(
+    def formatMapping(self, action):
+        """Returns a string label describing the mapping for a given action,
+        for displaying in a GUI. """
+        mapping = self.__map.get(action)
+        if not mapping:
+            return "Unmapped"
+
+        # Format the symbolic string from Panda nicely.  In a real-world game,
+        # you might want to look these up in a translation table, or so.
+        label = mapping[1].replace('_', ' ').title()
+        if mapping[0] == "axis":
+            return "Axis: " + label
+        else:
+            return "Button: " + label
+
+
+class ChangeActionDialog(object):
+    """Encapsulates the UI dialog that opens up when changing a mapping.  It
+    holds the state of which action is being set and which button is pressed
+    and invokes a callback function when the dialog is exited."""
+
+    def __init__(self, action, button_geom, command):
+        # This stores which action we are remapping.
+        self.action = action
+
+        # This will store the key/axis that we want to asign to an action
+        self.newInputType = ""
+        self.newInput = ""
+        self.setKeyCalled = False
+
+        self.__command = command
+
+        # Initialize the DirectGUI stuff.
+        self.dialog = OkCancelDialog(
             dialogName="dlg_device_input",
             pos=(0, 0, 0.25),
             text="Hit desired key:",
@@ -82,7 +95,7 @@ class App(ShowBase):
             text_align=TextNode.ACenter,
             fadeScreen=0.65,
             frameColor=VBase4(0.3, 0.3, 0.3, 1),
-            button_geom=self.buttonGeom,
+            button_geom=button_geom,
             button_scale=0.15,
             button_text_scale=0.35,
             button_text_align=TextNode.ALeft,
@@ -93,13 +106,73 @@ class App(ShowBase):
             button_frameColor=VBase4(0, 0, 0, 0),
             button_frameSize=VBase4(-1.0, 1.0, -0.25, 0.25),
             button_pressEffect=False,
-            command=self.closeDialog)
-        self.dlgInput.setTransparency(True)
-        self.dlgInput.configureDialog()
-        scale = self.dlgInput["image_scale"]
-        self.dlgInput["image_scale"] = (scale[0]/2.0, scale[1], scale[2]/2.0)
-        self.dlgInput["text_pos"] = (self.dlgInput["text_pos"][0], self.dlgInput["text_pos"][1] + 0.06)
-        self.dlgInput.hide()
+            command=self.onClose)
+        self.dialog.setTransparency(True)
+        self.dialog.configureDialog()
+        scale = self.dialog["image_scale"]
+        self.dialog["image_scale"] = (scale[0]/2.0, scale[1], scale[2]/2.0)
+        self.dialog["text_pos"] = (self.dialog["text_pos"][0], self.dialog["text_pos"][1] + 0.06)
+
+    def buttonPressed(self, button):
+        if any(button.guiItem.getState() == 1 for button in self.dialog.buttonList):
+            # Ignore events while any of the dialog buttons are active, because
+            # otherwise we register mouse clicks when the user is trying to
+            # exit the dialog.
+            return
+
+        text = str(button).replace('_', ' ').title()
+        self.dialog["text"] = "New event will be:\n\nButton: " + text
+        self.newInputType = "button"
+        self.newInput = button
+
+    def axisMoved(self, axis):
+        text = axis.name.replace('_', ' ').title()
+        self.dialog["text"] = "New event will be:\n\nAxis: " + text
+        self.newInputType = "axis"
+        self.newInput = axis
+
+    def onClose(self, result):
+        """Called when the OK or Cancel button is pressed."""
+        self.dialog.cleanup()
+
+        # Call the constructor-supplied callback with our new setting, if any.
+        if self.newInput and result == DGG.DIALOG_OK:
+            self.__command(self.action, self.newInputType, self.newInput)
+        else:
+            # Cancel (or no input was pressed)
+            self.__command(self.action, None, None)
+
+
+class MappingGUIDemo(ShowBase):
+    def __init__(self):
+        ShowBase.__init__(self)
+
+        self.setBackgroundColor(0, 0, 0)
+        # make the font look nice at a big scale
+        DGG.getDefaultFont().setPixelsPerUnit(100)
+
+        # Store our mapping, with some sensible defaults.  In a real game, you
+        # will want to load these from a configuration file.
+        self.mapping = InputMapping()
+        self.mapping.mapAxis("Move forward", InputDevice.Axis.left_y)
+        self.mapping.mapAxis("Move backward", InputDevice.Axis.left_y)
+        self.mapping.mapAxis("Move left", InputDevice.Axis.left_x)
+        self.mapping.mapAxis("Move right", InputDevice.Axis.left_x)
+        self.mapping.mapButton("Jump", GamepadButton.face_a())
+        self.mapping.mapButton("Use", GamepadButton.face_b())
+        self.mapping.mapButton("Break", GamepadButton.face_x())
+        self.mapping.mapButton("Fix", GamepadButton.face_y())
+
+        # The geometry for our basic buttons
+        maps = loader.loadModel("models/button_map")
+        self.buttonGeom = (
+            maps.find("**/ready"),
+            maps.find("**/click"),
+            maps.find("**/hover"),
+            maps.find("**/disabled"))
+
+        # Change the default dialog skin.
+        DGG.setDefaultDialogGeom("models/dialog.png")
 
         # create a sample title
         self.textscale = 0.1
@@ -135,6 +208,7 @@ class App(ShowBase):
             decMaps.find("**/dec_click"),
             decMaps.find("**/dec_hover"),
             decMaps.find("**/dec_disabled"))
+
         # create the scrolled frame that will hold our list
         self.lstActionMap = DirectScrolledFrame(
             # make the frame occupy the whole window
@@ -167,135 +241,116 @@ class App(ShowBase):
         idx = 0
         self.listBGEven = base.loader.loadModel("models/list_item_even")
         self.listBGOdd = base.loader.loadModel("models/list_item_odd")
-        for key, value in self.gamepadMapping.items():
-            item = self.__makeListItem(key, key, value, idx)
+        self.actionLabels = {}
+        for action in self.mapping.actions:
+            mapped = self.mapping.formatMapping(action)
+            item = self.__makeListItem(action, mapped, idx)
             item.reparentTo(self.lstActionMap.getCanvas())
             idx += 1
 
         # recalculate the canvas size to set scrollbars if necesary
         self.lstActionMap["canvasSize"] = (
             base.a2dLeft+0.05, base.a2dRight-0.05,
-            -(len(self.gamepadMapping.keys())*0.1), 0.09)
+            -(len(self.mapping.actions)*0.1), 0.09)
         self.lstActionMap.setCanvasSize()
 
-    def closeDialog(self, result):
-        self.dlgInput.hide()
-        if result == DGG.DIALOG_OK:
+    def closeDialog(self, action, newInputType, newInput):
+        """Called in callback when the dialog is closed.  newInputType will be
+        "button" or "axis", or None if the remapping was cancelled."""
+
+        self.dlgInput = None
+
+        if newInputType is not None:
             # map the event to the given action
-            self.gamepadMapping[self.actionToMap] = self.newActionKey
+            if newInputType == "axis":
+                self.mapping.mapAxis(action, newInput)
+            else:
+                self.mapping.mapButton(action, newInput)
+
             # actualize the label in the list that shows the current
             # event for the action
-            self.actualizeLabel["text"] = self.newActionKey
+            self.actionLabels[action]["text"] = self.mapping.formatMapping(action)
 
         # cleanup
-        self.dlgInput["text"] ="Hit desired key:"
-        self.actionToMap = ""
-        self.newActionKey = ""
-        self.actualizeLabel = None
         for bt in base.buttonThrowers:
+            bt.node().setSpecificFlag(True)
             bt.node().setButtonDownEvent("")
         for bt in base.deviceButtonThrowers:
+            bt.node().setSpecificFlag(True)
             bt.node().setButtonDownEvent("")
         taskMgr.remove("checkControls")
 
-    def changeMapping(self, action, label):
-        # set the action that we want to map a new key to
-        self.actionToMap = action
-        # set the label that needs to be actualized
-        self.actualizeLabel = label
-        # show our dialog
-        self.dlgInput.show()
+        # Now detach all the input devices.
+        for device in self.attachedDevices:
+            base.detachInputDevice(device)
+        self.attachedDevices.clear()
+
+    def changeMapping(self, action):
+        # Create the dialog window
+        self.dlgInput = ChangeActionDialog(action, button_geom=self.buttonGeom, command=self.closeDialog)
+
+        # Attach all input devices.
+        devices = base.devices.getDevices()
+        for device in devices:
+            base.attachInputDevice(device)
+            self.attachedDevices = devices
 
-        # catch all button events
+        # Disable regular button events on all button event throwers, and
+        # instead broadcast a generic event.
         for bt in base.buttonThrowers:
+            bt.node().setSpecificFlag(False)
             bt.node().setButtonDownEvent("keyListenEvent")
         for bt in base.deviceButtonThrowers:
+            bt.node().setSpecificFlag(False)
             bt.node().setButtonDownEvent("deviceListenEvent")
-        self.setKeyCalled = False
-        self.accept("keyListenEvent", self.setKey)
-        self.accept("deviceListenEvent", self.setDeviceKey)
 
-        # As there are no events thrown for control changes, we set up
-        # a task to check if the controls got moved
-        # This list will help us for checking which controls got moved
-        self.controlStates = {None:{}}
+        self.accept("keyListenEvent", self.dlgInput.buttonPressed)
+        self.accept("deviceListenEvent", self.dlgInput.buttonPressed)
+
+        # As there are no events thrown for control changes, we set up a task
+        # to check if the controls were moved
+        # This list will help us for checking which controls were moved
+        self.axisStates = {None: {}}
         # fill it with all available controls
-        for device in base.devices.get_devices():
-            for ctrl in device.controls:
-                if device not in self.controlStates.keys():
-                    self.controlStates.update({device: {ctrl.axis: ctrl.state}})
+        for device in devices:
+            for axis in device.axes:
+                if device not in self.axisStates.keys():
+                    self.axisStates.update({device: {axis.axis: axis.value}})
                 else:
-                    self.controlStates[device].update({ctrl.axis: ctrl.state})
+                    self.axisStates[device].update({axis.axis: axis.value})
         # start the task
         taskMgr.add(self.watchControls, "checkControls")
 
     def watchControls(self, task):
         # move through all devices and all it's controls
-        for device in base.devices.get_devices():
-            for ctrl in device.controls:
-                # if a control got changed more than the given puffer zone
-                if self.controlStates[device][ctrl.axis] + 0.2 < ctrl.state or \
-                   self.controlStates[device][ctrl.axis] - 0.2 > ctrl.state:
+        for device in self.attachedDevices:
+            if device.device_class == InputDevice.DC_mouse:
+                # Ignore mouse axis movement, or the user can't even navigate
+                # to the OK/Cancel buttons!
+                continue
+
+            for axis in device.axes:
+                # if a control got changed more than the given dead zone
+                if self.axisStates[device][axis.axis] + DEAD_ZONE < axis.value or \
+                   self.axisStates[device][axis.axis] - DEAD_ZONE > axis.value:
                     # set the current state in the dict
-                    self.controlStates[device][ctrl.axis] = ctrl.state
-                    # check which axis got moved
-                    if ctrl.axis == InputDevice.C_left_x:
-                        self.setKey("Left Stick X")
-                    elif ctrl.axis == InputDevice.C_left_y:
-                        self.setKey("Left Stick Y")
-                    elif ctrl.axis == InputDevice.C_left_trigger:
-                        self.setKey("Left Trigger")
-                    elif ctrl.axis == InputDevice.C_right_x:
-                        self.setKey("Right Stick X")
-                    elif ctrl.axis == InputDevice.C_right_y:
-                        self.setKey("Right Stick Y")
-                    elif ctrl.axis == InputDevice.C_right_trigger:
-                        self.setKey("Right Trigger")
-                    elif ctrl.axis == InputDevice.C_x:
-                        self.setKey("X")
-                    elif ctrl.axis == InputDevice.C_y:
-                        self.setKey("Y")
-                    elif ctrl.axis == InputDevice.C_trigger:
-                        self.setKey("Trigger")
-                    elif ctrl.axis == InputDevice.C_throttle:
-                        self.setKey("Throttle")
-                    elif ctrl.axis == InputDevice.C_rudder:
-                        self.setKey("Rudder")
-                    elif ctrl.axis == InputDevice.C_hat_x:
-                        self.setKey("Hat X")
-                    elif ctrl.axis == InputDevice.C_hat_y:
-                        self.setKey("Hat Y")
-                    elif ctrl.axis == InputDevice.C_wheel:
-                        self.setKey("Wheel")
-                    elif ctrl.axis == InputDevice.C_accelerator:
-                        self.setKey("Acclerator")
-                    elif ctrl.axis == InputDevice.C_brake:
-                        self.setKey("Break")
-        return task.cont
+                    self.axisStates[device][axis.axis] = axis.value
 
-    def setKey(self, args):
-        self.setKeyCalled = True
-        if self.dlgInput.buttonList[0].guiItem.getState() == 1:
-            # this occurs if the OK button was clicked. To prevent to
-            # always set the mouse1 event whenever the OK button was
-            # pressed, we instantly return from this function
-            return
-        self.dlgInput["text"] = "New event will be:\n\n" + args
-        self.newActionKey = args
+                    # Format the axis for being displayed.
+                    if axis.axis != InputDevice.Axis.none:
+                        #label = axis.axis.name.replace('_', ' ').title()
+                        self.dlgInput.axisMoved(axis.axis)
 
-    def setDeviceKey(self, args):
-        if not self.setKeyCalled:
-            self.setKey(args)
-        self.setKeyCalled = False
+        return task.cont
 
-    def __makeListItem(self, itemName, action, event, index):
+    def __makeListItem(self, action, event, index):
         def dummy(): pass
         if index % 2 == 0:
             bg = self.listBGEven
         else:
             bg = self.listBGOdd
         item = DirectFrame(
-            text=itemName,
+            text=action,
             geom=bg,
             geom_scale=(base.a2dRight-0.05, 1, 0.1),
             frameSize=VBase4(base.a2dLeft+0.05, base.a2dRight-0.05, -0.05, 0.05),
@@ -317,6 +372,8 @@ class App(ShowBase):
             )
         lbl.reparentTo(item)
         lbl.setTransparency(True)
+        self.actionLabels[action] = lbl
+
         buttonScale = 0.15
         btn = DirectButton(
             text="Change",
@@ -333,10 +390,10 @@ class App(ShowBase):
             pos=(base.a2dRight-(0.898*buttonScale+0.3), 0, 0),
             pressEffect=False,
             command=self.changeMapping,
-            extraArgs=[action, lbl])
+            extraArgs=[action])
         btn.setTransparency(True)
         btn.reparentTo(item)
         return item
 
-app = App()
+app = MappingGUIDemo()
 app.run()

+ 384 - 0
samples/gamepad/models/xbone-icons.egg

@@ -0,0 +1,384 @@
+<CoordinateSystem> { Y-Up }
+
+<Texture> xbone-icons {
+  xbone-icons.png
+  <Scalar> format { rgba }
+  <Scalar> alpha { dual }
+}
+
+<VertexPool> vpool {
+  <Vertex> 0 {
+    -0.5 0.5 0
+    <UV> { 0.00390625 0.9921875 }
+  }
+  <Vertex> 1 {
+    -0.5 -0.5 0
+    <UV> { 0.00390625 0.7578125 }
+  }
+  <Vertex> 2 {
+    0.5 -0.5 0
+    <UV> { 0.12109375 0.7578125 }
+  }
+  <Vertex> 3 {
+    0.5 0.5 0
+    <UV> { 0.12109375 0.9921875 }
+  }
+  <Vertex> 4 {
+    -0.5 0.5 0
+    <UV> { 0.12890625 0.9921875 }
+  }
+  <Vertex> 5 {
+    -0.5 -0.5 0
+    <UV> { 0.12890625 0.7578125 }
+  }
+  <Vertex> 6 {
+    0.5 -0.5 0
+    <UV> { 0.24609375 0.7578125 }
+  }
+  <Vertex> 7 {
+    0.5 0.5 0
+    <UV> { 0.24609375 0.9921875 }
+  }
+  <Vertex> 8 {
+    -0.5 0.5 0
+    <UV> { 0.25390625 0.9921875 }
+  }
+  <Vertex> 9 {
+    -0.5 -0.5 0
+    <UV> { 0.25390625 0.7578125 }
+  }
+  <Vertex> 10 {
+    0.5 -0.5 0
+    <UV> { 0.37109375 0.7578125 }
+  }
+  <Vertex> 11 {
+    0.5 0.5 0
+    <UV> { 0.37109375 0.9921875 }
+  }
+  <Vertex> 12 {
+    -0.5 0.5 0
+    <UV> { 0.37890625 0.9921875 }
+  }
+  <Vertex> 13 {
+    -0.5 -0.5 0
+    <UV> { 0.37890625 0.7578125 }
+  }
+  <Vertex> 14 {
+    0.5 -0.5 0
+    <UV> { 0.49609375 0.7578125 }
+  }
+  <Vertex> 15 {
+    0.5 0.5 0
+    <UV> { 0.49609375 0.9921875 }
+  }
+  <Vertex> 16 {
+    -0.5 0.5 0
+    <UV> { 0.50390625 0.9921875 }
+  }
+  <Vertex> 17 {
+    -0.5 -0.5 0
+    <UV> { 0.50390625 0.7578125 }
+  }
+  <Vertex> 18 {
+    0.5 -0.5 0
+    <UV> { 0.62109375 0.7578125 }
+  }
+  <Vertex> 19 {
+    0.5 0.5 0
+    <UV> { 0.62109375 0.9921875 }
+  }
+  <Vertex> 20 {
+    -0.5 0.5 0
+    <UV> { 0.62890625 0.9921875 }
+  }
+  <Vertex> 21 {
+    -0.5 -0.5 0
+    <UV> { 0.62890625 0.7578125 }
+  }
+  <Vertex> 22 {
+    0.5 -0.5 0
+    <UV> { 0.74609375 0.7578125 }
+  }
+  <Vertex> 23 {
+    0.5 0.5 0
+    <UV> { 0.74609375 0.9921875 }
+  }
+  <Vertex> 24 {
+    -0.5 0.5 0
+    <UV> { 0.75390625 0.9921875 }
+  }
+  <Vertex> 25 {
+    -0.5 -0.5 0
+    <UV> { 0.75390625 0.7578125 }
+  }
+  <Vertex> 26 {
+    0.5 -0.5 0
+    <UV> { 0.87109375 0.7578125 }
+  }
+  <Vertex> 27 {
+    0.5 0.5 0
+    <UV> { 0.87109375 0.9921875 }
+  }
+  <Vertex> 28 {
+    -0.5 0.5 0
+    <UV> { 0.87890625 0.9921875 }
+  }
+  <Vertex> 29 {
+    -0.5 -0.5 0
+    <UV> { 0.87890625 0.7578125 }
+  }
+  <Vertex> 30 {
+    0.5 -0.5 0
+    <UV> { 0.99609375 0.7578125 }
+  }
+  <Vertex> 31 {
+    0.5 0.5 0
+    <UV> { 0.99609375 0.9921875 }
+  }
+  <Vertex> 32 {
+    -0.5 0.5 0
+    <UV> { 0.12890625 0.7421875 }
+  }
+  <Vertex> 33 {
+    -0.5 -0.5 0
+    <UV> { 0.12890625 0.5078125 }
+  }
+  <Vertex> 34 {
+    0.5 -0.5 0
+    <UV> { 0.24609375 0.5078125 }
+  }
+  <Vertex> 35 {
+    0.5 0.5 0
+    <UV> { 0.24609375 0.7421875 }
+  }
+  <Vertex> 36 {
+    -0.5 0.5 0
+    <UV> { 0.00390625 0.7421875 }
+  }
+  <Vertex> 37 {
+    -0.5 -0.5 0
+    <UV> { 0.00390625 0.5078125 }
+  }
+  <Vertex> 38 {
+    0.5 -0.5 0
+    <UV> { 0.12109375 0.5078125 }
+  }
+  <Vertex> 39 {
+    0.5 0.5 0
+    <UV> { 0.12109375 0.7421875 }
+  }
+  <Vertex> 40 {
+    -0.5 0.5 0
+    <UV> { 0.25390625 0.7421875 }
+  }
+  <Vertex> 41 {
+    -0.5 -0.5 0
+    <UV> { 0.25390625 0.5078125 }
+  }
+  <Vertex> 42 {
+    0.5 -0.5 0
+    <UV> { 0.37109375 0.5078125 }
+  }
+  <Vertex> 43 {
+    0.5 0.5 0
+    <UV> { 0.37109375 0.7421875 }
+  }
+  <Vertex> 44 {
+    -0.5 0.5 0
+    <UV> { 0.37890625 0.7421875 }
+  }
+  <Vertex> 45 {
+    -0.5 -0.5 0
+    <UV> { 0.37890625 0.5078125 }
+  }
+  <Vertex> 46 {
+    0.5 -0.5 0
+    <UV> { 0.49609375 0.5078125 }
+  }
+  <Vertex> 47 {
+    0.5 0.5 0
+    <UV> { 0.49609375 0.7421875 }
+  }
+  <Vertex> 48 {
+    -0.5 0.5 0
+    <UV> { 0.62890625 0.7421875 }
+  }
+  <Vertex> 49 {
+    -0.5 -0.5 0
+    <UV> { 0.62890625 0.5078125 }
+  }
+  <Vertex> 50 {
+    0.5 -0.5 0
+    <UV> { 0.74609375 0.5078125 }
+  }
+  <Vertex> 51 {
+    0.5 0.5 0
+    <UV> { 0.74609375 0.7421875 }
+  }
+  <Vertex> 52 {
+    -0.5 0.5 0
+    <UV> { 0.50390625 0.7421875 }
+  }
+  <Vertex> 53 {
+    -0.5 -0.5 0
+    <UV> { 0.50390625 0.5078125 }
+  }
+  <Vertex> 54 {
+    0.5 -0.5 0
+    <UV> { 0.62109375 0.5078125 }
+  }
+  <Vertex> 55 {
+    0.5 0.5 0
+    <UV> { 0.62109375 0.7421875 }
+  }
+  <Vertex> 56 {
+    -0.5 0.5 0
+    <UV> { 0.75390625 0.7421875 }
+  }
+  <Vertex> 57 {
+    -0.5 -0.5 0
+    <UV> { 0.75390625 0.5078125 }
+  }
+  <Vertex> 58 {
+    0.5 -0.5 0
+    <UV> { 0.87109375 0.5078125 }
+  }
+  <Vertex> 59 {
+    0.5 0.5 0
+    <UV> { 0.87109375 0.7421875 }
+  }
+  <Vertex> 60 {
+    -0.5 0.5 0
+    <UV> { 0.87890625 0.7421875 }
+  }
+  <Vertex> 61 {
+    -0.5 -0.5 0
+    <UV> { 0.87890625 0.5078125 }
+  }
+  <Vertex> 62 {
+    0.5 -0.5 0
+    <UV> { 0.99609375 0.5078125 }
+  }
+  <Vertex> 63 {
+    0.5 0.5 0
+    <UV> { 0.99609375 0.7421875 }
+  }
+  <Vertex> 64 {
+    -0.5 0.5 0
+    <UV> { 0.00390625 0.4921875 }
+  }
+  <Vertex> 65 {
+    -0.5 -0.5 0
+    <UV> { 0.00390625 0.2578125 }
+  }
+  <Vertex> 66 {
+    0.5 -0.5 0
+    <UV> { 0.12109375 0.2578125 }
+  }
+  <Vertex> 67 {
+    0.5 0.5 0
+    <UV> { 0.12109375 0.4921875 }
+  }
+}
+<Group> face_a {
+  <Polygon> {
+    <TRef> { xbone-icons }
+    <VertexRef> { 0 1 2 3 <Ref> { vpool } }
+  }
+}
+<Group> face_b {
+  <Polygon> {
+    <TRef> { xbone-icons }
+    <VertexRef> { 4 5 6 7 <Ref> { vpool } }
+  }
+}
+<Group> dpad {
+  <Polygon> {
+    <TRef> { xbone-icons }
+    <VertexRef> { 8 9 10 11 <Ref> { vpool } }
+  }
+}
+<Group> dpad_down {
+  <Polygon> {
+    <TRef> { xbone-icons }
+    <VertexRef> { 12 13 14 15 <Ref> { vpool } }
+  }
+}
+<Group> dpad_left {
+  <Polygon> {
+    <TRef> { xbone-icons }
+    <VertexRef> { 16 17 18 19 <Ref> { vpool } }
+  }
+}
+<Group> dpad_right {
+  <Polygon> {
+    <TRef> { xbone-icons }
+    <VertexRef> { 20 21 22 23 <Ref> { vpool } }
+  }
+}
+<Group> dpad_up {
+  <Polygon> {
+    <TRef> { xbone-icons }
+    <VertexRef> { 24 25 26 27 <Ref> { vpool } }
+  }
+}
+<Group> XboxOne_LB {
+  <Polygon> {
+    <TRef> { xbone-icons }
+    <VertexRef> { 28 29 30 31 <Ref> { vpool } }
+  }
+}
+<Group> lstick {
+  <Polygon> {
+    <TRef> { xbone-icons }
+    <VertexRef> { 32 33 34 35 <Ref> { vpool } }
+  }
+}
+<Group> ltrigger {
+  <Polygon> {
+    <TRef> { xbone-icons }
+    <VertexRef> { 36 37 38 39 <Ref> { vpool } }
+  }
+}
+<Group> start {
+  <Polygon> {
+    <TRef> { xbone-icons }
+    <VertexRef> { 40 41 42 43 <Ref> { vpool } }
+  }
+}
+<Group> rshoulder {
+  <Polygon> {
+    <TRef> { xbone-icons }
+    <VertexRef> { 44 45 46 47 <Ref> { vpool } }
+  }
+}
+<Group> rstick {
+  <Polygon> {
+    <TRef> { xbone-icons }
+    <VertexRef> { 48 49 50 51 <Ref> { vpool } }
+  }
+}
+<Group> rtrigger {
+  <Polygon> {
+    <TRef> { xbone-icons }
+    <VertexRef> { 52 53 54 55 <Ref> { vpool } }
+  }
+}
+<Group> back {
+  <Polygon> {
+    <TRef> { xbone-icons }
+    <VertexRef> { 56 57 58 59 <Ref> { vpool } }
+  }
+}
+<Group> face_x {
+  <Polygon> {
+    <TRef> { xbone-icons }
+    <VertexRef> { 60 61 62 63 <Ref> { vpool } }
+  }
+}
+<Group> face_y {
+  <Polygon> {
+    <TRef> { xbone-icons }
+    <VertexRef> { 64 65 66 67 <Ref> { vpool } }
+  }
+}

二进制
samples/gamepad/models/xbone-icons.png