Part 17: Player Selection
Now that we have the basics of our menu in place it is time to turn our attention to our Players. At the moment there is a default Player being used, with their name being set from inside Unity. They have a bunch of details on the things that they control / own, but there is no sense of personalization, of real ownership, for the user. The goal this time is to expand the functionality of our game so that the user can choose which Player they are going to be playing as. This is going to involve enabling saving and loading of Player details to make sure that a user can choose to be the same Player as they were in a previous session. In the future we will be able to expand on this even further and do things like saving stats (games won, units killed, etc). For this time we will be happy with the ability to set a name, choose a custom avatar, and select an existing Player.
Player Selection Menu
We will start off by launching the game from a selection menu, where the user is able to choose which Player they want to be. If no Players exist then they will be able to create a new Player. In the Scripts folder found inside the Menu folder create a new C# script called SelectPlayerMenu.cs. This file should start with the code needed to display a text box where the user can enter their name and a button to select that Player.
using UnityEngine; using RTS; public class SelectPlayerMenu : MonoBehaviour { public GUISkin mySkin; private string playerName = "NewPlayer"; void OnGUI() { GUI.skin = mySkin; float menuHeight = GetMenuHeight(); float groupLeft = Screen.width / 2 - ResourceManager.MenuWidth / 2; float groupTop = Screen.height / 2 - menuHeight / 2; Rect groupRect = new Rect(groupLeft, groupTop, ResourceManager.MenuWidth, menuHeight); GUI.BeginGroup(groupRect); //background box GUI.Box(new Rect(0, 0, ResourceManager.MenuWidth, menuHeight), ""); //menu buttons float leftPos = ResourceManager.MenuWidth / 2 - ResourceManager.ButtonWidth / 2; float topPos = menuHeight - ResourceManager.Padding - ResourceManager.ButtonHeight; if(GUI.Button(new Rect(leftPos, topPos, ResourceManager.ButtonWidth, ResourceManager.ButtonHeight), "Select")) { SelectPlayer(); } //text area for player to type new name float textTop = menuHeight - 2 * ResourceManager.Padding - ResourceManager.ButtonHeight - ResourceManager.TextHeight; float textWidth = ResourceManager.MenuWidth - 2 * ResourceManager.Padding; playerName = GUI.TextField(new Rect(ResourceManager.Padding, textTop, textWidth, ResourceManager.TextHeight), playerName, 14); GUI.EndGroup(); } private float GetMenuHeight() { return ResourceManager.ButtonHeight + ResourceManager.TextHeight + 3 * ResourceManager.Padding; } private void SelectPlayer() { } }
This is drawing a menu in the middle of the screen. There is a text area at the top with the default text "NewPlayer". Under this is the button which will trigger the actual selection of the Player, although this does nothing just yet. We are also specifying a skin to be used for customizing the look / feel of the menu. An important thing to note is that we are limiting the length of the name a Player can set to be 14 characters long. Feel free to adjust this to suit your own needs.
With the basic code framework in place it is time to see this menu in action. Make sure that you have the MainMenu scene open. Add the script that we just created to the camera object. Then make sure that MainMenu.cs is disabled, so that we are not attempting to draw two menus at once. Add MenuSkin to the MySkin field of SelectPlayerMenu.cs. If you run your game now you should see a menu looking like the image below.
Things are looking good so far, but this is not interactive at all. In fact, at the moment it is impossible to progress beyond this point. We can change this by providing an implementation for the SelectPlayer() method.
private void SelectPlayer() { PlayerManager.SelectPlayer(playerName); GetComponent< SelectPlayerMenu >().enabled = false; MainMenu main = GetComponent< MainMenu >(); if(main) main.enabled = true; }
In order for this to work we want to create another manager whose purpose is to look after the Players present in the system. This will include saving and loading details, which we will get to shortly. To do this create a new C# script in the RTS folder called PlayerManager.cs with the following code in it.
using UnityEngine; using System.Collections.Generic; namespace RTS { public static class PlayerManager { private struct PlayerDetails { private string name; public PlayerDetails(string name) { this.name = name; } public string Name { get { return name; } } } private static List< PlayerDetails > players = new List< PlayerDetails >(); private static PlayerDetails currentPlayer; public static void SelectPlayer(string name) { //check player doesnt already exist bool playerExists = false; foreach(PlayerDetails player in players) { if(player.Name == name) { currentPlayer = player; playerExists = true; } } if(!playerExists) { PlayerDetails newPlayer = new PlayerDetails(name); players.Add(newPlayer); currentPlayer = newPlayer; } } public static string GetPlayerName() { return currentPlayer.Name == "" ? "Unknown" : currentPlayer.Name; } } }
We are using the struct to define the details we care about storing for a Player. We are also maintaining a list of the available Players for easy access too. There are a some important things to note here. First off, we are using the name of the Player as a unique identifier - that is, we cannot have more than one Player with the same name. This will be fine to maintain for the single player mode we are working on for this tutorial. However, if you want to support multiplayer at some point this may need to be modified. Next, the SelectPlayer() method either selects a Player that exists or creates a new Player to be tracked. At the moment this seems a little redundant, but it will be useful very shortly. Finally, the GetPlayerMethod() contains some notation that may seem strange to some of you. This is an in-line boolean check and is supported in a number of major languages now. The segment before the '?' character is a boolean check, in this case whether the name of the currentPlayer object is "", the default value it will have. The piece between '?' and ':' is the value we want to return if the boolean check returns true, in this case "Unknown" since no name has been set. The final piece after ':' is the value to return if the boolean check was false, and in this case we want to return the name of the Player since that is known. By the end of this part we will have provided a lot more functionality to our PlayerManager than this, but this is a good staring point.
If you run your game now you should be able to enter a Player name, click Select, and see the start menu from last time show up. From here you should be able to start your game.
Displaying Player Details
It now feels like we are at least trying to personalize things a bit, but there is no indication to a Player when they are in the game that anything has actually changed. We can improve on this by making sure that the name that they chose to play under is displayed in the HUD. To do this we need to adjust the code in OnGUI() found in HUD.cs. We want to add
DrawPlayerDetails();
to this method as the first line that is run inside the block if(player.human). Of course, for this to do anything we need to provide the definition for this method as well.
private void DrawPlayerDetails() { GUI.skin = playerDetailsSkin; GUI.BeginGroup(new Rect(0, 0, Screen.width, Screen.height)); float height = ResourceManager.TextHeight; float leftPos = ResourceManager.Padding; float topPos = Screen.height - height - ResourceManager.Padding; float minWidth = 0, maxWidth = 0; string playerName = PlayerManager.GetPlayerName(); playerDetailsSkin.GetStyle("label").CalcMinMaxWidth(new GUIContent(playerName), out minWidth, out maxWidth); GUI.Label(new Rect(leftPos, topPos, maxWidth, height), playerName); GUI.EndGroup(); }
The first thing that we need to do is to provide another GUISkin that can be customized for the display of Player details. Add
public GUISkin playerDetailsSkin;
to the top of HUD.cs. Next, create a new GUISkin called PlayerDetailsSkin inside the Skins folder found in the HUD folder. We want to adjust the settings for Label so that things look a little bit neater. Firstly, we want to set a background image for a Normal Label. I have used the same box we are using for the the background for the Menu.
To get this to sit properly I have set the Border to (6, 6, 6, 6), the Margin to (0, 0, 0, 0), and the padding to be (10, 10, 0, 0). I have also set the font to be Arial with Font Size = 18, Alignment = Middle Left, and Word Wrap turned on. The settings should look something like the screenshot below.
To get things to display properly you need to attach this newly created skin to the playerDetailsSkin field of the HUD element belonging to your Player prefab. This can be done by clicking the small arrow on the side of the prefab (when you look at it in the folder view). This arrow is shown in the screenshot below.
Clicking on this arrow expands the view of the prefab to show each of the elements that it contains (see screenshot below). At the moment we care about the HUD element. If you click on it you will see the properties for the HUD show up in the Inspector. It is here that you want to add the skin we created just moments ago. By adding it here we guarantee that it will be added to any Player prefabs present in our scene.
The rest of DrawPlayerDetails() simply handles drawing the name of the Player neatly in the lower left corner of the screen. The crazy looking CalcMinMaxWidth() method is simply making sure that the width of the Label is neatly surrounding the text provided. This method is mentioned in the Unity documentation.
If you run your game now you should see the name you entered in the first Menu being displayed correctly once you have started a new game. Feel free to play around with how this looks in your own time.
The other display that would be good to add is a simple welcome message to the Player to the top of all of our 'primary' menus. This can be achieved with some very simple tweaks to Menu.cs. We need to increase the height of the menu, which can be done by adding
float messageHeight = ResourceManager.TextHeight + ResourceManager.Padding;
to GetMenuHeight() and then making sure that value is added to the return statement for the method.
return ResourceManager.HeaderHeight + buttonHeight + paddingHeight + messageHeight;
We then need to update DrawMenu() to make sure that the message is drawn in the right place. Add
//welcome message float leftPos = ResourceManager.Padding; float topPos = 2 * ResourceManager.Padding + header.height; GUI.Label(new Rect(leftPos, topPos, ResourceManager.MenuWidth - 2 * ResourceManager.Padding, ResourceManager.TextHeight), "Welcome " + PlayerManager.GetPlayerName());
between the header image and the buttons sections of the method (noted by comments) and then update the definition of leftPos and topPos for the buttons to be the following.
leftPos = ResourceManager.MenuWidth / 2 - ResourceManager.ButtonWidth / 2; topPos += ResourceManager.TextHeight + ResourceManager.Padding;
If you run your game now you should see a simple welcome message displayed immediately below the header image.
Player Avatar
One more element of personalization I would like to add before we move on is a custom avatar for the Player to select. To start off we need the ability to display a list of potential avatars and allow the Player to scroll through these until they find one that they like. We want this list to be displayed above the text field. (Side note: we can make the text field look a bit better by selecting MenuSkin and setting Padding-Left = 8 and Alignment = MiddleLeft) Add the following code to OnGUI() in SelectPlayerMenu.cs immediately before GUI.EndGroup().
if(avatarIndex >= 0) { float avatarLeft = ResourceManager.MenuWidth / 2 - avatars[avatarIndex].width / 2; float avatarTop = textTop - ResourceManager.Padding - avatars[avatarIndex].height; float avatarWidth = avatars[avatarIndex].width; float avatarHeight = avatars[avatarIndex].height; GUI.DrawTexture(new Rect(avatarLeft, avatarTop, avatarWidth, avatarHeight), avatars[avatarIndex]); float buttonTop = textTop - ResourceManager.Padding - ResourceManager.ButtonHeight; float buttonLeft = ResourceManager.Padding; if(GUI.Button(new Rect(buttonLeft, buttonTop, ResourceManager.ButtonHeight, ResourceManager.ButtonHeight), "<")) { avatarIndex -= 1; if(avatarIndex < 0) avatarIndex = avatars.Length - 1; } buttonLeft = ResourceManager.MenuWidth - ResourceManager.Padding - ResourceManager.ButtonHeight; if(GUI.Button(new Rect(buttonLeft, buttonTop, ResourceManager.ButtonHeight, ResourceManager.ButtonHeight), ">")) { avatarIndex = (avatarIndex+1) % avatars.Length; } }
The idea is to draw a texture in the middle of the menu area that is the currently selected avatar. To either side of this we want to display a button to move forward / back through the list of avatars. When these buttons are clicked the value of avatarIndex is adjusted appropriately. Before this will work we need to add
public Texture2D[] avatars;
to the top of SelectPlayerMenu.cs to allow us to assign a collection of images to the Menu. We also need to add
private int avatarIndex = -1;
as well, so that we can reference the selected index. We should then add
if(avatars.Length > 0) avatarIndex = 0;
to the Start() method to make sure that we show a default avatar if there are any images to choose from. We also need to update GetMenuHeight() to make sure that we have space to draw an image in.
private float GetMenuHeight() { float avatarHeight = 0; if(avatars.Length > 0) avatarHeight = avatars[0].height + 2 * ResourceManager.Padding; return avatarHeight + ResourceManager.ButtonHeight + ResourceManager.TextHeight + 3 * ResourceManager.Padding; }
Of course, for this to work we need to add some some images to choose from. Inside the Player folder create a new folder called Avatars and add some images to it. The images I am using are the following:
Each of these images needs to be dragged onto the Avatars field of the SelectPlayerMenu script that we attached to our camera. Running your game now should show a Menu looking something like the screenshot below.To make sure that we actually use the avatar the Player selects we need to modify SelectPlayer() in PlayerManager.cs. The updated method should look like the following.
public static void SelectPlayer(string name, int avatar) { //check player doesnt already exist bool playerExists = false; foreach(PlayerDetails player in players) { if(player.Name == name) { currentPlayer = player; playerExists = true; } } if(!playerExists) { PlayerDetails newPlayer = new PlayerDetails(name ,avatar); players.Add(newPlayer); currentPlayer = newPlayer; } }
This requires that we also update the struct PlayerDetails in PlayerManager.cs so that we can store and access the value for the avatar.
private struct PlayerDetails { private string name; private int avatar; public PlayerDetails(string name, int avatar) { this.name = name; this.avatar = avatar; } public string Name { get { return name; } } public int Avatar { get { return avatar; } } }
Of course, for these changes to work we need to make sure that when PlayerManager.SelectPlayer() is called in SelectPlayerMenu.cs that we pass a value for avatar as well as name. This change needs to be made in SelectPlayer().
PlayerManager.SelectPlayer(playerName, avatarIndex);
It would be nice to see evidence that this selection has been made, so let's update the HUD to show the avatar the Player selected alongside their name. There is one small problem though, we need a way for our HUD to access the actual image needed to display the avatar, since all we are storing at the moment is an index value. We should provide access to this through the PlayerManager, since it is really being treated as a detail belonging to a Player. For this to work well we should add the entire list of potential avatars to the PlayerManager and then use the index stored for a given Player to retrieve the appropriate image. Start by adding
private static Texture2D[] avatars;
to the top of PlayerManager.cs. Then add the following method to allow us to populate this array.
public static void SetAvatarTextures(Texture2D[] avatarTextures) { avatars = avatarTextures; }
We can call this method from Start() in SelectPlayerMenu.cs to populate the list with the avatars that the Player is being allowed to select from.
PlayerManager.SetAvatarTextures(avatars);
The final change we want to make to PlayerManager.cs is to add the following method to allow us to retrieve the correct avatar whenever we need it to be displayed somewhere.
public static Texture2D GetPlayerAvatar() { if(currentPlayer.Avatar >= 0 && currentPlayer.Avatar < avatars.Length) return avatars[currentPlayer.Avatar]; return null; }
The check in this method is to make sure that we do not accidentally attempt to access an invalid index in the array. There is the possibility this could happen with old save files - (once we have implemented saving and loading details that is).
Now that our PlayerManager has been updated, we can turn our attention to HUD.cs. All we need to do is add a small snippet of code into the middle of DrawPlayerDetails() to handle the drawing of an avatar (if there is one). The updated method should look like this.
private void DrawPlayerDetails() { GUI.skin = playerDetailsSkin; GUI.BeginGroup(new Rect(0, 0, Screen.width, Screen.height)); float height = ResourceManager.TextHeight; float leftPos = ResourceManager.Padding; float topPos = Screen.height - height - ResourceManager.Padding; Texture2D avatar = PlayerManager.GetPlayerAvatar(); if(avatar) { //we want the texture to be drawn square at all times GUI.DrawTexture(new Rect(leftPos, topPos, height, height), avatar); leftPos += height + ResourceManager.Padding; } float minWidth = 0, maxWidth = 0; string playerName = PlayerManager.GetPlayerName(); playerDetailsSkin.GetStyle("label").CalcMinMaxWidth(new GUIContent(playerName), out minWidth, out maxWidth); GUI.Label(new Rect(leftPos, topPos, maxWidth, height), playerName); GUI.EndGroup(); }
If you run your game now you should be able to see the avatar you select from the first menu being displayed to the left of the name that you entered (once you are actually in the game, that is).
Saving Player Details
It is all very well being able to select which person you want to play as, but what we really want is for this choice to persist. This means that next time we come to play the game we want to be able to choose the person we were last time, without having to set things up every time. At the moment this is a fairly trivial amount of information. But as things progress we would like to be able to store things like custom settings, gameplay statistics, progress through a campaign, and potentially many other things. For most of these details it is simply not possible to enter them every time someone comes to play our game.
Of course, to enable this we need a simple way of saving data and then reloading it. In order to minimize the chances of random people hacking around with game stats, etc. it would be good to use a custom binary file format. But this is really not nice to use in the early stages of development. For this project we will store data as JSON instead, a plain text format that is widely used to encode data structures. The library which we are going to use to handle the JSON parsing for us is JSON.net which can be found at james.newtonking.com/json. This is the easiest library I have found which can easily be used with .NET, and Unity is ultimately sitting on top of an open-source implementation of this. The version I am using is slightly older now, but it still works just fine. We need to add into the Assets directory a new folder called Json.NET. Inside this folder we need to put the .pdb, .dll, and .xml files for Json.NET. Unfortunately I do not recall exactly which ones I grabbed, so you may be best to grab the ones I used from the github repository for this project. (When I first added this I was not thinking of writing a tutorial, so I did not pay that much attention to which version of the files I grabbed) Update: It appears that you need to use the Net20 version of the files, especially with Unity 4.3 (which does not seem to like the version of the files that I have up on github).
Once we have the library files in our project we can use them in any file we want by adding
using Newtonsoft.Json;
to the top of that file. We should do this in PlayerManager.cs now, since that is the first place where we want to enable saving of data. The first thing we want to do is to create a new directory for each Player that we create. We will make use of this folder later on to store any saved games that they create. Add
Directory.CreateDirectory("SavedGames" + Path.DirectorySeparatorChar + name);
into SelectPlayer() in PlayerManager.cs. We only want to add this when a new Player is created, so it needs to sit inside the check
if(!playerExists){ ... }
somewhere (the end of that block works fine). This will create a new directory inside whatever directory the executable for the game is located. When testing from inside Unity this is the main directory for your Unity project. We are using Path.DirectorySeparatorChar to provide cross-platform subdirectories. Before this will run we do need to add
using System.IO;
to the top of PlayerManager.cs to make sure that we can access Directory and Path. Now add
Save();
to the end of SelectPlayer() in PlayerManager.cs. We want to call this every time a Player is selected, since they may have changed their avatar. We want the following definition for this method.
public static void Save() { JsonSerializer serializer = new JsonSerializer(); serializer.NullValueHandling = NullValueHandling.Ignore; using(StreamWriter sw = new StreamWriter("SavedGames" + Path.DirectorySeparatorChar + "Players.json")) { using(JsonWriter writer = new JsonTextWriter(sw)) { writer.WriteStartObject(); writer.WritePropertyName("Players"); writer.WriteStartArray(); foreach(PlayerDetails player in players) SavePlayer(writer,player); writer.WriteEndArray(); writer.WriteEndObject(); } } }
Note that this method is public, since at a later date it is entirely possible that we will want to ask our PlayerManager to save Player details at arbitrary points in time (e.g. when closing an options menu). The point of this method is to save all details for the current list of Players in the file Players.json which will sit in the SavedGames folder that has been created. We set up some things for Json.NET, initialize a JsonWriter, and then write out an array for our Players. We then print out details for each of the Players that we know about using the method SaveDetails(), which we need to create now.
private static void SavePlayer(JsonWriter writer, PlayerDetails player) { writer.WriteStartObject(); writer.WritePropertyName("Name"); writer.WriteValue(player.Name); writer.WritePropertyName("Avatar"); writer.WriteValue(player.Avatar); writer.WriteEndObject(); }
This generates a Json object for the Player and writes data to the appropriate property fields, in this case the name and avatarIndex for the Player. If you run your game now and create a new Player called Testing with the Astronaut avatar (assuming you are using the same avatars as I am), then the contents of Players.json should be {"Players":[{"Name":"Testing","Avatar":1}]}. Of course, this will not show up the next time you run your game, since we are not loading Player details yet. But it does show that we are able to successfully save the details for the Player that we set up.
Loading Player Details
Now that we are able to save details about Players we should enable the loading of these details. Add the method Load() to PlayerManager.cs.
public static void Load() { players.Clear(); string filename = "SavedGames" + Path.DirectorySeparatorChar + "Players.json"; if(File.Exists(filename)) { //read contents of file string input; using(StreamReader sr = new StreamReader(filename)) { input = sr.ReadToEnd(); } if(input!=null) { //parse contents of file using(JsonTextReader reader = new JsonTextReader(new StringReader(input))) { while(reader.Read()) { if(reader.Value!=null) { if(reader.TokenType == JsonToken.PropertyName) { if((string)reader.Value == "Players") LoadPlayers(reader); } } } } } } }
We make sure that we first clear any Players that we already have details for. We then want to read the file Players.json (but only if it exists) and parse the contents of that file using a JsonReader. If we find the object "Players" then we want to load the details from this using the method LoadPlayers(), which we need to create now.
private static void LoadPlayers(JsonTextReader reader) { while(reader.Read()) { if(reader.TokenType==JsonToken.StartObject) LoadPlayer(reader); else if(reader.TokenType==JsonToken.EndArray) return; } }
This will continue parsing the input until it hits the end of the array of Players. Each new object it encounters is another Player and the parsing of that object is handed off to the method LoadPlayer(). This method also needs to be created before we can continue.
private static void LoadPlayer(JsonTextReader reader) { string currValue = "", name = ""; int avatar = 0; while(reader.Read()) { if(reader.Value!=null) { if(reader.TokenType == JsonToken.PropertyName) { currValue = (string)reader.Value; } else { switch(currValue) { case "Name": name = (string)reader.Value; break; case "Avatar": avatar = (int)(System.Int64)reader.Value; break; default: break; } } } else { if(reader.TokenType==JsonToken.EndObject) { players.Add(new PlayerDetails(name,avatar)); return; } } } }
Here we are storing the value for each property found. When we hit the end of the object we store the details for the Player and return. There are a couple of advantages to breaking the code up in this way rather than having it all in one big method as nested if statements (even though it ends up running like that anyway). The first is that our code is a lot more readable. The nested if statements would be horrible to look at and make it really hard to track what is going on, too. The second is that this is much easier to extend. When we end up adding more details to track for a Player we know that all we need to do is extend SavePlayer() to store those new properties and LoadPlayer() to load those new properties. The rest of our code will remain intact and continue to work like it already does.
In order to get our loading code to run we need to call it from somewhere. The sensible place to do this is in SelectPlayerMenu.cs, since that is the starting point for our game. Add
PlayerManager.Load();
to the beginning of the Start() method to handle this. If you run your game now you should get no errors being thrown when you start up.
Selecting an Existing Player
In theory our game is loading up the list of saved Players now. But there is currently no indication that this is the case. The last thing to get working in this part is to enable the selection of a Player that was saved at some previous time. We will do this by adding a list of Players to the top of our SelectPlayerMenu and allowing a Player to click on one of these to select it. When they click on one of these existing Players the appropriate avatar image should be displayed and the text box should contain the name of the selected Player. Clicking on Select will then select that Player, rather than creating a new one.
We will leave the handling of the selection list to a static class called SelectionList.cs. I won't go into the detail of how this works, since it is not really important for what we are doing. This is a standalone class that I created to handle a scrolling list with values that can be selected. It supports the detection of both single-click and double-click. It draws the list inside a specified area and will apply a GUISkin when drawing that list if a GUISkin is provided. This makes the selection list independent of the system that it is added to while still being customizable through a GUISkin. Create a new folder in your Assets folder called SelectionList. Inside this folder create a new C# script called SelectionList.cs and add the following code to it.
using UnityEngine; public static class SelectionList { private static string[] myEntries = {}; private static int gridIndex = 0; private static float scrollValue = 0.0f; private static float leftPos, topPos, areaWidth, areaHeight; private static float rowHeight = 25, sliderWidth = 10, sliderPadding = 5; public static void LoadEntries(string[] entries) { myEntries = entries; } public static string GetCurrentEntry() { if(gridIndex >= 0 && gridIndex < myEntries.Length) return myEntries[gridIndex]; else return ""; } public static void SetCurrentEntry(string entry) { gridIndex = -1; for(int i = 0; i < myEntries.Length; i++) { if(myEntries[i] == entry) gridIndex = i; } } public static bool Contains(string entry) { bool contains = false; for(int i = 0; i < myEntries.Length; i++) { if(myEntries[i] == entry) contains = true; } return contains; } public static bool MouseDoubleClick() { Event e = Event.current; Vector3 mousePos = Input.mousePosition; mousePos.y = Screen.height - mousePos.y; float selHeight = myEntries.Length * rowHeight; float selWidth = areaWidth; if(selHeight > areaHeight) selWidth -= (sliderWidth + 2 * sliderPadding); bool mouseInSelection = new Rect(leftPos, topPos, selWidth, areaHeight).Contains(mousePos); if(e != null && e.isMouse && e.type == EventType.MouseDown && e.clickCount == 2 && mouseInSelection) return true; else return false; } public static void Draw(float left, float top, float width, float height) { leftPos = left; topPos = top; areaWidth = width; areaHeight = height; DrawBox(); } public static void Draw(float left, float top, float width, float height, GUISkin skin) { leftPos = left; topPos = top; areaWidth = width; areaHeight = height; GUI.skin = skin; DrawBox(); } public static void Draw(Rect drawArea) { leftPos = drawArea.x; topPos = drawArea.y; areaWidth = drawArea.width; areaHeight = drawArea.height; DrawBox(); } public static void Draw(Rect drawArea, GUISkin skin) { leftPos = drawArea.x; topPos = drawArea.y; areaWidth = drawArea.width; areaHeight = drawArea.height; GUI.skin = skin; DrawBox(); } private static void DrawBox() { float selWidth = areaWidth; float selHeight = myEntries.Length * rowHeight; GUI.BeginGroup(new Rect(leftPos,topPos,areaWidth,areaHeight)); //there are more levels than will fit on screen at once so scrollbar will be shown if(selHeight > areaHeight) selWidth -= (sliderWidth + 2 * sliderPadding); GUI.Box(new Rect(0, 0, selWidth, areaHeight), ""); if(selHeight > areaHeight) { float sliderLeft = selWidth + sliderPadding; float sliderMax = selHeight - areaHeight; scrollValue = GUI.VerticalSlider(new Rect(sliderLeft, 0, sliderWidth, areaHeight), scrollValue, 0.0f, sliderMax); scrollValue -= Input.GetAxis("Mouse ScrollWheel") * rowHeight; if(scrollValue < 0.0f) scrollValue = 0.0f; if(scrollValue > sliderMax) scrollValue = sliderMax; } GUI.BeginGroup(new Rect(0,1,areaWidth,areaHeight-2)); float selGridTop = 0.0f - scrollValue; gridIndex = GUI.SelectionGrid(new Rect(0, selGridTop, selWidth, selHeight), gridIndex, myEntries, 1); GUI.EndGroup(); GUI.EndGroup(); } }
Inside this folder also create a new GUISkin called SelectionSkin. I have set a Normal Box to use the following image.
I have then set a Button to have no image for Normal with text color light gray, to use this image
for OnNormal with text color light gray, this image
for Hover with text color black, and this image
for Active with text color light gray. I then set the Button Border to (2, 2, 1, 1), the Button Margin to (0, 0, 0, 0), and the Button Padding to (10, 10, 2, 2).
The first thing we want to be able to do when using the SelectionList is to be able to draw it. This draw calls needs to be done at the end of OnGUI() in SelectPlayerMenu.cs. We will start by adding the following code there.
//selection list, needs to be called outside of the group for the menu float selectionLeft = groupRect.x + ResourceManager.Padding; float selectionTop = groupRect.y + ResourceManager.Padding; float selectionWidth = groupRect.width - 2 * ResourceManager.Padding; float selectionHeight = groupRect.height - GetMenuItemsHeight() - ResourceManager.Padding; SelectionList.Draw(selectionLeft, selectionTop, selectionWidth, selectionHeight, selectionSkin);
In order to be able to pass a skin to the SelectionList we need to add
public GUISkin selectionSkin;
to the top of SelectPlayerMenu.cs and then drag the GUISkin that we created just before onto this field of our menu inside Unity (make sure to drag it onto the script that is attached to the camera). To handle the height of the menu better we will adjust GetMenuHeight() by changing it's contents to
private float GetMenuHeight() { return 250 + GetMenuItemsHeight(); }
and then adding the method GetMenuItemsHeight().
private float GetMenuItemsHeight() { float avatarHeight = 0; if(avatars.Length > 0) avatarHeight = avatars[0].height + 2 * ResourceManager.Padding; return avatarHeight + ResourceManager.ButtonHeight + ResourceManager.TextHeight + 3 * ResourceManager.Padding; }
The value in GetMenuHeight() is how high we want the SelectionList to be, but writing things this way does allow us to guarantee that the SelectionList will fill the available height in the Menu. A little bit ugly, I know, but it gets the job done for now. If you run your game you should see an empty gray box is now displayed above the avatar selection area.
What we want to do now is to begin to interact with our SelectionList. The first thing that we want to do is to populate the list. To achieve this add
SelectionList.LoadEntries(PlayerManager.GetPlayerNames());
to the end of Start() in SelectPlayerMenu.cs and then add the method GetPlayerNames() to PlayerManager.cs.
public static string[] GetPlayerNames() { string[] playerNames = new string[players.Count]; for(int i = 0; i < playerNames.Length; i++) playerNames[i] = players[i].Name; return playerNames; }
Now when you run the game you should see the last name that you entered for a Player (the one that is currently saved in Players.json) is in the list.
To enable selection of an entry in the list there a couple of lines we need to add to various places in the method OnGUI() in SelectPlayerMenu.cs. Firstly, add
SelectionList.SetCurrentEntry(playerName);
immediately after
playerName = GUI.TextField(new Rect(ResourceManager.Padding, textTop, textWidth, ResourceManager.TextHeight), playerName, 14);
Next, add
string prevSelection = SelectionList.GetCurrentEntry();
immediately before the code we added to draw the SelectionList. Finally, add
string newSelection = SelectionList.GetCurrentEntry(); //set saveName to be name selected in list if selection has changed if(prevSelection != newSelection) { playerName = newSelection; avatarIndex = PlayerManager.GetAvatar(playerName); }
after the code handling the drawing of SelectionList. This requires that we also add
public static int GetAvatar(string playerName) { for(int i = 0; i < players.Count; i++) { if(players[i].Name == playerName) return players[i].Avatar; } return 0; }
to PlayerManager.cs to make sure that we set the avatar to be the one for the Player that was selected. You can test that this all works as expected by creating a number of Players with different avatars (and different names of course). Each time you run the game after adding a new Player it should show up in the SelectionList.
And finally we come to the close of this part. We now have the ability to create Players, save their details, and load those again the next time we play. We can select the Player we want from the list of created Players or create a new one. And we can personalize that Player a little bit by choosing the name we want (as long as it is not too long) as well as an avatar. The name and avatar are being displayed as part of the HUD while the Player is in a game too. Lots of useful things, and we will be able to build on much of this in coming parts. The ability to load / save details will come in useful very soon when we look at saved games. And the Player details will come in useful when we add in customization and progress tracking. The entire project can be found on github with the code for this part under the commit for Part 17. In our next part we will build on the knowledge of saving details by looking into saving a game so that a Player can come back to it at a later date.