Create a simple game menu with pygame pt. 3 – Recognising the keyboard

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 blogpost.

Hello and welcome back for the third part of my little mini tutorial series where I first teach myself how to write a game menu with pygame and then live to tell the tale in little articles such as this one 😛 . Last time we looked at the mouse and how to recognise when the cursor hovers over one of our menu items. We then implemented an effect that showed us what the cursor is currently recognising.

If you are just interested in the full code after this tutorial, then follow this link.

This time round, we shall do such a navigation also for the keyboard. Again, like last time, I took the time to compartmentalise some code into methods to make it all more readable, such as the introduction of a MenuItem method called is_mouse_selection() or GameMenu‘s set_mouse_selection() method. In both cases, the code did not change, all we did was tidy up a little.

Here’s the code and try to find the sections in the code that you have so far that correspond to these snippets.

In MenuItem:

    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

In GameMenu:

    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)

But let’s delve into this section properly. One of the first things, I thought about was trying to solve the riddle of what to do when the mouse cursor hovers over one menu item, while the user tries to navigate with the keyboard. Obviously, he doesn’t want to constantly return to the mouse selection. Thus, we need to recognise that a keyboard key was used and when that is the case we make sure the mouse stops working and disappears from sight.
So first, we make sure that the keyboard keys are recognised by adding the following line to the event loop.

                if event.type == pygame.KEYDOWN:

Let’s first make the mouse cursor disappear on usage of the keyboard. For this purpose we shall introduce a new variable to the GameMenu init, self.mouse_is_visible and place it in the if clause above, and then create a method that regularly checks if the mouse should be visible or not.

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

Then call this method inside the mainloop after the pygame.event loop but before self.screen.fill(self.bg_color). Obviously the reason for that is that all graphical operations should come last, but we still want to call this method after modifications made to the variable self.mouse_is_visible.

To prevent the mouse highlighting to interfere with the keyboard, we now only need to do one more thing. We need to use our previously defined variable in order to let the code only run self.set_mouse_selection() if the cursor is visible. Therefore we modify our pre-existing code like so:

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

This is all nice and well, the moment we touch a keyboard, our cursor will disappear. It is, however, not much use, if we can’t get it back. I figured, it is best to get it back, when we move the mouse. To recognise this, pygame provides a nice little function called pygame.mouse.get_rel() that gives the relative change of the mouse coordinates since the last check. In this case, we’re not interested in that, only that it “has” moved, not by how much. Therefore, a nice little check just before the call to self.set_mouse_visibility() in the mainloop, will do that trick.

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

Ok, now, we press the key on the keyboard and the mouse disappears and once we move the mouse again, it re-appears. Great! Now all that is left for us to do, is get the navigation with the keyboard’s up and down arrow buttons working.
For this purpose, we need to play around with the index of GameMenu‘s self.items index. We’ll now create a new attribute in the GameMenu.__init__, called self.cur_item and set it to None. You will see in a second why None and not 0, for example.

Re-visit now our pygame event loop, in particular the section that recognises the keyboard input and add a call to a new method to it.

                if event.type == pygame.KEYDOWN:
                    self.mouse_is_visible = False
                    self.set_keyboard_selection(event.key)

We shall now use this method to define our keyboard navigation. It is a couple of lines of code, but it is pretty easy. Check out the code, which is a method of the GameMenu class, obviously.

    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)

Bear with me on this one and I shall go through each section with you.

        if self.cur_item is None:
            self.cur_item = 0

This one is the easy part. Remember, I said, I would explain why I chose None? If self.cur_item is None, I know that we switched from mouse to keyboard. As a consequence, I will start at index 0 and everything is peachy. No confusing mark-up. Once I use the mouse again, I will put self.cur_item back to None, just to have reliable behaviour. It’s not rocket science or particularly clever even, it just helps me keep track.

        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

This is the largest section, but it really is very easy. Let me write it out in words what happens here:

  • If item not top item in the list and UP is pressed, move selection up one item
  • If item is top item in the list and UP is pressed, select the last item on the list.
  • If item not bottom item in the list and DOWN is pressed, move selection down one item
  • If item is bottom item in the list and DOWN is pressed, select the first item on the list.

Simple, huh?

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

You have seen something similar before with the mouse selection and again it is to highlight your selection accordingly.

Finally, I have kept the first lines of code for last. That is, because I actually wrote them last ;).

        for item in self.items:
            # Return all to neutral
            item.set_italic(False)
            item.set_font_color(WHITE)

As the comment mentions, here, we are just returning all items to neutral, so that we can mark up the new selection. Nothing much to it, really.

And this is it. We have now a Game Menu in which we can navigate without much problem using either mouse or keyboard. Cool, huh? Next time, we will connect them up to functions that “may” trigger the next step in the game or such as in our case simple print functions. We have managed to do the hard part, the last part is easy in comparison.

I hope you enjoyed it. Till next time! 🙂

————————————————
Here’s the full code after this tutorial

#!/usr/bin/python

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
        self.is_selected = 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)

    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=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.items = []
        for index, item in enumerate(items):
            menu_item = MenuItem(item, font, font_size, font_color)#, '/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 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_item_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)

    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)

            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_item_selection(event.key)

            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:
                    mpos = pygame.mouse.get_pos()
                    self.set_mouse_selection(item, mpos)
                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', 'Setting', 'Quit')

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

One thought on “Create a simple game menu with pygame pt. 3 – Recognising the keyboard

  1. Pingback: Create a simple game menu with pygame pt. 4 – Connecting it to functions | (Auto-)Didactic Programming

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