Save data management for Android game apps with Firebase Unity

8 minute read

I wanted to add a backup function to the smartphone game made with Unity for device migration and recovery from failure / loss.
I heard that Firebase’s free tier is sufficient for small scale, so I will try it.

Firebase project creation

In any case, go to Firebase (https://console.firebase.google.com/) and log in with your Google account.

Then select “Create Project”.
Give it an arbitrary project name.

image.png

You can use Google Analytics.
It’s free, so leave it on without thinking about anything.

image.png

Set the account appropriately.
(I’m not sure about this. I used Default Account for Firebase because it was troublesome to create a new one.)

image.png

App registration

To use Firebase, you need to register the app.
Since the app itself is under development, of course we haven’t released it yet, but we decided only the package name first and registered it.

Select the Unity logo above “Add an app to get started”.

image.png

This time, we will target Android. You can add iOS later, so register without worrying about it.
Enter the package name you have decided (e.g. com.company.appname).
Enter the nickname as needed.

image.png

You will be able to download the configuration file.
Download google-services.json and put it under Assets of Unity.
The place seems to be arbitrary, but I did not dig a hierarchy in particular and placed it directly below.

image.png

Get the Firebase Unity SDK.
When I pressed the button, I was able to download firebase_unity_sdk_6.15.2.zip.
This will be used later.
For the time being, I will complete the settings on the Firebase side

image.png

Once the app is added, select Realtime Database from the left pane and select Create Database

image.png

You will be asked for security rules.
For now, select Start in test mode as you can set it later. Now my goal is to move it.
I won’t omit it in this article, but as Google warns me, be sure to reset the rules before publishing.

image.png

The DB is now created.
As a test, create one piece of data.

This time, I created / users and stored the save data of each user under it. (Even if you don’t bother to make it here, it will be made without permission if you don’t have it later. This is just for confirmation)

image.png

Now make a backup of your data, like / users / {user ID} / xxx.

From here on, the work on the Unity side.

Put the required Assert in Unity.

Added Firebase SDK

Since FirebaseDatabase.unitypackage is included in \ firebase_unity_sdk \ dotnet4 in the Zip file downloaded from Firebase, import it into Unity. (The procedure is as displayed on the screen).
Whether it is dotnet3 or 4 depends on the application you are making.

At this time, the Platform on the Unity side was mistakenly set to PC, Mac & Linux Standalone, and after importing, the error ʻUnable to load options for default app occurred. ʻAttempting to load Assets / StreamingAssets \ google-services-desktop.json fails.
Switch Platform to Android without rushing to solve.

image.png

Updated PlayGamesPlugin

After installing the Firebase SDK, when I try to run Unity, the following error occurs.

System.MissingMethodException: bool GooglePlayServices.UnityCompat.SetAndroidMinSDKVersion(int)

Other errors like this

PrecompiledAssemblyException: Multiple precompiled assemblies with the same name Google.VersionHandler.dll included for the current platform. Only one assembly with the same name is allowed per platform. Assembly paths: Assets/ExternalDependencyManager/Editor/Google.VersionHandler.dll, Assets/PlayServicesResolver/Editor/Google.VersionHandler.dll
UnityEditor.Scripting.ScriptCompilation.EditorBuildRules.CreateTargetAssemblies (System.Collections.Generic.IEnumerable`1[T] customScriptAssemblies, System.Collections.Generic.IEnumerable`1[T] precompiledAssemblies) (at...

When I googled, this seems to be because the version of PlayGamesPlugin is old, so I will update it.
https://github.com/playgameservices/play-games-plugin-for-unity/issues/2877

PlayGamesPlugin will automatically go up if you update the resolver, so update that.
reference:

  • https://qiita.com/tkyaji/items/b838c97228f99f194bcd
  • https://qiita.com/kusu123/items/9aac7f1b899b95edde07

The resolver is obtained from the following. As you can see, it is a unity package, so just import it like the Firebase SDK.
https://github.com/googlesamples/unity-jar-resolver/blob/master/external-dependency-manager-latest.unitypackage

When asked “Are you sure you want to delete the Obsolete file?”, Delete it. (May come out several times. You can delete it each time)
If you do not delete this, you will get another error. If the dialog does not appear, it may appear when you restart Unity.

capture02.PNG

Do you want to add the PackageManager registry? I was asked, so I honestly ʻAdd Selected Registries`.
capture05.PNG

Since the migration of Package was advanced, this is also obediently ʻApply`.
capture06.PNG

Now the error disappears.

Unity sometimes freezes during Migrate of Package.
It was fixed by restarting Unity.

Implementation on the app side

After that, I will write the code to communicate with Firebase

Initialization

First, open the contents of google-services.json located directly under Assets and check the URL of the connection destination. That is the column for project_info.firebase_url. (It should be in the format https: // app name.firebaseio.com/)

Use this url to create an object to read and write the contents of firebase.

// Set up the Editor before calling into the realtime database.
FirebaseApp.DefaultInstance.SetEditorDatabaseUrl("https://app name.firebaseio.com/");

// Get the root reference location of the database.
DatabaseReference databaseRoot = FirebaseDatabase.DefaultInstance.RootReference;

Write data

Actually write data to firebase.

I’m going to put the data in the / users / {user ID} / created above, but I have to number the user ID first.
If this is duplicated, it will overwrite the data of others.

There are many games that ask each user to assign a unique ID by themselves, but Firebase also has a method (Push) for numbering IDs, so this time I will use it.
Push () just returns a unique string, so this alone will not write any data to Firebase.


//Create a new user ID
var newData= databaseRoot.Child("users").Push();
//Save the created ID locally
PlayerPrefs.SetString("user-id", userId);

Once you have an ID, try entering the actual data. For the time being, I tried to set the creation date as a test.

databaseRoot.Child("users").Child(userId).Child("created_at").SetValueAsync(DateTime.UtcNow.ToString("yyyy/MM/dd HH:mm:ss"));

It is properly written in firebase.

image.png

If it fails, you may have the wrong path or you may not have write permission, so double-check your rules.
If you still do not know the cause, add a callback described later and log the reason for the failure.

Save the entire object

With SetValueAsync, you can only write single data such as numbers and strings.
For example, when there are the following classes, it is troublesome to separate them all and save them.

class UserSaveData
{
    public int Level { get; set; }
    public string Name { get; set; }
    public List<string> RewardIds{ get; set; }
    public Dictionary<string, int> Items { get; set; }
}

For such a case, there is a method SetRawJsonValueAsync () for converting an object to Json data and recording it.

var userId = PlayerPrefs.GetString("user-id");
var reference = databaseRoot.Child("users").Child(userId);
var saveData = new UserSaveData()
{
    Level = 1,
    Name = "Ah ah",
    RewardIds = new List<string>() { "a0001", "b0002" },
    Items = new Dictionary<string, int> { { "Sword", 1 }, { "Shield", 2 } }
};
string data = JsonUtility.ToJson(saveData);
reference.SetRawJsonValueAsync(data);

However, the above is actually a failure example.
Even if I do this, I don’t think anything will be displayed on the Firebase side.
On the contrary, the created_at saved in the previous process also disappears.

The cause of this is as follows

–The conditions for saving the object are not met
–The class must be Serializable
–The property is NG (as is). Must be a field.
JsonUtility.ToJson () does not support Dictionary
–Since the data to be saved is empty this time, I overwrote / users / {user-id} / with empty data and blew away the already saved data.

To fix this, make the following modifications.

–Add the Serialozable attribute to the class
–Make the property a field (This spec is terribly unpleasant for those familiar with C # …)
–Change Dictionary to List
–When saving, dig one hierarchy instead of saving directly to / users / {user-id}

The result is below.

var userId = PlayerPrefs.GetString("user-id");
var reference = databaseRoot.Child("users").Child(UserId).Child("backup"); // user-Save to backup instead of id
var saveData = new UserSaveData()
{
    Level = 1,
    Name = "Ah ah",
    RewardIds = new List<string>() { "a0001", "b0002" },
    Items = new Dictionary<string, int> { { "Sword", 1 }, { "Shield", 2 } }
};
string data = JsonUtility.ToJson(saveData);
reference.SetRawJsonValueAsync(data);
[Serializable]
class UserSaveData : ISerializationCallbackReceiver
{
    public int Level;
    public string Name;
    public List<string> RewardIds;
    public Dictionary<string, int> Items;
        
    [SerializeField] private List<string> itemKeys;
    [SerializeField] private List<int> itemCounts;

    public void OnBeforeSerialize()
    {
        itemKeys = new List<string>();
        itemCounts = new List<int>();
        foreach (var item in Items)
        {
            itemKeys.Add(item.Key);
            itemCounts.Add(item.Value);
        }
    }

    public void OnAfterDeserialize()
    {
        Items = new Dictionary<string, int>();
        for (int i = 0; i < itemKeys.Count; i++)
        {
            Items.Add(itemKeys[i], itemCounts[i]);
        }
    }
}

Since the Dictionary cannot be saved, it is refilled in List before converting to Json.
This way, you can save the entire class in Json.

image.png

Receive a callback after writing

If you just run SetValueAsync (), this method will be executed asynchronously, so you won’t know if the write succeeded or failed.

So I get a callback.

string data = JsonUtility.ToJson(saveData);
reference.SetRawJsonValueAsync(data).ContinueWith(task =>
{
    if (task.IsFaulted)
    {
        Debug.LogError("firebase error: " + task.Exception);
    }
    else if (task.IsCompleted)
    {
        Debug.Log("firebase result:" + task.Status);
    }
});

Return callback to UI thread

Now that the callback has been implemented, I want to add a function that “displays a message on the screen if saving succeeds / fails”.
But this may also not work.
If you get into a situation where “no errors occur but the screen doesn’t update”, you might want to suspect the thread.
The screen cannot be refreshed if the thread on which the callback is executed is not a UI thread.

Details are omitted here, but in this case you can switch the context and execute the callback in the UI thread.
If you want to know more about UI threads, go to ʻUnity UI Thread or SynchronizationContext`.

// Action<bool> callback = xxx //Define some callback function
string data = JsonUtility.ToJson(saveData);

//Save the context for passing the callback as you may touch the UI thread
var context = System.Threading.SynchronizationContext.Current;

reference.SetRawJsonValueAsync(data).ContinueWith(task =>
{
    context.Post((obj) =>
    {
        if (obj is Action<bool>)
        {
            callback(task.IsFaulted);
        }
    }, callback);
});

Read data

Just use GetValueAsync () to read. simple.

I think that if you include the callbacks you wrote at the time of writing, thread switching, conversion to objects, etc., it will be as follows.

// Action<UserSaveData> callback = xxx //Define some callback function
//Save the context for passing the callback as you may touch the UI thread
var context = System.Threading.SynchronizationContext.Current;

DatabaseRoot.Child("users").Child(userId).Child("backup").GetValueAsync().ContinueWith(task =>
{
    context.Post((obj) =>
    {
        if (task.IsFaulted)
        {
            Debug.LogError($"firebase error: {obj} : {task.Exception}");
            callback(null);
        }
        else if (task.IsCompleted)
        {
            UserSaveData restoreData = JsonUtility.FromJson<UserSaveData>(task.Result.GetRawJsonValue());
            Debug.Log("Complete save data restore");
            callback(restoreData);
        }
    }, callback);
});

that’s all.
It’s true that it’s easy because I just put the SDK in and input / output, but there were a lot of traps and I spent a lot of time.