Working with settings

Saving data is always important, especially in games where you need to keep track of the player's progress or at the very least a track record of scores, plays, and other important data.

Within Unity, there is only one method of storing data natively, and that is PlayerPrefs. It is very simple to use and very flexible, although it does have a hard limit of 1 MB of storage for the web player. It is possible to serialize data into PlayerPrefs (and some developers do this), but generally if you need to serialize, most developers build their own system.

Using PlayerPrefs

PlayerPrefs is simply a key dictionary to store individual variables as a key in the Unity runtime data store. On its own, it has to read each and every scene at runtime, which is why most games use a static class to keep the state stored in PlayerPrefs and only use it between scenes for scene-specific configuration.

Using PlayerPrefs is very easy and simple. The process is the same as any other dictionary to save a setting for your call:

PlayerPrefs.SetInt("PlayerScore", currentScore);
PlayerPrefs.SetFloat("PlayerDamage", currentDamage);
PlayerPrefs.SetString("PlayerName", currentPlayerName);

Loading it back again when you need it again involves the following code:

currentScore = PlayerPrefs.GetInt("PlayerScore");
currentDamage = PlayerPrefs.GetFloat("PlayerDamage");
currentPlayerName = PlayerPrefs.GetString("PlayerName");

You can also supply defaults to values with a second parameter if the setting does not yet exist, as follows:

currentScore = PlayerPrefs.GetInt("PlayerScore", 0);
currentDamage = PlayerPrefs.GetFloat("PlayerDamage", 0);
currentPlayerName = PlayerPrefs.GetString
  ("PlayerName", "New Player");

By default, Unity will save the settings to disk when the application is closed. However, it's recommended that you save them intermittently when possible by calling the following:

PlayerPrefs.Save()

Note

Saving settings in Unity isn't necessarily a given and should not be treated as safe. The settings file has a hard limit of 1 MB of storage on the web player. If this is exceeded, it will throw an exception. This limit is per application.

So, you can either drastically limit what settings you store (recommended) or wrap your SET PlayerPrefs calls in a try/catch statement to be safe if you plan to deploy to the web player.

Other platforms do not have this limitation.

There are also delete functions to remove either a single key or to clear the cache completely.

For more information about PlayerPrefs, see the Unity reference guide at https://docs.unity3d.com/Documentation/ScriptReference/PlayerPrefs.html.

Serializing your data

To store any kind of complicated data or structure, you need to serialize it into a concatenated format. The result can then be stored in PlayerPref as mentioned previously or saved on a disk or the Web.

There are several types of serializers you can use, including the following:

  • Binary serialization: This is binary-formatted output and is non-human readable
  • XML serialization: This is the basic text output formatted into XML and is human readable
  • JSON serialization: This is a compressed standalone output in XML format; it is human readable and allows you to have a manual implementation
  • Custom serialization: This is DIY and is used to build your own serialized output

Each serializer has performance or security gains. There isn't a one size fits all; just choose the serializer that fits your purposes.

For our example, we will enhance our game to save our player's state. First, we will create a helper function to do the serialization for us, so create a new script called SerializationHelper in AssetsScriptsClasses and replace its contents with the following code:

using System.IO;
using System.Xml.Serialization;
 
public class SerilizerHelper {
}

Now, in this script, we will add two functions: one to serialize our player (pack it up) and one to deserialize it (unpack it). The serialize function is as follows:

public static byte[] Serialise<T>(T input)
{
    byte[] output = null;
    //Create an XML formatter
    var serializer = new XmlSerializer(typeof(T));
    try
    {
        //Create an in memory stream to hold our serialized output
        using (var stream = new MemoryStream())
        {
            //Serialize the data
            serializer.Serialize(stream, input);
            //Get the serialized output
            output = stream.GetBuffer();
        }
    }
    catch { }
 
    //Return the serialized output
    return output;
}

Note

I've implemented the serialization function using C# generics (type <T>). This allows you to build a function that will work for any type of class you supply it with. This saves us from creating a serialization function for each and every type of data we want to serialize.

To learn more about generics (a fairly advanced topic), check out the MSDN documentation at http://msdn.microsoft.com/en-gb/library/512aeb7t.aspx.

Not all platforms support all serializers, and also, some classes (such as MemoryStream) are not available on all platforms. You will sometimes have to tailor the approach you use to work with other platforms. If you do, however, make sure you do it within the helper classes so that all the platform-variant code is in one place and does not clutter up your game. More on supporting multiple platforms is covered in Chapter 12, Deployment and Beyond.

The code is commented to explain what each step actually does. If you wish, you can store the output of this function in PlayerPrefs. It's more likely, however, that you will either save it to the Web or to a disk using a different buffer than MemoryStream (see the following section). Other serializers work pretty much the same way using a different formatter (for example, binary serialization uses BinarySerialiser).

To deserialize the data, we simply do the reverse:

public static T DeSerialise<T>(Stream input)
{
    T output = default(T);
    //Create an XML formatter
    var serializer = new XmlSerializer(typeof(T));
    try
    {
        //Deserialize the data from the stream
        output = (T)serializer.Deserialize(input);
    }
    catch { }
    //Return the deserialized output
    return output;
}

So as you can see, both patterns are very similar; this just reverses the flow (doesn't cross the streams).

Serialization is important as it can be used anywhere you need to package data to be saved or even transmitted over the wire for a cloud backup or even network play.

For more information about serialization, see the MSDN .NET reference guide at http://msdn.microsoft.com/en-us/library/ms172360(v=vs.110).aspx.

Saving data to disk

A better way to manage your games to save data is to serialize it to disk, a method you will use to determine how fast and secure this is.

Instead of using PlayerPrefs, it is better to manage the saving and loading of your player data to a disk (or the Web; see the following sections). Thankfully, Mono (the C# engine behind Unity3D) and JS provide common functions to access the disk across all the platforms that Unity supports.

Note

There are exceptions, however, due to platform limitations or specializations in some platforms (such as Windows 8, where all disks access are accessed asynchronously). In these cases, Unity provides special classes to access platform components, for example, the UnityEngine.Windows namespace.

You can also write your disk access routines that are more platform-specific if you wish to make them more performant, but this requires you to write an interface and your platform-specific code for each routine (see Chapter 12, Deployment and Beyond, for information on DLL import).

Modeling your saved data

If we look to add the saving and loading options to our game, we need to take a few things into account first. Consider that we just had a basic class for our player's state; the following is an example:

[Serializable]
public struct Player {
 
    public string Name;
    public int Age;
}

Note

We attach the [Serializable] attribute to the class to tell the serializer that it is serializable data. This isn't mandatory as most sterilizers will work with most public classes and serialize the public properties of that class, but not private properties though.

We could then simply save the class directly to the disk. However, because our player definition inherits from our common Entity class and the Entity class inherits from ScriptableObject (so we could use it as a common base for all the characters of our game), this means we cannot perform a simple serialization.

Note

If you wish, you could change this implementation, moving all the properties from the Entity class to the Player class and then marking it as [Serializable]; it's your choice. I've kept it this way to show you the considerations needed to also serialize ScriptableObject. This is especially useful when (like we have in this game) ScriptableObjects are attached to our player, in this case, the player's inventory (the inventory items are part of the project, and we attach them to the player).

So, as the data we want to serialize is more complex, the best thing to do is build a separate Save State class, which will model the data we want to save.

By defining a Save model, we can also tailor it to contain more than just one type of data; it could contain other specific save information, such as the time in the world, enemy progress (if the enemy AI is also marching through the world), and the current state of the global economy. There is something you should keep in mind: it is a fairly common practice to create a separate Save model to save data.

Tip

Alternatively, it is also a good practice to have several save files, some of which you save very frequently (game/world state) and others you only write when the player asks to (the main save). The implementation comes down to your type of game and your saving/loading needs.

To create a Save model based on our player class in the game, create a new script called PlayerSaveState in AssetsScriptsClasses and replace its contents with the following code:

using System;
using System.Collections.Generic;
using UnityEngine;
 
[Serializable]
public struct PlayerSaveState {
 
    public string Name;
    public int Age;
    public string Faction;
    public string Occupation;
    public int Level;
    public int Health;
    public int Strength;
    public int Magic;
    public int Defense;
    public int Speed;
    public int Damage;
    public int Armor;
    public int NoOfAttacks;
    public string Weapon;
    public Vector2 Position;
    public List<string> Inventory;
}

This gives us the basic Save model for our player. Note that some of the properties are different, specifically the player's inventory. We'll come back to this later.

Now that we have our model, we need a way to convert an active class in the game, such as the player in it to its savable state and back again. Now we can write static methods in the preceding class; however, there is a better way to do this using Extension methods (like we did with WorldExtensions to convert WorldSpace to ScreenSpace coordinates).

So, add the following code to the very end of the preceding class (you could also just create a new script for this as before, but for now, let's just add it to the same class; this is just so we can see all of the conversion code in one place):

public static class PlayerSaveStateExtensions { }

Next, we need another extension method to convert a Player class into the new PlayerSaveState class. So, add the following code to the PlayerSaveStateExtensions class:

public static PlayerSaveState GetPlayerSaveState(this Player input)
{
    PlayerSaveState newSaveState = new PlayerSaveState();
    newSaveState.Age = input.Age;
    newSaveState.Armor = input.Armor;
    newSaveState.Damage = input.Damage;
    newSaveState.Defense = input.Defense;
    newSaveState.Faction = input.Faction;
    newSaveState.Health = input.Health;
    newSaveState.Level = input.Level;
    newSaveState.Magic = input.Magic;
    newSaveState.Name = input.Name;
    newSaveState.NoOfAttacks = input.NoOfAttacks;
    newSaveState.Occupation = input.Occupation;
    newSaveState.Position = input.Position;
    newSaveState.Speed = input.Speed;
    newSaveState.Strength = input.Strength;
    newSaveState.Weapon = input.Weapon;
 
    newSaveState.Inventory = new List<string>();
    foreach (var item in input.Inventory)
    {
        newSaveState.Inventory.Add(item.name);
    }
 
    return newSaveState;
}

This is fairly simple; we are just copying the properties across. Of course, you only need to copy savable properties. If there are values the player cannot affect, then there is no need to save them. Of note is that for the player's inventory, where we only capture the asset name of each item. This is because we don't need to serialize InventoryItems themselves (the game already knows about them), only the ones the player has.

Tip

If you have items that can wear out, then you will also need to create a savable state for InventoryItem so you can save just the important bits or changeable values.

Instead of creating a Save model, you can simply tag each property you want to serialize with a [SerializeField] attribute (including private variables) and those that you don't want to serialize with a [NonSerialized] attribute.

However, in practice, this can cause trouble or confusion when debugging your saved data. In my personal experience, it's better to define a separate Save model so that you always know what you are dealing with.

Then, you simply need another extension method to do the reverse, as follows:

public static Player LoadPlayerSaveState(this PlayerSaveState input, Player player)
{
    player.Age = input.Age;
    player.Armor = input.Armor;
    player.Damage = input.Damage;
    player.Defense = input.Defense;
    player.Faction = input.Faction;
    player.Health = input.Health;
    player.Level = input.Level;
    player.Magic = input.Magic;
    player.Name = input.Name;
    player.NoOfAttacks = input.NoOfAttacks;
    player.Occupation = input.Occupation;
    player.Position = input.Position;
    player.Speed = input.Speed;
    player.Strength = input.Strength;
    player.Weapon = input.Weapon;
    player.Inventory = new List<InventoryItem>();
    foreach (var item in input.Inventory)
    {
      player.Inventory.Add(
        (InventoryItem)Resources.Load("Inventory Items/" + item));
    }
    return player;
}

This is pretty much the same in reverse, except for the inventory. We cannot simply create a new inventory item because each InventoryItem is a ScriptableObject that we created in our game in the editor.

So to give the player the correct InventoryItems from our game's library, we call Resources.Load to pull the item from our game project, passing the path to InventoryItem and its name (which we saved earlier). Then, we add them to the player's inventory.

Hopefully, you can see why I stuck with the previous model to give you a more in-depth look at how to manage ScriptableObjects with serialization.

Making your game save and load functions

Using the serialization helper we created earlier and our Save model, we can now implement our Save and Load functions. So, open up the GameState script from AssetsScriptsClasses and add the following property to mark our save location on the disk:

static string saveFilePath = 
Application.persistentDataPath + "/playerstate.dat";

This just saves us from writing this over and over again. Alternatively, if you are using a slot-saving system, then this will need to be a list that would also need to be saved (probably in a PlayerPrefs property). Next, we will add the Save function as follows:

public static void SaveState()
{
    try
    {
        PlayerPrefs.SetString("CurrentLocation", 
          Application.loadedLevelName);
        using (var file = File.Create(saveFilePath))
        {
            var playerSerializedState = 
              SerializerHelper.Serialise<PlayerSaveState>
                (currentPlayer.GetPlayerSaveState());
            file.Write(playerSerializedState, 
               0, playerSerializedState.Length);
        }
    }
    catch
    {
        Debug.LogError("Saving data failed");
    }
}

So, when we need to save our game, we perform the following actions:

  1. Save the player's current location to PlayerPrefs as it is very simple data.
  2. Create a save file using Unity's File function (passing in the path to its location).
  3. Create a serialized copy of our player in a new PlayerSaveState property.
  4. Finally, we write our serialized data to our save file.

Tip

With any operation that writes data outside of your game, always wrap it in a try/catch block. This will ensure your game doesn't crash when one out of a million bad things could happen.

This is all very simple. Then, to retrieve the saved data from the disk, first we'll add a little helper function to tell us whether a save file already exists, which we can also use elsewhere in the game, as follows:

public static bool SaveAvailable
{
    get { return File.Exists(saveFilePath); }
}

This just uses another function of the File class to test the existence of a file. Now, we can add the Load method as follows:

public static void LoadState(Action LoadComplete)
{
    try
    {
        if (SaveAvailable)
        {
            //Get the file
            using (var stream = File.Open(saveFilePath, 
              FileMode.Open))
            {
              var LoadedPlayer = 
                SerializerHelper.DeSerialise<PlayerSaveState>
                  (stream);
              currentPlayer = 
                LoadedPlayer.LoadPlayerSaveState(currentPlayer);
            }
        }
    }
    catch
    {
        Debug.LogError("Loading data failed, file is corrupt");
    }
    LoadComplete();
}

Again, this is just the reverse of saving the file with one difference: you have to test whether the save file exists first, else it will result in an error in the worst way possible.

You should note that we do not return the saved data directly back to the calling function; instead, we use a delegate to tell the caller when it is finished. The reason for this is simple: accessing the disk is slow. So, we need to ensure we have finished loading all of our data before we continue with our game, which is obviously very important. You can, if you want, also do this with the Save function if you wish as well.

Testing your Save and Load functions

As a simple test for our saving and loading functions, we can add a basic menu to our game. So, create a new scene named MainMenu in AssetsScenes and a new script called MainMenu in AssetsScripts and replace its contents with the following code:

using UnityEngine;
 
[ExecuteInEditMode]
public class MainMenu : MonoBehaviour {
 
    bool saveAvailable;
    void Start()
    {
        saveAvailable = GameState.SaveAvailable;
    }
}

Here, we simply start by using a variable to see whether we have a saved file when the menu is loaded.

Then, we just add an OnGUI method as follows:

void OnGUI () {

  GUILayout.BeginArea(new Rect((Screen.width / 2) - 100,(Screen.height / 2) - 100, 200,200 ));
  if(GUILayout.Button("New Game"))
  {
    NavigationManager.NavigateTo("Home");
  }
  GUILayout.Space(50);
  if (saveAvailable)
  {
    if (GUILayout.Button("Load Game"))
    {
      GameState.LoadState(() =>
      {
        var lastLocation = PlayerPrefs.GetString(
          "CurrentLocation", "Home");
          NavigationManager.NavigateTo(lastLocation);
      });
    }
  }
  GUILayout.EndArea();
}

This is a very simple menu with two buttons. The first uses the NavigationManager script to load the Home scene, and the other only displays whether there is a load available and then performs the following operations:

  1. Loads the current state of the game.
  2. Once the Load delegate is complete, it also retrieves the player's last location from PlayerPrefs.
  3. Then, it navigates to the last scene the player was in.

Attach the script to the camera, save the scene, and add it to the Build settings, and we are almost set.

The last thing to do is ensure that we save the game. You could do this by implementing it via a pause menu in the game, but for simplicity, I just added it to the NavigationManager script to save the game whenever the player moves from scene to scene.

So, open up the NavigationManager script and add GameState.SaveState() before the call to FadeInOutManager in both the NavigateTo and GoBack methods.

Backing up to the Web

An alternative to the basic saving of data to a disk, a lot of games now (especially if they are targeting multiple platforms) support a web backend to store a player's data. It doesn't need to be heavy; just use a player name/ID key and store the serialized data.

The benefit of this approach is that the player can continue playing on any device, regardless of which device they were last playing on.

Tip

Halo Spartan Assault implemented this feature and its sales skyrocketed because players on Windows Phones could switch to playing on their desktop or Xbox when they got home or vice versa. A big selling point!

Implementing this approach depends on the backend service you use for your data; whether you roll your own or use Azure MWS, Amazon Web Services, or Parse, which all have plugins that work for Unity3D.

The simplest approach is to use the serialization methods described previously and post your data to a backend web service using the Unity WWW class. As a full example would be too complex to demonstrate, what follows are just some code snippets of the available Unity functions.

Note

Granted you will have to write your web service on a server to accept this data, which is out of scope of this book, but if you search on www.codeproject.com or stackoverflow.com, you will find many good examples of such implementations.

You could post the serialized data direct to a service using a function similar to the following code (as an example only):

void UploadSaveData1()
{
  string url = "http://mybackendserver.com/Upload.php";
  var playerSerializedState = SerializerHelper.Serialise<PlayerSaveState>
  (currentPlayer.GetPlayerSaveState());
  WWW www = new WWW(url, playerSerializedState);
 
  StartCoroutine(WaitForRequest(www));
}
IEnumerator WaitForRequest(WWW www)
{
  yield return www;
 
  //check for errors
  if (www.error == null)
  {
    Debug.Log("Successful: " + www.text);
  }
  else
  {
    Debug.Log("Error: " + www.error);
  }
}

This simply takes the byte array of the serialized saved data and posts it to your server.

Alternatively, you can post data to the server as a form (more common):

void WebPost2()
{
  string url = "http://mybackendserver.com/Upload.php";
  var playerSerializedState = SerializerHelper.Serialise<PlayerSaveState>
  (currentPlayer.GetPlayerSaveState());
  var data = Convert.ToBase64String(playerSerializedState);

  WWWForm saveForm = new WWWForm();
  saveForm.AddField("saveData", data);
  WWW www = new WWW(url, saveForm);

  StartCoroutine(WaitForRequest(www));
}

This makes a traditional HTTP post with parameters in the body of the request.

Getting the data from the server is much simpler. To do so, write a simple coroutine to download the data that you can call when it's needed:

IEnumerator GetSavedDataFromWWW()
{
  string url = "http://mybackendserver.com/DownloadSaveData.php";
  WWW www = new WWW(url);
  yield return www;

  if (www.error == null)
  {
    var restoredData = DeserializePlayerState(www.bytes);
  }
  else
  {
    Debug.LogError("Error: " + www.error);
  }
}

Note that the examples are over-simplified to show you how the WWW class works.

For more information about the WWW class, see the Unity scripting reference guide at https://docs.unity3d.com/Documentation/ScriptReference/WWW.html.

Tip

If you would rather not roll your services, you can use backends such as Azure for which some budding teams have put together plugins for Unity3D. Check them out at http://www.bitrave.com/azure-mobile-services-for-unity-3d/.

There is even a promising Unity implementation that connects to Google Services as well at https://github.com/kimsama/Unity-GoogleData.

I've not seen any implementation for AWS as yet, but keep an eye out for this or use the previous examples as a primer to start your own; if you do see any, please share!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.144.194