Main Menu Development

Now that we have the basic framework for the game in place, it’s time to take a look at out first ‘real’ state definition. This will be the main menu for the game. At this stage, the MainMenu class will be an incomplete placeholder, but it will serve to demonstrate some of the basic functionality required to implement a simple main screen. I’ll likely come back and revisit this after everything else is completed to add further refinements.

We’ve already seen the basic structure for the generic State class, although I have made a few minor edits along the course of development. Namely, all classes now have a reference back to the main Game instance set up during their initialisation. This saves having to pass the reference in on every call. Furthermore, there is some of the basic display setup functionality that has been placed in the State superclass so we don’t have to remember to do it for every single state we set up, hence the first thing we need to do in a state is call

super().__init__(game)

in the child state __init__ method, and similarly in the startup and cleanup methods we call

super().startup()
super().cleanup()

respectively.

So, let’s look at how we define a menu.

The bulk of the definition we store in an instance variable called ‘entries’. This is a list of tuples which each has the layout:

  • Text to display a simple string that will be displayed on the screen
  • Offset value an integer that determines the relative vertical position from the top of the menu that the entry will be displayed at (in pixels)
  • Colour a tuple of RGB values that will determine the colour of the entry
  • Lambda an inline expression that we can execute when the entry is selected

In the __init__ method, we also set up some boolean flags to trap user inputs (up, down and select) along with some housekeeping data for tracking the current menu item the user has selected.

The handle_events method is relatively simple, we simply run the standard event pump pattern and check for the pygame.QUIT event or the pygame.KEYDOWN event. If we detect the QUIT event, we simply tell the game to terminate, if we detect a KEYDOWN event, then we set the up, down and select flags within our object according to whether the cursor up, cursor down or space keys were depressed.

Eventually, I’ll plan to come back to this and allow for rebindable keys, but I’m keeping things simple for the time being. The code for the handle_events method is shown below.

    def handle_events(self):
        """Run the 'event pump' for this particular state.  Note that we
        don't call the superclass handler here as each state should
        individually process the state messages"""
        for event in pygame.event.get():
            # First, check to see if we're quitting the game
            if event.type == pygame.QUIT:
                self.game.is_running = False

            # Check to see if the user has pressed or released a key, if so,
            # set the appropriate flags
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_DOWN:
                    self.down = True
                if event.key == pygame.K_UP:
                    self.up = True
                if event.key == pygame.K_SPACE:
                    self.select = True
    # End handle_events

Notice also, that in the handle_events method, all I’m doing is handling the events. Any state update work takes place in the update method which I’ll discuss next.

The update method here performs the actual control of the menu state. We have two simple tests – the first, checks the up and down keys to move the selected menu item. Note that this check is throttled so that the check is only performed every 0.125 seconds; this stops the menu from updating too fast for the user to be able to adequately select the correct option. Once we’ve updated the menu we reset the key state for the up and down arrows so they don’t re-engage on the next update cycle if the user has released the keys.

The check for the select option has no need to be throttled, so this gets checked on every update cycle. The code here may look a little strange, but what we are doing is unpacking the tuple defined for the currently selected menu entry and discarding the three entries we aren’t interested in (hence we only retain the lambda function). This function object is assigned to a temporary label, then executed in the next line. You can see this highlighted in lines 17 and 18 below.

    def update(self, game_time, dt):
        """Update the simulation"""
        # We want to throttle the update of the menu items so that the cursor
        # doesn't move faster than the user can perceive it, so we'll limit
        # our updates to once every 0.25 seconds
        if self.last_update < game_time - 0.125:
            self.last_update = game_time

            if self.down is True and self.current_entry < self.max_menu:
                self.current_entry += 1
            if self.up is True and self.current_entry > 0:
                self.current_entry -= 1

            self.up = self.down = False

        if self.select is True:
            _, _, _, x = self.entries[self.current_entry]
            x()
            self.select = False
    # End update

Finally, the display method gets called to render all of the menu items to the screen. For now, it is using an external text drawing function to keep things simple. This function is defined in a Graphics module so it can be referenced by other states where needed and takes the following signature:

Renders a text string to the specified drawing surface

Parameters
----------
surface : pygame.Surface
    The surface to which we will render the text
text : str
    The text we want to render
font : str
    The filename of the font we want to use for display
size : int
    The height of the font (in pixels)
x, y : int
    The x and y coordinates of the centre of the string
colour : (int, int, int)
    The RGB tuple of the colour we want to display the font in

The surface we draw to is an off-screen canvas created when the state record was instantiated. This allows us to perform all our drawing functions off-screen, then simply ‘flip the page’ to display the final, finished image, thus preventing screen flicker.

We begin by clearing the canvas to a plain black background. This ensures that we don’t have any remnants of the previous frame affecting what we see. We then use our text drawing function to display the name of the game in large, yellow text, and beneath that, we render the menu by looping over the entries we have defined for the menu and drawing them accordingly. Note that for the selected entry, we modify its size based on a sine wave so that to the user the selected item appears to pulse, thus giving them essential feedback as to which item will trigger should they hit the select key.

The final thing we do in the display method is to use the ‘blit’ method of the game window to write the finished surface to the main display.

    def display(self):
        """Draw the current frame"""
        self.display_surface.fill(self.black)

        # Show the title text
        draw_text(self.display_surface,
                  "PONG",
                  pygame.font.get_default_font(), 200,
                  self.display_width // 2,
                  200, (255, 255, 161))

        # Render the menu entries
        for idx in range(len(self.entries)):
            text, y_offset, colour, _ = self.entries[idx]

            # Set the size so the current entry 'pulses'
            if idx == self.current_entry:
                size = int(25 + 5 * math.sin(
                    self.selected_pulse))
                self.selected_pulse += 2 * self.game.dt
            else:
                size = 20

            draw_text(self.display_surface,
                      text,
                      pygame.font.get_default_font(), size,
                      self.display_width // 2,
                      self.display_height // 2 + y_offset,
                      colour)

        # Display the drawing canvas on the game window...
        self.game.display_window.blit(self.display_surface, (0, 0))
    # End display

There’s much that could be done to improve the main menu. Even moving the menu itself out to a separate, reusable class to further simplify the implementation, but for now, this will suffice.

Whilst there’s currently little actual functionality in the menu since we don’t actually have any other states coded up at present for it to transition to, here’s a sample of how it currently looks:

Pong main menu animation

All the code is up on the GitHub project if you want to go take a look.

Research is especially important when we’re deciding on a new menu.

Ayesha Takia

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.