Thursday, May 1, 2014

Unity Endless Runner Tutorial #4 - High Scores

Original Tutorial
http://catlikecoding.com/unity/tutorials/runner/

My version of the game (Version Log)
http://www.hansadrian.com/Stuff/Endless%20Runner%20Tutorial/build.html

GitHub Gist (for full code files)
https://gist.github.com/HansNewbie/10005219/38e71e0c7358d15335412cbbbcdbb3facee36094

Completed Project Files (V0.6.1)
Buy this on Selz Selz powering ecommerce websites

For version 0.4, I added high score system. I was chasing for speed, so from software engineering view, I broke the architecture. I could not get past one problem.

The logic I am using for high score recording is very naive and unoptimized. So we have an array list (or dynamic array) that keeps all the current high scores. When a game is over, we will add the current score to the array list, then we sort the array list. We will then remove excess element (elements depending on implementation or bug presence) from the array list; let's say we keep 5 high scores, so we remove all scores after 5 if there are more than 5 high scores after the addition of the new score. We can have less than 5 high scores when the player is just starting to play the game, that's why we cannot just remove the excess element all the time. We then save the new high scores.

Ideally, what I want to do for high score system is to have a high score system manager that is triggered on game over to get the distance travelled by the Runner and process it into the high score record. GUI Manager will also show the high score board on game over by getting the current record which should have been updated. The trouble here is that I can't control the execution flow, and in the end the high score record that GUI Manager receives is for the previous game, i.e. the record that has not been updated. As such, I need to break the architecture to make things work.

To ensure the data shown by GUI Manager is the updated data, I will set the high scores to be shown in Runner - similar to the setting of the distance travelled and boosts left. At this point, Runner has broken the principle of separation of concern; it needs to concern about high score as well. However, I still want to keep things as nice as possible, thus the saving and loading of the high scores will be set by High Score Save Load Manager. All Runner does for high score is at game over, it will get the current high scores from High Score Save Load Manager, update that scores and tell both High Score Save Load Manager and GUI Manager to update their high scores.

Let's begin!

Let's start with the easiest thing first. GUI. Create a new GUI Text and name it High Scores Text. In GUIManager.cs, expose a new variable called highScoreText. Then disable and enable it respectively on Game Start and Game Over. If you are not familiar with disabling and enabling it, look at the other GUI Text. Next, we want to set the highScoreText. We will follow the implementation of SetDistance and SetBoostsby having a method called SetHighScoreBoard that takes in an ArrayList of integers consisting of the high scores. We process the ArrayList here to string, then set highScoreText to the processed string. Below is the method details. \n is a new line in string, in case you are not familiar.

public static void SetHighScoreBoard(ArrayList highScores) {
    string highScoreBoard = "High Scores\n";

    for (int i = 0; i < highScores.Count; i++) {
        highScoreBoard += (i+1).ToString() + ". ";
        highScoreBoard += highScores[i].ToString();
        highScoreBoard += "\n";
    }

    instance.highScoresText.text = highScoreBoard;
}

Next, we need to set, save and load the high score in High Score Save Load Manager. I will save the high scores after every update to the high scores happen, which is on Game Over. Here is the code for HighScoreSaveLoadManager.cs:

using UnityEngine;
using System;
using System.Collections;

public class HighScoreSaveLoadManager : MonoBehaviour {
    public int noOfHighScores;

    private ArrayList highScores = new ArrayList();

    private static HighScoreSaveLoadManager instance;

    void Start () {
        instance = this;

        LoadHighScore();
    }

    public static int GetHighScoresQuota() {
        return instance.noOfHighScores;
    }

    public static ArrayList GetHighScores() {
        return instance.highScores;
    }

    public static void SetHighScores(ArrayList updatedHighScore) {
        instance.highScores = updatedHighScore;
        SaveHighScore();
    }

    private static void SaveHighScore() {

        for (int i = 0; i < instance.highScores.Count; i++) {
            PlayerPrefs.SetInt("HighScore"+i.ToString(), (int) instance.highScores[i]);
        }
    }

    private static void LoadHighScore() {
        ArrayList loadHighScores = new ArrayList();

        int counter = 0;

        while (true) {
            if (PlayerPrefs.HasKey("HighScore"+counter.ToString())) {
                loadHighScores.Add(PlayerPrefs.GetInt("HighScore"+counter.ToString()));
                counter++;
            }
            else {
                break;
            }
        }

        instance.highScores = loadHighScores;
    }
}

To use ArrayList, you need to use System and System.Collections namespace.

I expose no. of high scores that you want to keep; I use 5 because the GUI only fit 5 and I am too lazy to create a GUI system for the high score board. Here, the manager takes care of updating high scores using PlayerPrefs, which works like hash tables in which it will take key-value pair. I do not do manual saving of high score data into hard disk on SaveHighScore(), which name might be misleading. I updated the high score record then. As stated in Unity documentation, PlayerPrefs is saved when user quits the application. You could add one line to forcefully save, of course, but I would like to keep it this way for simplicity.

Add this method to Runner. So what happens here is that I will get the high scores list from High Score Save Load Manager first, then update it locally within Runner with the current score. The logic is as I stated at the beginning of this post about my naive implementation. Then, I will do the update to GUI Manager for high scores by calling the corresponding method and also the update to High Score Save Load Manager. Call this function in Runner's Game Over.

public void RecordScoreIfNecessary() {
    int roundedScore = (int) Math.Round(distanceTraveled);

    ArrayList highScores = HighScoreSaveLoadManager.GetHighScores();
    int highScoresQuota = HighScoreSaveLoadManager.GetHighScoresQuota();
    
    highScores.Add(roundedScore);
    highScores.Sort();
    highScores.Reverse();
    
    if (highScores.Count > highScoresQuota) {
        highScores.RemoveRange(highScoresQuota, highScores.Count - highScoresQuota);
    }
    
    //      for (int i = 0; i < highScores.Count; i++) {
    //          Debug.Log(highScores[i]);
    //      }

    GUIManager.SetHighScoreBoard(highScores);
    HighScoreSaveLoadManager.SetHighScores(highScores);
}

Now we have high scores that would still be there even when you exit the game! For next step, I am looking into ability to pause the game. Hope you enjoy this post :)

No comments:

Post a Comment