Creating a High Score System in Unity


Creating a high score system was a fun challenge. I'll do my best to explain my process and approach for my game. Hopefully you'll find this information helpful if you'd like to add a similar system to your own game.

Let's start by looking at the finished product, so we have an idea of where we are going.

high-score-initials-screen

The Concept: When the Player gets a score that is higher than an existing high score in the top 10 list, they can enter their initials for the new high score.  They can cycle through three initials, which consist of the English alphabet, numbers, and the hashtag symbol (#). Once the new high score is entered, it appears in the top 10 list, which is then saved locally to the Player's device, using PlayerPrefs (See Unity's Documentation for more information on PlayerPrefs, as that is outside the scope of this devlog).

Now that you understand the concept behind it, let's break down the flow of the code:

1. When the Player gets a new score, I check if that score is higher than the lowest high score in the top 10 list (10th place)
2. If the new score is higher, I save the high score to a newScore variable, and I set a newHighScore variable to true(the new high score may be higher than 9th, or even 1st in the top 10 list, but for now, I just need to know if it makes the top 10 list or not (greater than 10th))
3. When the Player gets a game over, since newHighScore is true, I load the New High Score screen (pictured above) so they can enter their initials. This screen reads the existing high scores and corresponding initials from two separate arrays (one for initials and one for scores) that are saved to PlayerPrefs
4. Before the high scores are printed to the page (shown above), the existing high score that the new high score is replacing must be removed, and dropped down one number in the list, if applicable(if you replace 10th place high score, it is just removed from the list). This poses an interesting challenge: you have to "save" the existing high scores temporarily, along with their number in the listing, so you can move them all down one position in the list (more on that later).
5. Finally, the new high score flashes on the screen (see tweet below):


Elevader: Added local high scores to the game. It was a fun challenge. #gamedev #indiedev #screenshotsaturday #highscores #elevadergame #madewithunity pic.twitter.com/BumFXg2udW
— Mike (@AnimationMW) December 18, 2021

Here is how my Classes are broken down. All of my code is written in (Unity) CSharp:

First and foremost, I have a gameManager Class. This holds all my variables that I want to move between scenes and have access to, such as the number of continues the player has left, or the player's checkpoint. These variables must persist through a scene when the player dies, assuming they still have continues left. To do that, I have placed a simple Don't Destroy script on the GameObject in the scene that the gameManager script is attached to. 


public class dontDestroy : MonoBehaviour {
public string tagName; //the tag that is attached to the GameObject I don't want to destroy
     void Awake() {
      // When the GameObject is moved from one scene to another, the object may already exists
      // in that scene, in which case you can remove the copy
      if ((GameObject.FindGameObjectsWithTag(tagName)).Length > 1) {
           // Destroy the GameObject. This is a built-in function in Unity.
           Destroy(gameObject);
      }
      else {
           // If this is the only copy if the GameObject with that tag, don't destroy it in the scene.
           // This is also a built-in function in Unity. 
           DontDestroyOnLoad(this);
      }
     }
}

Back in the gameManager Class, I have two key variables:

public int newScore;
public bool newHighScore = false;

newScore as the name suggests is the new score, which in my case is the current Floor in the elevator the player is on. If the newScore is higher than the lowest high score, then newHighScore is true, as shown in the code snippet below:

// if the current floor the Player is on is higher then the 10th, lowest, 
// high score (9th index in the HighScoresList Array), 
// set newScore to the current floor number, and set newHighScore to true
if(elevatorFloorNumber > manager.GetComponent<gamemanager>().HighScoresList[9]){
    manager.GetComponent<gamemanager>().newScore = elevatorFloorNumber;
    manager.GetComponent<gamemanager>().newHighScore = true;
}

As I said before, when the Player gets a game over, the initials screen is loaded. Let's take a look at the initialsManager class to see how that works.

The available initials are stored in a List, declared at the top of the Class:

List<string> initialLetters = new List<string>{
  "A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U",
  "V","W","X","Y","Z","#","0","1","2","3","4","5","6","7","8","9"," "
};

In the Start() function, I read the newScore so it can be printed to the screen and set the initials to default to "AAA" (see the code snippet below):

  void Start () {
    // If the text GameObject is on the page set it to the newScore to display it
    if(highScoreNumberText){
      highScoreNumberText.GetComponent<unityengine.ui.text>().text = "" + manager.GetComponent<gamemanager>().newScore;
    }
    // If the first initial text is present, set it and the 2nd and 3rd initial to the first letter
    // of the alphabet (A, 0 in the index of the initialLetters Array)
    if(firstInitialText){
      firstInitialText.GetComponent<unityengine.ui.text>().text = initialLetters[0];
      secondInitialText.GetComponent<unityengine.ui.text>().text = initialLetters[0];
      thirdInitialText.GetComponent<unityengine.ui.text>().text = initialLetters[0];
    }
  }

Each initial has its own controllers for moving up or down. The code is the same for each, so I'll just cover the first initial. I have a public function firstInitialUp() that is called when the Player clicks the up arrow. Likewise, I have a firstInitialDown() function that is called when the Player clicks down. The functions increase/decrease the position in the initialLetters List and update the initial text to display the new string.

  public void firstInitialUp(){
    // Move the list index up one
    firstInitialNumber++;
    // If the index has reached the end of the List, loop the list back around to the beginning
    if(firstInitialNumber == initialLetters.Count){
      firstInitialNumber = 0;
    }
    // Display the new initial in the text
    firstInitialText.GetComponent<unityengine.ui.text>().text = initialLetters[firstInitialNumber];
  }
  public void firstInitialDown(){
    // Move the list index down one
    firstInitialNumber--;
    // If the index has reached the beginning of the List, loop the list back around to the end
    if(firstInitialNumber == -1){
      firstInitialNumber = initialLetters.Count - 1;
    }
    // Display the new initial in the text
    firstInitialText.GetComponent<unityengine.ui.text>().text = initialLetters[firstInitialNumber];
  }

And that's all for the basic controls. When the Player enters the new high score is where the real fun begins. My code may be a bit spaghetti, but I'll do my best to explain how I replace and rearrange the existing high scores.

First, I have to iterate/read the existing high scores and compare my new high score with each one. If the score is higher, I have to save the position in the high score list:

    // Read the List and compare each index number with the new high score
    for(int o=0;o<manager.getcomponent<gamemanager>().HighScoresList.Count;o++){
      // If the newScore is higher or equal to the existing index value, mark its index in the List for reference
      if(manager.GetComponent<gamemanager>().newScore >= manager.GetComponent<gamemanager>().HighScoresList[o]){
        // Save the index postion as as replacementScorePosition
        replacementScorePosition = o;
        manager.GetComponent<gamemanager>().replacementScorePosition = o;
        // Once I've found the score to replace, I can stop reading the List, break
        break;
      }
    }

Next, Save the existing high scores and corresponding initials from that index/point forward in the List to temporary Lists for reference, so I can move them:

    // iterate/read the existing HighScoresList List starting at the replacementScorePosition I saved earlier
    for(int r=replacementScorePosition;r<manager.getcomponent<gamemanager>().HighScoresList.Count;r++){
      // Save high scores from that index onward to tempHighScores List
      tempHighScores[r] = manager.GetComponent<gamemanager>().HighScoresList[r];
      // Save high score intials from that index onward to tempHighScoresInitials List
      tempHighScoreInitials[r] = manager.GetComponent<gamemanager>().HighScoresInitialsList[r];
    }

Next, rearrange/replace the high scores and corresponding initials:

    // Iterate/read the HighScoresList List, starting from the replacementScorePosition saved from earlier
    for(int c=replacementScorePosition;c<manager.getcomponent<gamemanager>().HighScoresList.Count;c++){
      // The temporary List has the old scores/initials at one index higher, since the new high score/initials
      // will replace where the old high scores/initials were.
      // Therefore, the count will be off by one after the new high score/initials have been written into the Lists
      // EX: Let's say new high score/initials are higher than the score at index 8. 8 will move to 9, 9 to 10, etc.
      // So the new high score and initials will write over the item at index 8. However, the old high scores/initials
      // were saved at their old indexes. so the count (int c up top) will no longer match in the for loop.
      // To account for that and read the proper item at the proper index, we will need to set the index count
      // back one to C-1. That way the proper old item/score at index 8 will be read and re-indexed at its new index.    
      int tempElement = (c-1);
      // If this is the score/initials being replaced, replace the items in the Lists with the new high score 
      // and new initials that the Player just entered
      // If this isn't the new replacement index, skip this and move the items down one instead
      if(c == replacementScorePosition){
        manager.GetComponent<gamemanager>().HighScoresList[c] = manager.GetComponent<gamemanager>().newScore;
        manager.GetComponent<gamemanager>().HighScoresInitialsList[c] = "" + initialLetters[firstInitialNumber] + initialLetters[secondInitialNumber] + initialLetters[thirdInitialNumber];
        // Since this item is updated, move on to the next items in the List to move them down one index
        continue;
      }
      // Write over the existing item in the index with the old high score that was saved to the temporary List
      // EX: if C = 8, writeover index 8 with the old score that was originally at index 7, to move it down one
      manager.GetComponent<gamemanager>().HighScoresList[c] = tempHighScores[tempElement];
      // Write over the existing item in the index with the matching old initials that were saved to the temporary List
      manager.GetComponent<gamemanager>().HighScoresInitialsList[c] = tempHighScoreInitials[tempElement];
    }

Finally, once the Lists/scores/initials are updated, they are saved to PlayerPrefs and the high score screen scene is loaded. Below is a code snippet that shows how this is done for the top high score and initials. The same is done for all 10 spots in the List:

PlayerPrefs.SetString("player01", manager.GetComponent<gamemanager>().HighScoresInitialsList[0]);
PlayerPrefs.SetInt("player01Score", manager.GetComponent<gamemanager>().HighScoresList[0]);
PlayerPrefs.Save();
SceneManager.LoadScene("highScore");

And that's it! I know that's a lot. I hope you found this useful, or at least some of the code useful. Thanks for reading, and keep an eye out for Elevader, which will be releasing soon!

Get Elevader

Download NowName your own price

Leave a comment

Log in with itch.io to leave a comment.