Programming
Article
By Zdravko Jakupec

Saving Data Between Scenes in Unity

By Zdravko Jakupec

This tutorial assumes basic knowledge of Unity Engine. If you don’t have your own project set up, you can freely grab the project example linked here. You will also find a download of the completed project at the end of this article.

If you’re struggling with saving data between two scenes, this is the tutorial for you.

unity-logo


Starting Point

Download the example project:

[GitHub Repository link]
[ZIP Download]

The logic

Unity is a game engine with its own philosophy. Even though there are quite a few alternatives, it’s quite unique in the way it handles the building blocks of any game – game objects, scenes, code, scene graph. And by unique, I mean how easy it is to understand it.

If you’ve tried giving Unity a test run because it’s free to download, you were probably introduced to how its scripting is implemented. „Scripts“, written in either C#, JavaScript (or, since Unity 5, UnityScript) or Boo language, are components that are attached to any game object. From within a script, you’re free to access and manipulate the object the script is attached to, or any linked objects. It’s quite intuitive, easy to use, and fun to build with.

Supposedly, then you tried building a second level; say your first level was the interior of the house, and then your second level is outside of the house. The transition is using the door to load the next scene.

Here’s the core problem we’ll be tackling today. Each level within the Unity engine is called a „Scene“. You can edit the Scene using the graphical editor however you like. You can transition the scene using a single line of code (that is triggered, perhaps, by the player touching the door, or using an object, etc). Each scene has objects, which have „components“.

1

Generic „Objects“ represent what ever else makes up your level (meshes, triggers, particles, etc)

Each scene is built in its initial state. Transitioning the scene to another one means the new scene will be loaded at its initial state (naturally). But what about the player’s statistics, for example his ammo count, or experience, or inventory?

How do we preserve the data, when we can only write code in „Scripts“ – components of Game Objects – which just get destroyed during scene transitions?

Here’s where we get a bit crafty. There IS a way to preserve game objects through scene transitions, effectively building not a scene-wide object, but a game-wide object, which will keep our data.

Here’s the basic workflow of the game using such an object:

2

We need to save the data, transition the scene, and then load the data back.

Here’s the basic breakdown of the logic we will be using:

  • Regardless of the scene we are in (even if it’s scene 1), FIRST initialize the player with starting data.
  • Then, copy over the data from the Global Object.

Global Object initialization logic:

  • Initialize with starting data, and preserve our instance through scene transitions.

What this flow does is ensure that scene 1 will always initialize the player with the starting data. Then, if you save data into the Global Object before any scene transition, you have ensured that this data will always be loaded into the next level’s Player object.

This assumes you have placed the same Player object (preferably a Prefabbed object) into every scene. Note that the „Player object’s initialization logic“ is applicable to any object which needs the illusion of being „preserved through scenes“; we’re only using the Player as the most obvious example.

The code

OK, enough of the graphs and abstract thinking for now. Let’s get to coding.

I am assuming that by this point, you do have two scenes, and an implementation for transitioning between them – refer to the starting project at the top of the article. You’re playing the same player avatar in both, and you just need to save the player’s data between them to give the illusion of the same „player object“.

Let’s first create the game-wide Global Object. It’s important we get this one down right, so let’s figure out what we need it to do:

  • We need to be able to access it from any other script, from any part of the game. A logical choice for it would be the Singleton design concept. If you don’t know what that is, cool, you’re about to learn something new.
  • We need it to only be initialized once, and carry over through the scene transitions.
  • We need it to hold any data we may need to carry over. We know variables we need to save, so we’ll just type those in.

First, go to your first scene, and create a new empty Game Object. Rename it to something fitting, like „GameMaster“ or „GlobalObject“.

Next, create a new C# script (preferably within a new folder – remember to keep things organized). Give it a fitting name. My script’s name is „GlobalControl“.

Attach the new empty C# script to the new Game Object, and open the Script in your editor of choice. MonoDevelop, which comes with Unity, is good, but you can also use Visual Studio.

Put this code into the GlobalControl Script:

public class GlobalControl : MonoBehaviour 
{
    public static GlobalControl Instance;

    void Awake ()   
       {
        if (Instance == null)
        {
            DontDestroyOnLoad(gameObject);
            Instance = this;
        }
        else if (Instance != this)
        {
            Destroy (gameObject);
        }
      }
}

The basic premise of the singleton design pattern is that there is one single public static instance of one class. Within the awake method (one to be called when the object is supposed to be loaded), we are making sure of that by saying „If there is another instance, destroy that one and make sure that the instance is this one“.

Notice a very peculiar function call within the Awake method, “Don’t Destroy On Load”. This is a part of our solution to the problem of cross-scene persistence. This is what will keep the gameObject this script is attached to alive and carry it over to the other scene. The rest of the Singleton concept ensures that if there is another copy of the object with this same script attached (and there will be, you need to put this object into every scene), then the other object will be destroyed and this one (original) will be saved.

If you wish, you can test this now. Put the GameMaster or GlobalObject (or whatever is carrying this script) into every scene you have, and try to transition the scenes at run-time. You will notice there is only one such global object in the scene at any given time.

If we now write data into it, it will be saved!

Now, onto the other part of the problem:
What do we need to save?

For this tutorial’s purpose, suppose your Player has three statistics:

  • HP, with starting value of 100,
  • Ammo, with starting value of 0,
  • XP, with starting value of 0.

These are saved somewhere within your Player object. Which script exactly, doesn’t really matter. We need to have the same variables within our GlobalObject as well, so add them to your code:

public class GlobalControl : MonoBehaviour 
{
//[...]
    public float HP;
    public float Ammo;
    public float XP;
//[...]

Now we are ready to save the data. We only need to save the data when we are transitioning the scene and load it when we are starting a scene.

Here’s how to save the data from the script where you keep your player’s variables:

public class PlayerState : MonoBehaviour 
{
//[...]

    public float HP;
    public float Ammo;
    public float XP;

//[...]

//Save data to global control   
    public void SavePlayer()
    {
        GlobalControl.Instance.HP = HP;
        GlobalControl.Instance.Ammo = Ammo;
        GlobalControl.Instance.XP = XP;
        }
//[...]

It’s wise to build a dedicated function for saving the player data into the instance.
There is now just one more step missing: loading from the GlobalControl. You can easily call that in the Start function of your Player’s State script:

public class PlayerState : MonoBehaviour 
{
//[...]

    public float HP;
    public float Ammo;
    public float XP;

//[...]


    //At start, load data from GlobalControl.
    void Start () 
    {   
        HP = GlobalControl.Instance.HP;
        Ammo = GlobalControl.Instance.Ammo;
        XP = GlobalControl.Instance.XP;
    }
//[...]

With this code, our data flow would look something like this:

3

This goes for every scene transition, universally. Even transitioning back from Scene 2 to Scene 1 will now keep your player’s statistics!

There are, of course, a few kinks to work out. For example, if you quit to main menu and start a new game (without quitting the game altogether), you need to reset the saved data, otherwise you’ll be starting a new game with player stats from the previous session!

Why not public static class?

At this point, if you’re familiar with C# and .NET programming, you might be wondering why we aren’t simply using something like this:

public static class GlobalObject

Contrary to what you might intuitively think, public static classes do not actually persist game-wide. Since any class (that is, any script) is attached to a game object, it will get destroyed when you load a new scene. Even if another scene has a new public static class in it, the data inside will be reset – that is, the static class will be initialized anew at scene load.

This is why we must use the DontDestroyOnLoad method, and a bit more code to ensure we have only one instance of the class we intend to carry across levels.

Polishing and preparing for the next tutorial

You have probably noticed that in this example it’s not hard to manually type the three needed values to GlobalData and back. But what if we have a larger, more complex game with dozens, if not hundreds of player variables to keep track of?

Below, we’ll polish our code to be not just prettier, but to offer additional functionality we’ll explain in the followup tutorial, along with dealing with Save and Load mechanics as well.

First, let’s create a new script within our project. This will be a bit different type of script, because it will not extend the MonoBehavior class nor will it be attached to any object.

We’ll call it “Serializables” and it will look like this:

using UnityEngine;
using System.Collections;
using System;
using System.Collections.Generic;

public class PlayerStatistics
{
    public float HP;
    public float Ammo;
    public float XP;
}

As you can see, there are no functions, no namespaces, only a single class with no constructor, holding our three known variables. Why are we doing this?

So that, in our Global Object, we can turn individual statistics into a single class to contain them:

public class GlobalControl : MonoBehaviour 
{
//[...]
    public float HP;
    public float Ammo;
    public float XP;
//[...]

…like this:

public class GlobalControl : MonoBehaviour 
{
//[...]
    public PlayerStatistics savedPlayerData = new PlayerStatistics();
//[...]

And then the same in the player’s variables:

public class PlayerState : MonoBehaviour 
{
//[...]

    public float HP;
    public float Ammo;
    public float XP;

//[...]

This gives us an additional layer of security. We can’t accidentally write wrong the player variables into saved variables (eg. XP = HP):

public class PlayerState : MonoBehaviour 
{
//[...]
    public PlayerStatistics localPlayerData = new PlayerStatistics();
//[...]

Now, when we want to save the data, we simply remove this:

//Save data to global control   
    public void SavePlayer()
    {
        GlobalControl.Instance.HP = HP;
        GlobalControl.Instance.Ammo = Ammo;
        GlobalControl.Instance.XP = XP;
    }

…and instead, copy the reference to the class that contains our data. All values inside will stay where they are supposed to:

//Save data to global control   
    public void SavePlayer()
    {
                GlobalControl.Instance.savedPlayerData = localPlayerData;        
    }

Same for loading the player data in the player’s start function!

Now we have all our player’s statistics in a class that represents only the player’s data, with nothing extra. During your game’s development, when you need more player variables saved and loaded, simply add them to the class – the saving and retrieving of player data to/from Global Object stays the same.

Conclusion

In the next article, we will go through saving and loading the entire class to the hard drive (not just a Global Object) by using serialization.

If you got stuck at any point or just want to see what the finished project looks like, you can download it here:

[GitHub Repository]
[ZIP Download]

Questions? Comments? Let us know in the area below!


Recommended
Sponsors
The most important and interesting stories in tech. Straight to your inbox, daily. Get Versioning.
Login or Create Account to Comment
Login Create Account