Lesson 15
Your Menu, Sir
In which we build menus with DirectGUI, and provide a way to restart after game-over
The final elements to add to our game are its menus. Specifically, we will be adding two: a “game-over” menu, that allows the player to either restart the level or quit, and a main menu, which is shown before play begins.
To this end, we’ll be using Panda3D’s built-in GUI toolkit, DirectGUI. It’s a bit of a clunky thing, but for many purposes it works. (It is possible to use third-party GUI toolkits with Panda3D, but we won’t touch on that here.)
Panda’s GUI elements are, like pretty much everything else, nodes in the node-heirarchy. As with any node, GUI elements can be “parented” under (that is, made the children of) other nodes, including other GUI nodes. Indeed, that’s how we’ll construct our menus from their parts: GUI-controls “contained” within others (such as a button in a menu) will be children of their “containers”. By default, GUI elements are parented under a special root-node called “aspect2d”.
As to laying out and customising GUI controls, this is generally done via a set of keyword-parameters passed into their constructors. (Although many of these can be changed at a later point, too.) There are a number of common parameters (like “frameSize”), and each GUI-element class generally has its own, specific parameters.
Almost all of the following code will be done in “Game.py”.
To start with, let’s import the various DirectGUI elements, ready to use:
That done, let’s build our “game-over” menu.
This will be “dialogue box”, a special sort of GUI-object that can obscure things behind it, and prevent mouse-clicks or keyboard-input from getting through.
To this we’ll add a heading that reads “game-over”, a label that gives the final score for the run, and buttons that allow the player to restart or quit:
With the basics of the game-over screen done, let’s implement the logic to show it when the player loses, and hide it when they start (or more to the point, restart) the game:
With all of that done, the game-over screen should now be functional! Try running the game and letting the player-character be killed. You should now see the game-over screen pop up, and have the option to either restart or quit.
By default, however, DirectGUI isn’t pretty. (Or at least, so I think.) So, let’s customise its appearance a bit.
Before we continue, a note for the sake of clarity: much of what we’re about to do involves adding new keyword-parameters to the GUI-objects that we created above, rather than making new ones. Indeed, we’ll only be adding a few new objects; hopefully it’ll be clear which things are new, and which we’ve already made!
First, we’ll replace the default font.
There are a few ways to handle fonts in Panda3D, but one simple way is to just load it via the “loader” object, as with models and sounds:
We can then use it when constructing our various DirectGUI elements, by including it as an appropriate keyword:
That’s better, but the backdrops of our GUI-items are still a flat grey.
To start with, the game-over screen itself. In this case, we’ll simply apply a texture to its geometry, via the “frameTexture” keyword:
Next, we’ll do something similar with our DirectButtons.
This is a little more complicated. While we could just apply a single texture and call it done, we’ll do something slightly more detailed: we’ll provide a custom image for each of the states that our buttons can have. There are four such states, and thus four images: normal, highlighted/rollover, pressed, and disabled. The one catch is that Panda expects them in a specific order:
- Normal
- Pressed
- Rollover
- Disabled.
By default, Panda makes buttons appear to be “raised”; this corresponds to setting the “relief” keyword-parameter to “DGG.RAISED”. Since our button-images incorporate that effect in their images, we want to disable this. To do so, we simply pass in the “relief” keyword-parameter ourselves, and set it to “DGG.FLAT”. (“DGG” being the “DirectGUI Globals” object.)
Next, we’ll make sure that our buttons have a size appopriate to our images. The images are 512x128 pixels each, so we want our buttons to similarly have a 4:1 width:height ratio. This we’ll apply by setting the “frameSize” keyword-parameter.
We’ll also adjust the position of the text on the buttons a bit, to better fit our images, via the “text_pos” parameter.
As our button-images have transparent corners, we’ll apply transparency to our button-objects.
And while we’re at it, we’ll give our buttons a nice sound-effect to be played when they’re pressed, via the “clickSound” keyword-parameter.
Thus we have the following:
One more thing: if we try our game now, we’ll see all of our image-backdrops–but the labels will still be surrounded by their standard grey backdrops! To remove these, we’ll simply set their “relief” keyword-parameters to “None”, resulting in the following code:
Thus the final result looks like this:
Next, we’ll build a simple main menu. It will have:
- A title (using three DirectLabels, for easy variation of colour, backing, and size)
- A “start game” button
- And a “quit” button.
The “start game” button will simply call the “startGame” method, and the “quit” button will call the “quit” method.
A more serious project might additionally have an options menu, where the player could set the sound- and music- volume, adjust the controls, and so on, and perhaps other menus too. But for tutorial purposes, this will do.
Building our main menu will use pretty much the same elements as the game-over screen, but instead of a DirectDialog, we’ll use a simple DirectFrame–one of the basic DirectGUI classes, from which most others derive.
We will be using one trick: our title menu will have a black backdrop, and we’ll make that backdrop cover the entire screen.
You may recall that DirectGUI elements are by default parented to “aspect2d”; “aspect2d” is itself a child of another node, called “render2d”. In short, “render2d” is set up such that its coordinates, from window-edge to window-edge, always run from -1 to 1 on both axes. That is, on the x-axis, the left-hand side of the window has an x-coordinate of -1, and the right-hand side of the window has an x-coordinate of 1, and similarly for the y-axis–regardless of the size of the window.
The “aspect2d” node is then scaled such that the range from -1 to 1 on both axes makes a square, regardless of the size of the screen.
This means that things parented to “aspect2d” have the correct aspect-ratio, while things parented to “render2d” may be stretched. But on the other hand, things parented to “render2d” always cover the same amount of the window, regardless of the window’s size and aspect ratio.
So, if we make a GUI object that’s parented to “render2d”, and that runs from -1 to 1 on both axes, it should cover the entire window, even if we resize that window.
And since our backdrop is nothing but flat blackness, stretching won’t be an issue.
Our title-menu is complete, and shows up if we run the game. The problem is that our level also starts in the background, even though we can’t see it. (Aside from the health-icons, which should still appear at the top-left.) This is, of course, because we’re calling “startGame” in our “__init__” method.
However, since we now have a “start game” button to call the “startGame” method, we can simply remove the call to “startGame” from our “__init__” method.
Conversely, when we do start the game, we don’t want the title menu (or its backdrop) to remain on-screen, covering the game itself. So, we’ll hide it, as we did with the game-over screen:
And finally, one more bit of polish: our score-text still uses the default font. So, in “GameObject.py”, we’ll add the “font” keyword to the constructor for our “scoreUI” object:
And there we have it! A full, working, playable game, complete with menus!
The next question then is that of how to distribute our game to others. I suppose that we could put the source-code and assets into an archive, and distribute that along with an installer for the Panda3D SDK–but that sounds really cumbersome, and for some users it might feel like too much trouble.
So let’s look into building a distributable…