浏览代码

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 direct.showbase.ShowBase import ShowBase
 from panda3d.core import TextNode, InputDevice, loadPrcFileData, Vec3
 from panda3d.core import TextNode, InputDevice, loadPrcFileData, Vec3
+from panda3d.core import TextPropertiesManager
 from direct.gui.OnscreenText import OnscreenText
 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):
 class App(ShowBase):
     def __init__(self):
     def __init__(self):
         ShowBase.__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(
         self.lblWarning = OnscreenText(
             text = "No devices found",
             text = "No devices found",
             fg=(1,0,0,1),
             fg=(1,0,0,1),
             scale = .25)
             scale = .25)
-        self.lblWarning.hide()
 
 
         self.lblAction = OnscreenText(
         self.lblAction = OnscreenText(
             text = "Action",
             text = "Action",
@@ -31,99 +61,133 @@ class App(ShowBase):
             scale = .15)
             scale = .15)
         self.lblAction.hide()
         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
         # 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("connect-device", self.connect)
         self.accept("disconnect-device", self.disconnect)
         self.accept("disconnect-device", self.disconnect)
 
 
         self.accept("escape", exit)
         self.accept("escape", exit)
-        self.accept("gamepad0-start", exit)
-        self.accept("flight_stick0-start", exit)
 
 
         # Accept button events of the first connected gamepad
         # 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 = loader.loadModel("environment")
         self.environment.reparentTo(render)
         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.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):
     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:
         else:
-            # no devices connected
+            # No devices.  Show the warning.
             self.lblWarning.show()
             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):
     def moveTask(self, task):
         dt = globalClock.getDt()
         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
             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
         # we will use the first found gamepad
         # Move the camera left/right
         # 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
         # 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
         return task.cont
 
 

+ 200 - 143
samples/gamepad/mappingGUI.py

@@ -16,62 +16,75 @@ from panda3d.core import (
     TextNode,
     TextNode,
     Vec2,
     Vec2,
     InputDevice,
     InputDevice,
-    loadPrcFileData)
+    loadPrcFileData,
+    GamepadButton,
+    KeyboardButton)
 
 
 # Make sure the textures look crisp on every device that supports
 # Make sure the textures look crisp on every device that supports
 # non-power-2 textures
 # non-power-2 textures
 loadPrcFileData("", "textures-auto-power-2 #t")
 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):
     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",
             dialogName="dlg_device_input",
             pos=(0, 0, 0.25),
             pos=(0, 0, 0.25),
             text="Hit desired key:",
             text="Hit desired key:",
@@ -82,7 +95,7 @@ class App(ShowBase):
             text_align=TextNode.ACenter,
             text_align=TextNode.ACenter,
             fadeScreen=0.65,
             fadeScreen=0.65,
             frameColor=VBase4(0.3, 0.3, 0.3, 1),
             frameColor=VBase4(0.3, 0.3, 0.3, 1),
-            button_geom=self.buttonGeom,
+            button_geom=button_geom,
             button_scale=0.15,
             button_scale=0.15,
             button_text_scale=0.35,
             button_text_scale=0.35,
             button_text_align=TextNode.ALeft,
             button_text_align=TextNode.ALeft,
@@ -93,13 +106,73 @@ class App(ShowBase):
             button_frameColor=VBase4(0, 0, 0, 0),
             button_frameColor=VBase4(0, 0, 0, 0),
             button_frameSize=VBase4(-1.0, 1.0, -0.25, 0.25),
             button_frameSize=VBase4(-1.0, 1.0, -0.25, 0.25),
             button_pressEffect=False,
             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
         # create a sample title
         self.textscale = 0.1
         self.textscale = 0.1
@@ -135,6 +208,7 @@ class App(ShowBase):
             decMaps.find("**/dec_click"),
             decMaps.find("**/dec_click"),
             decMaps.find("**/dec_hover"),
             decMaps.find("**/dec_hover"),
             decMaps.find("**/dec_disabled"))
             decMaps.find("**/dec_disabled"))
+
         # create the scrolled frame that will hold our list
         # create the scrolled frame that will hold our list
         self.lstActionMap = DirectScrolledFrame(
         self.lstActionMap = DirectScrolledFrame(
             # make the frame occupy the whole window
             # make the frame occupy the whole window
@@ -167,135 +241,116 @@ class App(ShowBase):
         idx = 0
         idx = 0
         self.listBGEven = base.loader.loadModel("models/list_item_even")
         self.listBGEven = base.loader.loadModel("models/list_item_even")
         self.listBGOdd = base.loader.loadModel("models/list_item_odd")
         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())
             item.reparentTo(self.lstActionMap.getCanvas())
             idx += 1
             idx += 1
 
 
         # recalculate the canvas size to set scrollbars if necesary
         # recalculate the canvas size to set scrollbars if necesary
         self.lstActionMap["canvasSize"] = (
         self.lstActionMap["canvasSize"] = (
             base.a2dLeft+0.05, base.a2dRight-0.05,
             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()
         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
             # 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
             # actualize the label in the list that shows the current
             # event for the action
             # event for the action
-            self.actualizeLabel["text"] = self.newActionKey
+            self.actionLabels[action]["text"] = self.mapping.formatMapping(action)
 
 
         # cleanup
         # cleanup
-        self.dlgInput["text"] ="Hit desired key:"
-        self.actionToMap = ""
-        self.newActionKey = ""
-        self.actualizeLabel = None
         for bt in base.buttonThrowers:
         for bt in base.buttonThrowers:
+            bt.node().setSpecificFlag(True)
             bt.node().setButtonDownEvent("")
             bt.node().setButtonDownEvent("")
         for bt in base.deviceButtonThrowers:
         for bt in base.deviceButtonThrowers:
+            bt.node().setSpecificFlag(True)
             bt.node().setButtonDownEvent("")
             bt.node().setButtonDownEvent("")
         taskMgr.remove("checkControls")
         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:
         for bt in base.buttonThrowers:
+            bt.node().setSpecificFlag(False)
             bt.node().setButtonDownEvent("keyListenEvent")
             bt.node().setButtonDownEvent("keyListenEvent")
         for bt in base.deviceButtonThrowers:
         for bt in base.deviceButtonThrowers:
+            bt.node().setSpecificFlag(False)
             bt.node().setButtonDownEvent("deviceListenEvent")
             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
         # 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:
                 else:
-                    self.controlStates[device].update({ctrl.axis: ctrl.state})
+                    self.axisStates[device].update({axis.axis: axis.value})
         # start the task
         # start the task
         taskMgr.add(self.watchControls, "checkControls")
         taskMgr.add(self.watchControls, "checkControls")
 
 
     def watchControls(self, task):
     def watchControls(self, task):
         # move through all devices and all it's controls
         # 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
                     # 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
         def dummy(): pass
         if index % 2 == 0:
         if index % 2 == 0:
             bg = self.listBGEven
             bg = self.listBGEven
         else:
         else:
             bg = self.listBGOdd
             bg = self.listBGOdd
         item = DirectFrame(
         item = DirectFrame(
-            text=itemName,
+            text=action,
             geom=bg,
             geom=bg,
             geom_scale=(base.a2dRight-0.05, 1, 0.1),
             geom_scale=(base.a2dRight-0.05, 1, 0.1),
             frameSize=VBase4(base.a2dLeft+0.05, base.a2dRight-0.05, -0.05, 0.05),
             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.reparentTo(item)
         lbl.setTransparency(True)
         lbl.setTransparency(True)
+        self.actionLabels[action] = lbl
+
         buttonScale = 0.15
         buttonScale = 0.15
         btn = DirectButton(
         btn = DirectButton(
             text="Change",
             text="Change",
@@ -333,10 +390,10 @@ class App(ShowBase):
             pos=(base.a2dRight-(0.898*buttonScale+0.3), 0, 0),
             pos=(base.a2dRight-(0.898*buttonScale+0.3), 0, 0),
             pressEffect=False,
             pressEffect=False,
             command=self.changeMapping,
             command=self.changeMapping,
-            extraArgs=[action, lbl])
+            extraArgs=[action])
         btn.setTransparency(True)
         btn.setTransparency(True)
         btn.reparentTo(item)
         btn.reparentTo(item)
         return item
         return item
 
 
-app = App()
+app = MappingGUIDemo()
 app.run()
 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