Saving player data between scenes

I have followed this really helpful YouTube tutorial for save data,

. I have been able to save the player position (once play mode is quit), as well as save the position of an object in scene and whether it has been destoryed or not.

Before I implemented this, I used a scriptable object to store where a player should start in the next scene. So from going from the main world to the inside of a building like in Stardew Valley. This made sure that the player always spawns in the correct place when going from the main world to a building or vice versa.

I currently can’t get the two systems to work together as the scriptable object is currently called in start so the player position that is stored on quitting play mode is not be used on the next time it loaded. I’m doing the save on a player movement script as this made the most sense to me to store the data.

Player movement script:

public class PlayerMovement : MonoBehaviour, IDataPersistence
{
    // Variables for player movement
    [Header("Player Movement")]
    [SerializeField] float speed = 200f;

    // For Unity new input system
    PlayerInput playerInput;
    Vector2 move;
    bool interactPressed;
    [SerializeField] bool allowHold;
    bool singlePress;

    Rigidbody2D rb;

    [SerializeField] VectorValue startingPosition; //Scriptable object
   
    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        playerInput = GetComponent<PlayerInput>();
    }

    // Start is called before the first frame update
    void Start()
    {
        transform.position = startingPosition.SavedPlayerPos; //Calling position from scriptable object
    }

    public void LoadData(GameData data)
    {
        this.transform.position = data.playerPosition;
    }

    public void SaveData(ref GameData data)
    {
        data.playerPosition = this.transform.position;
    }
}

File data handler script:

public class FileDataHandler
{
    private string dataDirPath = "";
    private string dataFileName = "";
    private bool useEncryption = false;
 
    public FileDataHandler(string dataDirPath, string dataFileName, bool useEncryption)
    {
        this.dataDirPath = dataDirPath;
        this.dataFileName = dataFileName;
        this.useEncryption = useEncryption;
    }

    public GameData Load()
    {
        //Path.Combine used to be able to save on different OS
        string fullPath = Path.Combine(dataDirPath, dataFileName);

        GameData loadedData = null;
        if(File.Exists(fullPath))
        {
            try
            {
                string dataToLoad = "";
                using(FileStream stream = new FileStream(fullPath, FileMode.Open))
                {
                    using(StreamReader reader = new StreamReader(stream))
                    {
                        dataToLoad= reader.ReadToEnd();
                    }
                }

                if(useEncryption)
                {
                    dataToLoad = EncryptDecrypt(dataToLoad);
                }

                loadedData = JsonUtility.FromJson<GameData>(dataToLoad);
            }
            catch (Exception e)
            {
                Debug.LogError("Error occured when trying to load data to file: " + fullPath + "\n" + e);
            }
        }
        return loadedData;
    }

    public void Save(GameData data)
    {
        //Path.Combine used to be able to save on different OS
        string fullPath = Path.Combine(dataDirPath, dataFileName);
        try
        {
            //Creates the directory the file will be written to if it is not on the system
            Directory.CreateDirectory(Path.GetDirectoryName(fullPath));

            //Serialize the game data object into JSON
            string dataToStore = JsonUtility.ToJson(data, true);

            if(useEncryption)
            {
                dataToStore = EncryptDecrypt(dataToStore);
            }

            //Write serialized data to the file
            using(FileStream stream = new FileStream(fullPath, FileMode.Create))
            {
                using(StreamWriter writer = new StreamWriter(stream))
                {
                    writer.Write(dataToStore);
                }
            }
        }
        catch(Exception e)
        {
            Debug.LogError("Error occured when trying to save data to file: " + fullPath + "\n" + e);
        }
       
    }

}

The tutorial also mentioned but never went over additive scene loading, would this be something I would want to look up for what I’m doing, as the player can go to different scenes such as buildings?

Loading and saving code needs to be debugged just like any other code.

See bottom of this message for more on loading / saving. This is an extremely well-traveled area of game development, so stay focused on the necessary steps.

Time to start debugging! Here is how you can begin your exciting new debugging adventures:

You must find a way to get the information you need in order to reason about what the problem is.

Once you understand what the problem is, you may begin to reason about a solution to the problem.

What is often happening in these cases is one of the following:

  • the code you think is executing is not actually executing at all
  • the code is executing far EARLIER or LATER than you think
  • the code is executing far LESS OFTEN than you think
  • the code is executing far MORE OFTEN than you think
  • the code is executing on another GameObject than you think it is
  • you’re getting an error or warning and you haven’t noticed it in the console window

To help gain more insight into your problem, I recommend liberally sprinkling Debug.Log() statements through your code to display information in realtime.

Doing this should help you answer these types of questions:

  • is this code even running? which parts are running? how often does it run? what order does it run in?
  • what are the names of the GameObjects or Components involved?
  • what are the values of the variables involved? Are they initialized? Are the values reasonable?
  • are you meeting ALL the requirements to receive callbacks such as triggers / colliders (review the documentation)

Knowing this information will help you reason about the behavior you are seeing.

You can also supply a second argument to Debug.Log() and when you click the message, it will highlight the object in scene, such as Debug.Log("Problem!",this);

If your problem would benefit from in-scene or in-game visualization, Debug.DrawRay() or Debug.DrawLine() can help you visualize things like rays (used in raycasting) or distances.

You can also call Debug.Break() to pause the Editor when certain interesting pieces of code run, and then study the scene manually, looking for all the parts, where they are, what scripts are on them, etc.

You can also call GameObject.CreatePrimitive() to emplace debug-marker-ish objects in the scene at runtime.

You could also just display various important quantities in UI Text elements to watch them change as you play the game.

Visit Google for how to see console output from builds. If you are running a mobile device you can also view the console output. Google for how on your particular mobile target, such as this answer for iOS: How To - Capturing Device Logs on iOS or this answer for Android: How To - Capturing Device Logs on Android

If you are working in VR, it might be useful to make your on onscreen log output, or integrate one from the asset store, so you can see what is happening as you operate your software.

Another useful approach is to temporarily strip out everything besides what is necessary to prove your issue. This can simplify and isolate compounding effects of other items in your scene or prefab.

Here’s an example of putting in a laser-focused Debug.Log() and how that can save you a TON of time wallowing around speculating what might be going wrong:

“When in doubt, print it out!™” - Kurt Dekker (and many others)

Note: the print() function is an alias for Debug.Log() provided by the MonoBehaviour class.


Load/Save steps:

An excellent discussion of loading/saving in Unity3D by Xarbrough:

Loading/Saving ScriptableObjects by a proxy identifier such as name:

When loading, you can never re-create a MonoBehaviour or ScriptableObject instance directly from JSON. The reason is they are hybrid C# and native engine objects, and when the JSON package calls new to make one, it cannot make the native engine portion of the object.

Instead you must first create the MonoBehaviour using AddComponent() on a GameObject instance, or use ScriptableObject.CreateInstance() to make your SO, then use the appropriate JSON “populate object” call to fill in its public fields.

If you want to use PlayerPrefs to save your game, it’s always better to use a JSON-based wrapper such as this one I forked from a fellow named Brett M Johnson on github:

Do not use the binary formatter/serializer: it is insecure, it cannot be made secure, and it makes debugging very difficult, plus it actually will NOT prevent people from modifying your save data on their computers.

I know what the problem is, I’m calling in Start the player position from scriptable object, so it is therefore ignoring the saved data of the actual player position, when play mode is quit. I therefore need to update the scriptable object with that data.

I’m using a collider to transition between the main world and a building, which is on a ‘Door’. So from a building to the main world I have an x position of say 5, and y -2. So the this is where the player will then spawn in the main world using the scriptable object. Likewise from the main world to the building the x position could be -2, and y 0.

Would there be a better way of making the player spwan in those positions instead of using a scriptable object, or would it be better just to update the save system to be able to update the scriptable object?

Steps to success:

  • have a default notion of where to spawn the player baked into the level

  • read that into a variable (or variables: might be position and rotation?)

  • see if there is saved data specifying the player should go elsewhere

  • if so, overwrite the variables with that save data

  • spawn your player at the correct location.

I have implemented a GameManager script, and I’m using this to store where the player should spawn when transferring scenes. This works with saving the player position as well, so I have got that aspect to work.

My other problem is loading the correct scene. I currently have just three scenes to keep things simple, they are a menu, a main level and then a building.

The menu has two scripts a main menu and a save slot menu. The main menu has this method for if the user presses the continue game button:

public void OnContinueGame()
    {
        DisableMenuButtons();

        DataPersistenceManager.instance.SaveGame();

        SceneManager.LoadSceneAsync("MainLevel");
    }

On the save slot menu I have these two methods:

public void OnSaveSlotClicked(SaveSlot saveSlot)
    {
        //disable all buttons
        DisableMenuButtons();

        //case - loading game
        if(isLoadingGame)
        {
            DataPersistenceManager.instance.ChangeSelectedProfileId(saveSlot.GetProfileId());
            SaveGameAndLoadScene();
        }
        //case - new game, but the save slot has data
        else if (saveSlot.hasData)
        {
            confirmationPopupMenu.ActivateMenu("Starting a New Game with this slot will override the currently saved data. Are you sure?",
                //function to execute if we select 'yes'
                () => {
                    DataPersistenceManager.instance.ChangeSelectedProfileId(saveSlot.GetProfileId());
                    DataPersistenceManager.instance.NewGame();
                    SaveGameAndLoadScene();
                },
                //function to execute if we select 'cancel'
                () => {
                    this.ActiveMenu(isLoadingGame);
                }
            );
        }
        //case - new game, and the save slot has no data
        else
        {
            DataPersistenceManager.instance.ChangeSelectedProfileId(saveSlot.GetProfileId());
            DataPersistenceManager.instance.NewGame();
            SaveGameAndLoadScene();
        }

    }

    private void SaveGameAndLoadScene()
    {
        //save the game anytime before loading a new game
        DataPersistenceManager.instance.SaveGame();
        //load the scene, which will in turn save the game
        SceneManager.LoadSceneAsync("MainLevel");
    }

So it will always load the main level scene. Is there a way to load the last saved scene that Unity has built in, or do I need to save the name of the last played scene and then pass the name to that method?

I have seen I can either use playerprefs of use the game manager I have created. I assume it would be better to use the game manager?

Considering you’re using a rigidbody, the issue probably is just that you’re not setting the position via physics. You should be using Rigibody2D.MovePosition.

Sort … of… yes for physics normal moves.

But this also “sweeps out” the collision, especially if in one of the Continuous collision detection modes.

Likely you wouldn’t want this at the start of your game, just to get to the correct spawn.

If OP wants to force a physics thing to a particular position you want to just set the Rigidbody.position directly.

And set Rigidbody.rotation directly too, for the same reason.

That’s how I would do it. That way lets you just see the scene as part of the save data if you are debugging.

Yeah you’re right there.

The issue might also be down trying to set the position of a rigidbody outside the physics loop, before something else also tries to move/position it within FixedUpdate. Whatever comes last, I believe, will ultimately win out.

I’ve had a few situations like this where I’m trying to position a rigidbody outside of fixed update (such as a checkpoint system), and, unsurprisingly, nothing happens. A few ways to get around this, such as just deferring the execution until the next FixedUpdate call.

1 Like