Anatomy of A Menu
Following not-so-hot on the heels of my not-so-hot request for UI design feedback (thanks to none of you who responded!), I wanted to post what I’ve gotten so far and talk a little about the standards that I’m going to try to apply to the UI work going forward.
As stated in the previous post, there’s not so much a “right way” to handle the organization of code and objects, but there are most certainly “wrong ways”. The quickest way to identify when you’ve turned down the wrong alley is to try and get your UI elements to work together, to access each other’s information, and to take actions that the UI is meant to enact. That’s why the hierarchy of UI elements is important.
This fine diagram to my left is what I came up with for my main menu hierarchy. I need to preface this by talking a bit about the data controller though. The data controller is an empty game object — which means you can’t see it in the game — that has one component: Database. Database is a C# script that handles tracking the state data (stuff that changes when you play, like your character and your progress) and main data (stuff that is always the same, like item stats). The empty game object that holds it is set to not be recycled when we switch scenes, so it will persist throughout the game. In addition, the code is set as a singleton, which means it is self-policing so that it ensures that there’s only ever one copy of the data. The data controller isn’t represented in this screencap, but know that it sits above and outside of the Main Menu Controller.
The Main Menu Controller is another empty game object. It is an umbrella beneath which we will find all of the UI elements that make up the main menu, the loading and saving screen, options, and confirmation dialogs like overwrite confirm and quit confirm. The benefit of this is that the empty game object can hold scripts that we can refer to from anywhere in the hierarchy, but more importantly, it allows us to make a prefab of the menu. A prefab is a “master copy” of an object that we can deploy anywhere in the project. When we make a change to the master object, it cascades to all deployed instances. If we make a change to deployed instance, we can either let that change stand just for that instance or have the instance update the master object. I’ve already created a prefab of the main menu hierarchy, which is why the text is in blue.
So what about the organization itself? There are five main levels: MainMenu, GameList, Options, QuitConfirm, and ConfirmOverwrite (yes, OCD folks, I will change the Quit and Overwrite to match naming patterns).
Main Menu is what you’d expect: the main menu. This is what you’d get when you start the game, or when you hit the ESC key while playing the game. It allows you to start a new game, load an existing game, set options, or quit.
This is where the difficulty comes in. Both NEW and LOAD share the same concept: choose a slot from three options. If it’s filled with details, we need to know what option we chose from the main menu: are we starting a new game? If so, we need to make sure the user knows she is going to overwrite an existing file. Are we loading an existing game? Then we need to get the save game data into Database from disk.
And here’s our load/new UI. The first slot is filled in with an existing game. We simply call it “Save game 1” and display the last save date. The other two slots are empty. If the user is starting a new game, she can choose the last two slots and just start the game. If she chooses “Save game 1” then we need to warn her.
If she chooses YES, then we’ll set up a new game and overwrite the slot that’s currently occupied.
What handles all of this? There are two scripts: Main Menu and Game List.
Main menu is attached to the MainMenuController game object because this handles the opening and closing of panels other than the main menu itself. One thing that UI systems need is a way to handle which windows are open and which are not. One way to do this in Unity is through GameObject.SetActive([boolean]). When boolean is false, the window is inactive. Unfortunately, when a game object is inactive, it is unaddressable, meaning we can’t tell it to show itself because it’s just not listening. To get around this, we put a script at a higher level than the object, create a property, and drag that object into the property. Now, when we want to show the windows, we do so from above the window itself…at the controller level. Currently, the main menu itself isn’t represented, which will get fixed later.
What we do not see are methods called event handlers. These are code bits which the buttons use. When someone clicks NEW GAME, the event handler “hears” the activity and takes an action. We point the button’s click event to that method in order to join the two. Here, we have three event handlers: One to display the game list panel, one to display the options panel, and one to display the quit confirmation panel.
The real tricky part was the game list panel.
In this case, we have the script attached to the UI element itself. Why not attach this to the Main Menu Controller? Well, for one reason: we need to take an action when the UI is activated — when the button on the main menu is clicked. If we put this on the Main Menu Controller then as soon as that object is created (at scene load) then those actions would fire. Since the game list panel isn’t active at that point, running that code is pointless.
The activation action runs the code within Database that loads the list of saved games. We use this to update the text of each button on the panel. If there’s a save game in slot one, then we need to change the text of slot one from EMPTY SLOT to the name and last saved date. We need to have references to these buttons inside the script in order to do this, so we create properties to hold those references. We could technically use hierarchy and search methods to find SaveGame1,2,3 buttons, but searching like that is expensive and doesn’t make the assignment visible through the inspector during design time. We also have a reference for Confirm Overwrite Panel because its visibility is controlled based on the state of the button the user presses, and the action she intends to enact (new or load).
If the user is starting a new game, then Database handles the initialization by loading the master data, creating a new Game State Object, merging data where necessary, and then making all of that available through the Database instance. If the user is loading a game, we load the master data and read the appropriate state file from disk. In the case of a new game, we will then send the user to another scene where she can make some limited customization of her character (since the character has no visible presence, it’s mostly just custom name, gender, and paper doll selection). Then, she’d have to decide whether or not to tackle the tutorial. If she’s loading a game, then we would send her to the scene where she left off when the game was saved.
So that’s it! 1300 words to describe a menu system (be thankful I didn’t include all the code!). Needless to say even something as “simple” as a menu can be more complicated than you know, so remember that the next time you’re ripping into a game about animations or physics — there’s a lot of moving parts, and what you see is only the end result of mountains of effort whose structures are really only as good as the organization of its parts.