Today I want to create a menu in pygame. Not a huge or complicated one, more a simple and easy to use one, where you select the different options with either mouse or keyboard. Each Item should be capable of being displayed somewhere in the center and should only consist of a text explaining the property it handles, e.g. starting or quitting the game.
There are already a bunch of classes and structures which create such menus in pygame, they can easily be found, if you search for menu on the pygame tag search page or do a google search for pygame menu. But because we want to learn how to handle menus ourselves, we implement everything line by line.
To begin, lets identify what a we need:
- A text entry, for each menu entry
- Event Handling, to enable the menu to be clicked or to change the selected entry
- User Events, to enable an event like an clicked Item
So far we have covered text entries in the game pong and event handling for inputs in the first game piece. Hence we have some new parts we try to cover during this game piece, e.g. custom user events or enabling and disabling of text.
The Menu Representation
But lets try to start with a simple Menu, which basically is a text list, containing the different items of our menu.
# code for our menu ourMenu = ("Start Game", "Settings", "Quit") if pygame.font: fontSize = 36 fontSpace= 4 # loads the standard font with a size of 36 pixels font = pygame.font.Font(None, fontSize) # calculate the height and startpoint of the menu # leave a space between each menu entry menuHeight = (fontSize+fontSpace)*len(ourMenu) startY = background.get_height()/2 - menuHeight/2 listOfTextPositions=list() # create the single menu entries, each entry is center in x for menuEntry in ourMenu: text = font.render(menuEntry,1,(250, 250, 250)) textpos = text.get_rect(centerx=background.get_width()/2,centery=startY+fontSize+fontSpace) # lets store the text position listOfTextPositions.append(textpos) #update startY position startY=startY+fontSize+fontSpace # draw the text background.blit(text, textpos)
Embedding this code into a standard pygame loop e.g. the one we used in pong our breakout, we would get a screen looking like the image below:
The event Handling
The next thing to do would be to identify if an entry in our pygame menu was clicked. Therefore we need to handle the Mouse Click Event and check if the text area of one of our menu items was clicked. During such an event we should check if the position of the mouse click is within the drawing area of the text, if this is the case a new event, containing the given event should be fired.
# Handle Input Events for event in pygame.event.get(): # quit the game if escape is pressed if event.type == QUIT: return elif event.type == KEYDOWN and event.key == K_ESCAPE: return elif event.type == MOUSEBUTTONDOWN: # initiate with menu Item 0 menuItem = 0 # get x and y of the current event eventX = event.pos eventY = event.pos # for each text position for textPos in listOfTextPositions: #check if current event is in the text area if eventX > textPos.left and eventX < textPos.right \ and eventY > textPos.top and eventY
OOP and Indicator
To make the reusable and much more easy to read we try to convert everything we have into multiple python classes. This also allows us easy tracking of the current selected entry e.g. if we want to select our menu entries with the help of the up and down keys of the keyboard. To do so we need at least two classes. The first class would be only one menu item, therefore it would contain the text, and a rectangular shape, which contains the position of the entry. The second class would then be a menu class, which contains the different menu items and handles the mouse input events.
class MenuItem (pygame.font.Font): ''' The Menu Item should be derived from the pygame Font class ''' def __init__(self,text, position,fontSize=36, antialias = 1, color = (255, 255, 255), background=None): pygame.font.Font.__init__(self,None, fontSize) self.text = text if background == None: self.textSurface = self.render(self.text,antialias,(255,255,255)) else: self.textSurface = self.render(self.text,antialias,(255,255,255),background) self.position=self.textSurface.get_rect(centerx=position,centery=position) def get_pos(self): return self.position def get_text(self): return self.text def get_surface(self): return self.textSurface
The above MenuItem Class will handle a single Item in our Menu, Because it holds the same information as the pygame Font class it is derived from it. Besides an initialization, where the text is rendered it has some functions returning information to the Menu Class, which is used to build the menu.
class Menu: ''' The Menu should be initalized with a list of menu entries it then creates a menu accordingly and manages the different print Settings needed ''' MENUCLICKEDEVENT = USEREVENT +1 def __init__(self,menuEntries, menuCenter = None): ''' The constructer uses a list of string for the menu entries, which need to be created and a menu center if non is defined, the center of the screen is used ''' screen = pygame.display.get_surface() self.area = screen.get_rect() self.background = pygame.Surface(screen.get_size()) self.background = self.background.convert() self.background.fill((0, 0, 0)) self.active=False if pygame.font: fontSize = 36 fontSpace= 4 # loads the standard font with a size of 36 pixels # font = pygame.font.Font(None, fontSize) # calculate the height and startpoint of the menu # leave a space between each menu entry menuHeight = (fontSize+fontSpace)*len(menuEntries) startY = self.background.get_height()/2 - menuHeight/2 #listOfTextPositions=list() self.menuEntries = list() for menuEntry in menuEntries: centerX=self.background.get_width()/2 centerY = startY+fontSize+fontSpace newEnty = MenuItem(menuEntry,(centerX,centerY)) self.menuEntries.append(newEnty) self.background.blit(newEnty.get_surface(), newEnty.get_pos()) startY=startY+fontSize+fontSpace def drawMenu(self): self.active=True screen = pygame.display.get_surface() screen.blit(self.background, (0, 0)) def isActive(self): return self.active def activate(self,): self.active = True def deactivate(self): self.active = False def handleEvent(self,event): if event.type == MOUSEBUTTONDOWN and isActive(): # initiate with menu Item 0 curItem = 0 # get x and y of the current event eventX = event.pos eventY = event.pos # for each text position for menuItem in self.menuEntries: textPos = menuItem.get_pos() #check if current event is in the text area if eventX > textPos.left and eventX < textPos.right \ and eventY > textPos.top and eventY
The Menu class basically wraps a lot of the previous function, i.e. the menu creation or representation in the constructor, the event handling in the handleEvent method and the menu drawing in the draw Menu Section. The isActive and activate/deactive methods are used to determine if the menu is visible or not. Hence it can be seen as a small Factory, which creates a menu for your needs. Besides it holds an value for a custom event, the MENUCLICKEDEVENT, which sends the item text as well as the item position of the element, which has been clicked.
The object oriented example
To use the whole menu you can use this example code below to make the menu visible with the ESC key and quit the game via the menu or resume the game/start the game with the other button. Depending on which was selected the menu is visible or not. The event handling for each single item is done in the basic event handling loop, depicting two methods to handle the event, via the text or via the item position.The whole example with all classes and the main function can be found on github.
# code for our menu ourMenu = ("Start Game", "Quit") myMenu = Menu(ourMenu) myMenu.drawMenu() # pygame.display.flip() # main loop for event handling and drawing while 1: clock.tick(60) # Handle Input Events for event in pygame.event.get(): myMenu.handleEvent(event) # quit the game if escape is pressed if event.type == QUIT: return elif event.type == KEYDOWN and event.key == K_ESCAPE: myMenu.activate() elif event.type == Menu.MENUCLICKEDEVENT: if event.text=="Quit": return elif event.item == 0: isGameActive = True myMenu.deactivate() screen.blit(background, (0, 0)) if myMenu.isActive(): myMenu.drawMenu() else: background.fill((0, 0, 0)) pygame.display.flip()