How can I my make buttons in pygame work properly? (HANGMAN GAME)

Viewed 102

Hi I'm a python noob and I'm trying to make a hangman game with pygame by myself, while avoiding help from YouTube tutorials as much as possible.

My issues are:

  1. When I hover over the button, the button changes color (which is good) but then it changes back even if I'm still hovering over the button. Also, the responsiveness of the buttons is very poor when hovering over multiple buttons.

  2. When I click a button, the program thinks I'm clicking the button multiple times. As it executes the print('clicked!') line multiple times.

  3. Lastly, when i click a button to blit a sprite, it only blits the sprite for a brief moment, and it automatically un-blits itself.

This is my code:

import pygame
pygame.init()

# DISPLAY
WIDTH, HEIGHT = 800, 500
window = pygame.display.set_mode((WIDTH, HEIGHT))
# TITLE BAR
TITLE = "Hangman"
pygame.display.set_caption(TITLE)
# HANGMAN SPRITES
man = [pygame.image.load(f"hangman{frame}.png") for frame in range(0, 7)]


class Button:

    def __init__(self, color, x, y, radius, text=""):
        self.radius = radius
        self.color = color
        self.x = x
        self.y = y
        self.width = 2
        self.text = text
        self.visible = True

    def draw(self, window, outline=None):
        if self.visible:
            if outline:
                # draws a bigger circle behind
                pygame.draw.circle(window, outline, (self.x, self.y), self.radius + 2, 0)
            pygame.draw.circle(window, self.color, (self.x, self.y), self.radius, 0)

        if self.text != "":
            if self.visible:
                font = pygame.font.SysFont("courier", 30)
                text = font.render(self.text, 1, (0, 0, 0))
                window.blit(text, (self.x - text.get_width() / 2, self.y - text.get_height() / 2))

    def hover(self, pos):
        if self.y + self.radius > pos[1] > self.y - self.radius:
            if self.x + self.radius > pos[0] > self.x - self.radius:
                return True
        return False


def main():
    run = True
    FPS = 60
    clock = pygame.time.Clock()
    large_font = pygame.font.SysFont("courier", 50)

    letters = []
    error = 0

    def redraw_window():
        window.fill((255, 255, 255))
        
        window.blit(man[0], (20, 100))
        # DRAWS LETTER BUTTONS
        for letter in letters:
            letter.draw(window, (0, 0, 0))
        pygame.display.update()

    while run:
        redraw_window()
        clock.tick(FPS)

        alphabet = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
        letter_x1, letter_y1 = 40, 375
        letter_x2, letter_y2 = 40, 435
        for i in range(13):
            letter_1 = Button((255, 255, 255), letter_x1, letter_y1, 25, alphabet[i])
            letters.append(letter_1)
            letter_x1 += 60
        for i in range(13, 26):
            letter_2 = Button((255, 255, 255), letter_x2, letter_y2, 25, alphabet[i])
            letters.append(letter_2)
            letter_x2 += 60

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run = False
            elif event.type == pygame.MOUSEMOTION:
                for letter in letters[:]:
                    if letter.hover(pygame.mouse.get_pos()):
                        letter.color = (0, 255, 0)
                    else:
                        letter.color = (255, 255, 255)
            elif event.type == pygame.MOUSEBUTTONDOWN:
                for letter in letters:
                    if letter.hover(pygame.mouse.get_pos()):
                        print("clicked!")
                        window.blit(man[1], (20, 100))
                        pygame.display.update()
    quit()
main()

Also, I got the sprites from the Tech With Tim Hangman Tutorial on YouTube (i just got the sprites without watching him code the game, as I want to try to do it by myself so I can learn more). And i got the code for my button class from Tech With Tim's video as well.

2 Answers

Fist of all do the initialization of the buttons before the application loop rather than continuously in the loop

def main():

    # init buttons
    alphabet = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
    letter_x1, letter_y1 = 40, 375
    letter_x2, letter_y2 = 40, 435
    for i in range(13):
        letter_1 = Button((255, 255, 255), letter_x1, letter_y1, 25, alphabet[i])
        letters.append(letter_1)
        letter_x1 += 60
    for i in range(13, 26):
        letter_2 = Button((255, 255, 255), letter_x2, letter_y2, 25, alphabet[i])
        letters.append(letter_2)
        letter_x2 += 60

    # application loop
    while run:
        # [...]

Add an attribute clicked to the button, which stores the button (similar the visible attribute):

class Button:

    def __init__(self, color, x, y, radius, text=""):
        # [...]

        self.visible = True
        self.clicked = False

Set the attribute, when the button is clicked:

def main():
    # [...]

    while run:
        # [...]
        for event in pygame.event.get():
            # [...]
            elif event.type == pygame.MOUSEBUTTONDOWN:
                for letter in letters:
                    if letter.hover(pygame.mouse.get_pos()):
                        letter.clicked = True

Now you can draw objects dependent on the clicked state of the button:

def main():
    # [...]

    def redraw_window():
        window.fill((255, 255, 255))
        
        window.blit(man[0], (20, 100))
        # DRAWS LETTER BUTTONS
        for letter in letters:
            letter.draw(window, (0, 0, 0))
             
            if letter.clicked:
                # [...]

        pygame.display.update()

    # [...]
    while run:
        redraw_window()
        # [...]        

Alternatively or additionally you can store the last button which was clicked to a variable (lastLetterClicked) and draw something dependent on the variable:

def main():
    # [...]

    def redraw_window():
        # [...]

        if lastLetterClicked:
            # [...]

        pygame.display.update()

    lastLetterClicked = None
    while run:
        redraw_window()
        # [...]

        for event in pygame.event.get():
            # [...]
            elif event.type == pygame.MOUSEBUTTONDOWN:
                for letter in letters:
                    if letter.hover(pygame.mouse.get_pos()):
                        # [...]
                        lastLetterClicked = letter

        # [...]

ok, lets start with the first one, i suspect some changes here might also help solve the others. nonice, you are creating 'initial colored buttons' inside the 'while run' loop, which means it will happen agin and again, but you redye buttons inside the event for loop, which happens only when a new event emerges. do you see the problem? the next minut after the hover event happens, the program will just draw a regular button! i'd say that this line

  letter.color = (0, 255, 0) 

is conciderd a bad habit in OOP , bnecause you dont want to change an objects attribute outside of the class. instead lets build a "set_color" method

 def set_color(self , color):
     self.color = color

and initiate the buttons letter_1 = Button((255, 255, 255), letter_x1, letter_y1, 25, alphabet[i])

before the start of the game, outside of the while run loop

in the while loop you can just add a loop to draw them.

 for letrer in letters:
       letter.draw()
Related