Storing Local Data - PlayerPrefs and Serialization
Part 1 - Intro to PlayerPrefs
PlayerPrefs is intended to store Preferences or Options, however it provides us a reasonable 3mb storage cache that is internally built-in to Unity so we can actually store data without access to the local devices file-system or a server. This is very useful when attempting to store small amounts of data. The main limitation of PlayerPrefs is that it can only store very specifci types of data. The folowing data types can be stored this way:
- Int (Integer data, being only whole numbers).
- Float (Floating Point number which can be positive, negative, or contain decimals).
- String (A set of characters comprising a string or word).
As you can see these primitive data types can be a bit restricting because they will not allow us to store complex objects, however most complex objects are comprised of these simple data types. If used creatively, you can store nearly anything this way. As well one of the main bonuses to using PlayerPrefs is that the Application can store data on a mobile device without requesting storage access.
PlayerPrefs has two main functions for each data type, being Get and Set, which act very much like a Dictionary because they use a string as the key. An example usage of setting a PlayerPref would look like this.
PlayerPrefs.SetInt("myPref", 5);
And an example of getting the data from PlayerPrefs would look like this.
myint = PlayerPrefs.GetInt("myInt");
Which can be overloaded to include a default parameter if none has been assigned, like this.
myint = PlayerPrefs.GetInt("myInt", 5);
As you can see, using PlayerPrefs is very simple. Next we will discuss how to use the effectively to store Player data.
Part 2 - Using PlayerPrefs Effectively
Now we just discussed how PlayerPrefs works, but before we can use it effectively we need to understand Getting/Setting.
Every attribute or field if you will, that is contained in a class has a built-in GET and SET function that is called whenever it is either referenced, or being set. We are going to take advantage of these functions and basically turn our Player attributes into references to PlayerPrefs. So for our example, lets assume our Player has the following attributes.
A Name (being a string).
Experience (being an integer).
A Level (being an integer).
An amount of money (being a float, we’ll use the decimals as change).
So we will representing each one of these in our Player class. Normally these would be represented in a simple manner like this.
using UnityEngine;
namespace MyProject
{
public class Player
{
public string Name;
public int Level;
public int Experience;
public float Money;
}
}
This is the simplest way of representing these attributes in the Player class, but we will need to alter them to become purely reference attributes, and we will create a set of constant strings to represent each attributes key. So to make it simpler, we’ll add each constant string above the actual attribute like so.
private const string name = “Name”;
public string Name;
Then we will want to extend each attribute to have a get and set function that actually calls to PlayerPrefs, like so.
using UnityEngine;
namespace MyProject
{
public class Player
{
private const string name = "Name";
public string Name
{
get { return PlayerPrefs.GetString(name); }
set { PlayerPrefs.SetString(name, value); }
}
private const string level = "Level";
public int Level
{
get { return PlayerPrefs.GetInt(level, 1); }
set { PlayerPrefs.SetInt(level, value); }
}
private const string exp = "Experience";
public int Experience
{
get { return PlayerPrefs.GetInt(exp); }
set { PlayerPrefs.SetInt(exp, value); }
}
private const string money = "Money";
public float Money
{
get { return PlayerPrefs.GetFloat(money); }
set { PlayerPrefs.SetFloat(money, value); }
}
}
}
Another way would be to use PlayerPrefs as a sort of Save/Load Method, and have the ability to save multiple versions of the Player, simply by passing an integer of a save slot, like this.
using UnityEngine;
namespace MyProject
{
public class Player
{
private const string name = "Name";
public string Name;
private const string level = "Level";
public int Level;
private const string exp = "Experience";
public int Experience;
private const string money = "Money";
public float Money;
public void Save(int SaveSlot)
{
PlayerPrefs.SetString("SaveSlot_" + SaveSlot.ToString() + "_" + name, Name);
PlayerPrefs.SetInt("SaveSlot_" + SaveSlot.ToString() + "_" + level, Level);
PlayerPrefs.SetInt("SaveSlot_" + SaveSlot.ToString() + "_" + exp, Experience);
PlayerPrefs.SetFloat("SaveSlot_" + SaveSlot.ToString() + "_" + money, Money);
}
public void Load(int SaveSlot)
{
Name = PlayerPrefs.GetString("SaveSlot_" + SaveSlot.ToString() + "_" + name, Name);
Level = PlayerPrefs.GetInt("SaveSlot_" + SaveSlot.ToString() + "_" + level, Level);
Experience = PlayerPrefs.GetInt("SaveSlot_" + SaveSlot.ToString() + "_" + exp, Experience);
Money = PlayerPrefs.GetFloat("SaveSlot_" + SaveSlot.ToString() + "_" + money, Money);
}
}
}
As you can see, we are utilizing the key string a little more in-depth by adding the slot and the attribute name to get the attribute itself. There are other ways of utilizing PlayerPrefs, but I hope this helps you to understand how it can be used to save simple data in an effective and easy way. Of course, this is not always enough, sometimes you need to save entire objects or a current state of the game. Which brings us to our next section.
Part 3 - Serializing Complex Objects
There are many types of serialization, but we are going to choose the most common type known as Binary Serialization. We will be serializing and deserializing our objects from the file-system. Effectively saving and loading the state of the object. Another effective use of a Binary Formatter is the ability to clone objects with it, you can serialize, and then deserialize an object, effectively creating a completely seperate copy.
For this example, lets go back to the basic Player class, with one addition, we will add [Serializable] above the class name.
using System;
using UnityEngine;
namespace MyProject
{
[Serializable]
public class Player
{
public string Name;
public int Level;
public int Experience;
public float Money;
}
}
This is what allows us to Serialize the object, otherwise we will get an error from attempting to serialize and object not marked as [Serializable].
Because we won’t be needing our serializer constantly, we will make a new object to handle the process that we can create when needed.
This class will contain a Save and Load method, which create a temporary Binary Formatter, and a FileStream. An important thing to note about a FileStream is that is must be closed with a Close() call, to free up the resources.
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;
namespace MyProject
{
public class StorageHandler
{
/// <summary>
/// Serialize an object to the devices File System.
/// </summary>
/// <param name="objectToSave">The Object that will be Serialized.</param>
/// <param name="fileName">Name of the file to be Serialized.</param>
public void SaveData(object objectToSave, string fileName)
{
// Add the File Path together with the files name and extension.
// We will use .bin to represent that this is a Binary file.
string FullFilePath = Application.persistentDataPath + "/" + fileName + ".bin";
// We must create a new Formattwr to Serialize with.
BinaryFormatter Formatter = new BinaryFormatter();
// Create a streaming path to our new file location.
FileStream fileStream = new FileStream(FullFilePath, FileMode.Create);
// Serialize the objedt to the File Stream
Formatter.Serialize(fileStream, objectToSave);
// FInally Close the FileStream and let the rest wrap itself up.
fileStream.Close();
}
/// <summary>
/// Deserialize an object from the FileSystem.
/// </summary>
/// <param name="fileName">Name of the file to deserialize.</param>
/// <returns>Deserialized Object</returns>
public object LoadData(string fileName)
{
string FullFilePath = Application.persistentDataPath + "/" + fileName + ".bin";
// Check if our file exists, if it does not, just return a null object.
if (File.Exists(FullFilePath))
{
BinaryFormatter Formatter = new BinaryFormatter();
FileStream fileStream = new FileStream(FullFilePath, FileMode.Open);
object obj = Formatter.Deserialize(fileStream);
fileStream.Close();
// Return the uncast untyped object.
return obj;
}
else
{
return null;
}
}
}
}
This will act as a creatable object that can be used to the purpose of saving and loading. This should be called from a location outside the player class, it is usually best to have some sort of maangement class, you could easily have a static management class that contains your data objects and the methods to save and load them. For example:
namespace MyProject
{
public static class GameManager
{
/// <summary>
/// Current player of this instance of game.
/// </summary>
public static Player player
{
// Default retrieval call
get
{
if (_player == null)
{
StorageHandler storageHandler = new StorageHandler();
_player = (Player)storageHandler.LoadData("player");
if (_player == null)
{
_player = new Player();
}
}
return _player;
}
// Default defining call
set
{
_player = value;
}
}
private static Player _player;
/// <summary>
/// Save Player data to the file system
/// </summary>
private static void Save()
{
StorageHandler storageHandler = new StorageHandler();
storageHandler.SaveData(player, "player");
}
}
}
Now you see how easy it is to use our saving and loading system. This system can be used to save and load any Serializable object that you have. If a serializable object contains an attribute that is another object, that object must also be
This is just one example of using Serialization to store data, but this works on all devices. You need to make sure that for mobile devices, you request access to the
card, even if it doesn’t have one, this is what defines that our app has access to read and write.
Between these 2 methods of storing data, you should have no problem saving your players progress! May the code be with you, and I hope you enjoyed this tutorial.