Create a simple game menu with pygame pt. 2 – Recognising the mouse

Please note that I used python 2.7 for this tutorial. If you are using python3.x, this code may not work. I have adapted this tutorial to Python3 in this blog post.

Ok, folks it’s been awhile since I wrote my first part of Games Menu tutorial with pygame, but serendipity didn’t kiss me since then. I am trying to make up for this with the second part of my tutorial. We will cover the recognition of mouse movement. Again, like last time, the entire code is at the end of this blog entry.

The last time, we wrote the options we wanted to the screen and centred them. We used a list of list to do this. Today, we will start off by changing this list of list for a list of objects (MenuItems). This makes the entire thing easier to handle and clearer.

So just add this piece of code in between your imports and the GameMenu class.

class MenuItem(pygame.font.Font):
    def __init__(self, text, font=None, font_size=30,
                 font_color=(255, 255, 255), (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.pos_x = pos_x
        self.pos_y = pos_y
        self.position = pos_x, pos_y

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

and replace this section:

        for index, item in enumerate(items):
            label = self.font.render(item, 1, font_color)

            width = label.get_rect().width
            height = label.get_rect().height

            posx = (self.scr_width / 2) - (width / 2)
            # t_h: total height of text block
            t_h = len(items) * height
            posy = (self.scr_height / 2) - (t_h / 2) + (index * height)

            self.items.append([item, label, (width, height), (posx, posy)])

for the following

        for index, item in enumerate(items):
            menu_item = MenuItem(item)

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

and finally in the while mainloop: loop

            for name, label, (width, height), (posx, posy) in self.items:
                self.screen.blit(label, (posx, posy))

for

            for item in self.items:
                self.screen.blit(item.label, item.position)

Please have a look at what has been exchanged and you will find that it is essentially still all the same. All I did, was move all the info for the Font class into a separate class called MenuItem.
If you have questions about this change, please leave a comment with the sections, that need clarification.

The next point will be the recognition of whether the mouse pointer hovers over one of our menu items or not. To do this all we need to is figure out if the position of the mouse is within the boundaries. To this end, we will introduce a new method to our MenuItem class, called is_mouse_selection().

    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

This may look like much, but essentially we are just asking if mouse coordinates x and y are between the left and right border and the top and bottom border of MenuItem, respectively.

Furthermore, we obviously want something to happen, so that we know that something has been selected. This can be anything you want. I chose to change the color of the font. This is quite easy. We just need to write a short setter method and append it to the MenuItem class once more.

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

I hope you can see by now, why I chose to go away from a list of values to an actual object. It makes it all this little bit easier 😉 Now we only need to put this all together and see the great effect. I chose red as a color and for the fun of it on selection, I let the font be italic, too. This section is put into the while mainloop loop. I am sure you can figure out in which section (Otherwise, see below)

            for item in self.items:
                if item.is_mouse_selection(pygame.mouse.get_pos()):
                    item.set_font_color((255, 0, 0))
                    item.set_italic(True)
                self.screen.blit(item.label, item.position)

If you have tried this out, you will have sure noticed that once you hovered over an item it will stay red and italic. That’s not really what we want, right? We obviously have to return to neutral, too.

                else:
                    item.set_font_color((255, 255, 255))
                    item.set_italic(False)

And there you go! We now recognise the menu items by mouse 🙂 Simple, isn’t it.

——————————————–
The entire code:

#!/usr/bin/python

import pygame

pygame.init()

class MenuItem(pygame.font.Font):
    def __init__(self, text, font=None, font_size=30,
                 font_color=(255, 255, 255), (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 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)

    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

class GameMenu():
    def __init__(self, screen, items, bg_color=(0,0,0), font=None, font_size=30,
                    font_color=(255, 255, 255)):
        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.items = []
        for index, item in enumerate(items):
            menu_item = MenuItem(item)#, '/home/nebelhom/.fonts/SHOWG.TTF')

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

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

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

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

            for item in self.items:
                if item.is_mouse_selection(pygame.mouse.get_pos()):
                    item.set_font_color((255, 0, 0))
                    item.set_italic(True)
                else:
                    item.set_font_color((255, 255, 255))
                    item.set_italic(False)
                self.screen.blit(item.label, item.position)

            pygame.display.flip()

if __name__ == "__main__":
    # Creating the screen
    screen = pygame.display.set_mode((640, 480), 0, 32)

    menu_items = ('Start', 'Quit')

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

8 thoughts on “Create a simple game menu with pygame pt. 2 – Recognising the mouse

  1. Pingback: Create a simple game menu with pygame pt. 3 – Recognising the keyboard | (Auto-)Didactic Programming

  2. I’m having issue with the posx and posy values. In line 10 I had to change (pos_x, pos_y)=(0, 0) to pos_x=0 , pos_y=0 to make it work.

    Now after adding in the code for mouseover highlighting there is issue with Line 32 having invalid syntax. This is the second time now that Python doesn’t like the (x,y) format in the initial “def” line of a method.

    I could get around the first instance, but I’m not sure how to troubleshoot the mouseover highlighting process because the game returns an answer in the (x,y) format when we run pygame.mouse.get_pos(). I’m running Python 3.3.

    Any ideas?

    • Hi SWJ,

      your problem is version based. I used python2.7 while you use python3.3. Bear in mind that version 3.x is not backward compatible with 2.x (which is also the reason why many people are slow to adopt 3.x unfortunately). They tried to iron out inconsistencies with the new version.

      To solve your problem, I would suggest you read PEP 3113. Please see http://www.python.org/dev/peps/pep-3113/.

      Hope that helps. I will make a note in the articles that version 2.7 was used. Sorry about the confusion.

  3. I know this is a late comment, but I just thought I’d point out a simple bug and its solution, that you may or may not have addressed in the next part.

    The bug:
    When you position the mouse just at the end of the option ‘Start’ and above the option ‘Quit’, I mean at the very bottom pixel of ‘Start’, you end up having BOTH options highlighted.

    The fix:
    This line in GameMenu’s __init__:
    pos_y = (self.scr_height / 2) – (t_h / 2) + (index * menu_item.height)
    changed into:
    pos_y = (self.scr_height / 2) – (t_h / 2) + ((index*2) + index * menu_item.height)

    index*2 will equal zero on the first menu item so the position should still be the same as before,
    but on the next enumeration it will equal to two and add a couple of pixel’s space between ‘Start’
    and ‘Quit’.

    • @Ariel
      It is true that I have not looked at this blog for quite some time, but every comment is welcome.

      I will try your suggestion out myself first and see how it works for me. Once I am content, I will annotate the text with your contribution. Thanks for your comment. It’s always good to know that people are reading the stuff I put up 😉 Keep on enjoying to program 🙂

      • Happy to help, this is a very enlightening tutorial and anything I can do to thank you for writing it will make me happy.
        I should mention that this line still needs fixing on the other parts. 😛
        If only because I am a pedantic person.

  4. Hi,

    Using Python 3.5.0 and struggling to get this to run – keep getting the below error:

    if item.is_mouse_selection(pygame.mouse.get_pos()):
    TypeError: is_mouse_selection() missing 1 required positional argument: ‘posy’

    I have tried with both:
    pos_y = (self.scr_height / 2) – (t_h / 2) + (index * menu_item.height)
    pos_x = (self.scr_width / 2) – (menu_item.width / 2)
    pos_y = (self.scr_height / 2) – (t_h / 2) + ((index * 2) + index * menu_item.height)

    Is this due to the version of Python I am using? What am I doing wrong please?

  5. Hi Danny,

    I’m not really familiar with python 3. I never made the transition.

    Having said that a quick Google search on your error message gave me this link:

    http://stackoverflow.com/questions/20815887/missing-1-required-positional-argumentself

    According to this you did not create an instance of the class containing the method that you want to use. Maybe check if you did this.

    Just in case, you create an instance like so:

    instance = class()

    I hope that helps…

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