54
\$\begingroup\$

I am developing my first 2D game in Unity and I have come across what seems an important question.

How do I handle data between scenes?

There seems to be different answers to this:

  • Someone mention using PlayerPrefs, while other people told me this should be used to store other things like screen brightness and so on.

  • Someone told me that the best way was to make sure to write everything into a savegame everytime that I changed scenes, and to make sure that when the new scene loads, get the info from the savegame again. This seemed to me wasteful in performance. Was I wrong?

  • The other solution, which is the one I have implemented so far is to have a global game object that isn't destroyed between scenes, handling all the data between scenes. So when the game starts, I load a Start Scene where this object is loaded. After this ends, it loads the first real game scene, usually a main menu.

This is my implementation:

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class GameController : MonoBehaviour {

    // Make global
    public static GameController Instance {
        get;
        set;
    }

    void Awake () {
        DontDestroyOnLoad (transform.gameObject);
        Instance = this;
    }

    void Start() {
        //Load first game scene (probably main menu)
        Application.LoadLevel(2);
    }

    // Data persisted between scenes
    public int exp = 0;
    public int armor = 0;
    public int weapon = 0;
    //...
}

This object can be handled on my other classes like this:

private GameController gameController = GameController.Instance;

While this has worked so far, it presents me with one big problem: If I want to load directly a scene, let's say for instance the final level of the game, I can't load it directly, since that scene does not contain this global game object.

Am I handling this problem the wrong way? Are there better practices for this kind of challenge? I would love to hear your opinions, thoughts and suggestions on this issue.

Thanks

\$\endgroup\$
65
\$\begingroup\$

Listed in this answer are the fundamental ways of handling this situation. Although, most of these methods do not scale well to large projects. If you want something more scalable and are not afraid of getting your hands dirty, check out the answer by Lea Hayes about Dependency Injection frameworks.


1. A static script for holding data only

You can create a static script to hold data only. Since it is static, you don't need to assign it to a GameObject. You can simply access your data like ScriptName.Variable = data; etc.

Pros:

  • No instance or singleton required.
  • You can access data from everywhere in your project.
  • No extra code to pass values between scenes.
  • All variables and data in a single database-like script make it easy to handle them.

Cons:

  • You will not be able to use a Coroutine inside the static script.
  • You will probably end up with huge lines of variables in a single class if you don't organize well.
  • You can't assign fields/variables inside the editor.

An Example:

public static class PlayerStats
{
    private static int kills, deaths, assists, points;

    public static int Kills 
    {
        get 
        {
            return kills;
        }
        set 
        {
            kills = value;
        }
    }

    public static int Deaths 
    {
        get 
        {
            return deaths;
        }
        set 
        {
            deaths = value;
        }
    }

    public static int Assists 
    {
        get 
        {
            return assists;
        }
        set 
        {
            assists = value;
        }
    }

    public static int Points 
    {
        get 
        {
            return points;
        }
        set 
        {
            points = value;
        }
    }
}

2. DontDestroyOnLoad

If you need your script to be assigned to a GameObject or derive from MonoBehavior, then you can add DontDestroyOnLoad(gameObject); line to your class where it can be executed once (Placing it in Awake() is usally the way to go for this).

Pros:

  • All MonoBehaviour jobs (for example Coroutines) can be done safely.
  • You can assign fields inside the editor.

Cons:

  • You will probably need to adjust your scene depending on the script.
  • You will probably need to check which secene is loaded to determine what to do in Update or other general functions/methods. For example, if you are doing something with UI in Update(), then you need to check if correct scene is loaded to do the job. This causes loads of if-else or switch-case checks.

3. PlayerPrefs

You can implement this if you also want your data to be stored even if the game gets closed.

Pros:

  • Easy to manage since Unity handles all background process.
  • You can pass data not only between scenes but also between instances (game sessions).

Cons:

  • Uses file system.
  • Data can easily be changed from prefs file.

4. Saving to a file

This is a bit overkill for storing values between scenes. If you don't need encryption, I discourage you from this method.

Pros:

  • You are in control of data saved as opposed to PlayerPrefs.
  • You can pass data not only between scenes but also between instances (game sessions).
  • You can transfer the file (user-generated content concept relies on this).

Cons:

  • Slow.
  • Uses file system.
  • Possibility of reading/loading conflicts caused by stream interruption while saving.
  • Data can easily be changed from the file unless you implement an encryption (Which will make the code even slower.)

5. Singleton pattern

Singleton pattern is a really hot topic in object-oriented programming. Some suggest it, and some don't. Research it yourself and make the appropriate call depending on your project's conditions.

Pros:

  • Easy to both set-up and use.
  • You can access data from everywhere in your project.
  • All variables and data in a single database-like script make it easy to handle them.

Cons:

  • Lots of boilerplate code whose only job is to maintain and secure the singleton instance.
  • There are strong arguments against the use of singleton pattern. Be cautious and make your research beforehand.
  • Possibility of data clash due to poor implementation.
  • Unity may have a difficulty handling singleton patterns1.

1: In the summary of OnDestroy method of Singleton Script provided in Unify Wiki, you can see the author describing ghost objects which bleed into the editor from runtime:

When Unity quits, it destroys objects in a random order. In principle, a Singleton is only destroyed when application quits. If any script calls Instance after it have been destroyed, it will create a buggy ghost object that will stay on the Editor scene even after stopping playing the Application. Really bad! So, this was made to be sure we're not creating that buggy ghost object.

\$\endgroup\$
8
\$\begingroup\$

A slightly more advanced option is to perform dependency injection with a framework like Zenject.

This leaves you free to structure your application however you want; for instance,

public class PlayerProfile
{
    public string Nick { get; set; }
    public int WinCount { get; set; }
}

You can then bind the type to the IoC (inversion of control) container. With Zenject this action is performed inside a MonoInstaller or a ScriptableInstaller:

public class GameInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        this.Container.Bind<PlayerProfile>()
            .ToSelf()
            .AsSingle();
    }
}

The singleton instance of PlayerProfile is then injected into other classes that are instantiated via Zenject. Ideally through constructor injection but property and field injection is also possible by annotating them with Zenject's Inject attribute.

The latter attribute technique is used to automatically inject the game objects of your scene since Unity instantiates these objects for you:

public class WinDetector : MonoBehaviour
{
    [Inject]
    private PlayerProfile playerProfile = null;


    private void OnCollisionEnter(Collision collision)
    {
        this.playerProfile.WinCount += 1;
        // other stuff...
    }
}

For whatever reason you might also want to bind an implementation by interface rather than by the implementation type. (Disclaimer, the following isn't supposed to be an amazing example; I doubt you'd want Save/Load methods in this particular location... but this just shows an example of how implementations could vary in behaviour).

public interface IPlayerProfile
{
    string Nick { get; set; }
    int WinCount { get; set; }

    void Save();
    void Load();
}

[JsonObject]
public class PlayerProfile_Json : IPlayerProfile
{
    [JsonProperty]
    public string Nick { get; set; }
    [JsonProperty]
    public int WinCount { get; set; }


    public void Save()
    {
        ...
    }

    public void Load()
    {
        ...
    }
}

[ProtoContract]
public class PlayerProfile_Protobuf : IPlayerProfile
{
    [ProtoMember(1)]
    public string Nick { get; set; }
    [ProtoMember(2)]
    public int WinCount { get; set; }


    public void Save()
    {
        ...
    }

    public void Load()
    {
        ...
    }
}

Which can then be bound to the IoC container in a similar way as before:

public class GameInstaller : MonoInstaller
{
    // The following field can be adjusted using the inspector of the
    // installer component (in this case) or asset (in the case of using
    // a ScriptableInstaller).
    [SerializeField]
    private PlayerProfileFormat playerProfileFormat = PlayerProfileFormat.Json;


    public override void InstallBindings()
    {
        switch (playerProfileFormat) {
            case PlayerProfileFormat.Json:
                this.Container.Bind<IPlayerProfile>()
                    .To<PlayerProfile_Json>()
                    .AsSingle();
                break;

            case PlayerProfileFormat.Protobuf:
                this.Container.Bind<IPlayerProfile>()
                    .To<PlayerProfile_Protobuf>()
                    .AsSingle();
                break;

            default:
                throw new InvalidOperationException("Unexpected player profile format.");
        }
    }


    public enum PlayerProfileFormat
    {
        Json,
        Protobuf,
    }
}
\$\endgroup\$
3
\$\begingroup\$

You're doing things in a good way. It's the way I do it, and clearly the way many people do it because this autoloader script (you can set a scene to automatically load first whenever you hit Play) exists: http://wiki.unity3d.com/index.php/SceneAutoLoader

Both of the first two options are also things your game may need for saving the game between sessions, but those are wrong tools for this problem.

\$\endgroup\$
  • \$\begingroup\$ I just read a little of the link you posted. Seems like there is a way to autoload the inicial scene where I am loading the global Game Object. It looks a little complex so I will need some time to decide if it is something that solves my problem. Thanks for your feedback! \$\endgroup\$ – Enrique Moreno Tent Nov 7 '15 at 17:05
  • \$\begingroup\$ The script I linked to sort of solves that problem, in that you can hit play in any scene rather than having to remember to switch to the startup scene every time. It still starts the game from the beginning though, rather than starting directly in the last level; you could put in a cheat to allow you to skip to any level, or just modify the autoload script to pass the level to the game. \$\endgroup\$ – jhocking Nov 7 '15 at 17:05
  • \$\begingroup\$ Yeah, well. The trouble was not as much the "annoyance" of having to remember to switch to the start scene, as much as having to hack around to load the specific level in mind. Thanks anyway! \$\endgroup\$ – Enrique Moreno Tent Nov 7 '15 at 17:07
1
\$\begingroup\$

An ideal way to store variables between scenes is through a singleton manager class. By creating a class to store persistent data, and setting that class to DoNotDestroyOnLoad(), you can ensure it is immediately accessible and persists between scenes.

Another option you have is to use the PlayerPrefs class. PlayerPrefs is designed to allow you to save data between play sessions, but it will still serve as a means to save data between scenes.

Using a singleton class and DoNotDestroyOnLoad()

The following script creates a persistent singleton class. A singleton class is a class that is designed to only run a single instance at the same time. By providing such functionality, we can safely create a static self-reference, to access the class from anywhere. This means that you can directly access the class with DataManager.instance, including any public variables inside the class.

using UnityEngine;

/// <summary>Manages data for persistance between levels.</summary>
public class DataManager : MonoBehaviour 
{
    /// <summary>Static reference to the instance of our DataManager</summary>
    public static DataManager instance;

    /// <summary>The player's current score.</summary>
    public int score;
    /// <summary>The player's remaining health.</summary>
    public int health;
    /// <summary>The player's remaining lives.</summary>
    public int lives;

    /// <summary>Awake is called when the script instance is being loaded.</summary>
    void Awake()
    {
        // If the instance reference has not been set, yet, 
        if (instance == null)
        {
            // Set this instance as the instance reference.
            instance = this;
        }
        else if(instance != this)
        {
            // If the instance reference has already been set, and this is not the
            // the instance reference, destroy this game object.
            Destroy(gameObject);
        }

        // Do not destroy this object, when we load a new scene.
        DontDestroyOnLoad(gameObject);
    }
}

You can see the singleton in action, below. Note that as soon as I run the initial scene, the DataManager object moves from the scene-specific heading to the "DontDestroyOnLoad" heading, on the hierarchy view.

A screen recording of multiple scenes being loaded, while the DataManager persists under the "DoNotDestroyOnLoad" heading.

Using the PlayerPrefs class

Unity has a built in class to manage basic persistent data called PlayerPrefs. Any data committed to the PlayerPrefs file will persist across game sessions, so naturally, it is capable of persisting data across scenes.

The PlayerPrefs file can store variables of types string, int and float. When we insert values into the PlayerPrefs file, we provide an additional string as the key. We use the same key to later retrieve our values from the PlayerPref file.

using UnityEngine;

/// <summary>Manages data for persistance between play sessions.</summary>
public class SaveManager : MonoBehaviour 
{
    /// <summary>The player's name.</summary>
    public string playerName = "";
    /// <summary>The player's score.</summary>
    public int playerScore = 0;
    /// <summary>The player's health value.</summary>
    public float playerHealth = 0f;

    /// <summary>Static record of the key for saving and loading playerName.</summary>
    private static string playerNameKey = "PLAYER_NAME";
    /// <summary>Static record of the key for saving and loading playerScore.</summary>
    private static string playerScoreKey = "PLAYER_SCORE";
    /// <summary>Static record of the key for saving and loading playerHealth.</summary>
    private static string playerHealthKey = "PLAYER_HEALTH";

    /// <summary>Saves playerName, playerScore and 
    /// playerHealth to the PlayerPrefs file.</summary>
    public void Save()
    {
        // Set the values to the PlayerPrefs file using their corresponding keys.
        PlayerPrefs.SetString(playerNameKey, playerName);
        PlayerPrefs.SetInt(playerScoreKey, playerScore);
        PlayerPrefs.SetFloat(playerHealthKey, playerHealth);

        // Manually save the PlayerPrefs file to disk, in case we experience a crash
        PlayerPrefs.Save();
    }

    /// <summary>Saves playerName, playerScore and playerHealth 
    // from the PlayerPrefs file.</summary>
    public void Load()
    {
        // If the PlayerPrefs file currently has a value registered to the playerNameKey, 
        if (PlayerPrefs.HasKey(playerNameKey))
        {
            // load playerName from the PlayerPrefs file.
            playerName = PlayerPrefs.GetString(playerNameKey);
        }

        // If the PlayerPrefs file currently has a value registered to the playerScoreKey, 
        if (PlayerPrefs.HasKey(playerScoreKey))
        {
            // load playerScore from the PlayerPrefs file.
            playerScore = PlayerPrefs.GetInt(playerScoreKey);
        }

        // If the PlayerPrefs file currently has a value registered to the playerHealthKey,
        if (PlayerPrefs.HasKey(playerHealthKey))
        {
            // load playerHealth from the PlayerPrefs file.
            playerHealth = PlayerPrefs.GetFloat(playerHealthKey);
        }
    }

    /// <summary>Deletes all values from the PlayerPrefs file.</summary>
    public void Delete()
    {
        // Delete all values from the PlayerPrefs file.
        PlayerPrefs.DeleteAll();
    }
}

Note that I take additional precautions, when handling the PlayerPrefs file:

  • I have saved each key as a private static string. This allows me to guarantee I am always using the right key, and it means that if I have to change the key for any reason, I do not need to ensure I change all references to it.
  • I save the PlayerPrefs file to the disk after writing to it. This probably won't make a difference, if you do not implement data persistence across play sessions. PlayerPrefs will save to the disk during a normal application close, but it may not naturally call if your game crashes.
  • I actually check that each key exists in the PlayerPrefs, before I attempt to retrieve a value associated with it. This might seem like pointless double-checking, but it is a good practice to have.
  • I have a Delete method that immediately wipes the PlayerPrefs file. If you do not intend to include data persistence across play sessions, you might consider calling this method on Awake. By clearing the PlayerPrefs file at the start of each game, you ensure that any data that did persist from the previous session is not mistakenly handled as data from the current session.

You can see PlayerPrefs in action, below. Note that when I click "Save Data", I am directly calling the Save method, and when I click "Load Data", I am directly calling the Load method. Your own implementation will likely vary, but it demonstrates the basics.

A screen recording of data persisting passed being overwritten from the inspector, through the Save() and Load() functions.


As a final note, I should point out that you can expand upon the basic PlayerPrefs, to store more useful types. JPTheK9 provides a good answer to a similar question, in which they provide a script for serialising arrays into string form, to be stored in a PlayerPrefs file. They also point us to the Unify Community Wiki, where a user has uploaded a more expansive PlayerPrefsX script to allow support for a greater variety of types, such as vectors and arrays.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.