1
votes

I'm designing a simple emotion recognition game that displays faces to the screen for one second, before disappearing and letting participants click a button to decide which emotion was displayed. However, I struggled to get it to only display for a second, I eventually used a while loop to display it, but ever since I incorporated that my game is lagging and running really slowly. Here's the relevant code, from the function I defined to display the images. I have it set so it creates a transparent version of the image and then blits that instead, to provide the illusion of the image disappearing after 1 second.

def askQuestion(imageNumber):
    mouse = pygame.mouse.get_pos()
    last = pygame.time.get_ticks()
    while pygame.time.get_ticks() - last <= 1000:
        screen.blit(images[imageNumber], (((display_width/2) - (images[imageNumber].get_size()[0]/2)), (((display_height/2) - (images[imageNumber].get_size()[1]/2)))))
    else:
        images[imageNumber].fill((255,255,255,0))

I strongly suspect that the reason it is lagging is that, by having the while loop, Python is continually blitting the image to the screen, as opposed to just doing it once, thus using up a lot of resources. Could anyone suggest an easier way to display this image for only one second without using a while loop? (Also, I know my code for the image co-ordinates is really messy - it's on my to-do list!)

Update: I tried to incorporate the "state machine" suggested by @Kingsley, however it still doesn't appear to be working, as the image remains on the screen indefinitely. Here's what I have so far:

running = True
while running:
    screen.fill((255, 255, 255))
    answerDetection()
    pygame.draw.circle(screen, (0,0,0), (int(display_width/2), int(display_height/2)), 10, 2)
    displayButtons("sad", 0, 700, 200, 100)
    displayButtons("happy", 240, 700, 200, 100)
    displayButtons("neutral", 480, 700, 200, 100)
    displayButtons("angry", 720, 700, 200, 100)
    displayButtons("afraid", 960, 700, 200, 100)
    imageNumber = len(answers) + 1    
    
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                running = False

    gameState = States.VIEWING
    timeNow = pygame.time.get_ticks()
    startTime = timeNow
    if ( gameState == States.VIEWING):
        if( timeNow <= startTime + 1000):
            askQuestion(imageNumber)
        else:
            gameState == States.ANSWERING

    if (gameState == States.ANSWERING):
        images[imageNumber].fill((255,255,255,0))
        for rects in rectList:
            for event in pygame.event.get():
                if event.type == pygame.MOUSEBUTTONUP:
                    if pygame.Rect(rects).collidepoint(mouse):
                        gameState == States.VIEWING   

Just to clarify, answerDetection() is a function to detect which button a user has clicked. In the States.ANSWERING section, I use "images[imageNumber].fill((255, 255, 255, 0)) as a way to make the image transparent, because all I want to do in the VIEWING state is to make the image disappear (as the buttons are continually displayed anyway). I'm not sure if the issue is that it isn't changing state, or that when it is changing state nothing is happening.

Any help would be really appreciated!

Edit 2: Further experimentation has revealed the problem: With the way I have timeNow = pygame.time.get_ticks() followed immediately by startTime = timeNow, the loop is continually updating the timeNow variable, followed by re-setting the startTime variable to the same number immediately after - whereas as I understand, we want the startTime to be a static number to indicate the time the "viewing" state started. Otherwise, timeNow will never be 1000ms more than startTime and it will never move to the "answering" state. I just need to find a way to set the time at the start of the viewing state and have that be a static time in the startTime variable.

1

1 Answers

0
votes

You're exactly correct. Pygame is re-blitting that image as fast as it can. You're also blocking the event-loop during this time, so the window can't be closed, resized, etc. and eventually the OS will consider it "non responsive". You need another approach.

Your game has two phases, "Questioning" and "Answering". A good way of thinking about your game is that of a State Diagram. It starts in the "Display Question" state, and then after the time-limit expires, it moves to the "Answer Question" State. If the user provides and answer, it moves back to the "Display Question" state again. You'd also have some extra states, like "Game Over", and maybe "Intro Display", etc.

States like this can just be represented by a symbolic number. Most programming languages have Enumerated Types just for this sort of thing:

## Enumerated Type to keep a set of constants together
class States( enum.IntEnum ):
    GAME_OVER = 0
    VIEWING   = 1
    ANSWERING = 2

So to know what state you're in, you just compare it against these.

if ( current_state == States.GAME_OVER ):
    drawGameOverScreen()
    waitForKeyPress()

So in your main loop, the code can use the state to control the painting and event handling path:

while not done:

    # Handle user-input
    for event in pygame.event.get():
        if ( event.type == pygame.QUIT ):
            done = True

    # Update the window
    window.fill( WHITE )

    # State Machine
    time_now = pygame.time.get_ticks()
    if ( game_state == States.VIEWING ):
        # If the viewing time is not expired, paint the question image
        if ( time_now <= start_time + IMAGE_VIEW_TIME ):
            emotion_image = emotion_images[ image_index ]
            image_rect = emotion_image.get_rect()
            image_rect.center = ( WINDOW_WIDTH//2, WINDOW_HEIGHT//2 )
            window.blit( emotion_image, image_rect )
        else:
            # The timer has expired, shift to the next state
            game_state = States.ANSWERING
            start_time = time_now
            # ( screen will repaint next frame )

    elif ( game_state == States.ANSWERING ):
        # TODO: draw the multiple choice answers 
        #       determine if the user-input was correct, etc.
        if ( answer_key_pressed != None ):
            if ( //answer correct// ):
                # TODO
            else: 
                # TODO
            
            # Answer has been made, Switch to Question-Viewing State
            game_state = States.VIEWING
            start_time = time_now
            image_index = random.randrange( 0, len( emotion_images ) )
            # ( screen will repaint next frame )

Using a state-transition to drive your game can be a bit tedious. But it does allow you to map-out all the different game phases, and move between them without resorting to independent drawing and control functions that (like your example) act to the detriment of the entire program.

EDIT: You're changes are mostly correct. Some parts of your code I don't understand - this is why it should have comments. Future-you wont understand parts of it either.

With a state-transition setup, typically you check the state for each operation. Need to paint the screen => check the state to see what to draw. Got some user-input => check the state to see whether it's ignored or corrected, etc. You check the state in the given scenario, and then handle it bit-by-bit.

The main problem with your edited code, is that it re-sets the state to VIEWING in every loop-pass. So your state is always VIEWING, even if it changes, the very next frame it's changed back.

I've re-arranged your code a little, checked the state in the mouse-click handler. Oh, and added some comments. Please try to write at least this level of comments. It will save your bacon one day.

gameState     = States.VIEWING  # The initial state
answerClicked = -1              # The answer-box the user clicked on

running = True
while running:

    # update the screen
    screen.fill((255, 255, 255))
    pygame.draw.circle(screen, (0,0,0), (int(display_width/2), int(display_height/2)), 10, 2)
    displayButtons("sad", 0, 700, 200, 100)
    displayButtons("happy", 240, 700, 200, 100)
    displayButtons("neutral", 480, 700, 200, 100)
    displayButtons("angry", 720, 700, 200, 100)
    displayButtons("afraid", 960, 700, 200, 100)
    imageNumber = len(answers) + 1    

    # reset the clock, and any previous answer
    timeNow       = pygame.time.get_ticks()
    answerClicked = -1

    #answerDetection()  what does this do?  Not Here anyway.
    
    # Handle events and user-input
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                running = False
        elif event.type == pygame.MOUSEBUTTONUP:
            # Mouse was clicked, are we in answer-mode?
            if ( gameState == States.ANSWERING ):
                for i,rects in enumerate( rectList ):
                    if pygame.Rect(rects).collidepoint(mouse):
                        print( "Answer-box was cicked in rect %d" % ( i ) )
                        answerClicked = i
                
    # Handle Game State and Transitions
    if ( gameState == States.VIEWING):
        if( timeNow <= startTime + 1000):
            # Draw the questions to the screen
            askQuestion( imageNumber )
        else:
            # question time is up
            gameState == States.ANSWERING

    elif (gameState == States.ANSWERING):
        images[imageNumber].fill((255,255,255,0))  # why do this?
        # Did the user click an answer-rect?
        if ( answerClicked != -1 ):
            # USer has clicked an answer, was it correct?
            print( "Handling correct/incorrect answer! Add Code HERE!!!" )
            # Maybe we now Change back into question mode ?
            # not sure how failures/retries/etc. should be handled
            gameState == States.VIEWING   
            startTime = timeNow