Search code examples
pythonperformancepygame

Creating Multiple Buttons but the Program would Stucks a Second in a Step


I am trying create multiple buttons with each button consists of 2 rectangles in Pygame. However, the program would stuck for a second in a certain step probably due to a nested loop. Specifically, the "while loop" marked by the code fence in the main loop only works correctly only when being executed for the first time. If it is executed after the first time, the program would stop responding for a second. Is there anyway to correct that? I am currently testing it on on window 10 64 bit , Python 3.7.4, PyGame 1.9.6.

import pygame
import time
import random

pygame.init()

# Screen for displaying everything
display_width = 1200
display_height = 700
game_display = pygame.display.set_mode((display_width,display_height),pygame.RESIZABLE)
pygame.display.set_caption('Currently testing')

# Color for later use
red = (200,0,0)
green = (0,200,0)
bright_red = (255,0,0)
bright_green = (0,255,0)
black= (0,0,0)
white= (225,225,225)
grey = (166,166,166)
font_color= black
background_color= white

def create_button(x,y,w,h,name,callback):
    # lower part of the button
    button_upper= pygame.Rect(x, y, w, h)
    # upper part of the button
    button_lower= pygame.Rect(x, y+ h, w, h)
    # define the area of the button for later interaction
    interactable= pygame.Rect(x, y, w, 2*h)
    button_info= {'button_lower':button_lower,
                  'button_upper':button_upper, 
                  'button_name':name,
                  'interaction':interactable,
                  'button function':callback,
                  'color_upper':red, 'color_lower':green}
    return button_info

def draw_button(button_info):
    # Drawing lower part of the button
    pygame.draw.rect(game_display, 
                    button_info['color_lower'],
                    button_info['button_lower'])
    # Drawing upper part
    pygame.draw.rect(game_display, 
                    button_info['color_upper'],
                    button_info['button_upper'])

# Text object for later use
def text_object(text,font):
    textSurface= font.render(text,True,font_color)
    return textSurface, textSurface.get_rect()
def central_text(text,size,pos):
    largeText = pygame.font.Font('freesansbold.ttf',size)
    textSurface, textRect = text_object(text,largeText)
    textRect.center = (pos)
    game_display.blit(textSurface,textRect)

def main():

    # The function of the button, temporarily
    def print_name():
        nonlocal button
        nonlocal done
        game_display.fill(background_color)
        central_text('Button'+' '+button['button_name']+' '+'clicked',80,(display_width/2,display_height/2))
        pygame.display.update()
        time.sleep(3)         
        done= True
    # function of non-interactable block
    def do_nothing():
        pass

    # Actually create those button
    button_1= create_button(display_width*0.3,display_height*0.5,40,40,'1',print_name)
    button_2= create_button(display_width*0.35,display_height*0.5,50,50,'2',print_name)
    button_3= create_button(display_width*0.4,display_height*0.5,30,30,'3',print_name)
    button_4= create_button(display_width*0.45,display_height*0.5,20,20,'4',print_name)
    button_5= create_button(display_width*0.5,display_height*0.5,40,30,'5',print_name)
    button_6= create_button(display_width*0.55,display_height*0.5,50,40,'6',print_name)
    button_7= create_button(display_width*0.6,display_height*0.5,50,30,'7',print_name)
    button_8= create_button(display_width*0.65,display_height*0.5,50,50,'8',print_name)
    button_9= create_button(display_width*0.7,display_height*0.5,30,40,'9',print_name)
    button_10= create_button(display_width*0.75,display_height*0.5,60,70,'10',print_name)
    # Create non-interactable rectangles
    block_1= create_button(display_width*0.75,display_height*0.8,40,40,'10',do_nothing)
    block_2= create_button(display_width*0.7,display_height*0.8,40,40,'9',do_nothing)
    block_3= create_button(display_width*0.7,display_height*0.8,40,40,'8',do_nothing)
    # Select and store those button in different list, with a non-interactable 
    # rectangles in each list
    list_1=[button_1, button_2, button_3, button_4, button_5, 
            button_6, button_7, button_8, button_9, button_10]
    list_2=[button_1, button_2, button_3, button_4, button_5, 
            button_6, button_7, block_3, button_9, button_10]
    list_3=[button_1, button_2, button_3, button_4, button_5, 
            button_6, button_7, button_8, block_2, button_10]
    list_4=[button_1, button_2, button_3, button_4, button_5, 
            button_6, button_7, button_8, button_9, block_1]

    # Attempt to control how many times and in what sequence those
    # button list would be used
    index= [1,1,2,2,2,3,3,4,4]
    random.shuffle(index)
    text=' ' # a text that would accompany all the buttons
    for i in range (len(index)):
        done = False
        if index[i] == 1:
            button_list= list_1
            text= 'text 1'
        elif index[i] == 2:
            button_list= list_2
            text= 'text 2'
        elif index[i] == 3:
            button_list= list_3
            text= 'text 3'
        else:
            button_list= list_4
            text= 'text 4'
        # display message 1
        game_display.fill(background_color)
        central_text('test_text 1',80,(display_width/2,display_height/2))
        pygame.display.update()
        time.sleep(2)
        # display message 2
        game_display.fill(background_color)
        central_text('test_text 2',80,(display_width/2,display_height/2))
        pygame.display.update()       
        time.sleep(3)
       '''This is where the program would stuck for a second'''
        game_display.fill(background_color)
        central_text(text,30,(display_width/2,display_height*0.2))
        while not done:
            for event in pygame.event.get():
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        pygame.display.quit()
                        pygame.quit()
                        quit()     
                # block that would be executed when left mouse button is pressed
                elif event.type == pygame.MOUSEBUTTONDOWN:
                    if event.button == 1:
                        for button in button_list:
                            if button['interaction'].collidepoint(event.pos):
                                button['button function']()
                elif event.type == pygame.MOUSEMOTION:
                    # When the mouse gets moved, change the color of the
                    # buttons if they collide with the mouse.
                    for button in button_list:
                        if not button['button function']== do_nothing:
                            if button['interaction'].collidepoint(event.pos):
                                button['color_upper']= red
                                button['color_lower']= green
                            else:
                                button['color_upper']= bright_red
                                button['color_lower']= bright_green
            for button in button_list:
                # Turn non-interactable blocks into grey
                if button['button function']== do_nothing:
                    button['color_upper']= grey
                    button['color_lower']= grey                    
                draw_button(button)
            pygame.display.update()
        '''the block above would some times stucks the program'''
main()

Solution

  • You need to handle the events by either by either pygame.event.pump() or pygame.event.get(), when you do delays and display updates. When you use pygame then you should use pygame.time.delay() or pygame.time.wait().

    I recommend to change the procedure. Use 1 main loop and 1 event loop. Implement the game procedure in this loop. Instead of

    for i in range (len(index)):
    
       # [...]
    
       while not done:
           for event in pygame.event.get():
       
               # [...]
    
       # [...]
    

    change the procedure to

    done = False
    run = True
    i = 0
    while run:
    
        if done:
            done = False
            if i < len(index)-1:
                i += 1
            else:
                run = False
    
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run = False
    
            # [...]
    
        # [...]
    

    To implement the different state of the game, I recommend to use a game state. e.g.:

    from enum import Enum
    class GameState(Enum):
        START = 0
        TEXT1 = 1
        TEXT2 = 2
        RUN = 3
        BUTTON = 4
    
    state = GameState.START
    

    Use a timer event (pygame.time.set_timer()) to switch the states of the game. e.g.:

    clicked_button = None
    my_event_id = pygame.USEREVENT + 1
    while run:
    
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run = False
    
            # timer event
            elif event.type == my_event_id:
                 if state == GameState.TEXT1:
                    pygame.time.set_timer(my_event_id, 3000)
                    state = GameState.TEXT2
                elif state == GameState.TEXT2:
                    pygame.time.set_timer(my_event_id, 0)
                    state = GameState.RUN
                else:
                    pygame.time.set_timer(my_event_id, 0)
                    state = GameState.START
                    done = True
    
            if state == GameState.RUN:
                if event.type == pygame.MOUSEBUTTONDOWN:
                    if event.button == 1:
                        for button in button_list:
                            if button['interaction'].collidepoint(event.pos):
                                pygame.time.set_timer(my_event_id, 3000)
                                state = GameState.BUTTON
                                clicked_button = button
                # [...]
    

    Draw the scene dependent on the game state:

    while run:
    
        # [...]
        
        game_display.fill(background_color)
    
        if state == GameState.START:
            pygame.time.set_timer(my_event_id, 2000)
            state = GameState.TEXT1
        elif state == GameState.TEXT1:
            # display message 1
            central_text('test_text 1',80,(display_width/2,display_height/2))
        elif state == GameState.TEXT2:
            # display message 2
            central_text('test_text 2',80,(display_width/2,display_height/2))
        elif state == GameState.RUN:
            central_text(text,30,(display_width/2,display_height*0.2))
            # [...]
        else:
            clicked_button['button function']()
                
        pygame.display.update()
    

    The game loop and the main game procedure may look like this:

    from enum import Enum
    class GameState(Enum):
        START = 0
        TEXT1 = 1
        TEXT2 = 2
        RUN = 3
        BUTTON = 4
    
    def main():
    
        # The function of the button, temporarily
        def print_name():
            nonlocal clicked_button
            central_text('Button'+' '+clicked_button['button_name']+' '+'clicked',80,(display_width/2,display_height/2))        
        # function of non-interactable block
        def do_nothing():
            pass
    
        # Actually create those button
        button_1= create_button(display_width*0.3,display_height*0.5,40,40,'1',print_name)
        button_2= create_button(display_width*0.35,display_height*0.5,50,50,'2',print_name)
        button_3= create_button(display_width*0.4,display_height*0.5,30,30,'3',print_name)
        button_4= create_button(display_width*0.45,display_height*0.5,20,20,'4',print_name)
        button_5= create_button(display_width*0.5,display_height*0.5,40,30,'5',print_name)
        button_6= create_button(display_width*0.55,display_height*0.5,50,40,'6',print_name)
        button_7= create_button(display_width*0.6,display_height*0.5,50,30,'7',print_name)
        button_8= create_button(display_width*0.65,display_height*0.5,50,50,'8',print_name)
        button_9= create_button(display_width*0.7,display_height*0.5,30,40,'9',print_name)
        button_10= create_button(display_width*0.75,display_height*0.5,60,70,'10',print_name)
        # Create non-interactable rectangles
        block_1= create_button(display_width*0.75,display_height*0.8,40,40,'10',do_nothing)
        block_2= create_button(display_width*0.7,display_height*0.8,40,40,'9',do_nothing)
        block_3= create_button(display_width*0.7,display_height*0.8,40,40,'8',do_nothing)
        # Select and store those button in different list, with a non-interactable 
        # rectangles in each list
        list_1=[button_1, button_2, button_3, button_4, button_5, 
                button_6, button_7, button_8, button_9, button_10]
        list_2=[button_1, button_2, button_3, button_4, button_5, 
                button_6, button_7, block_3, button_9, button_10]
        list_3=[button_1, button_2, button_3, button_4, button_5, 
                button_6, button_7, button_8, block_2, button_10]
        list_4=[button_1, button_2, button_3, button_4, button_5, 
                button_6, button_7, button_8, button_9, block_1]
    
        # Attempt to control how many times and in what sequence those
        # button list would be used
        index= [1,1,2,2,2,3,3,4,4]
        random.shuffle(index)
        text=' ' # a text that would accompany all the buttons
    
     
        my_event_id = pygame.USEREVENT + 1
        done = False
        run = True
        state = GameState.START
        i = 0
        clicked_button = None
        while run:
    
            if done:
                done = False
                if i < len(index)-1:
                    i += 1
                else:
                    run = False
    
            if index[i] == 1:
                button_list= list_1
                text= 'text 1'
            elif index[i] == 2:
                button_list= list_2
                text= 'text 2'
            elif index[i] == 3:
                button_list= list_3
                text= 'text 3'
            else:
                button_list= list_4
                text= 'text 4' 
    
            # event loop
           
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    run = False
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        run = False  
                elif event.type == my_event_id:
                    if state == GameState.TEXT1:
                        pygame.time.set_timer(my_event_id, 3000)
                        state = GameState.TEXT2
                    elif state == GameState.TEXT2:
                        pygame.time.set_timer(my_event_id, 0)
                        state = GameState.RUN
                    else:
                        pygame.time.set_timer(my_event_id, 0)
                        state = GameState.START
                        done = True
                        
                if state == GameState.RUN:
                    # block that would be executed when left mouse button is pressed
                    if event.type == pygame.MOUSEBUTTONDOWN:
                        if event.button == 1:
                            for button in button_list:
                                if button['interaction'].collidepoint(event.pos):
                                    pygame.time.set_timer(my_event_id, 3000)
                                    state = GameState.BUTTON
                                    clicked_button = button
    
                    elif event.type == pygame.MOUSEMOTION:
                        # When the mouse gets moved, change the color of the
                        # buttons if they collide with the mouse.
                        for button in button_list:
                            if not button['button function']== do_nothing:
                                if button['interaction'].collidepoint(event.pos):
                                    button['color_upper']= red
                                    button['color_lower']= green
                                else:
                                    button['color_upper']= bright_red
                                    button['color_lower']= bright_green
    
            # draw
    
            game_display.fill(background_color)
    
            if state == GameState.START:
                pygame.time.set_timer(my_event_id, 2000)
                state = GameState.TEXT1
            elif state == GameState.TEXT1:
                # display message 1
                central_text('test_text 1',80,(display_width/2,display_height/2))
            elif state == GameState.TEXT2:
                # display message 2
                central_text('test_text 2',80,(display_width/2,display_height/2))
            elif state == GameState.RUN:
                central_text(text,30,(display_width/2,display_height*0.2))
                for button in button_list:
                    # Turn non-interactable blocks into grey
                    if button['button function']== do_nothing:
                        button['color_upper']= grey
                        button['color_lower']= grey                    
                    draw_button(button)
            else:
                clicked_button['button function']()
                
            pygame.display.update()