For the first part, and the easier to implement and grasp version of saving and loading, but less secure and scalable, read:

How To Handle Saving And Loading In Unity: Part One – Player Prefs.



Since this is part two; I’ll just do a quick recap on what this tutorial is about:

Essentially Saving and Loading is a scary concept to approach as it needs to be handled right, but it’s surprisingly easy to work with once you have a grasp of the concept.

Player Prefs can be implemented quickly, but is not secure and has limited scaling. Serialization takes a little longer and is more difficult to grasp, but is easily scalable, and much more secure if you’re handling sensitive information.

So let’s dive right in!

Serialization

What exactly is Serialization?

Quite simply, the Microsoft Documentation on Serialization for C# puts it best :

Serialization is the process of converting an object into a stream of bytes to store the object or transmit it to memory, a database, or a file. Its main purpose is to save the state of an object in order to be able to recreate it when needed. The reverse process is called deserialization. Microsoft Documentation –

https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/serialization/

This probably describes more conventionally what you would expect a Save File to be, and do; as opposed to Player Prefs which is Unity Specific. What this means, it that all we need to make this work, is an Object we want to Serialize, and some C# libraries. With C# being an Object Oriented Programming Language, and Unity being built upon Game Objects, we need very little to actually get us up and running.

In no time, wWhen we’re done with this tutorial, we’ll have a .dat file, that consists of some variable names, and the values we hold within them. Due to the conversion, even if people were to discover your file, there is little they can do to alter the values of it, and alter our information. Especially with a little bit of creative honey-potting*.

Implementation of Serialization

So for illustrative purposes, the example we’re going to use is that we’re making a Sci-Fi First Person Shooter, where we wan to keep track of certain Weapons we’ve unlocked throughout the game. We also want to keep track of the player level we’ve reached, and our player’s name.

Let’s set up a scene same as before, although this time we’re going to dive more into what Serialization can do, so we won’t spend time setting up buttons for everything:

Create a new project and a new scene in Unity. Call both the project and scene whatever you feel like.

Go to the GameObject Menu at the top of the screen, and select Empty GameObject. Click on the new GameObject, and rename it to “GameController”. Go to Add Component, and add a new Script Called “GameControllerScript”.

Go to the GameObject Menu at the top of the screen, and select Empty GameObject. Click on the new GameObject, and rename it to “SavingLoadingController”. Go to Add Component, and add a new Script Called “SavingLoadingScript”.







Below I’ve included both scripts. Simply copy and paste the GameControllerScript below into the corresponding Script in your scene, and do the same for the SavingLoadingScript.

using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameControllerScript : MonoBehaviour { /* Normally the following variables should be private and we would * access them only using the functions below. However, to allow us * to edit the variables in the inspector while we test, we'll keep * them as public. */ public List<string> weaponsUnlocked; public string playerName; public int playerLevel; public void SetWeaponsUnlocked(List<string> w){ weaponsUnlocked = w; } public void SetPlayerName(string p){ playerName = p; } public void SetPlayerLevel(int p){ playerLevel = p; } public List<string> GetWeaponsUnlocked(){ return weaponsUnlocked; } public string GetPlayerName(){ return playerName; } public int GetPlayerLevel(){ return playerLevel; } }

While You’re Here – Check Out Some Of My Most Recent Dev Logs!

using System.Collections; using System.Collections.Generic; using UnityEngine; using System; using System.Runtime.Serialization.Formatters.Binary; // Needed to format binary files using System.IO; // Need to access filing system using UnityEditor; public class SavingLoadingScript : MonoBehaviour { public GameControllerScript gameControllerRef; private const string GAME_SAVE = "/game_save.dat"; private void OnEnable() { if (!gameControllerRef) { // If we haven't set the Game Controller Ref in the inspector. gameControllerRef = GameObject.Find("GameController").GetComponent<GameControllerScript>(); } Load(); // Load our Data } private void OnDisable() { Save(); // Save our game on level quit. } public void Load() { Debug.Log("Loading Game..."); if (File.Exists(Application.persistentDataPath + GAME_SAVE)) { BinaryFormatter bf = new BinaryFormatter(); FileStream file = File.Open(Application.persistentDataPath + GAME_SAVE, FileMode.Open); GameData data = (GameData)bf.Deserialize(file); file.Close(); gameControllerRef.SetWeaponsUnlocked(data.weaponsUnlocked); gameControllerRef.SetPlayerName(data.playerName); gameControllerRef.SetPlayerLevel(data.playerLevel); Debug.Log("Load Complete."); } else { Debug.Log("No Save File Found"); } } public void Save() { Debug.Log("Saving Game..."); BinaryFormatter bf = new BinaryFormatter(); FileStream file = File.Open(Application.persistentDataPath + GAME_SAVE, FileMode.Create); GameData data = new GameData { weaponsUnlocked = gameControllerRef.GetWeaponsUnlocked(), playerName = gameControllerRef.GetPlayerName(), playerLevel = gameControllerRef.GetPlayerLevel() }; bf.Serialize(file, data); file.Close(); Debug.Log("Save Successful!"); } public void Delete() { Debug.Log("Delete Save File"); if (File.Exists(Application.persistentDataPath + GAME_SAVE)) { File.Delete(Application.persistentDataPath + GAME_SAVE); Caching.ClearCache(); #if UNTIY_EDITOR UnityEditor.AssetDatabase.Refresh(); #endif Debug.Log("Save File Deleted"); } else { Debug.Log("Deletion Failed: Could not find save file."); } } } [Serializable] class GameData { //Game Save Data public List<string> weaponsUnlocked; public string playerName; public int playerLevel; }

Now that we’ve got our scripts in the respective GameObjects, we can Play our scene.

On the first play, since we haven’t actually SAVED our game yet, nothing will load. You’ll get the Debug Log that we set saying, “No Save File Found.” – Don’t worry – of course we’ve expected that and anticipated it.

Navigate to your GameController game object, and edit the values we created. Change the player name to something memorable, or absurd, edit the player level too, and add a few values to our list.

When you’re done editing the scene, stop it, and thanks to our OnDisable method which Saves the game as the last thing we do before the game ends, when we replay the same scene, our saved values will be there!





And that’s it! Our files are securely being save and loaded at Runtime!

Now this is all your need to get Serialization working, and to expand or alter this list, simply add variable to both Scripts – but let’s look a little closer at exactly what’s going on and why.

Breakdown

I’m not going to break down what the GameControllerScript does, because it simply holds variables we can then use or reference in our scene later on. So for instance in our example, if we load a weapon prefab, but we want to know if the player has unlocked it or not, we would simply iterate through the weaponsUnlocked List of the GameControllerScript, and if it’s found, keep it, if it’s not, GET RID.

The SavingLoadingScript however, looks like it has a lot going on, but it’s surprisingly easy to understand when broken down. First we grab the libraries we need, and create our Class:

using System.Collections; using System.Collections.Generic; using UnityEngine; using System; using System.Runtime.Serialization.Formatters.Binary; // Needed to format binary files using System.IO; // Need to access filing system using UnityEditor; public class SavingLoadingScript : MonoBehaviour { public GameControllerScript gameControllerRef; private const string GAME_SAVE = "/game_save.dat";

We also need a reference to our GameControllerScript so that we can actually edit it, and for safety (so we don’t misspell our game_save data file later on) we set a private const string GAME_SAVE.

Next, we add our OnEnable and OnDisable methods, which will allows us to Load the Game on Scene Start, and Save it when the Scene Ends.

private void OnEnable() { if (!gameControllerRef) { // If we haven't set the Game Controller Ref in the inspector. gameControllerRef = GameObject.Find("GameController").GetComponent<GameControllerScript>(); } Load(); // Load our Data } private void OnDisable() { Save(); // Save our game on level quit. }

Now we’re actually going to skip the saving and loading methods, and come back to them in a second. You see, as per the Microsoft C# documentation at the top of the page, we actually need an object to save in order for this to all work. So we need to do that first, and that’s where the GameData class at the bottom of our script comes into play.

[Serializable] class GameData { //Game Save Data public List<string> weaponsUnlocked; public string playerName; public int playerLevel; }

The “[Serializable]” is required before the class, to even allow the object to be serialized in the first place. Without it, none of this script will work, so be sure that’s included. Then it’s simply a case of creating variables that will hold our save data.

*Note, that there are some restrictions to saving, like you cannot simply save a whole GameObject unfortunately. There are workarounds to this, such as saving the variables instead, and then using them in a newly instantiated prefab when you load the game.

Now that we have our GameData object, we can look into what’s happening when we load that object!

public void Load() { Debug.Log("Loading Game..."); if (File.Exists(Application.persistentDataPath + GAME_SAVE)) // If the file exists { BinaryFormatter bf = new BinaryFormatter(); // Create the binary formatter which will allow our file to be converted from bytes, to usable variables etc FileStream file = File.Open(Application.persistentDataPath + GAME_SAVE, FileMode.Open); // Get the file GameData data = (GameData)bf.Deserialize(file); // Create an instance of the GameData object and set it to the result of our Deserialized file i.e. our converted file file.Close(); // We're done with the file as we have the object we need, so close it. // Set our GameControllerScript variables to the one's we've found in our saved datafile gameControllerRef.SetWeaponsUnlocked(data.weaponsUnlocked); gameControllerRef.SetPlayerName(data.playerName); gameControllerRef.SetPlayerLevel(data.playerLevel); Debug.Log("Load Complete."); } else // If we can't find the save file, it hasn't been created yet... { Debug.Log("No Save File Found"); } }

In the above I’ve covered everything in the documentation/comments so that you can see exactly what’s going on, line by line. But essentially it’s this:

Check Save File Exists -> Create Binary Formatter Instance ->Get The Save File -> Create A New Game Data Object and Set The Values Based on the Values of the Binary Converted Save File -> Close the Save File -> Set The GameControllerScript variables to the Save File Ones.

And quite simply, the Save Method works in a similar fashion, but instead of getting the values, it’s setting them. So it’s pretty much doing the same thing, but the key parts are reversed:

public void Save() { Debug.Log("Saving Game..."); BinaryFormatter bf = new BinaryFormatter(); // Create the binary formatter which will allow our file to be converted from variables to bytes for storage FileStream file = File.Open(Application.persistentDataPath + GAME_SAVE, FileMode.Create); // Create our file in the location specified // Create a new Game Data object and set its values to the values we have in our GameControllerScript GameData data = new GameData { weaponsUnlocked = gameControllerRef.GetWeaponsUnlocked(), playerName = gameControllerRef.GetPlayerName(), playerLevel = gameControllerRef.GetPlayerLevel() }; /* Serialize the game file. Essentially write to our file, the * resulting bytes from converting our data GameData object to * bytes. */ bf.Serialize(file, data); file.Close(); // Close the file, since we're done with it Debug.Log("Save Successful!"); }

Again I’ve documented it all, but the saving works as follows:



Create Binary Formatter Instance -> Create The Save File -> Create A New Game Data Object and Set The Values Based on the Values of The GameControllerScript -> Serialize the file.

In a weird way – the loading part actually looks more confusing than the saving part. Saving is, relatively easy!

Lastly, although it has nothing to do with serialization, I’ve included a Delete Method should you want to Delete Your Save File and Reset!

Debug.Log("Delete Save File"); // If the file exists if (File.Exists(Application.persistentDataPath + GAME_SAVE)) { // Delete it File.Delete(Application.persistentDataPath + GAME_SAVE); // Clear any Caches to ensure it doesn't persist Caching.ClearCache(); // If in the Unity Editor, refresh the asset Database #if UNITY_EDITOR UnityEditor.AssetDatabase.Refresh(); #endif Debug.Log("Save File Deleted"); } else { Debug.Log("Deletion Failed: Could not find save file."); }

And that’s it! Everything you should need to know to be able to apply Serialization to your game project!

Of course, there are limitations and if you want to find out more about why they happen or how to work around them, I strongly suggest reading the Unity Docs – but hopefully this Dev Log has helped to De-Demonise the whole concept, and you’ve found it useful enough to implement in your future projects!

Pros and Cons of Serialization

Pros:



+ Secure

+ Scalable

+ Reliable Cons:



-More difficult to grasp

-A little more clunky for quick testing

Essentially, Serialization is the way to go, once your project gets going, and certainly it’s required if ever you want to hold any sensitive information. It has very few limitations on DataTypes it can hold in terms of C# DataTypes, it just so happens that it doesn’t like holding entire GameObjects as this is Unity Specific. But as mentioned, there are workarounds to this.





Conclusion

Thanks very much to those who made it this far! If you enjoyed this, please get vocal in the comments, like, follow, and of course check out the rest of the content both here, and on Twitter @dalriadaconnor

I really appreciated the response to the first part of this tutorial, so hopefully you like this one too.

If you have any more questions also, please feel free to ask questions in the comments, and ask questions on Twitter too! Additionally, if you feel any more information may be helpful, feel free to get in touch there too!

Until next time!

BONUS

As promised – a bonus Unity Tip that will save you countless hours in the long run! It’s such a useful tip that I might make a tutorial on this alone one day, just to spread the word – but for now I’ll include it below. It’s saved me countless hours already while I develop Pathfinder, the mobile game I’m working on (check out the other Dev Logs on this site)

What is this Bonus? … Personalised Menu Items / Development Tools! See the screenshot below, where I’ve created my own personal menu item at the top of my Editor, and I can then use this to achieve Saving and Loading etc, or whatever I want!

To do this, simply create a new Script in your Unity Project. I like to name it something like “DevelopmentToolsScript” so that’s what I’ll use, but you can name it anything you want.

Then simply add the following code :

using UnityEngine; using UnityEditor; #if (UNITY_EDITOR) // Only apply the below code if we're in the editor, don't attempt it in the build! public class MenuItems { private const string MENU_NAME = "Development Tools/"; // Create a constant for our menu name, to keep things consistent private const string SAVING_MENU = "Saving/"; // Create a constant for our saving name, to keep things consistent // Saves Game [MenuItem(MENU_NAME + SAVING_MENU + "Save", false, 1)] private static void Save() { SavingLoadingScript saveRef = GameObject.Find("SavingLoadingController").GetComponent<SavingLoadingScript>(); saveRef.Save(); } // Loads Game [MenuItem(MENU_NAME + SAVING_MENU + "Load", false, 2)] private static void Load() { SavingLoadingScript loadRef = GameObject.Find("SavingLoadingController").GetComponent<SavingLoadingScript>(); loadRef.Load(); } // Delete Save [MenuItem(MENU_NAME + SAVING_MENU + "Delete Save", false, 3)] private static void DeleteSave() { SavingLoadingScript deleteRef = GameObject.Find("SavingLoadingController").GetComponent<SavingLoadingScript>(); deleteRef.Delete(); } } #endif

That is literally it… Such an easy tip, but just a hard to find one. If you go back to your Unity Scene, you’ll see your menu at the top updated, and you can now click any of these to Save, Load, or Delete your save files!

To add more, simply copy everything between the commented lines, and use the same syntax.

The MenuItem syntax goes as follows, MenuItem(string itemName, bool isValidateFunction, int priority)

itemName The itemName is the menu item represented like a pathname. For example the menu item could be “GameObject/Do Something”. isValidateFunction If isValidateFunction is true, this is a validation function and will be called before invoking the menu function with the same itemName . priority The order by which the menu items are displayed.

Then add the function you would like to attach to it, directly below!

Enjoy this handy tip!

*Side note – all functions underneath it, must be static functions. You can read up more on why this is: in the Unity Documentation for Menu Items.

*Honeypotting –

https://en.wikipedia.org/wiki/Honeypot_(computing) Interestingly enough… Don’t look this up without safe search on.