From c4b3b558c92f7a30eb060f7d943d787919ab6c4d Mon Sep 17 00:00:00 2001 From: rdb Date: Fri, 28 Sep 2018 20:48:50 +0200 Subject: [PATCH] samples: fixes and improvements to gamepad and mappingGUI sample --- samples/gamepad/gamepad.py | 194 ++++++++----- samples/gamepad/mappingGUI.py | 345 ++++++++++++---------- samples/gamepad/models/xbone-icons.egg | 384 +++++++++++++++++++++++++ samples/gamepad/models/xbone-icons.png | Bin 0 -> 29275 bytes 4 files changed, 714 insertions(+), 209 deletions(-) create mode 100644 samples/gamepad/models/xbone-icons.egg create mode 100644 samples/gamepad/models/xbone-icons.png diff --git a/samples/gamepad/gamepad.py b/samples/gamepad/gamepad.py index 82154838f6..c684c01253 100644 --- a/samples/gamepad/gamepad.py +++ b/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() - - # list of connected gamepad devices - gamepads = base.devices.getDevices(InputDevice.DC_gamepad) - - # 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 - + self.reset() self.taskMgr.add(self.moveTask, "movement update task") 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() + """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 + + # 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") + + # 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() + """Event handler that is called when a device is removed.""" - 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() + 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) - # 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)) + 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 - # calculate movement - base.camera.setX(base.camera, 100 * dt * movementVec.getX()) - base.camera.setY(base.camera, 100 * dt * movementVec.getY()) + # 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 + 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 diff --git a/samples/gamepad/mappingGUI.py b/samples/gamepad/mappingGUI.py index ce9234bd1d..84f26a9f60 100644 --- a/samples/gamepad/mappingGUI.py +++ b/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() - # catch all button events + 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 + + # 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") + self.axisStates[device][axis.axis] = axis.value + + # Format the axis for being displayed. + if axis.axis != InputDevice.Axis.none: + #label = axis.axis.name.replace('_', ' ').title() + self.dlgInput.axisMoved(axis.axis) + return task.cont - 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 - - def setDeviceKey(self, args): - if not self.setKeyCalled: - self.setKey(args) - self.setKeyCalled = False - - 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() diff --git a/samples/gamepad/models/xbone-icons.egg b/samples/gamepad/models/xbone-icons.egg new file mode 100644 index 0000000000..cc2fb417ad --- /dev/null +++ b/samples/gamepad/models/xbone-icons.egg @@ -0,0 +1,384 @@ + { Y-Up } + + xbone-icons { + xbone-icons.png + format { rgba } + alpha { dual } +} + + vpool { + 0 { + -0.5 0.5 0 + { 0.00390625 0.9921875 } + } + 1 { + -0.5 -0.5 0 + { 0.00390625 0.7578125 } + } + 2 { + 0.5 -0.5 0 + { 0.12109375 0.7578125 } + } + 3 { + 0.5 0.5 0 + { 0.12109375 0.9921875 } + } + 4 { + -0.5 0.5 0 + { 0.12890625 0.9921875 } + } + 5 { + -0.5 -0.5 0 + { 0.12890625 0.7578125 } + } + 6 { + 0.5 -0.5 0 + { 0.24609375 0.7578125 } + } + 7 { + 0.5 0.5 0 + { 0.24609375 0.9921875 } + } + 8 { + -0.5 0.5 0 + { 0.25390625 0.9921875 } + } + 9 { + -0.5 -0.5 0 + { 0.25390625 0.7578125 } + } + 10 { + 0.5 -0.5 0 + { 0.37109375 0.7578125 } + } + 11 { + 0.5 0.5 0 + { 0.37109375 0.9921875 } + } + 12 { + -0.5 0.5 0 + { 0.37890625 0.9921875 } + } + 13 { + -0.5 -0.5 0 + { 0.37890625 0.7578125 } + } + 14 { + 0.5 -0.5 0 + { 0.49609375 0.7578125 } + } + 15 { + 0.5 0.5 0 + { 0.49609375 0.9921875 } + } + 16 { + -0.5 0.5 0 + { 0.50390625 0.9921875 } + } + 17 { + -0.5 -0.5 0 + { 0.50390625 0.7578125 } + } + 18 { + 0.5 -0.5 0 + { 0.62109375 0.7578125 } + } + 19 { + 0.5 0.5 0 + { 0.62109375 0.9921875 } + } + 20 { + -0.5 0.5 0 + { 0.62890625 0.9921875 } + } + 21 { + -0.5 -0.5 0 + { 0.62890625 0.7578125 } + } + 22 { + 0.5 -0.5 0 + { 0.74609375 0.7578125 } + } + 23 { + 0.5 0.5 0 + { 0.74609375 0.9921875 } + } + 24 { + -0.5 0.5 0 + { 0.75390625 0.9921875 } + } + 25 { + -0.5 -0.5 0 + { 0.75390625 0.7578125 } + } + 26 { + 0.5 -0.5 0 + { 0.87109375 0.7578125 } + } + 27 { + 0.5 0.5 0 + { 0.87109375 0.9921875 } + } + 28 { + -0.5 0.5 0 + { 0.87890625 0.9921875 } + } + 29 { + -0.5 -0.5 0 + { 0.87890625 0.7578125 } + } + 30 { + 0.5 -0.5 0 + { 0.99609375 0.7578125 } + } + 31 { + 0.5 0.5 0 + { 0.99609375 0.9921875 } + } + 32 { + -0.5 0.5 0 + { 0.12890625 0.7421875 } + } + 33 { + -0.5 -0.5 0 + { 0.12890625 0.5078125 } + } + 34 { + 0.5 -0.5 0 + { 0.24609375 0.5078125 } + } + 35 { + 0.5 0.5 0 + { 0.24609375 0.7421875 } + } + 36 { + -0.5 0.5 0 + { 0.00390625 0.7421875 } + } + 37 { + -0.5 -0.5 0 + { 0.00390625 0.5078125 } + } + 38 { + 0.5 -0.5 0 + { 0.12109375 0.5078125 } + } + 39 { + 0.5 0.5 0 + { 0.12109375 0.7421875 } + } + 40 { + -0.5 0.5 0 + { 0.25390625 0.7421875 } + } + 41 { + -0.5 -0.5 0 + { 0.25390625 0.5078125 } + } + 42 { + 0.5 -0.5 0 + { 0.37109375 0.5078125 } + } + 43 { + 0.5 0.5 0 + { 0.37109375 0.7421875 } + } + 44 { + -0.5 0.5 0 + { 0.37890625 0.7421875 } + } + 45 { + -0.5 -0.5 0 + { 0.37890625 0.5078125 } + } + 46 { + 0.5 -0.5 0 + { 0.49609375 0.5078125 } + } + 47 { + 0.5 0.5 0 + { 0.49609375 0.7421875 } + } + 48 { + -0.5 0.5 0 + { 0.62890625 0.7421875 } + } + 49 { + -0.5 -0.5 0 + { 0.62890625 0.5078125 } + } + 50 { + 0.5 -0.5 0 + { 0.74609375 0.5078125 } + } + 51 { + 0.5 0.5 0 + { 0.74609375 0.7421875 } + } + 52 { + -0.5 0.5 0 + { 0.50390625 0.7421875 } + } + 53 { + -0.5 -0.5 0 + { 0.50390625 0.5078125 } + } + 54 { + 0.5 -0.5 0 + { 0.62109375 0.5078125 } + } + 55 { + 0.5 0.5 0 + { 0.62109375 0.7421875 } + } + 56 { + -0.5 0.5 0 + { 0.75390625 0.7421875 } + } + 57 { + -0.5 -0.5 0 + { 0.75390625 0.5078125 } + } + 58 { + 0.5 -0.5 0 + { 0.87109375 0.5078125 } + } + 59 { + 0.5 0.5 0 + { 0.87109375 0.7421875 } + } + 60 { + -0.5 0.5 0 + { 0.87890625 0.7421875 } + } + 61 { + -0.5 -0.5 0 + { 0.87890625 0.5078125 } + } + 62 { + 0.5 -0.5 0 + { 0.99609375 0.5078125 } + } + 63 { + 0.5 0.5 0 + { 0.99609375 0.7421875 } + } + 64 { + -0.5 0.5 0 + { 0.00390625 0.4921875 } + } + 65 { + -0.5 -0.5 0 + { 0.00390625 0.2578125 } + } + 66 { + 0.5 -0.5 0 + { 0.12109375 0.2578125 } + } + 67 { + 0.5 0.5 0 + { 0.12109375 0.4921875 } + } +} + face_a { + { + { xbone-icons } + { 0 1 2 3 { vpool } } + } +} + face_b { + { + { xbone-icons } + { 4 5 6 7 { vpool } } + } +} + dpad { + { + { xbone-icons } + { 8 9 10 11 { vpool } } + } +} + dpad_down { + { + { xbone-icons } + { 12 13 14 15 { vpool } } + } +} + dpad_left { + { + { xbone-icons } + { 16 17 18 19 { vpool } } + } +} + dpad_right { + { + { xbone-icons } + { 20 21 22 23 { vpool } } + } +} + dpad_up { + { + { xbone-icons } + { 24 25 26 27 { vpool } } + } +} + XboxOne_LB { + { + { xbone-icons } + { 28 29 30 31 { vpool } } + } +} + lstick { + { + { xbone-icons } + { 32 33 34 35 { vpool } } + } +} + ltrigger { + { + { xbone-icons } + { 36 37 38 39 { vpool } } + } +} + start { + { + { xbone-icons } + { 40 41 42 43 { vpool } } + } +} + rshoulder { + { + { xbone-icons } + { 44 45 46 47 { vpool } } + } +} + rstick { + { + { xbone-icons } + { 48 49 50 51 { vpool } } + } +} + rtrigger { + { + { xbone-icons } + { 52 53 54 55 { vpool } } + } +} + back { + { + { xbone-icons } + { 56 57 58 59 { vpool } } + } +} + face_x { + { + { xbone-icons } + { 60 61 62 63 { vpool } } + } +} + face_y { + { + { xbone-icons } + { 64 65 66 67 { vpool } } + } +} diff --git a/samples/gamepad/models/xbone-icons.png b/samples/gamepad/models/xbone-icons.png new file mode 100644 index 0000000000000000000000000000000000000000..e8fd66b487f8895846b3c2270cb6eca4be34fcca GIT binary patch literal 29275 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