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()
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()