1

I am new to programming , PyGame and OOP especially. I can't figure out how to make a button execute a specific command in PyGame. I tried making a class for a button, and if you look at this code, I am able to execute the hover method/function as an example, but I am struggling to make the game close when I press the exit button. I can't seem to understand how to pass an argument that would make the main_menu be false for when exit is executed and main_game be true when play is executed.

from ColorsAndCoordinates import *

pygame.init()

screen = pygame.display.set_mode((1000, 700))
font = pygame.font.Font("freesansbold.ttf", 42)


class Button:
    main_menu = True

    def __init__(self, color, x, y, width, height, text):
        self.color = color
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.text = text

    def display(self, color):
        self.color = color
        pygame.draw.rect(screen, self.color, (self.x, self.y, self.width, self.height))
        text = font.render(self.text, True, red)
        screen.blit(text, (self.x, self.y))

    def hover(self, color):
        mouse = pygame.mouse.get_pos()
        if self.x + self.width > mouse[0] > self.x and self.y + self.height > mouse[1] > self.y:
            Button.display(self, color)

    def clicked(self):
        mouse = pygame.mouse.get_pos()
        if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
            if self.x + self.width > mouse[0] > self.x and self.y + self.height > mouse[1] > self.y:
                pass


play_button = Button(blue, 200, 300, 95, 46, "Play")
exit_button = Button(blue, 700, 300, 95, 46, "Exit")
tutorial_button = Button(blue, 410, 550, 165, 46, "Tutorial")

main_menu = True
main_game = False
while main_menu:

    screen.fill(black)
    play_button.display(blue)
    exit_button.display(blue)
    tutorial_button.display(blue)
    play_button.hover(black)
    exit_button.hover(black)
    tutorial_button.hover(black)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            main_menu = False

    exit_button.clicked()

    pygame.display.update()
Lone Creation
  • 59
  • 1
  • 6

3 Answers3

1

There are different solutions for this, for instance polymorphism, an action or an event.
An obvious and simple solution, is to add an action argument to the the method clicked. The argument is an action function, which is invoked when the button is clicked:

class Button:
    # [...]

    def clicked(self, action = None):
        mouse = pygame.mouse.get_pos()
        if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
            if self.x + self.width > mouse[0] > self.x and self.y + self.height > mouse[1] > self.y:

                if action:
                    action()

Create a function, which changes the states. Consider to use the global statement for the variables in global name space main_menu and main_game:

def action_exit():
    global main_menu, main_game 
    main_menu = False
    main_game = True

Pass the action to the exit_button.clicked:

while main_menu:
    # [...]

    exit_button.clicked(action_exit)

Furthermore changed Button.display(self, color) to self.display(color) in the method display().

Complete example:

import pygame
from pygame.locals import *

pygame.init()

screen = pygame.display.set_mode((1000, 700))
font = pygame.font.Font("freesansbold.ttf", 42)

red = (255, 0, 0)
green = (0, 255, 0)
blue = (0, 0, 255)
black = (0, 0, 0)

class Button:
    main_menu = True

    def __init__(self, color, x, y, width, height, text):
        self.color = color
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.text = text

    def display(self, color):
        self.color = color
        pygame.draw.rect(screen, self.color, (self.x, self.y, self.width, self.height))
        text = font.render(self.text, True, red)
        screen.blit(text, (self.x, self.y))

    def hover(self, color):
        mouse = pygame.mouse.get_pos()
        if self.x + self.width > mouse[0] > self.x and self.y + self.height > mouse[1] > self.y:
            self.display(color)

    def clicked(self, action = None):
        mouse = pygame.mouse.get_pos()
        if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
            if self.x + self.width > mouse[0] > self.x and self.y + self.height > mouse[1] > self.y:

                if action:
                    action()

def action_exit():
    global main_menu, main_game 
    main_menu = False
    main_game = True

play_button = Button(blue, 200, 300, 95, 46, "Play")
exit_button = Button(blue, 700, 300, 95, 46, "Exit")
tutorial_button = Button(blue, 410, 550, 165, 46, "Tutorial")

main_menu = True
main_game = False
while main_menu:

    screen.fill(black)
    play_button.display(blue)
    exit_button.display(blue)
    tutorial_button.display(blue)
    play_button.hover(black)
    exit_button.hover(black)
    tutorial_button.hover(black)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            main_menu = False

    exit_button.clicked(action_exit)

    pygame.display.update()
Rabbid76
  • 202,892
  • 27
  • 131
  • 174
  • Thank you for answering! I have one question about this solution, when I do everything that is said the game just closes itself after I open it. I supposed I have done something wrong but I can't understand what. – Lone Creation May 01 '20 at 16:45
  • Yea, I had done that exact thing, the game closes as soon as I open it. – Lone Creation May 01 '20 at 16:54
  • Nvm I am an idiot, I had exit_button.clicked(action_ext()) instead of exit_button.clicked(action_exit), sorry and thank you! – Lone Creation May 01 '20 at 17:04
1

make main_menu and main_game variable global inside your clicked function

    def clicked(self):
        global main_menu, main_game
        mouse = pygame.mouse.get_pos()
        if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
            if self.x + self.width > mouse[0] > self.x and self.y + self.height > mouse[1] > self.y:
                main_menu = False
                main_game = True

Mohammad Sartaj
  • 111
  • 1
  • 4
  • The `Button` is used for different actions. How would you handle the actions for `tutorial_button` and `play_button`? – Rabbid76 May 01 '20 at 09:46
  • what action you want to take when tutorial_button and play_button get pressed – Mohammad Sartaj May 01 '20 at 09:49
  • The point is you cannot distinguish if `exit_button.clicked()`, `tutorial_button.clicked()` or `play_button.clicked()` was called inside the `click` method. – Rabbid76 May 01 '20 at 09:51
  • for that we can use self.text.... if self.text perform this action – Mohammad Sartaj May 01 '20 at 10:01
  • Sorry but that is an anti pattern. For each new button, you would have to implement an extra case in the method `clicked`. Please never choose such a design. There are different solutions for this, for instance polymorphism, an action or an event, ... – Rabbid76 May 01 '20 at 10:03
1

Here's an example that I wrote for another question. I changed it to have a button quit the game:

import pygame

pygame.init()

display_width = 1200
display_height = 600

# use python style variable names (lowercase)
screen = pygame.display.set_mode((display_width, display_height))
pygame.display.set_caption('Log In')
clock = pygame.time.Clock()

# load the font only once instead of every frame
font = pygame.font.SysFont('comicsans', 20)

# class name should be singular
class Button(pygame.sprite.Sprite):
    # 1) no need to have 4 parameters for position and size, use pygame.Rect instead
    # 2) let the Button itself handle which color it is
    # 3) give a callback function to the button so it can handle the click itself 
    def __init__(self, color, color_hover, rect, callback, text='', outline=None):
        super().__init__()
        self.text = text
        # a temporary Rect to store the size of the button
        tmp_rect = pygame.Rect(0, 0, *rect.size)

        # create two Surfaces here, one the normal state, and one for the hovering state
        # we create the Surfaces here once, so we can simple blit them and dont have
        # to render the text and outline again every frame
        self.org = self._create_image(color, outline, text, tmp_rect)
        self.hov = self._create_image(color_hover, outline, text, tmp_rect)

        # in Sprites, the image attribute holds the Surface to be displayed...
        self.image = self.org
        # ...and the rect holds the Rect that defines it position
        self.rect = rect
        self.callback = callback

    def _create_image(self, color, outline, text, rect):
        # function to create the actual surface
        # see how we can make use of Rect's virtual attributes like 'size'
        img = pygame.Surface(rect.size)
        if outline:
            # here we can make good use of Rect's functions again
            # first, fill the Surface in the outline color
            # then fill a rectangular area in the actual color
            # 'inflate' is used to 'shrink' the rect
            img.fill(outline)
            img.fill(color, rect.inflate(-4, -4))
        else:
            img.fill(color)

        # render the text once here instead of every frame
        if text != '':
            text_surf = font.render(text, 1, pygame.Color('black'))
            # again, see how easy it is to center stuff using Rect's attributes like 'center'
            text_rect = text_surf.get_rect(center=rect.center)
            img.blit(text_surf, text_rect)
        return img

    def update(self, events):
        # here we handle all the logic of the Button
        pos = pygame.mouse.get_pos()
        hit = self.rect.collidepoint(pos)
        # if the mouse in inside the Rect (again, see how the Rect class
        # does all the calculation for use), use the 'hov' image instead of 'org'
        self.image = self.hov if hit else self.org
        for event in events:
            # the Button checks for events itself.
            # if this Button is clicked, it runs the callback function
            if event.type == pygame.MOUSEBUTTONDOWN and hit:
                self.callback(self)

run = True

# we store all Sprites in a Group, so we can easily
# call the 'update' and 'draw' functions of the Buttons
# in the main loop
sprites = pygame.sprite.Group()
sprites.add(Button(pygame.Color('green'), 
                   pygame.Color('red'), 
                   pygame.Rect(150, 200, 90, 100), 
                   lambda b: print(f"Button '{b.text}' was clicked"),
                   'Press',
                   pygame.Color('black')))

def close(*args):
    global run
    run = False

sprites.add(Button(pygame.Color('dodgerblue'), 
                   pygame.Color('lightgreen'), 
                   pygame.Rect(300, 200, 90, 100), 
                   close,
                   'Close'))

while run:
    events = pygame.event.get()
    for event in events:
        if event.type == pygame.QUIT:
            pygame.quit()
            quit()

    # update all sprites
    # it now doesn't matter if we have one or 200 Buttons
    sprites.update(events)
    # clear the screen
    screen.fill(pygame.Color('white'))
    # draw all sprites/Buttons
    sprites.draw(screen)
    pygame.display.update()
    # limit framerate to 60 FPS
    clock.tick(60)

We simply pass the Button-class a callback function that gets called if the button is clicked. For closing the window, we call a little function that captures the run variable and sets it to False.

sloth
  • 99,095
  • 21
  • 171
  • 219