Create a simple game menu with pygame pt. 4 – Connecting it to functions

Please note that I used python 2.7 for this tutorial. If you are using python3.x, this code may not work. There have been many changes that do not allow backwards compatibility. If you run into problems, these websites could be useful: http://docs.python.org/3/whatsnew/3.0.html ; http://www.python.org/dev/peps/pep-3113/ ; https://wiki.python.org/moin/Python2orPython3

Hello again :). This is the final part in my mini tutorial series on creating a simple game menu with pygame. Last time we looked at the implementation of the keyboard navigation and thus we completed the navigation section. Now we are wrapping it all up by connecting the navigation to functions, so that we can link it to our game in the making.

Follow this link here to the end of the article where the full code is.

For a change, I did not do any refactoring of the code in between articles, so there is nothing to figure out. Sorry for that by the way, but I teach myself how to write it, as much as then tell you about it 😉

To start us off, we shall do one very important addition to GameMenu.__init__(). We will add a funcs argument to it. This will be a dictionary of strings to functions.

class GameMenu():
    def __init__(self, screen, items, funcs, bg_color=BLACK, font=None, font_size=30,
                 font_color=WHITE):

        ...

        self.funcs = funcs

As a consequence, we now need to define a new dictionary with respective functions in our if __name__ == '__main__: block. As you can see, the strings correspond to the names of our menu items and I chose to just do a simple ‘Hello World!’ print out. To avoid errors, you could use dict.keys() as the list to be passed in as menu items, but in most cases the resultant unordered list, does not give the order that you want. You could try it and see, otherwise you may need to type it out manually.

if __name__ == "__main__":
    def hello_world():
        print "Hello World!"

    # Creating the screen
    screen = pygame.display.set_mode((640, 480), 0, 32)

    funcs = {'Start': hello_world,
             'Quit': sys.exit}

    pygame.display.set_caption('Game Menu')
    gm = GameMenu(screen, funcs.keys(), funcs)
    gm.run()

That was all nice and easy, now let’s keep it that way and connect up the keyboard to these functions.

                if event.type == pygame.KEYDOWN:
                    self.mouse_is_visible = False
                    self.set_keyboard_selection(event.key)
                    # Finally check if Enter or Space is pressed
                    if event.key == pygame.K_SPACE or event.key == pygame.K_RETURN:
                        text = self.items[self.cur_item].text
                        mainloop = False
                        self.funcs[text]()

The first three lines, we already know, but the rest not so much. So finally we check if either the Space Key or the Return key is pressed. If that is the case, we get the text of the menu item via its attribute, terminate the mainloop (We don’t want to have several main loops running at the same time, do we?) and eventually call the function.

Now we do the exact same for the mouse.

                if event.type == pygame.MOUSEBUTTONDOWN:
                    mpos = pygame.mouse.get_pos()
                    for item in self.items:
                        if item.is_mouse_selection(mpos):
                            mainloop = False
                            self.funcs[item.text]()

This is essentially the same thing. We check if a mouse button was pressed (we make no distinction between left or right here. Why bother?), get the cursor coordinates and then we check if the cursor was on top of one of the menu items. If so, we again, terminate the mainloop and call the function associated to the menu item.
To avoid an extra call to pygame.mouse.get_pos(), you could move that call to the beginning of the mainloop as it is now used twice. If you are a purist that is. I did it in the final code, but left it in here for the sake of clarity.

And that is it! Nothing more to it and we have now successfully written a simple Game menu for any odd game. I hope it is flexible enough for you to play around with it. You can change the effect on mark up very easily and a change to the background should not be a problem either.

If you want to adjust this code and publish it, you are free to do so, but please mention me as the original source and also put a link in the comments here so that I have access to it, too. Thanks a lot, folks 🙂

NOTE: After using this class for a bit now, I have come to notice that instead of calling the function directly from the Menu, returning a fitting value (such as the text of the option) to a Main class is more advantageous and cleaner in the long run. To do that, just remove the `funcs` parameter from GameMenu and instead of calling the function on choosing just return the text itself. If you got questions feel free to comment and I will try to answer.

————————–
Here’s the full code

#!/usr/bin/python

import sys
import pygame

pygame.init()

WHITE = (255, 255, 255)
RED = (255, 0, 0)
BLACK = (0, 0, 0)

class MenuItem(pygame.font.Font):
    def __init__(self, text, font=None, font_size=30,
                 font_color=WHITE, (pos_x, pos_y)=(0, 0)):

        pygame.font.Font.__init__(self, font, font_size)
        self.text = text
        self.font_size = font_size
        self.font_color = font_color
        self.label = self.render(self.text, 1, self.font_color)
        self.width = self.label.get_rect().width
        self.height = self.label.get_rect().height
        self.dimensions = (self.width, self.height)
        self.pos_x = pos_x
        self.pos_y = pos_y
        self.position = pos_x, pos_y

    def is_mouse_selection(self, (posx, posy)):
        if (posx >= self.pos_x and posx <= self.pos_x + self.width) and \
            (posy >= self.pos_y and posy <= self.pos_y + self.height):
                return True
        return False

    def set_position(self, x, y):
        self.position = (x, y)
        self.pos_x = x
        self.pos_y = y

    def set_font_color(self, rgb_tuple):
        self.font_color = rgb_tuple
        self.label = self.render(self.text, 1, self.font_color)

class GameMenu():
    def __init__(self, screen, items, funcs, bg_color=BLACK, font=None, font_size=30,
                 font_color=WHITE):
        self.screen = screen
        self.scr_width = self.screen.get_rect().width
        self.scr_height = self.screen.get_rect().height

        self.bg_color = bg_color
        self.clock = pygame.time.Clock()

        self.funcs = funcs
        self.items = []
        for index, item in enumerate(items):
            menu_item = MenuItem(item, font, font_size, font_color)

            # t_h: total height of text block
            t_h = len(items) * menu_item.height
            pos_x = (self.scr_width / 2) - (menu_item.width / 2)
            # This line includes a bug fix by Ariel (Thanks!)
            # Please check the comments section of pt. 2 for an explanation
            pos_y = (self.scr_height / 2) – (t_h / 2) + ((index*2) + index * menu_item.height)

            menu_item.set_position(pos_x, pos_y)
            self.items.append(menu_item)

        self.mouse_is_visible = True
        self.cur_item = None

    def set_mouse_visibility(self):
        if self.mouse_is_visible:
            pygame.mouse.set_visible(True)
        else:
            pygame.mouse.set_visible(False)

    def set_keyboard_selection(self, key):
        """
        Marks the MenuItem chosen via up and down keys.
        """
        for item in self.items:
            # Return all to neutral
            item.set_italic(False)
            item.set_font_color(WHITE)

        if self.cur_item is None:
            self.cur_item = 0
        else:
            # Find the chosen item
            if key == pygame.K_UP and \
                    self.cur_item > 0:
                self.cur_item -= 1
            elif key == pygame.K_UP and \
                    self.cur_item == 0:
                self.cur_item = len(self.items) - 1
            elif key == pygame.K_DOWN and \
                    self.cur_item < len(self.items) - 1:
                self.cur_item += 1
            elif key == pygame.K_DOWN and \
                    self.cur_item == len(self.items) - 1:
                self.cur_item = 0

        self.items[self.cur_item].set_italic(True)
        self.items[self.cur_item].set_font_color(RED)

        # Finally check if Enter or Space is pressed
        if key == pygame.K_SPACE or key == pygame.K_RETURN:
            text = self.items[self.cur_item].text
            self.funcs[text]()

    def set_mouse_selection(self, item, mpos):
        """Marks the MenuItem the mouse cursor hovers on."""
        if item.is_mouse_selection(mpos):
            item.set_font_color(RED)
            item.set_italic(True)
        else:
            item.set_font_color(WHITE)
            item.set_italic(False)

    def run(self):
        mainloop = True
        while mainloop:
            # Limit frame speed to 50 FPS
            self.clock.tick(50)

            mpos = pygame.mouse.get_pos()

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    mainloop = False
                if event.type == pygame.KEYDOWN:
                    self.mouse_is_visible = False
                    self.set_keyboard_selection(event.key)
                if event.type == pygame.MOUSEBUTTONDOWN:
                    for item in self.items:
                        if item.is_mouse_selection(mpos):
                            self.funcs[item.text]()

            if pygame.mouse.get_rel() != (0, 0):
                self.mouse_is_visible = True
                self.cur_item = None

            self.set_mouse_visibility()

            # Redraw the background
            self.screen.fill(self.bg_color)

            for item in self.items:
                if self.mouse_is_visible:
                    self.set_mouse_selection(item, mpos)
                self.screen.blit(item.label, item.position)

            pygame.display.flip()

if __name__ == "__main__":
    def hello_world():
        print "Hello World!"

    # Creating the screen
    screen = pygame.display.set_mode((640, 480), 0, 32)

    menu_items = ('Start', 'Quit')
    funcs = {'Start': hello_world,
             'Quit': sys.exit}

    pygame.display.set_caption('Game Menu')
    gm = GameMenu(screen, funcs.keys(), funcs)
    gm.run()
Advertisements

4 thoughts on “Create a simple game menu with pygame pt. 4 – Connecting it to functions

  1. Can you please:
    1. Make the QUIT button actually close the window
    2. Make the PLAY button actually play the game
    3. The title should be in huge letters above the letters (mine is “STANM”)
    4. I want music to play during the menu only (in my case, it’s “Fortunate Son” by CCR)

    • Hi Charlie. Thanks for your feedback. This is a pure hobby blog with no intentions of being the be all and all. Because of that I apologise for the mistake in the post.
      By the time it took me to reply to your comment, I think you can already guess that I have currently unfortunately very little time (although I really wish that I had the time).

      Can “you” maybe alter the code so that it does what you want? They sound like fairly straight forward manipulations. If you manage I can post it here as Pt. 5 😉 Best of luck 🙂

  2. Just to give you a heads up, you might want to change this line of code event.type == pygame.KEYDOWN: to something like this if keys[pygame.K_UP] or keys[pygame.K_DOWN]:(before you do that, make sure to call keys = pygame.key.get_pressed() at the beginning of the for loop) Even though your previous line code only highlights the first text box(pygame.KEYDOWN: evaluates to true for any key), it still gives the impression that they can move with any key. Besides that, you did a great job.

  3. The minus sign in this code throws an error.
    pos_y = (self.scr_height / 2) – (t_h / 2) + ((index*2) + index * menu

    It is better like this:
    pos_y = (self.scr_height / 2) – (t_h / 2) + ((index*2) + index * menu

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s