A gamepad-friendly menu class

Sample class for a vertically-arranged menu of options. Options can be selected with mouse, keyboard arrows, or gamepad dpad, and confirmed with mouse click, enter, spacebar, or gamepad A buttton.

#!/usr/bin/env python3
from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
from direct.gui.OnscreenImage import OnscreenImage
from direct.gui.DirectGui import *

color_ready=(0.5,0.5,1.0,1)
color_hilite=(1,1,1,1)
color_locked=(0.5,0.5,0.5,1)

class Menu():
    def __init__(self,gamedata,name="",scale=(1,1,1),pos=(0,0,0),choices=[],textsize=0.125):
        #gamedata['font'] should be an object containing your desired font, the rest is ignored
        self.gamedata = gamedata 
        self.pos = pos
        self.textsize = textsize
        self.name=name
        self.choices=choices
        self.frames=[]
        self.nodesReady=[]
        self.nodesHilite=[]
        self.selected = 0
        zoff = textsize
        zstart = (textsize * (len(choices)-1))/2.25
        
        # replace this with your own background texture for the menu
        self.bg = OnscreenImage(image='data/menu/menu_bg.png', pos=pos, scale=scale)
        self.bg.setTransparency(TransparencyAttrib.MAlpha)
        self.bg.setAlphaScale(0.75)
        
        x, y, z = self.pos
        z += zstart
        i = 0
        for text in choices:   
            # have nodes for each state to swap as needed
            textnodeH = self.MakeTextNode(text,color_hilite)
            textnodeR = self.MakeTextNode(text,color_ready)
                
            frame = DirectFrame(geom=textnodeR, frameColor=(0,0,0,0))
            frame.setTransparency(TransparencyAttrib.MAlpha)
            frame.setPos(x,y,z)
            frame.setScale(self.textsize)
            
            # mouse controls
            frame.guiItem.setActive(True)
            frame.bind(DGG.WITHIN, self.Select, [i])
            frame.bind(DGG.B1PRESS, self.Return, [i])
                        
            z -= zoff
            i += 1
            self.nodesReady.append(textnodeR)
            self.nodesHilite.append(textnodeH)
            self.frames.append(frame)
            
        # default to top option
        self.Select(0, 0)
        
        # gamepad controls
        base.accept("gamepad-dpad_up", self.SelectUp)
        base.accept("gamepad-dpad_down", self.SelectDown)
        base.accept("gamepad-face_a", self.Confirm)
        
        # keyboard controls
        base.accept("arrow_up", self.SelectUp)
        base.accept("arrow_down", self.SelectDown)
        base.accept("space", self.Confirm)
        base.accept("enter", self.Confirm)
        
            
    def Select(self, i, junk):
        #unselect every option
        for j in range(0, len(self.choices)):
            l = self.nodesReady[j]
            f = self.frames[j]
            f.setGeom(l)
        #then select the desired option
        self.selected = i
        l = self.nodesHilite[i]
        f = self.frames[i]
        f.setGeom(l)
        
    def SelectDown(self):
        i = self.selected
        i += 1
        if i >= len(self.choices):
            i = 0
        self.Select(i, 0)
            
    def SelectUp(self):
        i = self.selected
        i -= 1
        if i < 0:
            i = len(self.choices) - 1
        self.Select(i, 0)
        
    def UnSelect(self, i, junk):
        self.selected = None
        l = self.nodesReady[i]
        f = self.frames[i]
        f.setGeom(l)
        
    def Confirm(self):
        i = self.selected
        self.Return(i,0)
        
    def Return(self, i, junk):
        # send an event indicating that we've chosen an option on this menu
        # NOTE: it is the responsibility of the MenuReturn event handler to hide this menu
        option = self.choices[i]
        messenger.send('MenuReturn',[self.name,option])

    def MakeTextNode(self, text, color):
        # helper function to make text nodes easily
        label = TextNode('label')
        label.setFont(self.gamedata['font'])
        label.setTextColor(color)
        label.clearShadow()
        label.setText(text)
        label.setAlign(TextNode.ACenter)

        node=render.attachNewNode(label)
        return node
        
    def hide(self):
        # hide the whole menu
        self.bg.hide()
        for button in self.frames:
            button.hide()
            
    def show(self):
        # show the menu after being hidden
        self.bg.show()
        for button in self.frames:
            button.show()
2 Likes