Login | Register
Nerd ParadiseArtisanal tutorials since 1999
One annoying thing about PyGame is the joystick API doesn't really give you much high level flexibility. It only serves as a small rickety rope bridge to the underlying hardware. You can poll to see if a button is pressed, and that's it. If you wanted to create a game where you could use a joystick but fall back to a keyboard if a joystick was not present, then you're in for some fun times. You'll either write complicated stuff in your main game loop during the input phase turning your core engine into spaghetti code, or you need to write an abstraction layer that makes the keyboard and joystick events work seamlessly in the same fashion that the rest of your code can easily process. The latter solution is what I've done here. This looks like a lot of code, but it mostly falls into 2 main chunks...

  • The InputManager: This chunk can basically be copy and pasted into your code. This is where all the magic happens. It features a get_events method that acts as a proxy to PyGame's get_events method. It also features an is_pressed method. Both of these methods will take into account both the joystick and keyboard and work seamlessly together so you can safely fall back to keyboard input when a joystick is not present.
  • My arbitrary game with sample code: This is a simple game where you have a blue circle move around the screen according to the arrow keys pressed or the game pad directions pressed. Much of the code here is irrelevant but the important part is it demonstrates how to use the InputManager not only for game play, but also for configuring the joystick buttons. I tried to keep this code fairly minimalistic. It's not a good layout for scene or graphics management. Don'tchyou judge me!

The button names I use are also different than the PyGame event names. PyGame event names are constants that map to the keyboard and have names like "K_TAB". Contextually this doesn't make sense. What does a tab have to do with firing a bazooka in a game? Nothing, really. For this reason, the InputManager can have arbitrarily named buttons. It's not important what these names are since they are only used as consistent identifiers for the buttons. I used the SNES/XBox controller designations as an example (up, down, left, right, start, A, B, X, Y, L, R) but it's probably most wise to give these names that contextually make sense for your game. If you have the right idea, you should change this line...
self.buttons = ['up''down''left''right''start''A''B''X''Y''L''R']

to...
self.buttons = [
  'up''down''left''right''inventory'
  'jump''run''shoot''dash''use_item'
  'squat''do_a_jig'#etc.

These can be as arbitrary as you want. In fact, notice I changed the number of entries. As long as there are enough buttons on the joystick, this list can be as long or short as you want.

Without further ado, here is the code:
import time
import pygame
from pygame.locals import *

# The individual event object that is returned
# This serves as a proxy to pygame's event object
# and the key field is one of the strings in the button list listed below 
# in the InputManager's constructor
# This comment is actually longer than the class definition itself. 
class InputEvent:
    def __init__(self, key, down):
        self.key = key
        self.down = down
        self.up = not down

# This is where all the magic happens
class InputManager:
    def __init__(self):

        self.init_joystick()

        # I like SNES button designations. My decision to use them are arbitrary
        # and are only used internally to consistently identify buttons. 
        # Or you could pretend that these were XBox button layout designations.
        # Either way. Up to you. You could change them altogether if you want. 
        self.buttons = ['up''down''left''right''start''A''B''X''Y''L''R']
        
        # If you would like there to be a keyboard fallback configuration, fill those out
        # here in this mapping. If you wanted the keyboard keys to be configurable, you could
        # probably copy the same sort of system I use for the joystick configuration for the
        # keyboard. But that's getting fancy for a simple tutorial. 
        self.key_map = {
            K_UP : 'up',
            K_DOWN : 'down',
            K_LEFT : 'left',
            K_RIGHT : 'right',
            K_RETURN : 'start',
            K_a : 'A',
            K_b : 'B',
            K_x : 'X',
            K_y : 'Y',
            K_l : 'L',
            K_r : 'R'
        }
        
        # This dictionary will tell you which logical buttons are pressed, whether it's
        # via the keyboard or joystick
        self.keys_pressed = {}
        for button in self.buttons:
            self.keys_pressed[button] = False
        
        # This is a list of joystick configurations that will be populated during the
        # configuration phase
        self.joystick_config = {}
        
        # Quitting the window is raised as an input event. And typically you also want
        # that event raised when the user presses escape which is not something you 
        # want to configure on the joystick. That's why it's wired separately from
        # everything else. When escape is pressed or the user closes the window via its
        # chrome, this flag is set to True. 
        self.quit_attempt = False
    
    # button is a string of the designation in the list above
    def is_pressed(self, button):
        return self.keys_pressed[button]
    
    # This will pump the pygame events. If this is not called every frame,
    # then the PyGame window will start to lock up. 
    # This is basically a proxy method for pygame's event pump and will likewise return
    # a list of event proxies. 
    def get_events(self):
        events = []
        for event in pygame.event.get():
            if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
                self.quit_attempt = True
            
            # This is where the keyboard events are checked
            if event.type == KEYDOWN or event.type == KEYUP:
                key_pushed_down = event.type == KEYDOWN
                button = self.key_map.get(event.key)
                if button != None:
                    events.append(InputEvent(button, key_pushed_down))
                    self.keys_pressed[button] = key_pushed_down
        
        # And this is where each configured button is checked...
        for button in self.buttons:
            
            # determine what something like "Y" actually means in terms of the joystick
            config = self.joystick_config.get(button)
            if config != None:
                
                # if the button is configured to an actual button...
                if config[0] == 'is_button':
                    pushed = self.joystick.get_button(config[1])
                    if pushed != self.keys_pressed[button]:
                        events.append(InputEvent(button, pushed))
                        self.keys_pressed[button] = pushed
                
                # if the button is configured to a hat direction...
                elif config[0] == 'is_hat':
                    status = self.joystick.get_hat(config[1])
                    if config[2] == 'x':
                        amount = status[0]
                    else:
                        amount = status[1]
                    pushed = amount == config[3]
                    if pushed != self.keys_pressed[button]:
                        events.append(InputEvent(button, pushed))
                        self.keys_pressed[button] = pushed
                
                # if the button is configured to a trackball direction...
                elif config[0] == 'is_ball':
                    status = self.joystick.get_ball(config[1])
                    if config[2] == 'x':
                        amount = status[0]
                    else:
                        amount = status[1]
                    if config[3] == 1:
                        pushed = amount > 0.5
                    else:
                        pushed = amount < -0.5
                    if pushed != self.keys_pressed[button]:
                        events.append(InputEvent(button, pushed))
                        self.keys_pressed[button] = pushed
                
                # if the button is configured to an axis direction...
                elif config[0] == 'is_axis':
                    status = self.joystick.get_axis(config[1])
                    if config[2] == 1:
                        pushed = status > 0.5
                    else:
                        pushed = status < -0.5
                    if pushed != self.keys_pressed[button]:
                        events.append(InputEvent(button, pushed))
                        self.keys_pressed[button] = pushed
                
        return events        
    
    # Any button that is currently pressed on the game pad will be toggled
    # to the button designation passed in as the 'button' parameter.
    # (as long as it isn't already in use for a different button)
    def configure_button(self, button):
        
        js = self.joystick
        
        # check buttons for activity...
        for button_index in range(js.get_numbuttons()):
            button_pushed = js.get_button(button_index)
            if button_pushed and not self.is_button_used(button_index):
                self.joystick_config[button] = ('is_button', button_index)
                return True
        
        # check hats for activity...
        # (hats are the basic direction pads)
        for hat_index in range(js.get_numhats()):
            hat_status = js.get_hat(hat_index)
            if hat_status[0] < -.5 and not self.is_hat_used(hat_index, 'x', -1):
                self.joystick_config[button] = ('is_hat', hat_index, 'x', -1)
                return True
            elif hat_status[0] > .5 and not self.is_hat_used(hat_index, 'x'1):
                self.joystick_config[button] = ('is_hat', hat_index, 'x'1)
                return True
            if hat_status[1] < -.5 and not self.is_hat_used(hat_index, 'y', -1):
                self.joystick_config[button] = ('is_hat', hat_index, 'y', -1)
                return True
            elif hat_status[1] > .5 and not self.is_hat_used(hat_index, 'y'1):
         &nbnbsp;      self.joystick_config[button] = ('is_hat', hat_index, 'y'1)
                return True
        
        # check trackballs for activity...
        # (I don't actually have a gamepad with a trackball on it. So this code
        #  is completely untested! Let me know if it works and is typo-free.)
        for ball_index in range(js.get_numballs()):
            ball_status = js.get_ball(ball_index)
            if ball_status[0] < -.5 and not self.is_ball_used(ball_index, 'x', -1):
                self.joystick_config[button] = ('is_ball', ball_index, 'x', -1)
                return True
            elif ball_status[0] > .5 and not self.is_ball_used(ball_index, 'x'1):
                self.joystick_config[button] = ('is_ball', ball_index, 'x'1)
                return True
            if ball_status[1] < -.5 and not self.is_ball_used(ball_index, 'y', -1):
                self.joystick_config[button] = ('is_ball', ball_index, 'y', -1)
                return True
            elif ball_status[1] > .5 and not self.is_ball_used(ball_index, 'y'1):
                self.joystick_config[button] = ('is_ball', ball_index, 'y'1)
                return True
        
        # check axes for activity...
        # (that's plural of axis. Not a tree chopping tool. Although a USB Axe would be awesome!)
        for axis_index in range(js.get_numaxes()):
            axis_status = js.get_axis(axis_index)
            if axis_status < -.5 and not self.is_axis_used(axis_index, -1):
                self.joystick_config[button] = ('is_axis', axis_index, -1)
                return True
            elif axis_status > .5 and not self.is_axis_used(axis_index, 1):
                self.joystick_config[button] = ('is_axis', axis_index, 1)
                return True
                
        return False
    
    # The following 4 methods are helper methods used by the above method
    # to determine if a particular button/axis/hat/trackball are already 
    # configured to a particular button designation
    def is_button_used(self, button_index):
        for button in self.buttons:
            config = self.joystick_config.get(button)
            if config != None and config[0] == 'is_button' and config[1] == button_index:
                return True
        return False
    
    def is_hat_used(self, hat_index, axis, direction):
        for button in self.buttons:
            config = self.joystick_config.get(button)
            if config != None and config[0] == 'is_hat':
                if config[1] == hat_index and config[2] == axis and config[3] == direction:
                    return True
        return False
    
    def is_ball_used(self, ball_index, axis, direction):
        for button in self.buttons:
            config = self.joystick_config.get(button)
            if config != None and config[0] == 'is_ball':
                if config[1] == ball_index and config[2] == axis and config[3] == direction:
                    return True
        return False
    
    def is_axis_used(self, axis_index, direction):
        for button in self.buttons:
            config = self.joystick_config.get(button)
            if config != None and config[0] == 'is_axis':
                if config[1] == axis_index and config[2] == direction:
                    return True
        return False
    
    # Set joystick information.
    # The joystick needs to be plugged in before this method is called (see main() method)
    def init_joystick(self):
        joystick = pygame.joystick.Joystick(0)
        joystick.init()
        self.joystick = joystick
        self.joystick_name = joystick.get_name()
    
# A simple player object. This only keeps track of position. 
class Player:
    def __init__(self):
        self.x = 320
        self.y = 240
        self.speed = 4
    
    def move_left(self):
        self.x -= self.speed
    def move_right(self):
        self.x += self.speed
    def move_up(self):
        self.y -= self.speed
    def move_down(self):
        self.y += self.speed

# The main method...duh!
def main():
    
    fps = 30
    
    print("Plug in a USB gamepad. Do it! Do it now! Press enter after you have done this.")
    wait_for_enter()
    
    pygame.init()
    
    num_joysticks = pygame.joystick.get_count()
    if num_joysticks < 1:
        print("You didn't plug in a joystick. FORSHAME!")
        return
    
    input_manager = InputManager()
    
    screen = pygame.display.set_mode((640480))
    
    button_index = 0
    
    player = Player()
    
    
    # The main game loop
    while not input_manager.quit_attempt:
        start = time.time()
        
        screen.fill((0,0,0))
        
        # There will be two phases to our "game".
        is_configured = button_index >= len(input_manager.buttons)
        
        # In the first phase, the user will be prompted to configure the joystick by pressing
        # the key that is indicated on the screen
        # You would probably do this in an input menu in your real game. 
        if not is_configured:
            success = configure_phase(screen, input_manager.buttons[button_index], input_manager)
            # if the user pressed a button and configured it...
            if success:
                # move on to the next button that needs to be configured
                button_index += 1
        
        # In the second phase, the user will control a "character" on the screen (which will 
        # be represented by a simple blue ball) that obeys the directional commands, whether
        # it's from the joystick or the keyboard.
        else:
            interaction_phase(screen, player, input_manager)
        
        pygame.display.flip()
        
        # maintain frame rate
        difference = start - time.time()
        delay = 1.0 / fps - difference
        if delay > 0:
            time.sleep(delay)

def configure_phase(screen, button, input_manager):
    
    # need to pump windows events otherwise the window will lock up and die
    input_manager.get_events() 
    
    # configure_button looks at the state of ALL buttons pressed on the joystick
    # and will map the first pressed button it sees to the current button you pass
    # in here. 
    success = input_manager.configure_button(button)
    
    # tell user which button to configure
    write_text(screen, "Press the " + button + " button"100100)
    
    # If a joystick button was successfully configured, return True
    return success

def interaction_phase(screen, player, input_manager):
    # I dunno. This doesn't do anything. But this is how 
    # you would access key hit events and the like.
    # Ideal for "shooting a weapon" or "jump" sort of events
    for event in input_manager.get_events():
        if event.key == 'A' and event.down:
            pass # weeeeeeee
        if event.key == 'X' and event.up:
            input_manager.quit_attempted = True
    
    # ...but for things like "move in this direction", you want
    # to know if a button is pressed and held
    
    if input_manager.is_pressed('left'):
        player.move_left()
    elif input_manager.is_pressed('right'):
        player.move_right()
    if input_manager.is_pressed('up'):
        player.move_up()
    elif input_manager.is_pressed('down'):
        player.move_down()
    
    # Draw the player
    pygame.draw.circle(screen, (00255), (player.x, player.y), 20)

# There was probably a more robust way of doing this. But
# command line interaction was not the point of the tutorial.
def wait_for_enter():
    try: input()
    exceptpass

# This renders text on the game screen. 
# Also not the point of this tutorial.
cached_text = {}
cached_font = None
def write_text(screen, text, x, y):
    global cached_text, cached_font
    image = cached_text.get(text)
    if image == None:
        if cached_font == None:
            cached_font = pygame.font.Font(pygame.font.get_default_font(), 12)
        image = cached_font.render(text, True, (255255255))
        cached_text[text] = image
    screen.blit(image, (x, y - image.get_height()))

# Kick things off. 
main()

# fin.


Note: This code (or any PyGame joystick code) will cause a crazy amount of spurious output in certain versions of PyGame. This is not my code's fault. This is a bug in PyGame where some baffoon left a printf in his code when he checked in and it somehow made it into the public release.
Hey, there, Python folks. Hope you enjoyed the post. I just wanted to give a quick shout-out for a weekly Python code golf that I recently started up over on StringLabs.io. If you like Python puzzles please come check it out!