1

This is a project that I have been working on for a while.

Here's the situation:

The code completes the end goal. It's meant to simulate the location of a ball suspended in air by four cables, and should be controlled by a simple gamepad. The code does this, so it accomplishes the goal in the most basic definition.

Here's the problem:

The ball only moves in one point when the "LT" button is pressed, which it is supposed to do. However, after this one movement, the ball doesn't move until the button is pressed again, even if the "LT" button is held. The same thing applies for the "RT" button to move the ball away. (Although the joystick's trackball doesn't exactly act like the inward-outward problem, I consider this to be the same problem.) As I said, the most basic definition of this program's goal has been accomplished. However, the true spirit of the goal is coming up just short.

Here's my question:

How should I get the ball to move as long as the "LT/RT" buttons are pressed and/or the trackball is being moved?

The code:

#Import modules
from tkinter import *
import pygame


#Initialize joystick
pygame.joystick.init()
if pygame.joystick.get_count() == 0:
    print('No joystick detected')
else:
    joystick = pygame.joystick.Joystick(0)
    joystick.init()


###
# Interpret Joystick inputs
###


class Find_Joystick:
    def __init__(self, root):
        self.root = root

        ## initialize pygame and joystick
        pygame.init()
        if(pygame.joystick.get_count() < 1):
            # no joysticks found
            print("Please connect a joystick.\n")
            self.quit()
        else:
            # create a new joystick object from
            # ---the first joystick in the list of joysticks
            joystick = pygame.joystick.Joystick(0)
            # tell pygame to record joystick events
            joystick.init()

        ## bind the event I'm defining to a callback function


        ## start looking for events
        self.root.after(0, self.find_events)

    def find_events(self):
        ## check everything in the queue of pygame events
        events = pygame.event.get()
        joystick = pygame.joystick.Joystick(0)
        axisX = joystick.get_axis(2)
        axisY = joystick.get_axis(3)
        LT = joystick.get_button(6)
        RT = joystick.get_button(7)
        for event in events:
            # event type for pressing any of the joystick buttons down
            if event.type == pygame.JOYBUTTONDOWN:
                if LT == 1:
                    self.root.event_generate('<<LT>>')
                if RT == 1:
                    self.root.event_generate('<<RT>>')

            if event.type == pygame.JOYAXISMOTION:
                #Move left
                if axisX < 0:
                    self.root.event_generate('<<Left>>')
                #Move right
                if axisX > 0:
                    self.root.event_generate('<<Right>>')
                #Move upwards
                if axisY < -0.008:
                    self.root.event_generate('<<Up>>')
                #Move downwards
                if axisY > -0.008:
                    self.root.event_generate('<<Down>>')

        ## return to check for more events in a moment
        self.root.after(20, self.find_events)


def main():
#################
### Setup Window
#################


    ###
    #Build All Components
    ###


    #Window 1
    root = Tk()
    app = Find_Joystick(root)
    frame = Canvas(root, width=1000, height = 1000)
    frame.pack()

    #Ball
    global ball
    global ballBox1
    global ballBox2
    ballBox1 = [495, 245]
    ballBox2 = [505, 255]
    ball = frame.create_oval(ballBox1, ballBox2, outline = 'red', fill = 'red')
    ballCenter = [500, 250]
    ballCoords = [50, 50, 50]

    global ballLine1
    global ballLine2
    global ballLine3
    global ballLine4
    ballLine1 = frame.create_line((300, 150), ballCenter, fill = 'red')
    ballLine2 = frame.create_line((400, 100), ballCenter, fill = 'red')
    ballLine3 = frame.create_line((600, 150), ballCenter, fill = 'red')
    ballLine4 = frame.create_line((700, 100), ballCenter, fill = 'red')

    ballLines = (ballLine1, ballLine2, ballLine3, ballLine4)


    #Cube
    cubeFront = frame.create_rectangle([300, 150], [600, 400], width = 4.0, outline = 'blue')   #Front Face
    cubeBack = frame.create_rectangle([400, 100], [700, 350], outline = 'blue')                 #Back Face

    leftEdgeTop = frame.create_line([400, 100], [300, 150], width = 4.0, fill = 'blue')         #Top Left Edge
    leftEdgeBottom = frame.create_line([300, 400], [400, 350], fill = 'blue')                   #Bottom Left Edge
    rightEdgeTop = frame.create_line([700, 100], [600, 150], width = 4.0, fill = 'blue')        #Top Right Edge
    rightEdgeBottom = frame.create_line([600, 400], [700, 350], width = 4.0, fill = 'blue')     #Bottom Right Edge
    backEdgeTop = frame.create_line([400,100],[700,100], width = 4.0, fill = 'blue')            #Top Back Edge
    backEdgeRight = frame.create_line([700, 100], [700, 350], width = 4.0, fill = 'blue')       #Right Back Edge


    ###
    # Position Chart
    ###

    #Box
    positionBox = frame.create_rectangle([450, 475], [550, 550])

    positionBoxLine1 = frame.create_line([500, 475], [500, 550])
    positionBoxLine2 = frame.create_line([450, 500], [550, 500])
    positionBoxLine3 = frame.create_line([450, 525], [550, 525])

    positionBoxTextX = frame.create_text([475, 487.5], font = ('Arial', 14), text = 'X')
    positionBoxTextY = frame.create_text([475, 512.5], font = ('Arial', 14), text = 'Y')
    positionBoxTextZ = frame.create_text([475, 537.5], font = ('Arial', 14), text = 'Z')


    global positionBoxCoordsX
    global positionBoxCoordsY
    global positionBoxCoordsZ
    positionBoxCoordsX = frame.create_text([525, 487.5], font = ('Arial', 14), text = ballCoords[0])
    positionBoxCoordsY = frame.create_text([525, 512.5], font = ('Arial', 14), text = ballCoords[1])
    positionBoxCoordsZ = frame.create_text([525, 537.5], font = ('Arial', 14), text = ballCoords[2])

    positionCoords = [positionBoxCoordsX, positionBoxCoordsY, positionBoxCoordsZ]

    ##################
    ### Move the Ball       
    ##################

    ###
    # Redrawing Function
    ###


    #Redraw the object for it's position
    def redrawObjects():
        global ballLine1
        global ballLine2
        global ballLine3
        global ballLine4
        global positionBoxCoordsX
        global positionBoxCoordsY
        global positionBoxCoordsZ
        global ball
        frame.delete(ballLine1)
        frame.delete(ballLine2)
        frame.delete(ballLine3)
        frame.delete(ballLine4)
        frame.delete(ball)

        frame.delete(positionBoxCoordsX)
        frame.delete(positionBoxCoordsY)
        frame.delete(positionBoxCoordsZ)


        ballLine1 = frame.create_line((300, 150), ballCenter, fill = 'red')
        ballLine2 = frame.create_line((400, 100), ballCenter, fill = 'red')
        ballLine3 = frame.create_line((600, 150), ballCenter, fill = 'red')
        ballLine4 = frame.create_line((700, 100), ballCenter, fill = 'red')

        ball = frame.create_oval(ballBox1, ballBox2, outline = 'red', fill = 'red')

        positionBoxCoordsX = frame.create_text([525, 487.5], font = ('Arial', 14), text = ballCoords[0])
        positionBoxCoordsY = frame.create_text([525, 512.5], font = ('Arial', 14), text = ballCoords[1])
        positionBoxCoordsZ = frame.create_text([525, 537.5], font = ('Arial', 14), text = ballCoords[2])


    ###
    # Define each movement function
    ###


    #Indicate that the window is selected
    def callback(event):
        frame.focus_set()

    #Ball moves left
    def moveLeftFunc(event):

        if ballCoords[0] > 0:
            global ballBox1
            global ballBox2

            ballBox1[0] -= 3
            ballBox2[0] -= 3
            ballCenter[0] -= 3
            ballCoords[0] -= 1

        redrawObjects() 

    #Ball moves right
    def moveRightFunc(event):
        if ballCoords[0] < 100:
            global ballBox1
            global ballBox2


            ballBox1[0] += 3
            ballBox2[0] += 3
            ballCenter[0] += 3
            ballCoords[0] += 1

        redrawObjects() 

    #Ball moves up
    def moveUpFunc(event):
        if ballCoords[1] < 100:
            global ballBox1
            global ballBox2

            ballBox1[1] -= 2.5
            ballBox2[1] -= 2.5
            ballCenter[1] -= 2.5
            ballCoords[1] += 1

        redrawObjects()

    #Ball moves down
    def moveDownFunc(event):
        if ballCoords[1] > 0:
            global ballBox1
            global ballBox2

            ballBox1[1] += 2.5
            ballBox2[1] += 2.5
            ballCenter[1] += 2.5
            ballCoords[1] -= 1

        redrawObjects()

    #Ball moves inward
    def moveInFunc(event):
        if ballCoords[2] > 0:
            global ballBox1
            global ballBox2

            ballBox1[0] -= 1
            ballBox1[1] += 0.5
            ballBox2[0] -= 1
            ballBox2[1] += 0.5

            ballCenter[0] -= 1
            ballCenter[1] += 0.5
            ballCoords[2] -= 1

        redrawObjects()

    #Ball moves outwards
    def moveOutFunc(event):
        if ballCoords[2] < 100:
            global ballBox1
            global ballBox2

            ballBox1[0] += 1
            ballBox2[0] += 1
            ballBox1[1] -= 0.5
            ballBox2[1] -= 0.5
            ballCenter[0] += 1
            ballCenter[1] -= 0.5
            ballCoords[2] += 1

        redrawObjects()


    ###
    # Bind keys to movement
    ###


    #Initiate movement when window is clicked
    frame.bind("<Button-1>", callback)

    #Move ball left with "Left Arrow" Key
    frame.bind('<Left>', moveLeftFunc)

    #Move ball right with "Right Arrow" Key
    frame.bind('<Right>', moveRightFunc)

    #Move ball up with "Up Arrow" Key
    frame.bind('<Up>', moveUpFunc)

    #Move ball down with "Down Arrow" Key
    frame.bind('<Down>', moveDownFunc)

    #Move ball inwards with "Tab" Key
    frame.bind('<Tab>', moveInFunc)

    #Move ball outwards with "Return" Key
    frame.bind("<\>", moveOutFunc)


    #############
    #I think this is where the problem is
    #############


    def find_events():
        ## check everything in the queue of pygame events
        events = pygame.event.get()
        joystick = pygame.joystick.Joystick(0)
        axisX = joystick.get_axis(2)
        axisY = joystick.get_axis(3)
        LT = joystick.get_button(6)
        RT = joystick.get_button(7)
        for event in events:
            # event type for pressing any of the joystick buttons down
            if event.type == pygame.JOYBUTTONDOWN:
                if LT == 1:
                    global ballBox1
                    global ballBox2

                    ballBox1[0] += 1
                    ballBox2[0] += 1
                    ballBox1[1] -= 0.5
                    ballBox2[1] -= 0.5
                    ballCenter[0] += 1
                    ballCenter[1] -= 0.5
                    ballCoords[2] += 1

                if RT == 1:
                    #root.event_generate('<<RT>>')
                    global ballBox1
                    global ballBox2

                    ballBox1[0] -= 1
                    ballBox1[1] += 0.5
                    ballBox2[0] -= 1
                    ballBox2[1] += 0.5

                    ballCenter[0] -= 1
                    ballCenter[1] += 0.5
                    ballCoords[2] -= 1

            if event.type == pygame.JOYAXISMOTION:
                #Move left
                if axisX < 0:
                    #root.event_generate('<<Left>>')

                    global ballBox1
                    global ballBox2

                    ballBox1[0] -= 3
                    ballBox2[0] -= 3
                    ballCenter[0] -= 3
                    ballCoords[0] -= 1
                #Move right
                if axisX > 0:
                    #root.event_generate('<<Right>>')

                    global ballBox1
                    global ballBox2


                    ballBox1[0] += 3
                    ballBox2[0] += 3
                    ballCenter[0] += 3
                    ballCoords[0] += 1
                #Move upwards
                if axisY < -0.008:
                    #root.event_generate('<<Up>>')

                    global ballBox1
                    global ballBox2

                    ballBox1[0] -= 1
                    ballBox1[1] += 0.5
                    ballBox2[0] -= 1
                    ballBox2[1] += 0.5

                    ballCenter[0] -= 1
                    ballCenter[1] += 0.5
                    ballCoords[2] -= 1
                #Move downwards
                if axisY > -0.008:
                    #root.event_generate('<<Down>>')

                    global ballBox1
                    global ballBox2

                    ballBox1[1] += 2.5
                    ballBox2[1] += 2.5
                    ballCenter[1] += 2.5
                    ballCoords[1] -= 1


    #Move In
    root.bind('<<LT>>', moveInFunc)

    #Move Out
    root.bind('<<RT>>', moveOutFunc)

    #Move Left
    root.bind('<<Left>>', moveLeftFunc)

    #Move Right
    root.bind('<<Right>>', moveRightFunc)

    #Move Up
    root.bind('<<Up>>', moveUpFunc)

    #Move Down
    root.bind('<<Down>>', moveDownFunc)


    #Main loop
    root.mainloop()


if __name__ == '__main__':
    main()

Sorry for the really long code. I highlighted the area that I think is where the problem is, but I included the entire code because (as most code is) the entire program is heavily interconnected, and I wanted you guys to be able to test the program yourselves.

1 Answers1

0

Interesting demo! Looking through your code on a quick first pass, its organization is a bit confusing. In particular, there appear to be two find_events() functions where you appear to loop through pygame events. It might not hurt to do some refactoring to clean this and some of those pesky global variables up.

However, code judgement aside, I think you are right about where the problem is for your joystick input is. Let's take a look at the relevant portion of your code:

def find_events():
    ## check everything in the queue of pygame events
    events = pygame.event.get()
    #bunch of joystick button-fetching (skipping this)
    for event in events:
        # event type for pressing any of the joystick buttons down
        if event.type == pygame.JOYBUTTONDOWN:
            pass#lots of stuff I am cutting out here
        if event.type == pygame.JOYAXISMOTION:
            pass#lots of stuff I am cutting out here as well :)

The fundamental issue is that you are only making changes to your simulation when the pygame events initially fire. Even though you presumably call find_events() every frame and update the pygame queue, pygame will only add events to the queue when something changes. This explains why you need to keep repeatedly pressing those joystick buttons over and over again; each time you press the button, you add a pygame.JOYBUTTONDOWN event to the queue. The result is that your button press only lasts a single frame.

So what is the solution? We need to keep track of the current state of each of these buttons. There are actually two ways you could go about doing this:

  1. Track and update the state of each button after looping through the pygame.event.get() queue in your own dictionary and use this dictionary to control input in your simulation.

  2. Ignore the whole pygame.event.get() queue and use the get_button() function that pygame provides in its joystick class to get the current state of the button, regardless of whether or not you pressed it or released it in a given frame.

I will just focus on option 1, since with option 2, it is possible to miss user input (e.g. think what could happen if you only check once a frame, and a user has ninja-like reflexes and presses and releases the button quickly within that frame?). This is not so much an issue for joystick input, but rather for keyboard input where you could miss typed letters, but the same idea applies. With that preamble out of the way, let's look at some code:

def find_events():
    #your dictionary for updating button states (pressed == True)
    #also, ew, globals!
    global joystick_button_state

    events = pygame.event.get()
    joystick = pygame.joystick.Joystick(0)
    axisX = joystick.get_axis(2)
    axisY = joystick.get_axis(3)
    LT = joystick.get_button(6)
    RT = joystick.get_button(7)
    buttons = [joystick, axisX, axisY, LT, RT]#ideally, do NOT create a list of buttons every time you call this!

    for event in events:
        if event.type == pygame.JOYBUTTONDOWN:
            for button in buttons:
                if event.button == button:
                    joystick_button_state[button] = True

        elif event.type == pygame.JOYBUTTONUP:
            for button in buttons:
                if event.button = button:
                    joystick_button_state[button] = False

Once you do that, now all you have to do is look at joystick_button_state[the_one_button_i_care_about] and see if it is True/False to see if it is pressed or not. This should work nicely for basic buttons, and you can store events from pygame.JOYAXISMOTION in a similar way (perhaps another dict?). Hope this explanation clears up your issue!

CodeSurgeon
  • 2,435
  • 2
  • 15
  • 36
  • I think this might work, however, I'm not 100% sure about how to implement this. Also, when I run this, I get getting an error joystick_button_state being undefined (not sure what the values would be present in this dictionary). Sorry for my confusion, as the answer is most likely right in front of my face. – Garrett Crayton Oct 11 '17 at 06:02
  • `joystick_button_state` is just a global dictionary variable. You would need to create an empty dictionary outside of this function (writing something like `joystick_button_state = {}` where all of your main code is located should most likely fix that). The dictionary is empty at first, then gets populated with values from the `find_events()` function. – CodeSurgeon Oct 11 '17 at 07:09