Friday 19 December 2014

How to Save and Load Your Players' Progress in Unity

In this tutorial, you'll learn how to implement a simple system to create and manage savegames for your Unity games. We will build the framework for a Final Fantasy-like main menu that enables players to create new, unique save files, and to load existing ones. The principles demonstrated will allow you to extend them to whatever needs your game has. 
By the end of the tutorial, you will have learned how to:
  • save and load game data within Unity3D using serialization
  • use static variables to persist data across scene changes
Note: This approach to saving and loading game data works on all platforms except for the Web Player. For information on saving game data in the Web Player, take a look at the official docs on Unity Web Player and browser communication.
The first thing we're going to do is to create some code that allows us to serialize our game data—that is, convert it to a format that can be saved and later restored. For this, let's create a C# script and call it SaveLoad. This script will handle all the saving and loading functionality. 
We will reference this script from other scripts, so let's make it a static class by adding the word static between public and class. Let's also remove the : MonoBehaviour part, because we don't need to attach it to a GameObject. And since it no longer inherits from MonoBehaviour, let's delete the Start and Updatefunctions. 
The resulting code should look like this:
1
2
3
4
5
6
using UnityEngine;
using System.Collections;
 
public static class SaveLoad {
 
}
Now, we're going to want to add some new functionality to this script, so immediately under where it says using System.Collections;, add the following:
1
2
3
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
The first line allows us to use dynamic lists in C#, but this is not necessary for serialization. The second line is what enables us to use the operating system's serialization capabilities within the script. In the third line, IO stands forInput/Output, and is what allows us to write to and read from our computer or mobile device. In other words, this line allows us to create unique files and then read from those files later.
We're now ready to serialize some data!
Now that our script has the ability to serialize, we are going to have to set up some classes to be serialized. If you think about a basic RPG, like Final Fantasy, it offers players the ability to create and load different saved games. So, create a new C# script called Game and give it some variables to hold three objects: a knight, arogue, and a wizard. Change the code of this script to look like this:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
using UnityEngine;
using System.Collections;
 
[System.Serializable]
public class Game {
 
    public static Game current;
    public Character knight;
    public Character rogue;
    public Character wizard;
 
    public Game () {
        knight = new Character();
        rogue = new Character();
        wizard = new Character();
    }
         
}
The [System.Serializable] line tells Unity that this script can be serialized—in other words, that we can save all the variables in this script. Cool! According to the official docs, Unity can serialize the following types:
  • All basic data types (like intstringfloat, and bool).
  • Some built-in types (including Vector2Vector3Vector4Quaternion,Matrix4x4ColorRect, and LayerMask).
  • All classes inheriting from UnityEngine.Object (including GameObject,ComponentMonoBehaviorTexture2D, and AnimationClip).
  • Enums.
  • Arrays and lists of a serializable type.
The first variable, current, is a static reference to a Game instance. When we create or load a game, we're going to set this static variable to that particular game instance so that we can reference the "current game" from anywhere in the project. By using static variables and functions, we don't have to use a gameObject'sGetComponent() function. Handy! 
Notice that it's referencing something called a Character? We don't have that yet, so let's create a new script to house this class, and call it Character:
01
02
03
04
05
06
07
08
09
10
11
12
using UnityEngine;
using System.Collections;
 
[System.Serializable]
public class Character {
 
    public string name;
 
    public Character () {
        this.name = "";
    }
}
You may be wondering why we needed a whole new class if we're just storing a string variable. Indeed, we could just replace Character in the Game script to usestring instead. But I want to show you how deep this rabbit hole can go: you can save and load classes that reference other classes, and so on, as long as each class is serializable
Now that our classes are set up to be saved and loaded, let's hop back over to ourSaveLoad script and add the ability to save games.
A "Load Game" menu  usually shows a list of saved games, so let's create a Listof type Game and call it savedGames. Make it a static List, so that there's only one list of saved games in our project. The code should look like this:
01
02
03
04
05
06
07
08
09
10
11
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
 
public static class SaveLoad {
 
    public static List<Game> savedGames = new List<Game>();
 
}
Next, let's create a new static function to save a game:
1
2
3
4
5
6
7
public static void Save() {
    savedGames.Add(Game.current);
    BinaryFormatter bf = new BinaryFormatter();
    FileStream file = File.Create (Application.persistentDataPath + "/savedGames.gd");
    bf.Serialize(file, SaveLoad.savedGames);
    file.Close();
}
Line 2 adds our current game to our list of saved games. That list is what we're going to serialize. To do so, we first need to create a new BinaryFormatter, which will handle the serialization work. This is what Line 3 does.
In Line 4, we're creating a FileStream, which is essentially a pathway to a new file that we can send data too, like fish swimming downstream in a river. We useFile.Create() to create a new file at the location we pass in as its parameter. Conveniently, Unity has a built-in location to store our game files (which updates automatically based on what platform your game is built to) that we can reference using Application.persistentDataPath
Since we're creating a new file, however, we can't just say where the file is, we also have to cap off this pathway with the name of the actual file itself. There are two parts to this file:
  1. the file name
  2. the file type
We'll use savedGames for the file name, and we'll use a custom data type gd (for "game data") for the file type. Our result is a game file called savedGames.gd at the location set by Application.persistentDataPath. (In the future, you could save other types of things to this data type; for example, you could save the users' options settings as options.gd.)
Note: You can make the file type anything you want. For example, the Elder Scrolls series uses .esm as its file type. You could have as easily saidsavedGames.baconAndGravy.
In Line 5, we're calling the Serialize functionality of the BinaryFormatter to save our savedGames list to our new file. After that, we have the close the file that we created, in Line 6. 
Badda bing, badda boom. Our games are saved.
In the Save function, we serialized our list of saved games at a specific location. Conversely, the code to load our games should look like this:
1
2
3
4
5
6
7
8
public static void Load() {
    if(File.Exists(Application.persistentDataPath + "/savedGames.gd")) {
        BinaryFormatter bf = new BinaryFormatter();
        FileStream file = File.Open(Application.persistentDataPath + "/savedGames.gd", FileMode.Open);
        SaveLoad.savedGames = (List<Game>)bf.Deserialize(file);
        file.Close();
    }
}
In Line 2, we check whether a saved game file exists. (If it doesn't, there will be nothing to load, obviously.) In Line 3, we create a BinaryFormatter the same way we did in the Save function. In Line 4, we create a FileStream—but this time, our fish are swimming upstream from the file. Thus, we use File.Open, and point to where our savedGames.gd exists using the same Application.persistentDataPathstring. 
Line 5 is a bit dense, so let's unpack it: 
  • The bf.Deserialize(file) call finds the file at the location we specified above and deserializes it. 
  • We can't just spit binary at Unity and expect it to work, however, so we convert (or cast) our deserialized file to the data type we want it to be, which in this case is a List of type Game. 
  • We then set that list as our list of saved games. 
Lastly, in Line 6, we close that file the same way we did in the Save function. 
Note: The data type to which you cast the deserialized data can change depending on what you need it to be. For example, Player.lives = (int)bf.Deserialize(file);.
Our SaveLoad script is now complete, and should look like this:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
 
public static class SaveLoad {
 
    public static List<Game> savedGames = new List<Game>();
             
    //it's static so we can call it from anywhere
    public static void Save() {
        SaveLoad.savedGames.Add(Game.current);
        BinaryFormatter bf = new BinaryFormatter();
        //Application.persistentDataPath is a string, so if you wanted you can put that into debug.log if you want to know where save games are located
        FileStream file = File.Create (Application.persistentDataPath + "/savedGames.gd"); //you can call it anything you want
        bf.Serialize(file, SaveLoad.savedGames);
        file.Close();
    }  
     
    public static void Load() {
        if(File.Exists(Application.persistentDataPath + "/savedGames.gd")) {
            BinaryFormatter bf = new BinaryFormatter();
            FileStream file = File.Open(Application.persistentDataPath + "/savedGames.gd", FileMode.Open);
            SaveLoad.savedGames = (List<Game>)bf.Deserialize(file);
            file.Close();
        }
    }
}
Those are the basics of saving and loading in Unity. In the attached project file, you'll find some other scripts which show how I handle calling these functions and how I display the data using Unity's GUI.

No comments:

Post a Comment