• Welcome to Final Fantasy Hacktics. Please login or sign up.
 
March 28, 2024, 09:36:28 pm

Joystick Support

Started by lirmont, October 04, 2012, 08:14:15 am

lirmont

October 04, 2012, 08:14:15 am Last Edit: October 13, 2012, 12:44:09 pm by lirmont
Reading a Human-Interface Device USB joystick:



If you've been following the game states thread, the lobby is now capable of showing Playstation(R) style keys on auto-generated buttons. Now, most of the time, it'll show keyboard keys (re: the letter 'v' in "v: Create Party"), but, after finishing that button feature and extending a font to include Playstation's symbols, I wanted to do a demo with joystick support. Say, an options screen that let's a user see keys/pick the control method/remap keys. You know, like a normal game. Anyway, Panda3d, the engine Tethical is built on, does not have support for joysticks/gamepads/etc. Instead, the forum posts there suggest using an entirely different game engine on top of Panda3d to get joystick support. That's ridiculous, but that's why this screenshot of my command prompt is even important to this project. What it shows is my code using a Python-based wrapper to interface with a USB backend to read the USB device and programmatically decode the HID-compliant message into x, y, z, rz, hat-switch, and button states.


import usb.core
import usb.util

class findAllJoysticks(object):
def __init__(self, bInterfaceClass = 0x03, bInterfaceSubClass = 0x00):
self.bInterfaceClass = bInterfaceClass
self.bInterfaceSubClass = bInterfaceSubClass
# Due to the fact that USB devices can defer their device class and subclass to specific interfaces they have to be compatible with more than one class, it's necessary to check through all a given device's interfaces to figure out which USB devices are joysticks.
def __call__(self, device):
if device.bDeviceClass == self.bInterfaceClass and device.bDeviceSubClass == self.bInterfaceSubClass:
return True
else:
for cfg in device:
intf = usb.util.find_descriptor(
cfg,
bInterfaceClass = self.bInterfaceClass,
bInterfaceSubClass = self.bInterfaceSubClass
)
if intf is not None:
return True
return False

# Get a list of USB joysticks.
joysticks = usb.core.find(find_all = True, custom_match = findAllJoysticks())

def getNameOfUSBDevice(device):
# Get human-readable name (ex. Logitech Dual Action).
return usb.util.get_string(device, 256, 2)

for joystick in joysticks:
print getNameOfUSBDevice(joystick), joystick, "@ Bus #%s, Location %s" % (joystick.bus, joystick.address)
joystick.set_configuration()
# From the 0x81 Human Interface Device (HID) endpoint, read the 0x8-length joystick payload.
eightByteJoystickStatusArray = joystick.read(0x81, 0x8)
if not eightByteJoystickStatusArray is None:
state = eightByteJoystickStatusArray
# Bytes 1 & 2: Location (0x0 to 0xFF, 0x0 to 0xFF).
# Left Thumb-Stick; 0%, 0% is all the way left and up; 100%, 100% is all the way right and down.
x = "{0:.0f}%".format(state[0]/255.0 * 100)
y = "{0:.0f}%".format(state[1]/255.0 * 100)
# Bytes 3 & 4: Location (0x0 to 0xFF, 0x0 to 0xFF).
# Right Thumb-Stick; 0%, 0% is all the way left and up; 100%, 100% is all the way right and down.
z = "{0:.0f}%".format(state[2]/255.0 * 100)
Rz = "{0:.0f}%".format(state[3]/255.0 * 100)
# Byte 5: Buttons 1 - 4 & Hat Switch (D-Pad) Bitmask.
oneThroughFourButtonBitmask = state[4] >> 4 # Move the first for bits to the right four digits, resulting in being left with only the first 4 bits.
hatSwitchBitmask = state[4] & 0b00001111 # AND out the first 4 bits, setting them to 0, resulting in being left with only the last 4 bits.
# Byte 6: Buttons 5 - 12 Bitmask.
fiveThroughTwelveButtonBitmask = state[5] # 1 Byte bitmask for buttons 5 through 12.
hatSwitchStates = ["UP", "UP-RIGHT", "RIGHT", "DOWN-RIGHT", "DOWN", "DOWN-LEFT", "LEFT", "UP-LEFT", "ORIGIN"]
print "D-Pad"
print "Position: %s" % hatSwitchStates[hatSwitchBitmask]
print "Left Thumb-Stick"
print "X, Y:", "%s, %s" % (x, y), "(%s, %s)" % (hex(state[0]), hex(state[1]))
print "Right Thumb-Stick"
print "Z (left-to-right), Rz (top-to-bottom):", "%s, %s" % (z, Rz), "(%s, %s)" % (hex(state[2]), hex(state[3]))
print "Buttons"
# Iterate over the first 4 buttons; they exist in the first half of byte 5.
i = 1
for button in [0b0001, 0b0010, 0b0100, 0b1000]:
# Perform a bitwise AND operation using the button's bitmask against the 1 through 4 button bitmask.
on = True if oneThroughFourButtonBitmask & button == button else False
print "%s:" % (i), "%s" % ("ON" if on is True else "OFF")
i += 1
# Iterate over buttons 5 through 12; they exist in the byte 6.
for button in [0b0001, 0b0010, 0b0100, 0b1000, 0b10000, 0b100000, 0b1000000, 0b10000000]:
# Perform a bitwise AND operation using the button's bitmask against the 1 through 4 button bitmask.
on = True if fiveThroughTwelveButtonBitmask & button == button else False
print "%s:" % (i), "%s" % ("ON" if on is True else "OFF")
i += 1



--



So, long story short, Tethical wasn't set up to use event handling in a way that supported having keys bound to actions. That is, keys were treated as actions. I've got a work-in-progress rewrite of this feature done so that multiple keys/input can be bound to an action. Now, you won't be asking for some specific key, you'll be asking for an event name (ex: 'Accept' or 'Cancel'). Input tied to such an action is polled (because my HID USB device code required polling to implement) and, if it is active, it sends off the appropriate event. At any rate, no need to stuff PyGame into Panda3d now for gamepad support. However, gamepad support is quite limited, in that the device has to be installed as an Human Interface Device (versus any other USB class). This works for me and will likely work with cheap or otherwise low-button controllers (dpad + 2 thumb sticks + 12 buttons). Enthusiast controllers will not be supported with this, but the infrastructure is in there now so that other pads/classes can be added later. For instance, it would be easy to recognize edge-case devices by vendor and product ID's and then encapsulate those devices into appropriate non-HID class implementations.