Part 15: Simple Menu
From what we have done so far we now have the broad framework for a real time strategy game in place. We can control Units and Buildings, create new Units and Buildings, and destroy enemy Units and Buildings. We can also collect Resources to fund the creation of those new Units and Buildings. Sure, there are plenty of gaps that need filling. Pathfinding, collisions, an AI to control computer players, an AI to control individual Units (and defensive Buildings), the list goes on. At a later date we will begin to fill in some of those details, but for now we will leave that part of the game there.
However, there are more things that we need to put into place before we have a complete package. At the moment we are stuck with a single static map. Any time a Player wants to begin a game they must start at the same place. This might work well for arcade games, but it is not what we expect when we come to play a real time strategy game. Typically these have a much larger scope than the simple arcade game does.
The next few parts of this tutorial are therefore going to be about adding the things we need to flesh out the UI and overall user experience. This will involve adding in a menu system, the ability to save games and then to reload them, user accounts so that we can personalize the experience a little bit, and once that is in placewe will implement the structure needed to create a basic single-player campaign.
To get things started we will implement a basic pause menu that can be triggered from within our game. Initially this will not do much, but it will lay the platform for more complicated menu interactions later on. We want this to be triggered by input from the Player, so we need to adjust the Update() method in UserInput.cs. Add
if(Input.GetKeyDown(KeyCode.Escape)) OpenPauseMenu();
immediately before the call to MoveCamera(). Here we are specifying that when the escape key is hit we want to open the pause menu. That method needs to be defined before we can continue.
private void OpenPauseMenu() { Time.timeScale = 0.0f; GetComponentInChildren< PauseMenu >().enabled = true; GetComponent< UserInput >().enabled = false; Screen.showCursor = true; ResourceManager.MenuOpen = true; }
The first line tells Unity to basically stop time. If we do not do this then actions initiated before the menu was opened carry on - so Units keep moving, etc. In the second line we are enabling the script that will handle the pause menu itself. Then we disable the UserInput script, since we want the Player to be interacting with the menu, rather than the game. The fourth line re-enables the system cursor, so that we do not have to implement a custom cursor for use within menus (although you could do so if you wished). Finally we are letting our ResourceManager know that a menu is open at the moment. This gives us an easy way for code to check whether a menu is open or not. To be able to set this value we need to add it to ResourceManager.cs.
public static bool MenuOpen { get; set; }
Note: this is the C# way of setting up a public getter and setter on a private variable.
The next important step is to create the script that will handle the pause menu. We will create a new folder to hold all menu related things (since we will be building up quite a bit over the next few posts). Inside the main assets folder create a new folder called Menu. Inside this folder create a new folder call Scripts and then inside that folder create a new C# script called PauseMenu.cs. Set the initial code to be as follows.
using UnityEngine; using RTS; public class PauseMenu : MonoBehaviour { public GUISkin mySkin; public Texture2D header; private Player player; private string[] buttons = {"Resume", "Exit Game"}; void Start () { player = transform.root.GetComponent< Player >(); } void Update () { if(Input.GetKeyDown(KeyCode.Escape)) Resume(); } }
We are defining a couple of public variables that we can set within Unity - a GUISkin that we can use to adjust the look and feel of the menu quickly and easily, and a header image that we want to display at the top of the menu. We then have a reference to the Player this menu belongs to (initialised in the usual way in the Start() method), and a list of the buttons we want to display (represented by the strings we want to display on each button). In the Update() method we are also defining that we want to use the escape key as a shortcut to resume gameplay. Let us now define that Resume() method.
private void Resume() { Time.timeScale = 1.0f; GetComponent< PauseMenu >().enabled = false; if(player) player.GetComponent< UserInput >().enabled = true; Screen.showCursor = false; ResourceManager.MenuOpen = false; }
This method is basically the inverse of the method that we used to open the pause menu in the first place. We restore the time scale back to 1, disable the pause menu, find and re-enable the UserInput script for the correct Player, hide the system cursor, and tell the ResourceManager that the menu has now been closed.
The actual drawing of the menu will be handled by the OnGUI() method provided by Unity, which we need to implement now.
void OnGUI() { GUI.skin = mySkin; float groupLeft = Screen.width / 2 - ResourceManager.MenuWidth / 2; float groupTop = Screen.height / 2 - ResourceManager.PauseMenuHeight / 2; GUI.BeginGroup(new Rect(groupLeft, groupTop, ResourceManager.MenuWidth, ResourceManager.PauseMenuHeight)); //background box GUI.Box(new Rect(0, 0, ResourceManager.MenuWidth, ResourceManager.PauseMenuHeight), ""); //header image GUI.DrawTexture(new Rect(ResourceManager.Padding, ResourceManager.Padding, ResourceManager.HeaderWidth, ResourceManager.HeaderHeight), header); //menu buttons 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])) { switch(buttons[i]) { case "Resume": Resume(); break; case "Exit Game": ExitGame(); break; default: break; } } } GUI.EndGroup(); }
There are several important sections here where work is being done. First off we want to set the GUISkin being used to be the one attached to the pause menu. Next we want to define the part of the screen that we will be drawing in, which will be a rectangle in the centre of the screen defined by GUI.BeginGroup(). Then we draw a background rectangle for the menu followed by adding the header at the top of this box. Finally we want to draw a vertical list of buttons below the header. Note: Unity includes the click handler for a button in with the definition of where to draw the button. We can determine which button was clicked by evaluating the text of that button. Once we know this we can call the appropriate method to handle the click for that button.
There is an extra method that this code requires us to define now
private void ExitGame() { Application.Quit(); }
which simply tells our game to quit. Note: This will not do anything if you launch your game from within Unity. It will only work when running your actual game.
You will notice that there are lots of references to values in ResourceManager being used in the OnGUI() method. This is so that we can separate the details of layout from the logic of the creation of the menu. In order for things to work we need to define all of these inside ResourceManager.cs.
private static float buttonHeight = 40; private static float headerHeight = 32, headerWidth = 256; private static float textHeight = 25, padding = 10; public static float PauseMenuHeight { get { return headerHeight + 2 * buttonHeight + 4 * padding; } } public static float MenuWidth { get { return headerWidth + 2 * padding; } } public static float ButtonHeight { get { return buttonHeight; } } public static float ButtonWidth { get { return (MenuWidth - 3 * padding) / 2; } } public static float HeaderHeight { get { return headerHeight; } } public static float HeaderWidth { get { return headerWidth; } } public static float TextHeight { get { return textHeight; } } public static float Padding { get { return padding; } }
Here we have defined some private variables that form the basis of our layout. We use these, in conjunction with some C# getters, to define the actual values that are to be returned.
There are just a couple of things to do before we can run our project and display the menu. Inside the Menu folder create two new folders called Images and Skins. Inside the skins folder create a new GUISkin called MenuSkin. Inside the Images folder we want to add a header image to display at the top of the menu. This could be a logo, something that provides a bit more theming for your game. I have gone for a simple placeholder like the image below.
We now have all of the components for opening our pause menu, but they need to be attached somewhere logical before we can use of them. What we want to do is to add them to the HUD object belonging to each of our Players. This makes sense, since they are part of the display being presented to the Player. However, we do not want to have to attach one to each of the Players in the game. The way to handle this is to add them to the Player Prefab that we created. In order to do this first add a new instance of that Prefab into your game. We will use this instance to edit and update the Prefab (since the other Players already in the game already have a lot of other things added that we do not want to be part of a new Player). Now attach the PauseMenu script to the HUD object of your new Player. Add the header image and the MenuSkin to the appropriate fields of the PauseMenu you just attached. Once you have done this, make sure to disable the PauseMenu script, since the default state for our game is 'playing' rather than 'paused'. You can do this by unchecking the checkbox next to the title of the script. To save these changes back to the Prefab drag this new Player object onto the Player Prefab in the Player folder. You should be prompted whether you want to do this. When this prompt shows up you need to choose the 'save over' option, since we want to replace the existing Prefab with the updated Prefab. These changes should now be present in all of the Players already in our game.
The primary customization I have done on MenuSkin is to change the background image that is used on the box element. I set the background image for a normal box to the image below.
You should now be able to run your game and open the newly created pause menu by using the escape key. Clicking on resume, or hitting escape, should return you to your game. Once you have built your game clicking on exit should close your game.
And that brings things nicely to a close. Our game now contains a simple pause menu. As I mentioned earlier, we will build on this over the next couple of parts. All of the code for this part can be found on github under the commit for Part 15. Next time we will build on this simple menu and introduce a main menu to our game.