UnityとFirebaseでアプリ開発する際にユーザーデータの取り扱い方まとめ

こちらは [Firebase Advent Calendar 2020](https://qiita.com/advent-calendar/2020/firebase) の 16日目の記事です。


## 概要

本記事は、[Unity #2 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/unity2) の 8日目の記事（前半）[Unity 超初心者が Firebase でアプリ開発する際に必要になるスキル](https://qiita.com/gremito/items/51b7bad4c4923c503b05) の後半内容となります。
そのため、全て内容を引き継いで解説を進めるため、本記事を読む前に前半をご覧くださいmm


### ふりかえり

前半では、iOS/Android アプリを開発する際にユーザーデータを Firebase Realtime Database (Firebase Database) で保存取得しましょうと話し、その際にセキュリティ面を考えて rule でユーザー毎でアクセスできる設定のために Firebase Authentication (Firebase Auth) を使うことを解説しましたが、実装に関する解説をこれから解説していきます。

## Firebase Auth

前半で [認証方法をメアド/パスワードで取り扱う](https://qiita.com/gremito/items/51b7bad4c4923c503b05#%E8%AA%8D%E8%A8%BC%E6%96%B9%E5%BC%8F) 解説をしました。
もう少し詳しく話すと、例えばユーザーデータを取り扱っているようなカジュアルゲームでニックネームなどは設定するもののメアドやパスワードは設定しないと思います。全てのアプリがそうではないですが、あの**裏側ではよしなにユーザー情報が作成されており、ユーザー ↔︎ アプリを勝手に紐づけ**ています。

この方法でFirebase Authのメアド/パスワードで実装するとしたら、具体的に次の内容を考えてました。

1. @前半をUUID/UDIDなどを用いて@後半はFirebaseが勝手に作っているドメインを扱ってメアド生成メソッドを作る
2. 英数字記号をランダムで作ってくれるメアド生成メソッドを作る
3. 2と3を`CreateUserWithEmailAndPasswordAsync`に渡してアカウント生成
4. `FirebaseAuth.DefaultInstance.CurrentUser`でユーザー情報が取得できたらログイン中で取得できない場合は自動ログイン
5. 自動ログインの際に[`SignInWithEmailAndPasswordAsync`](https://firebase.google.com/docs/auth/unity/password-auth#sign_in_a_user_with_an_email_address_and_password)のために2と3を `PlayerPrefs` で保存取得できるようにしておく


ですが、[匿名認証](https://firebase.google.com/docs/auth/unity/anonymous-auth) を使うことで1~3を飛ばして4と5を実現することができます。

　<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82601/c6745d6f-8aac-c132-8b8c-fe8719402fbc.png">

アプリの要件にもよりますが、アプリ初起動時に裏側で勝手に匿名認証のユーザー作成を行い、ユーザー設定のようなメニューを設けて「アプリ引き継ぎ設定」を作ることで上記1~3はやらなくてよくなります。
このように一般的なWeb/Mobileのアプリ開発とは異なり、ゲームやコンテンツのアプリはどれだけユーザーに面白い・楽しいと思ってもらいとにかく面倒な要素を取っ払うか重要になります。


### FirebaseAuth.SignInAnonymouslyAsync

早速、匿名証明でユーザー作成をやってみましょう。
公式のサンプルコードを参考にし、`delegate`を用いて`FirebaseAuth`だけを管理するクラスで以下のように実装するとよいでしょう。

```csharp:FirebaseAuthManager.cs
using UnityEngine;
using Firebase.Auth;

public class FirebaseAuthManager : MonoBehaviour
{
    FirebaseAuth _auth;
    FirebaseUser _user;
    public FirebaseUser UserData { get { return _user; } }
    public delegate void CreateUser(bool result);

    void Awake()
    {
        // 初期化
        _auth = FirebaseAuth.DefaultInstance;
        // すでにユーザーが作られているのか確認
        if (_auth.CurrentUser.UserId == null)
        {
            // まだユーザーができていないためユーザー作成
            Create((result) =>
            {
                if (result)
                {
                    Debug.Log($"成功: #{_user.UserId}");
                }
                else
                {
                    Debug.Log("失敗");
                }
            });
        }
        else
        {
            _user = _auth.CurrentUser;
            Debug.Log($"ログイン中: #{_user.UserId}");
        }
    }

    /// <summary>
    /// 匿名でユーザー作成
    /// </summary>
    public void Create(CreateUser callback)
    {
        _auth.SignInAnonymouslyAsync().ContinueWith(task => {
            if (task.IsCanceled)
            {
                Debug.LogError("SignInAnonymouslyAsync was canceled.");
                callback(false);
                return;
            }
            if (task.IsFaulted)
            {
                Debug.LogError("SignInAnonymouslyAsync encountered an error: " + task.Exception);
                callback(false);
                return;
            }

            _user = task.Result;
            Debug.Log($"User signed in successfully: {_user.DisplayName} ({_user.UserId})");
            callback(true);
        });
    }
}
```

　
`delegate`は、上記のようにdelegateの型に合うメソッドを用意すれば他クラスにメソッドの引数として渡すことができ、その他クラスのメソッドの処理終わりのタイミングで渡したメソッドが呼び出される仕組みとなります。[++C++; // 未確認飛行 C さんが解説されている記事](https://ufcpp.net/study/csharp/sp_delegate.html) がとてもわかりやすいので、こちらを参考にされてくださいmm


### 「匿名アカウントを永久アカウントに変換する」

先ほども解説した通り、[最初に匿名でユーザーを作り、後からメアド/パスワードもしくはSNS連携に切り替える](https://firebase.google.com/docs/auth/unity/anonymous-auth)ことができます。
方法としては、[サンプルコードで解説されている通り`FirebaseAuth.DefaultInstance.CurrentUser.LinkWithCredentialAsync`](https://firebase.google.com/docs/auth/unity/account-linking)を扱うようです。


```csharp:FirebaseAuthManager.cs
    /// <summary>
    /// メアド/パスワードでユーザー作成
    /// </summary>
    public void Create(string email, string password, CreateUser callback)
    {
        // すでにユーザーが作られているのか確認
        if (_auth.CurrentUser.UserId == null)
        {
            // 新規でユーザー作成
            _auth.CreateUserWithEmailAndPasswordAsync(email, password).ContinueWith(task =>
            {
                if (task.IsCanceled)
                {
                    Debug.LogError("CreateUserWithEmailAndPasswordAsync was canceled.");
                    callback(false);
                    return;
                }
                if (task.IsFaulted)
                {
                    Debug.LogError("CreateUserWithEmailAndPasswordAsync encountered an error: " + task.Exception);
                    callback(false);
                    return;
                }

                _user = task.Result;
                Debug.Log($"Firebase user created successfully: {_user.DisplayName} ({_user.UserId})");

                callback(true);
            });
        }
        else
        {
            // 認証方法追加
            Credential credential = EmailAuthProvider.GetCredential(email, password);
            _auth.CurrentUser.LinkWithCredentialAsync(credential).ContinueWith(task => {
                if (task.IsCanceled)
                {
                    Debug.LogError("LinkWithCredentialAsync was canceled.");
                    callback(false);
                    return;
                }
                if (task.IsFaulted)
                {
                    Debug.LogError("LinkWithCredentialAsync encountered an error: " + task.Exception);
                    callback(false);
                    return;
                }

                _user = task.Result;
                Debug.Log($"Credentials successfully linked to Firebase user: {_user.DisplayName} ({_user.UserId})");

                callback(true);
            });
        }
    }
```

　
これでAuthの対応は一旦終わりで、目的のDatabaseの実装に進んでいきます。


## Firebase Database

最後にDatabaseでは、「Authで作ったユーザー情報をもとにruleの設定」「データの保存と取得」の2点を解説して終わりとなります。
Authでユーザー情報を扱えるようになり、ruleでAuthのユーザーだけDatabaseとやりとりできるようになりました。

### ruleの設定

設定はとても簡単で、[公式にユーザー認証した際のルール設定方法](https://firebase.google.com/docs/database/security?authuser=0#section-authorization)の内容をそのまま以下のように設定すれば大丈夫です。

　<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82601/4c731332-57e3-f6f7-0a4e-e6627597f27a.png">


databaseを初めて使うときにテストモードにしていると上記スクショのようにコメントアウトします。

また、今回はサンプルとして`users`の下にデータを保存と取得する内容でまとめていますが、他にもランキング情報やチャット情報を取り扱う場合は、`rankings`や`chats`などのように別keyになると思います。
ポイントは、`$uid`が先ほどの`FirebaseAuth.CurrentUser.UserId`と同じものでなければ許可しないように設定することです。


### ユーザーデータの保存と取得

`Start`メソッドに保存と取得の処理を書いてますが、本来は別クラスから処理を呼び出すことになります。例として、ゲームのクリアタイムをニックネームとともに保存するサンプルを用意してみました。

```csharp:FirebaseDatabaseManager.cs
using UnityEngine;
using Firebase.Database;

public class UserPlayData
{
    public string username;
    public float time;

    public UserPlayData(string username, float time)
    {
        this.username = username;
        this.time = time;
    }
}

public class FirebaseDatabaseManager : MonoBehaviour
{
    readonly string USER_DATA_KEY = "users";
    DatabaseReference reference;
    [SerializeField]
    FirebaseAuthSample _auth;
    public delegate void GetUserDataCallback(UserPlayData result);

    void Start()
    {
        reference = FirebaseDatabase.DefaultInstance.RootReference;

        // サンプル: 保存
        var userData = new UserPlayData("gremito", 10.5f);
        SaveUserData(userData);

        // サンプル: 取得
        GetUserData((result) => {
            if(result == null)
            {
                Debug.LogWarning("失敗");
            }
            else
            {
                Debug.Log($"username: {result.username}");
                Debug.Log($"time: {result.time}");
            }
        });
    }

    /// <summary>
    /// ユーザーデータをJson化してdatabaseに保存（SetRawJsonValueAsync）
    /// </summary>
    public void SaveUserData(UserPlayData data)
    {
        // 公式サンプル方法: https://firebase.google.com/docs/database/unity/save-data?authuser=0#write_update_or_delete_data_at_a_reference
        var json = JsonUtility.ToJson(data);
        reference.Child(USER_DATA_KEY).Child(_auth.UserData.UserId).SetRawJsonValueAsync(json);
    }

    /// <summary>
    /// ユーザーデータを取得
    /// </summary>
    public void GetUserData(GetUserDataCallback callback)
    {
        FirebaseDatabase.DefaultInstance.GetReference(USER_DATA_KEY)
            .Child(_auth.UserData.UserId).GetValueAsync().ContinueWith(task =>
            {
                if (task.IsFaulted)
                    callback(null);

                else if (task.IsCompleted)
                    callback(new UserPlayData(
                        task.Result.Child("username").Value.ToString(),
                        float.Parse(task.Result.Child("time").Value.ToString())));
            });
    }
}
```

　<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82601/80fdded8-9e97-8be4-d1bf-87052a6dd835.png">　<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82601/5ad345dd-305e-2664-c2d8-b9b3ead03d05.png">

右スクショのFirebaseコンソールのDatabaseで保存したデータが反映されているようにUnityのデータを保存取得することができました。
これは、最低限の実装なのでAppStore/GooglePlayのストアにアプリをリリースする品質を考えると[トランザクション](https://firebase.google.com/docs/database/unity/save-data?authuser=0#save_data_as_transactions)や[イベント](https://firebase.google.com/docs/database/unity/retrieve-data?authuser=0#listen_for_events)、[並べ替えとフィルタリング](https://firebase.google.com/docs/database/unity/retrieve-data?authuser=0#sorting_and_filtering_data)なども上手く扱う必要が出てくるかもしれません。


### オフライン機能

[Realtime Databaseにはオフライン機能](https://firebase.google.com/docs/database/unity/save-data?authuser=0#write_data_offline)があり、最後に通信した最新のデータをFriebase SDK側でデータを保持して取り扱うことができます。

アプリの要件に応じて最新情報が必須な場面は、意図的にアプリを使用できなくする機能実装が必要になります。
反対に最新情報が必要でなくてもアプリを操作できる場面は、特に対応しなくていいかもしれません。



例えば、カジュアルゲームのアプリでランキング機能の場合、理想は最新情報が欲しいけど別に後から同期させて最新のランキング情報にすればいいから、オフラインでもゲームを楽しめるケースが考えられます。


## まとめ

これでFirebaseのAuthとDatabaseを組み合わせて、しっかりセキュリティを担保したユーザーデータの取り扱い方を知れたと思います。

先ほども解説した通り、ここまでの内容は最低限のサンプルでFirebaseには他にも機能が豊富で、アプリの要件に応じてここで解説していない使える機能があると思います。

これをきっかけにさらにFirebaseを知ることでサーバーサイドの知識も得られますし、一石二鳥以上のスキルアップになると思うのでぜひ活用していきましょう！