Day 33 of 100 Days of VR: Implementing the High Score System

Josh Unity VR Development Leave a Comment

Welcome back to day 33!

Yesterday, we looked at 3 ways we can save and load data in Unity: with PlayerPrefs, Data Serialization, and saving our data to a server.

Today we’re going to use what we learned the previous day to save our score in our simple FPS.

Here’s the goal for today:

  1. Implement our SaveManager to help us save score
  2. Update our UI to show our high score when the game is over

So, let’s get started!

Step 1: Saving our Data

Of the methods we’ve talked about, I’m going to use the PlayerPrefs to help us save our data.

While we can technically use our PlayerPrefs anywhere we want between our scripts, it’s better for us to create a manager where we will centralize everything all our Saving/Loading work so that when we need to make changes, we don’t have to comb through all our Script to fix things.

Step 1.1: Creating our SaveManager

The first step to saving our score is to create our ScoreManager script and attach it to our GameManager game object.

  1. Select our GameManager game object in our hierarchy.
  2. In the Inspector, click Add Component and create a new ScoreManager

In our SaveManager, we want to be able to save and load our high score. Here’s what we’ll have:

using UnityEngine;

public class SaveManager : MonoBehaviour
{
    private string _highScoreKey = "highscore";

    public void SaveHighScore(float score)
    {
        PlayerPrefs.SetFloat(_highScoreKey, score);
    }

    public float LoadHighScore()
    {
        if (PlayerPrefs.HasKey(_highScoreKey))
        {
            return PlayerPrefs.GetFloat(_highScoreKey);
        }
        return 99999999999;
    }
}

Variables Used

For our SaveManager, we only create a string _highScoreKey that we use to store the text that we want to use for our score.

We never want to manually type our key in as that might lead to us mistyping and many hours spent debugging over a single spelling mistake.

Walking Through the Code

Our SaveManager script is only used to help us access our PlayerPrefs in one centralized location.

The beauty of this system is that if one day we decide that we don’t want to use PlayerPrefs and use DataSerialization instead, we can just change SaveManager, but everything else that’s using it can stay the same.

Here’s the code flow:

  1. In SaveHighScore() we save the score that we’re given to our high score.
  2. In LoadHighScore() we return the high score that we saved. It’s important to note that if we don’t have a value in our Prefab, we would return 0, however, in our case, a lower score is better, instead we return a very high score.

Step 1.2: Modifying our ScoreManager to Expose Our Score

Previously, we had our ScoreManager change the text of the score in our page, however, if we want to be able to show our high score at the end of the game.

To do that, we need to use the Victory and GameOver panels that we made in the past. Luckily for us, in our GameManager, we already have some code that uses them.

Now, for our GameManager to access our time (and save it with our SaveManager), we need to expose the score for other scripts to access them.

Here are our changes to ScoreManager:

using System;
using UnityEngine;
using UnityEngine.UI;

public class ScoreManager : MonoBehaviour
{
    public Text Score;

    private string _time;
    private bool _gameOver;
    private float _score;

	void Start ()
	{
	    _time = "";
	    _gameOver = false;
	    _score = 9999999999;
	}

    void Update()
    {
        if (!_gameOver)
        {
            UpdateTime();
        }
    }

    private void UpdateTime()
    {
        _score = Time.time;
        _time = ScoreManager.GetScoreFormatting(Time.time);
        Score.text = _time;
    }

    public void GameOver()
    {
        _gameOver = true;
    }

    public float GetScore()
    {
        return _score;
    }

    // we can call this function anywhere we want, we don't need to have an instance of this class
    public static string GetScoreFormatting(float time)
    {
        int minutes = Mathf.FloorToInt(time / 60);
        int seconds = Mathf.FloorToInt(time % 60);
        float miliseconds = time * 100;
        miliseconds = miliseconds % 100;
        return string.Format("{0:0}:{1:00}:{2:00}", minutes, seconds, miliseconds);
    }
}

 New Variables Used

We create a new float _score that we’ll use to keep track of the time that passed for the player.

Walking Through the Code

Most of the code is the same. We just wrote some new functions, here’s what we did:

  1. In Start() we set _score to have a starting value
  2. In UpdateTime() we update _score to be the current time in the game. I also moved the code that gave us the time in a nice: minutes:seconds:milliseconds format to a static function called GetScoreFormatting() where we used to set our _time. Notice how I use GetScoreFormatting()? GetScoreFormatting() can be used just like this anywhere else in our game now, even if we don’t have an instance to ScoreManager.
  3. GetScoreFormatting() is just a copy and paste of what we originally had in UpdateTime().
  4. Finally, we create a public function GetScore() to get the score the player earned in GameManager when they win

That’s it! Now let’s combine everything we’ve worked on to create our high score system.

Step 1.3: Use Everything in Our GameManager

Now that we have everything we want, let’s use it in our GameManager script!

Now we’re going to write some code that allows us to save our when the player wins.

Here’s what we’ll have:

using UnityEngine;

public class GameManager : MonoBehaviour
{
    public Animator GameOverAnimator;
    public Animator VictoryAnimator;

    private GameObject _player;
    private SpawnManager _spawnManager;
    private ScoreManager _scoreManager;
    private SaveManager _saveManager;

    void Start()
    {
        _player = GameObject.FindGameObjectWithTag("Player");
        _spawnManager = GetComponentInChildren<SpawnManager>();
        _scoreManager = GetComponent<ScoreManager>();
        _saveManager = GetComponent<SaveManager>();
    }

    public void GameOver()
    {
        GameOverAnimator.SetBool("IsGameOver", true);
        DisableGame();
        _spawnManager.DisableAllEnemies();
    }

    public void Victory()
    {
        VictoryAnimator.SetBool("IsGameOver", true);
        DisableGame();

        if (_scoreManager.GetScore() < _saveManager.LoadHighScore())
        {
            _saveManager.SaveHighScore(_scoreManager.GetScore());
        }
    }

    private void DisableGame()
    {
        _player.GetComponent<PlayerController>().enabled = false;
        _player.GetComponentInChildren<MouseCameraContoller>().enabled = false;

        PlayerShootingController shootingController = _player.GetComponentInChildren<PlayerShootingController>();
        shootingController.GameOver();
        shootingController.enabled = false;

        Cursor.lockState = CursorLockMode.None;
        _scoreManager.GameOver();
    }
}

New Variables Used

The first thing we did was we got ourselves an instance of SaveManager: _saveManager.

Now with our SaveManager in GameManager, we can save and load scores.

Walking Through the Changes

The code that we’re adding uses our SaveManager to save our score.

  1. In Start() we instantiate our SaveManager which is also attached to the GameManager game object.
  2. When Victory() is called, we check their current score and then compare it with our high score from our SaveManager if our time is lower than the high score, than we set our score as the new high score

With our changes to the GameManager, we now have a working high score system.

Now the problem? We have no way of seeing if any of this works!

Worry not, that’s going to be the next step of our work!

Step 2: Update the Game Over Panels to Show Our Score

Now that we have the code to change our high score, we’re going to work on displaying our high score in our UI.

To do this, we’re going to make a couple of changes to our script, we’re going to:

  1. Change our GameOverUIManager to change the Text that we show.
  2. Change our GameManager to get our GameOverUIManager from our game over panels and then set the high score to show when the game is over.

Step 2.1: Making Changes to GameOverUIManager

If you recall, our GameOverUIManager was created to help us detect when the player clicks on the start over a button in our panel.

We’re going to make some changes to change the text in our Panel to also say what our high score is.

Let’s get to it! Here are the changes that were made:

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class GameOverUIManager : MonoBehaviour
{
    private Button _button;
    private Text _text;

	void Start () {
        _button = GetComponentInChildren<Button>();
        _button.onClick.AddListener(ClickPlayAgain);

	    _text = GetComponentInChildren<Text>();
	}

    public void ClickPlayAgain()
    {
        SceneManager.LoadScene("Main");
    }

    public void SetHighScoreText(string score, bool didWin)
    {
        print(_text.text);
        if (didWin)
        {
            _text.text = "You Win! \n" +
                         "High Score: " + score;
        }
        else
        {
            _text.text = "Game Over! \n" +
                         "High Score: " + score;
        }
        print(_text.text);
    }
}

New Variables Used

The only new variable that we used is a Text UI that we call _text. Specifically, this is the UI element that we use to tell the player that they won (or lost) the game.

Walking Through the Changes

The only changes we did was:

  1. Instantiate our Text UI in Start(). In the case of our panel, the Text was a child the panel that GameOverUIManager was attached to, so we have to look for the component in our child.
  2. Once we have an instance of our text, I created a new SetHighScoreText() that, depending on if we won or loss, change our text to show the high score.

One important thing I want to mention. Do you notice the “\n”?

\n is an escape character for new line. Which means that in our UI, we’ll see something like:

Game Over

High Score: 1:21:12

Step 2.2: Calling our GameOverUIManager from GameManager

Next up, we want to be able to set the score from our GameOverUIManager from our GameManager.

We must make some pretty big changes to our Game Panels that we use. Before we just grabbed the animator component, now, we need that and the GameOverUIManager.

Besides that, we just need to call our GameOverUIManager script and set the text with our high score.

Here’s what we’ve done:

using UnityEngine;
using UnityEngine.UI;

public class GameManager : MonoBehaviour
{
    public GameObject GameOverPanel;
    public GameObject VictoryPanel;

    private GameObject _player;
    private SpawnManager _spawnManager;
    private ScoreManager _scoreManager;
    private SaveManager _saveManager;

    void Start()
    {
        _player = GameObject.FindGameObjectWithTag("Player");
        _spawnManager = GetComponentInChildren<SpawnManager>();
        _scoreManager = GetComponent<ScoreManager>();
        _saveManager = GetComponent<SaveManager>();
    }

    public void GameOver()
    {
        DisableGame();
        _spawnManager.DisableAllEnemies();
        ShowPanel(GameOverPanel, false);
    }

    public void Victory()
    {
        DisableGame();

        if (_scoreManager.GetScore() < _saveManager.LoadHighScore())
        {
            _saveManager.SaveHighScore(_scoreManager.GetScore());
        }
        ShowPanel(VictoryPanel, true);
    }

    private void ShowPanel(GameObject panel, bool didWin)
    {
        panel.GetComponent<Animator>().SetBool("IsGameOver", true);
        panel.GetComponent<GameOverUIManager>().SetHighScoreText(ScoreManager.GetScoreFormatting(_saveManager.LoadHighScore()), didWin);
    }

    private void DisableGame()
    {
        _player.GetComponent<PlayerController>().enabled = false;
        _player.GetComponentInChildren<MouseCameraContoller>().enabled = false;

        PlayerShootingController shootingController = _player.GetComponentInChildren<PlayerShootingController>();
        shootingController.GameOver();
        shootingController.enabled = false;

        Cursor.lockState = CursorLockMode.None;
        _scoreManager.GameOver();
    }
}

New Variables Used

The biggest change is with the GameOverPanel and VictoryPanel.

Before these were Animators that we used to show our panel, now we have the game objects themselves because we need to access more than just the animator.

New Functions Created

We created a new function: ShowPanel(), which takes in the GamePanel that we’re changing the text to and whether or not we won the game.

From this information, we play our animator and get the GameOverUIManager and call SetHighScoreText() to change the text.

Walking Through the Code

Here’s how our new code gets used:

  1. Whenever the game is over, either the player lost or won, we would call GameOver() and Victory().
  2. From there, we would disable our game and then call our new function ShowPanel()
  3. Depending on whether we won or not, we would pass in the correct Panel and state we’re into ShowPanel()
  4. Finally, in ShowPanel(), we would play the animation to show our Panel and call setHighScoreText() from our GameOverUIManager to change the text to display our high score.

Step 2.3: Attaching our Panels back into GameManager

Now with everything in place, we need to add our GameOverPanel and VictoryPanel, because when we changed them, we got rid of any reference to our previous models.

Here’s what to do:

  1. Select our GameManager game object from the hierarchy.
  2. Look for the GameManager script component, drag our Panels (Victory and GameOver) from HUD in our game hierarchy, and put them in the appropriate slots.

With that done, we should have something like this:

Step 2.4: Fixing our Text UI Display

Now with all of this implemented, we can finally play our game!

After winning (or losing) in our game, we’ll get our BRAND-NEW panel:

New… right?

Wrong! It looks the same as what we had!

What happened?

It turns out, the text is still there, however, we didn’t have enough space in our Text UI to show the remaining text!

This problem can be solved easily. We just need to increase the size of our Text UI in our GameOver and Victory Panel in our hierarchy.

The first thing we need to do is reveal our Panels that we hid.

In our Panels, find the CanvasGroup component, we previously set our alpha to be 0 to hide our panel. Let’s change that back to 1 so we can see our panel again.

Just don’t forget to change it back to 0.

In the same Panel, select its child game object, Text.

We want to try playing around with the Width and Height field in our Rect Transform component.

I ended up with:

Width: 300

Height: 80

I also added some new Text for a demo of what to expect. Here’s what we have now:

Make sure that you make this change to both our GameOver Panel and our Victory Panel in the hierarchy.

Now if we were to play the game, here’s what we’ll have when we win:

And when we lose:

Don’t ask how I won in 2 seconds. I modified our scripts a bit, okay?

Conclusion

With all of this, we’re done with day 33! Which means we’re officially 1/3 of the way through the 100 days of VR challenge!

Not only that, now that we have this high score system, I’m going to officially call our simple FPS finished!

Tomorrow, I’m finally going to start looking more into how to do VR development!

Until then, I’ll see you all on day 34!

Day 32 | 100 Days of VR | Day 34

Side topic: Choosing a phone and platform to develop VR in

Home

Subscribe To Our Weekly Newsletter!
Like these coding articles? Join my mailing list for the latest updates and influence the code that I write!
We hate spam. Your email address will not be sold or shared with anyone else.

Leave a Reply

Your email address will not be published. Required fields are marked *