Monday, July 21, 2014

Manage save games in Unity without pain using NDatabase




As developers, we all know that save games are an important aspect to most of our games, even more so on mobile where the game can be disrupted at any time.
But how could we solve this issue? How can we store our game data in a performant and easy way without the major limitations that Unitys PlayerPrefs put on our titles?

Today, I want to introduce you to NDatabase!
NDatabase is an easy to use data storage solution.
NDatabase is written purely in C# without native code (like SQLite), making it capable of going to all Unitys platforms that can handle file operations.
You can DOWNLOAD our ready to use Unity project containing all sources to a simple game using these classes for you to follow this blog with full sources to experiment right away.
The game is fully commented and contains additional game elements.

Prerequirements

Before we get started, we need to ensure that we have everything ready.
For simplicity we will use the precompiled library.
You can download the ‘Download for .net 3.5′ version from https://ndatabase.codeplex.com.
After you downloaded it, extract the files content and move it into your projects Assets/Plugins folder.
Thats already all you need to start saving and loading your game data.

Define what you want to store

For this introduction, we will store data for an endless runner:
  • the current player position
  • the current score
  • the remaining lives
  • the inventory (a list of names)
We will store this information in a simple, lightweight data object that we can easily pass around as required.
We will not attempt to save a whole MonoBehavior or GameObject.
MonoBehavior and GameObject are very heavy weight objects.
Attempting to save them severly limits the complexity of the data you can store performantly.

The classes

NDatabase makes it very easy to store data.
All it requires is classes with data to store and it will handle it invisibly.
This is for example the case for object, float, int, string, bool or arrays, lists and dictionaries of these.
In our case, we want to store an array of floats for the position, two ints for score and lives as well as a list of strings for the inventory.
If we put this all together, this results in the following class:
PlayerData.cs
public class PlayerData {
    public float[] PositionData;
    public int Score;
    public int Lives;
    public List Inventory = new List ();


    /// 
    /// Gets or sets the position array
    /// 
    /// The position.
    public Vector3 Position {
        get {
            if (PositionData == null) {
                return Vector3.zero;
            }
            Vector3 result;
            for (int i = 0; i < 3; i++) {
                result [i] = PositionData [i];
            }
            return result;
        }
        set{
            PositionData = new float[3]{ value.x, value.y, value.z };
        }
    }
}
To focus on the specific NDatabase operations in the remaining post, I also want to provide the sources of the Player class you can find in the download. The Player class manages the PlayerData including the loading and saving of the data.
Player.cs
public class Player : MonoBehaviour
{
    private string _savegamePath;
    private IOdb _dataStorage;
    public PlayerData Data { get; private set; }

    private void Awake ()
    {
        _savegamePath = Application.persistentDataPath + "/savegame";
        LoadFirstSavegame ();
    }

    private void LoadFirstSavegame ()
    {
        _dataStorage = OdbFactory.Open (_savegamePath);

        Data = _dataStorage.QueryAndExecute ().GetFirst ();
        if (Data == null) {
            Data = new PlayerData ();
        } else {
            transform.position = Data.Position;
        }
    }

    private void OnApplicationPause (bool pause)
    {
        if (pause) {
            SavePlayerData ();
        } else {
            if (_dataStorage == null || _dataStorage.IsClosed ()) {
                LoadFirstSavegame ();
            }
        }
    }

    private void OnApplicationQuit ()
    {
        if (_dataStorage != null) {
            SavePlayerData ();
            _dataStorage.Dispose ();
        }
    }

    private void SavePlayerData ()
    {
        Data.Position = transform.position;
        _dataStorage.Store (Data);
        _dataStorage.Commit ();
    }
}

Creating the data storage

NDatabase makes creating the data storage as simple as it can be. All you need is a single line of code:
IOdb _dataStorage = OdbFactory.Open (savegamePath);
If you keep this reference stored on your player or a global manager, you can access the data at any time.

Saving your game the easy way

To prepare a new object to be stored, all we need to do is call
_dataStorage.Store (theObject);
This though will not yet write the data to disk. We need to finalize the changes to have them written to disk calling
_dataStorage.Commit();
In our case, we would like to update the existing save game instead of creating more and more entries in the database.
NDatabase makes this very easy, you simply need to store a previously fetched object.
As we assigned the latest player data to Data in the call to LoadFirstSavegame, all we need to do is update above Store call to
_dataStorage.Store (Data);

Loading the game made simple

Loading your save game is straight forward.
To get all stored PlayerData object, the simplest way is to call
var result = _dataStorage.QueryAndExecute ();
For the start, we will focus on getting the first PlayerData object, assuming there will only be one at any time.
This is done through the call to GetFirst:
Data = result.GetFirst();
Using Data, you can now restore the players position, score, lives and inventory by reassigning them to the appropriate objects.

Lookout

With the knowledge we now have we can already handle complex save game data sets through the ease of NDatabase. But we are still limited to the amount of data we can store in realtime as writing a lot of data takes time.
In the next article of this series, we will present you with different strategies to challenge the current limits in realtime save game sizes.
We will continue our coverage on NDatabase in future blog posts, covering NDatabase more indepth, showing you how to optimize your queries beyond the simple call, use indices to accelerate accesses even further and leveraging the In Memory database capabilities of NDatabase to manage large, complex data sets during runtime.
If you currently have a challenge with your save game handling that you want us to cover, let us know through the comments!

No comments: