106
\$\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\$
1

6 Answers 6

112
\$\begingroup\$

Listed in this answer are the fundamental ways of handling this situation. However, 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 class for holding data only

You can create a static class to hold data only. Since it is static, you don't need to assign it to a GameObject (i.e. you don't need to create an instance of it). You can simply access your data such as ClassName.Variable = data; etc.

Pros:

  • No instance or singleton is 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 class make it easy to manage them.

Cons:

  • You cannot use a Coroutine inside a static class.
  • You will probably end up with a huge class if you don't organize well.
  • Since static classes cannot be assigned to GameObjects, you can't assign fields/variables inside the editor.

An Example:

public static class PlayerStats
{
    public static int Kills { get; set; }
    public static int Deaths { get; set; }
    public static int Assists { get; set; }
    public static float Points { get; set; }
}

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 usually 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 scene 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 the correct scene is loaded to do the job. This may cause you to write lots of long if-else or switch-case statements.

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 logic.
  • You can pass data not only between scenes but also between instances (game sessions).

Cons:

  • Uses file system.
  • Data can easily be changed from the 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 encryption, which will make the code even slower.

5. Singleton pattern

Singleton pattern is a really controversial 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 needs and scale.

Pros:

  • Easy to both setup and use.
  • You can access data from everywhere in your project.
  • All variables and data in a single database-like class make it easy to manage 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 random order. In principle, a Singleton is only destroyed when the application quits. If any script calls Instance after it has 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\$
4
  • \$\begingroup\$ Fantastic answer. Personally I believe you shouldn't write to a file unless the user explicitly hits a save button (or updates a setting). I like the simplicity of the first option, and would recommend that to anyone starting out. \$\endgroup\$
    – ineedahero
    Jan 11, 2021 at 16:53
  • \$\begingroup\$ It's also possible to mix those solution together such as having a static function (no instance in the game) to save onto a file. In general, PlayerPrefs is better for publicly accessible data (which is usually the options settings), but it comes with restriction on the amount of data you can store into a single variable (since it's hardware-locked to 256bits per variable stored). Using a file allows you to save a many thing as you need, but comes with both slower read (disk-read) and edit. It's good to save characters data. DontDestroyOnLoad() is good for holding temporary data between scene \$\endgroup\$ Feb 15, 2021 at 18:18
  • \$\begingroup\$ It's crazy how even with this amazing answer, I'm still confused as to which route I should go. PlayerPref seems most simple to get started with, and I guess I don't really care too much if people decide to cheat in my game. \$\endgroup\$ Jan 28, 2022 at 0:55
  • \$\begingroup\$ @DanielSunYang My suggestion would be to start with singletons as they are easy to use. With time you will see the damage to architecture they do and start looking for better options such as DI. For simple games, however, they will suffice. \$\endgroup\$ Jan 28, 2022 at 5:12
11
\$\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\$
4
\$\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\$
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\$
3
  • \$\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\$ Nov 7, 2015 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, 2015 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\$ Nov 7, 2015 at 17:07
3
\$\begingroup\$

Many good answers but for a more clear picture, here is the picture cause picture tells a thousand words: enter image description here

At your local system, you can effectively utilize PlayerPrefs, DontDestoryOnLoad, Static variable(Data) script, System.IO File handling, or SQLite database (The pros and cons already mentioned in above answer). While you can also use the server-side (which a bit slow but share across many many application instances). It can contain your custom code (your own server-side language and logics) or a third party(firebase, back4app, etc).

\$\endgroup\$
1
\$\begingroup\$

I use a functional approach I call Stateless Scenes. I don't like the God class, Singleton, DontDestroyOnLoad or static variables as they all have their quirks.

using UnityEngine;
public class MySceneBehaviour: MonoBehaviour {
    private static MySceneParams loadSceneRegister = null;

    public MySceneParams sceneParams;

    public static void loadMyScene(MySceneParams sceneParams, System.Action<MySceneOutcome> callback) {
        MySceneBehaviour.loadSceneRegister = sceneParams;
        sceneParams.callback = callback;
        UnityEngine.SceneManagement.SceneManager.LoadScene("MyScene");
    }

    public void Awake() {
        if (loadSceneRegister != null) sceneParams = loadSceneRegister;
        loadSceneRegister = null; // the register has served its purpose, clear the state
    }

    public void endScene (MySceneOutcome outcome) {
        if (sceneParams.callback != null) sceneParams.callback(outcome);
        sceneParams.callback = null; // Protect against double calling;
    }
}

[System.Serializable]
public class MySceneParams {
    public System.Action<MySceneOutcome> callback;
    // + inputs of the scene 
}

public class MySceneOutcome {
    // + outputs of the scene 
}

You can keep global state in the caller's scope, so scene inputs and outputs states can be minimized (makes testing easy). To use it you can use anonymous functions:-

MyBigGameServices services ...
MyBigGameState bigState ...

Splash.loadScene(bigState.player.name, () => {
   FirstLevel.loadScene(bigState.player, (firstLevelResult) => {
       // do something else
       services.savePlayer(firstLevelResult);
   })
)}

More info at https://corepox.net/devlog/unity-pattern:-stateless-scenes

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .