Simplifying UI Programming with the GUI stack.
You have a ton of menus in your game (graphical user interfaces, or GUIs for short). Sometimes when a new menu appears, you want it to appear over the previous menu, but keep the previous menu visible so that when the new menu goes away, the parent menu is still there. Sometimes you want a menu to replace another menu when it comes in. How do you handle all of this menu logic? How do you control which menus are active and how do you change menus? Here is a summary of all the problems that our menu system needs to address:
- It should be easy for one menu that is coming in to swap out another menu.
- It should be easy for an incoming menu to lay on top of the previous menu.
- Control should switch between active menus correctly, without having to add additional logic when we add additional menus.
- Menus should be able to transition in and transition out with a simple effect (like a slide).
In Cash Cow, I tried so many different ways of handling menus that it was a complete mess. The final code uses about 4 totally different concepts for how menus work, reflecting how I continually revised my concept of a menu and how the control should flow. All I could agree with myself on was that a menu should be some sort of class, but I didn’t know how complex that class should be or how it should work.
Some of my “menu” classes were very elaborate constructs that controlled everything on the screen. Sometimes the menus were really minimal, and were operated by a switch statement in a bigger menu class that handled the logic for a bunch of similar menus. Needless to say, Cash Cow’s menus took me incredibly long to program using all of these different systems. They were also very inflexible.
Ever wonder why there are two separate menus for confirming that you want to quit and confirming that you want to save in Cash Cow, when they easily could be combined into the same menu? Well, actually they couldn’t easily be the same menu. The system I used to code those simple confirmation menus was so restrictive that it would have taken a massive rework of those two menus just to combine them into one. Luckily, by the time we started Primate Panic, we had a better idea.
Our Solution: The GUI Stack
The basic idea is to encapsulate a GUI (graphical user interface) into a single class and maintain a stack of these GUIs in the current Scene (see the Scene System article). A GUI is defined by the following:
- A list of widgets (buttons, check boxes, etc).
- Position and state variables.
- Logic to keep the widgets coordinated, relying on the state and position variables to tell the widgets where they are supposed to be.
Each implementation of the abstract GUI class also defines listener functions to respond to clicks on the widgets. In Primate Panic, all widgets listen to mouse input and call a single function that looks like this
in the parent GUI (which each widget has a reference to). The parent compares the caller to its widget references to decide which one is making the function call, then executes logic based on the widget.
For example, let’s say that the in-game menu GUI has a reference to a quit button widget. This button is listening on its own for when it gets clicked. When this happens, it calls parentGUI.widgetClicked(this). The parent GUI checks its widget references, finds that the caller was the quit button, and now knows to transition to a confirmation menu.
These GUIs are kept in a stack that redraws ALL of the GUIs, but only updates the TOP GUI. This solves the problem of switching control between the GUIs. Only one GUI is active at a time, but all of the GUIs are drawing. You essentially get a control structure for free using this stack system, and it requires no additional code when new GUIs are added to function properly. To switch a GUI out for another one, just do a pop() followed by a push() on the GUI stack. To lay a GUI on top of another one, just do a push() now, and then a pop() when the new GUI is done.
In the context of the scene system, the SceneManager contains a single reference to a GUIStack, and calls update() and redraw() on this stack after the current scene updates and redraws. Because the Scene Manager is a singleton, the GUIs have the power to manipulate the GUI stack or change Scenes. This is really important since it is usually GUI input that controls the change of menus and Scenes.
How does the GUI stack solve the original problems?
- It should be easy for one menu that is coming in to swap out another menu. Once the first menu is done, pop() it off the GUI stack. As the next one comes in, push() it on the GUI stack.
- It should be easy for an incoming menu to lay on top of the previous menu. Just push() the new GUI onto the stack as it comes in, and pop() it when it’s done.
- Control should switch between active menus correctly, without having to add additional logic when we add additional menus. Only call update() on the top GUI and this problem is solved. The stack structure itself switches control between GUIs.
- Menus should be able to transition in and transition out with a simple effect (like a slide). The GUI class contains all the necessary data to coordinate the movement/transitions of all of the widgets, and can execute this logic itself.
Sometimes I ask myself, why not a GUI list (as in, the same as a GUI stack but all of the members update rather than just the top one). That way, you can have menus that come in and out as they please, with the ability to have several active GUIs at once.
So far, I haven’t ever needed this capability since all of our menus are designed to be run by themselves. If there were several GUIs active at once, things could get confusing for the user as well as the programmer. If say, you had three GUIs all running at the same time, and one of them executes a command to transition out and change the Scene when it’s done, you are going to have to somehow tell the other GUIs that this happened. Otherwise, all of the other GUIs would still be active while the first one was transitioning out, and things could get messed up pretty fast. Part of the great thing about the GUI stack is that adding more GUIs doesn’t require additional code to change control between them. Doing a GUI list creates that problem again, the one that we just solved with a stack.