Part 16: Main Menu
Now that we have a basic menu in place it is time to extend that a bit. As you are aware, a game does not typically just start when you open it. The standard starting point is a main menu, from which you actually launch the game. The goal this time is to create a main menu, start a game from this menu, and add the ability to return to that menu from our game.
Generic Menu
The basic layout for our MainMenu is going to be similar to that of the PauseMenu that we created last week. Our interactions with it are going to be different, but the overall structure should at least be familiar. In fact, the structure is so similar that we should actually make a base Menu class which does most of the work for us. Inside the Scripts folder located in the Menu folder create a new C# script and call it Menu.cs. Set the code to be the following.
using UnityEngine; using System.Collections; using RTS; public class Menu : MonoBehaviour { public GUISkin mySkin; public Texture2D header; protected string[] buttons; protected virtual void Start () { SetButtons(); } protected virtual void OnGUI() { DrawMenu(); } protected virtual void DrawMenu() { //default implementation for a menu consisting of a vertical list of buttons GUI.skin = mySkin; float menuHeight = GetMenuHeight(); float groupLeft = Screen.width / 2 - ResourceManager.MenuWidth / 2; float groupTop = Screen.height / 2 - menuHeight / 2; GUI.BeginGroup(new Rect(groupLeft, groupTop, ResourceManager.MenuWidth, menuHeight)); //background box GUI.Box(new Rect(0, 0, ResourceManager.MenuWidth, menuHeight), ""); //header image GUI.DrawTexture(new Rect(ResourceManager.Padding, ResourceManager.Padding, ResourceManager.HeaderWidth, ResourceManager.HeaderHeight), header); //menu buttons if(buttons != null) { float leftPos = ResourceManager.MenuWidth / 2 - ResourceManager.ButtonWidth / 2; float topPos = 2 * ResourceManager.Padding + header.height; for(int i = 0; i < buttons.Length; i++) { if(i > 0) topPos += ResourceManager.ButtonHeight + ResourceManager.Padding; if(GUI.Button(new Rect(leftPos, topPos, ResourceManager.ButtonWidth, ResourceManager.ButtonHeight), buttons[i])) { HandleButton(buttons[i]); } } } GUI.EndGroup(); } protected virtual void SetButtons() { //a child class needs to set this for buttons to appear } protected virtual void HandleButton(string text) { //a child class needs to set this to handle button clicks } protected virtual float GetMenuHeight() { float buttonHeight = 0; if(buttons != null) buttonHeight = buttons.Length * ResourceManager.ButtonHeight; float paddingHeight = 2 * ResourceManager.Padding; if(buttons != null) paddingHeight += buttons.Length * ResourceManager.Padding; return ResourceManager.HeaderHeight + buttonHeight + paddingHeight; } protected void ExitGame() { Application.Quit(); } }
The core of this code is what we added to PauseMenu.cs last time. A couple of things have been adjusted to allow us to define different variations easily. By default we will not specify a value for our array of buttons. A specific menu can define this array through the method SetButtons(). The check
buttons != null
is therefore needed in a number of places to make sure that we only use the buttons array if it is specified. This means that by default a menu will actually just have the header image with no buttons underneath it. By using GetMenuHeight() we can define the height of the menu based on the number of buttons that are specified, all nicely padded and looking tidy. The final method that allows variation is HandleButton(). The basic menu has no knowledge of which buttons will be present and therefore it has no idea what to do with a button when it is clicked. A specific menu can use this method to define how to handle the buttons that it has defined. We will also add the method that handles exiting the game to Menu.cs since we want to be able to call this from any menu that wants it.
Before we carry on we should tidy up our PauseMenu from last time, making sure that it extends our newly created Menu. We should also make sure that it still behaves as expected. Update PauseMenu.cs so that it now has the following code in it.
using UnityEngine; using RTS; public class PauseMenu : Menu { private Player player; protected override void Start () { base.Start(); player = transform.root.GetComponent< Player >(); } void Update () { if(Input.GetKeyDown(KeyCode.Escape)) Resume(); } protected override void SetButtons () { buttons = new string[] {"Resume", "Exit Game"}; } protected override void HandleButton (string text) { switch(text) { case "Resume": Resume(); break; case "Exit Game": ExitGame(); break; default: break; } } private void Resume() { Time.timeScale = 1.0f; GetComponent< PauseMenu >().enabled = false; if(player) player.GetComponent< UserInput >().enabled = true; Screen.showCursor = false; ResourceManager.MenuOpen = false; } }
As you can see, this is a much simpler class now. The methods SetButtons() and HandleButton() define what buttons are present and how to handle the click for each of them. Note that in Start() we need to make sure that we call base.Start() - this makes sure that any initialization specified in the base class is still performed. If you now run your project now you should see that the pause menu still behaves the same as it did before.
Main Menu
Now that we have a base implementation of a menu in place it is time to create our main menu. The first thing we actually want to do is to create a new Scene for our game. We will use this to manage our main menu (which will get more complex over time). Create a new scene (File -> New Scene) and then save it inside the Menu folder (as something sensible like MainMenu). Next, create a new C# script inside the Scripts folder located in the Menu folder and call this MainMenu.cs. Add this script to the camera object so that we can use it shortly. Note: we could create an empty object to attach scripts to, but I do not think that is necessary for this scene.
Now it is time to specify the code for our main menu. Set MainMenu.cs to contain the following code.
using UnityEngine; using System.Collections; using RTS; public class MainMenu : Menu { protected override void SetButtons () { buttons = new string[] {"New Game", "Quit Game"}; } protected override void HandleButton (string text) { switch(text) { case "New Game": NewGame(); break; case "Quit Game": ExitGame(); break; default: break; } } private void NewGame() { ResourceManager.MenuOpen = false; Application.LoadLevel("Map"); //makes sure that the loaded level runs at normal speed Time.timeScale = 1.0f; } }
Once again we specify the buttons we want to have and how to handle the click on each of them. The only interesting code in this class is found in the NewGame() method. Here we are wanting to start a new game using the map that we have been working on for the majority of this tutorial. Unity provides us the ability to change scenes with the use of Application.LoadLevel(). However, for this to work we need to know a couple of things. First up, we need to know the name of the Scene that we want to change to. This is the parameter that gets passed in to LoadLevel(). It is all well and good knowing the name of the scene that we want to load, but the more important thing is to let Unity know all of the scenes that we have in our game. This is done through the build settings for our project (File -> Build Settings). At the top of the dialog that shows up is a list of scenes that are to be included in our build. Any of the scenes in this list can be loaded by using Application.LoadLevel(). To add a scene to this list find the appropriate scene in your Assets directory (wherever it was saved to) and drag it onto the list. The first scene in the list is the one that is loaded when Unity starts up, so we want to make sure that our MainMenu scene is listed first. Then we need to add our Map scene so that we can load it from the MainMenu. Your Build Settings should look like the screenshot below.
To make sure that the main menu is styled correctly and shows the header image we need to attach MenuSkin to the MySkin field of the MainMenu that we attached to the camera earlier. We also need to add our header image to the header field.
With this done you should be able to run your game (starting in the MainMenu scene) and see a menu show up with the options "Start Game" and "Quit Game". Clicking "Start Game" should load up the familiar map from the rest of the tutorial.
Updated Pause Menu
With this in place we actually want to change the behaviour of our PauseMenu slightly. At the moment when we click "Exit Game" from the pause menu we are exiting our game. What we actually want to do is to return to the main menu, rather than closing the game. To do this we need to change the case for "Exit Game" in HandleButton() inside PauseMenu.cs to call
ReturnToMainMenu()
rather than
ExitGame()
like it does at the moment. We then need to set the definition of ReturnToMainMenu() as follows.
private void ReturnToMainMenu() { Application.LoadLevel("MainMenu"); Screen.showCursor = true; }
This loads the MainMenu scene again and makes sure that the normal cursor is shown for the menu. Ideally, this method should prompt the Player for confirmation that they wish to exit their game, along with the option to save it or not. We will add this in once we have added the ability to save games in a couple of parts time. Feel free to play round with a confirm prompt if you like though.
And as simple as that we now have our game starting from a menu, the ability to start a new game using the map we have been working on, and the ability to return to the main menu from within our game. The entire project can be found on github with the code for this part under the commit for Part 16. In our next part we will look at fleshing out the concept of a Player a bit more and allowing a user to select which Player it is that they will be playing as.