Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
A continuation of John Boardman's introduction to Unity 3D development with C# and JavaScript. Particular emphasis on implementing a high score table using PlayerPrefs, a dialog with text fields to collect user data & cheat codes.
Welcome back for Part 2! We covered the following topics in Part 1 of this Unity 3D tutorial:
Introduction to Unity
Introduction to KeyShot
Using multiple cameras to implement a background logo
JavaScript and C# Implementations (including how to use nested generics in JavaScript and how to call C# scripts from JavaScript)
How to use 3D models with axes that don’t line up with normal “Y-up” Unity standards
So if you missed it, be sure to skip over and read it first!
Implementing a high score table using PlayerPrefs
How to implement a dialog with text fields to collect user data
How to implement cheat codes in Unity
The code is now on GitHub. This will be much more code-intensive than Part 1, so load Unity, click a script, and follow along!
PlayerPrefs can seem fairly limited when you first look at the API. It has 10 methods including DeleteAll(), DeleteKey(), GetFloat(), GetInt(), GetString(), HasKey(), Save(),SetFloat(), SetInt(), and SetString().
One glaring omission is a method that retrieves all keys. To me, that’s like going to a bank, depositing money, and then coming back later to make a withdrawal and being asked what the serial numbers of the bills were.
So, if there is no way to find out what is in PlayerPrefs, how do we save the high scores? We don’t know the user names ahead of time…the scores themselves are no help…hmmmm. Ah, a clue is the HasKey() method. We can ask the object if it has a key. So, if we come up with our own known keys we can use these to save and load the high scores. Let’s look at how that is done.
I decided to use the prefix “playerData” as the key’s known part, and then append an index to that to create a unique key. In scriptSceneManager I defined static strings to use when accessing keys in PlayerPrefs. This keeps the programmer from frustrations caused by mistyping key names, and locates all of the keys in one place so it is easy to keep track of what is being stored.
When 30 seconds have passed or the player has no more lives, saveHighScores()is called. Let’s work through the important bits of that code. Whenever there is a C# or Unity class, method, or property throughout the code, I’ll link it to the respective documentation. If there is no link, that means it is one of my methods.
One of the first things to happen is a call to MakePlayerKey(), which concatenates the data that the user entered in CSV format and returns it as a string. Wait…wasn’t the key “playerData” plus an index? Well…one of this game’s requirements is that any unique player’s data is only saved the first time the game is played (user exercise: remove this limitation ). This is because the game was used to enter players in a contest (this was the “shroud of secrecy” mentioned in part 1). So, the PlayerIndex() method searches using the “real” keys (“playerData” plus an index) to try to find the information the user entered. If it is found, the user can still play the game…the score will just be ignored. Since index 0 is not used to store player data, 0 is returned for the keyIndex if the player is found. If the player is not found, the index just past the last player is returned and a List is created to hold the high scores that are about to be read. A List of KeyValuePair is used so the score can be separated from the rest of the data to make sorting easier.
// this data is how we form the key to search for the player
string playerData = MakePlayerKey();
int keyIndex = PlayerIndex(playerData);
if (keyIndex > 0) {
int maxPlayerIndex = MaxPlayerIndex();
List<KeyValuePair> highScores = new List<KeyValuePair>();
Now we don’t just read-in the top 10, although that is all that is displayed. Another requirement was to save all users that played the game. Hey, this is marketing! Don’t worry, Keyhole has no nefarious plans to sell user data – your data is safe with us. So anyway, instead of loading 1-10, we load 1 – maxPLayerIndex. and place them into the list. Since score is the value on the end of each piece of data, it can be grabbed and used separately in the KeyValuePair. Hmm you say…I don’t see any sorting in here. That’s because the list is saved in sorted order, so no full sort ever has to occur
// read in scores & names
for (int i = 1; i <= maxPlayerIndex; i++) {
string currentData = PlayerPrefs.GetString(PREF_PLAYER_DATA + i);
if (currentData.Length > 0) { int currentScore =
int.Parse(currentData.Substring(currentData.LastIndexOf(",") + 1));
KeyValuePair highScore = new KeyValuePair(currentScore, currentData);
highScores.Add(highScore);
}
}
Now that the scores are in the list, the new one needs to be added. This is a simple linear search through the list, comparing the current score with the score in the list. Since the first player to make a score should be higher in the list than any other player with the same score, only greater is used instead of greater or equal. In this way we store equal scores in the order that they were made. If the score was too low to be found, it is added to the end of the list.
// add current score in sorted position
playerData += "," + score; KeyValuePair newScore = new
KeyValuePair(score, playerData);
bool playerInserted = false;
for (int i = 0; i < highScores.Count; i++) {
if (score > highScores[i].Key) {
highScores.Insert(i, newScore);
playerInserted = true;
break;
}
}
if (!playerInserted) {
highScores.Add(newScore);
}
Now that the new list has been created, the list can be overwritten with the new data, including the new score. The loop switches to a normal 0-Count iteration over the list. If the list was changed, the prefs are persisted. Since Unity supports so many platforms, it takes care of what “persisted” means for each platform. All we have to know is that it worked. If the player was found by the initial search, another temporary pref is set to indicate that fact. This will be used later when the high scores are displayed. Finally, the level is loaded to display the high scores.
// write out new scores including new player
for (int i = 0; i < highScores.Count; i++) {
PlayerPrefs.SetString(PREF_PLAYER_DATA + (i + 1), highScores[i].Value);
}
PlayerPrefs.Save();
} else {
PlayerPrefs.SetString(PREF_DOES_PLAYER_EXIST, "TRUE");
}
Application.LoadLevel("sceneScreenWin");
}
Displaying the high scores can be found in the sceneScreenWin script. I’ll briefly cover that screen.
First, we retrieve the flag to tell if the user exists. Regardless of that fact, we always display the score using the PREF_SCORE temporary pref. The way to tell these prefs are temporary is to look at the scriptScreenGetPlayerInfo script, where the keys are deleted each time the game starts. That screen will be covered in a later section. If the player already existed, the screen shows that the score they just achieved won’t effect the high scores.
bool doesPlayerExist = layerPrefs.HasKey(scriptSceneManager.PREF_DOES_PLAYER_EXIST);
float y = 0;
GUI.Label(new Rect(60.0f, y, 290.0f, 50.0f), "Score: " + PlayerPrefs.GetInt(scriptSceneManager.PREF_SCORE));
if (doesPlayerExist) {
y += 30.0f;
GUI.Label(new Rect(60.0f, y, 290.0f, 50.0f), "Player found! Score not recorded!");
}
Now let’s skip down to the scores. For each of the top 10, the data is retrieved. Because 10 players may not have yet played, checking to see if data is present is crucial to prevent runtime scripting errors. If data is present, the stored CSV data is parsed into its respective parts, and then displayed in a formatted list. I didn’t have time to figure out how to display the data using a monospaced font or other cool stuff, but the basics are here and they work.
for (int i = 1; i <= 10; i++) {
y += 20.0f;
string currentData = PlayerPrefs.GetString(scriptSceneManager.PREF_PLAYER_DATA + i);
int score = 0;
string firstName = "";
string lastName = "";
if (currentData.Length > 0) {
score = int.Parse(currentData.Substring(currentData.LastIndexOf(",") + 1));
int index = currentData.IndexOf(",");
firstName = currentData.Substring(0, index);
int index2 =currentData.IndexOf(",", index + 1);
lastName = currentData.Substring(index + 1, index2 - index - 1);
}
GUI.Label(new Rect(60.0f, y, 290.0f, y + 20.0f), string.Format("{0,2}. {1,10} : {2}", i, score, firstName + " " + lastName)); }
The other screen that shows player data is scriptScreenShowData, which is reached by using the cheat code that we’ll cover later. The code there is fairly obvious so I won’t cover it here.
Now that high scores have been covered, how did the data get there? The answer is that Unity has a rich set of user interface controls built in. I only used a few, with no validations, because this was a quick ‘n dirty game for a conference. When I code a game for publication it will have full validation, and I might do a Part 3 to cover that functionality if anyone is interested.
MonoBehavior is a Unity class that is the base class for every Unity script. It provides a wealth of functionality and is easy to extend. In JavaScript, the C# class generated automatically extends from MonoBehavior - but when coding in C#, the programmer must explicitly extend from it.
The OnGUI() Unity method is called on every frame. It is easy to prove this by adding a print statement to the method and then running the scene. This is why the firstName, lastName, phone, and email fields are defined at the class level instead of inside the method, because we want them to update instead of being re-initialized with each frame.
public class scriptScreenGetPlayerInfo : MonoBehaviour {
public float buttonWidth = 90.0f;
public float buttonHeight = 40.0f;
string firstName = "";
string lastName = "";
string phone = "";
string email = "";
As discussed earlier, when Start() is called the temporary pref keys are removed.Start() is called by Unity once each time a screen is loaded.
Now, to collect user data the GUI.TextField() API is used. A Rect is defined to give the field dimensions on the screen, the data to display is next, and the maximum number of characters to accept is the last parameter. The data entered by the user is returned. Here is one of the field definitions, along with a label to display to the left of it:
float y = 30.0f;
GUI.Label(new Rect(10.0f, y, 80.0f, 40.0f), "First Name"); firstName =
GUI.TextField(new Rect(90.0f, y, 200.0f, 30.0f), firstName, 40);
With the data fields displayed, the only thing left is to harvest it. To do this the user clicks on the “Save Info” button. When the button is clicked, GUI.Button() returns true. At that point the fields are saved in prefs and the next screen is loaded.
if (GUI.Button(new Rect(90.0f, y, buttonWidth, buttonHeight), "Save Info")) {
// here is where you would validate data if it is required PlayerPrefs.SetString(scriptSceneManager.PREF_PLAYER_FIRST_NAME, firstName); PlayerPrefs.SetString(scriptSceneManager.PREF_PLAYER_LAST_NAME, lastName); PlayerPrefs.SetString(scriptSceneManager.PREF_PLAYER_PHONE, phone); PlayerPrefs.SetString(scriptSceneManager.PREF_PLAYER_EMAIL, email); Application.LoadLevel("sceneScreenLoad");
}
When the game ends, the fields will be waiting in the temporary pref fields.
This was tricky for me because the documentation for the API was very light. I wanted a way to turn on the admin functions that the user would be unlikely to figure out during the short time they were playing the game. Two admin requirements were present for KeyShot.
The high scores needed to be reset just before the game was made available to players so the game could be tested before “real” play began.
The data that had been entered by all users needed to be able to be displayed after the conference was over so it could be harvested. Another way to do this would be to implement a remote call to send the data to a server, but I didn’t have time to set that up. Since the data is stored separately from the game, I can always add that after the conference is over if it is necessary.
The key to implementing cheat codes is the field inputString in the Unity UI classInput. This field holds the key (if any) that was pressed on the current frame, even if there are no input fields on the screen. The documentation for this API is unclear about that and makes it seem like any text that the user types will be held in the field, but this is not the case. The field resets for each frame. So, if a cheat code longer than 1 character is needed (most are…) a little more work needs to be done to make that available for use.
Each time Update() is called, the value in inputString is examined. If a value is present, it is concatenated to the hidden field, which is a class variable so it exists between calls to Update(). Now hidden can be checked to see if the string “key” has been typed. The Contains() method is used so keys can be typed before the cheat code and it will still work. If more than 10 characters have been typed without “key” being present, the hidden field is reset so it doesn’t become too long. Note that if k or e is the 11 character this can prevent the cheat code from working the first time. There is a low chance of that happening, but it certainly can happen. By checking for other strings, any number of cheat codes can be checked for in this manner.
If “key” is present, the numberOfButtons field is increased to 6, indicating that the admin screen is now active.
public class scriptScreenMainMenu : MonoBehaviour {
string hidden = ""; int numberOfButtons = 4;
// Update is called once per frame
void Update ()
{ string input = Input.inputString;
if (input.Length > 0) {
hidden += input; }
if (hidden.Contains("key"))
{ numberOfButtons = 6;
} else if (hidden.Length > 10) {
hidden = "";
}
}
If the numberOfButtons is 6, then the “Show Data” and “Clear Data” buttons are shown and usable.
if (numberOfButtons == 6) {
y += buttonHeight + 10.0f;
if (GUI.Button(Rect(10.0f, y, buttonWidth, buttonHeight), "Show Data")) { Application.LoadLevel("sceneScreenShowData");
}
y += buttonHeight + 10.0f;
if (GUI.Button(Rect(10.0f, y, buttonWidth, buttonHeight), "Clear Data")) { PlayerPrefs.DeleteAll();
}
}
Here’s the “Show Data” screen, with one score. Basic stuff, but fills the requirement.
So, that wraps it up for Part 2! Make sure to check the code out on GitHub. I truly appreciate your time and interest, and I hope it helps you develop using Unity!
– John Boardman, [email protected]
(Originally posted on the Keyhole Software Employee Blog on May 6th, 2013.)
Read more about:
BlogsYou May Also Like