Unity
Firebase

Firebase for UnityのRealtimeDBで何か作ってみる。

More than 1 year has passed since last update.

Firebase for Unityの話です。最近やっと触り始めたので感触など話そうかと思います。
作ったものを示しつつどんな感じにFirebase for Unityを使ったのかを書いていきます。
基本的にRealtimeDBしか使ってないのでそんな感じでよろしくお願いします。

何を作っているか

身の回りのものを管理してくれるキャラクターエージェントアプリを作っています。
"GmailとかSlackとかTwitterのリプライとかを通知してくれる機能"や、"デバイス間キャラクターワープ"などが実装されてスマートフォン上で動いています。
モバイルアプリだし3Dは厳しいよな~と思ってLive2Dを使ってます。現状パフォーマンスチューニングなどはしてないんですが、端末が熱くなったことは今のところ無いです。素晴らしい!

今回は後者についてお話します。動画は↓

RealtimeDBについて

https://firebase.google.com/docs/database/?hl=ja
を見ると

データを保存して NoSQL クラウド データベースと同期できます。データはすべてのクライアントにわたってリアルタイムで同期され、アプリがオフラインになっても、利用可能な状態を保ちます。

つまり、DBの方で何か変化があった場合、そのDBに接続しているクライアントにその値が通知されるということです。

作ってみよう

このRealtimeDBの性質を考えると、かなり簡単な実装になります。
実装方針:
・FirebaseのRealtimeDBにキャラクターがどの端末にいるかの状態を持っておく。
・クライアント側でボタンを押したらその状態を変化させる
・全てのクライアントで変化が通知されるので、その通知情報(どの端末にいるかの情報)からキャラクターを表示/非表示にする。

1つずつみていきましょう。

FirebaseのRealtimeDBにキャラクターがどの端末にいるかの状態を持っておく。

firebaseSS.png

(ぼかしているのは、Firebaseプロジェクト名と余計なプロパティです。)

ProjectName.Agent.Placeが"Mobile"となっています。
これがどの端末にAgent(キャラクター)がいるかどうかの状態です。

ここらへんの導入はググれば一杯出てきますので省略。大したことないです。

クライアント側でボタンを押したらその状態を変化させ、それを全てのクライアントで取得する。

ここからUnityのコーディング話になります。
Firebase for Unity SDKをダウンロードして、FirebaseDatabase.unitypackage をUnityのプロジェクトに入れましょう。

ボタンを押したときにFirebase上の情報を更新する。

ボタンをクリックした時、OnClick()を呼ぶようにする。

HogeButton.cs
        public override void OnClick()
        {
            var p = SystemState.place == SystemState.Place.Home;
            //自分が作成したFirebaseManagerというクラスに特定のプロパティを更新するメソッドが書いてあるのでそれを呼ぶ。
            FirebaseManager.UpdateState("Agent", "Place", p ? "Mobile" : "Home");
        }

Onclick()で呼ばれる処理は以下。
FirebaseのRealtimeDBに値をセットする処理です。

FirebaseManager.cs
        public static void UpdateState(string refString, string subject, string value)
        {
            //AgentのReferenceを取得
            var fref = FirebaseDatabase.DefaultInstance.GetReference(refString);
            //更新するプロパティ名と値をセット。今回の場合は{"Place","Home"}となる。
            //<string,object>の辞書配列であることに注意。<string,string>だとエラーが起きます。
            var set = new Dictionary<string, object>
            {
                { subject, value }
            };
            //更新処理を走らせる。エラーハンドリングなどは省略
            fref.UpdateChildrenAsync(set).ContinueWith(task =>
            {
                if (task.IsCompleted)
                {
                    Debug.Log("Success" + set[subject]);
                }
            });
        }

以上でボタンを押したときに更新するようにできました。

更新された情報を取得する

Firebaseで更新されたというイベントを管理するため以下のような処理を用意します。
これはFirebaseのRealtimeDBの特定のプロパティを監視して、変化があれば特定のActionを呼ぶということをします。

FirebaseManager.cs
    public class FirebaseManager : MonoBehaviour
    {
        //Placeが変化した時に起こるActionの定義。
        public static Action<SystemState.Place> OnPlaceChanged;

        DependencyStatus dependencyStatus = DependencyStatus.UnavailableOther;
        //初期化
        void Start()
        {
            dependencyStatus = FirebaseApp.CheckDependencies();

            if (dependencyStatus != DependencyStatus.Available)
            {
                FirebaseApp.FixDependenciesAsync().ContinueWith(task =>
                {
                    dependencyStatus = FirebaseApp.CheckDependencies();
                    if (dependencyStatus == DependencyStatus.Available)
                    {
                        InitializeFirebase();
                    }
                    else
                    {
                        Debug.LogError(
                            "Could not resolve all Firebase dependencies: " + dependencyStatus);
                    }
                });
            }
            else
            {
                InitializeFirebase();
            }
        }


        private void InitializeFirebase()
        {
            var instance = FirebaseApp.DefaultInstance;
            //Property.firebaseURLBaseとは上で言うところの"https://{{ProjectName}}.firebaseio.com"である。
            instance.SetEditorDatabaseUrl(Property.firebaseURLBase);

            if (instance.Options.DatabaseUrl != null)
            {
                instance.SetEditorDatabaseUrl(instance.Options.DatabaseUrl);
            }
            //ここでAgentのFirebaseReferenceを取得する。
            var agentRef = FirebaseDatabase.DefaultInstance.GetReference("Agent");
            //Referenceを使い、Placeのプロパティを監視する。もし値に変化があればPlaceChanged()を呼ぶ。
            agentRef.Child("Place").ValueChanged += PlaceChanged;
        }

        private void PlaceChanged(object sender, ValueChangedEventArgs e)
        {
            //eventArgsの中に変化した値(例:"Mobile")が入っている。しかしobject型なのでStringにする。
            var res = e.Snapshot.Value.ToString();

            //SystemStateというクラスに列挙体でPlaceという変数名で(Mobile,Home)という列挙子が定義されている。
            //上で得た"Mobile"をPlace.Mobileに変換しているだけ。
            var place = (SystemState.Place)Enum.Parse(typeof(SystemState.Place), res);

            //一番最初に定義したAction、OnPlaceChangedに変化した値をのっけて通知する。
            OnPlaceChanged(place);
            SystemState.place = (SystemState.Place)Enum.Parse(typeof(SystemState.Place), res);
        }
     }

これだけでAgent.Placeが監視し、その値によって何かをする処理を書くことができます。
「何かの処理」については、デリゲートとしてOnPlaceChangedを設定したので、その処理を他のクラスに記述することができます。

全てのクライアントで変化が通知されるので、その通知情報(どの端末にいるかの情報)からキャラクターを表示/非表示にする。

OnPlaceChanged()に処理を登録することで、更新された瞬間にその処理を行うことができる。その処理内容の記述と登録について書いてあるクラスです。

PlaceChangeEffectManager.cs
namespace Agent.Command.PlaceChange
{
    public class PlaceChangeEffectManager : MonoBehaviour
    {
        //いろんなエフェクト、3種類用いている(最初のビリビリParticle、その後現れる六角形のエフェクト2種)
        public ParticleSystem particle;
        public GameObject hexEffect, hexEffectLine;
        private Material hexEffectMat, hexEffectLineMat;

        //Live2dモデルのGameObject
        public GameObject character;


        void Start()
        {
            hexEffectMat = hexEffect.GetComponent<Renderer>().material;
            hexEffectLineMat = hexEffectLine.GetComponent<Renderer>().material;

            //ここで場所状態に変化があった場合の処理を登録する。
            FirebaseManager.OnPlaceChanged += OnPlaceChanged;
        }

        //変化があった時の処理
        //各自思い思いの処理を書きましょう。
        private void OnPlaceChanged(SystemState.Place place)
        {
            //自分のところから何処かに移動する時
            if (SystemState.place == Property.systemPlace)
            {
                StartCoroutine(MakeEffect(false));
            }
            //他のところから自分のところに移動してくる時
            else if (Property.systemPlace == place)
            {
                StartCoroutine(MakeEffect(true));
            }
        }

        private IEnumerator MakeEffect(bool b)
        {
            var wait = new WaitForSeconds(1f);
            particle.gameObject.SetActive(true);
            //ビリビリParticle開始して1秒待つ。
            particle.Play();
            yield return wait;

            //エフェクト類をActiveにする。
            hexEffect.SetActive(true);
            hexEffectLine.SetActive(true);

            //エフェクトのアニメーションを開始
            //独自のShaderを使っているためSetFloat("_AnimationProgress",t)は参考にならないかも。
            //各自思い思いの表現を書きましょう。
            for (var t = 0.0f; t < 1.0f; t += Time.deltaTime)
            {
                hexEffectMat.SetFloat("_AnimationProgress", t);
                hexEffectLineMat.SetFloat("_AnimationProgress", t);
                yield return null;
            }

            //ここでキャラクターを表示(非表示)にする
            character.SetActive(b);

            //エフェクトを消すアニメーションを開始
            for (var t = 1.0f; t > 0f; t -= Time.deltaTime)
            {
                if (t <= 0) t = 0;
                hexEffectMat.SetFloat("_AnimationProgress", t);
                hexEffectLineMat.SetFloat("_AnimationProgress", t);
                yield return null;
            }
            //エフェクト類を消す
            particle.gameObject.SetActive(false);
            hexEffect.SetActive(false);
            hexEffectLine.SetActive(false);

        }
    }
}

あとはビルドしてAndroidアプリが動く端末を2つ用意することによって動画のようなことができます。
動画で分かる通り、クライアントごとで更新したときにほとんどラグはありません。凄い!
ただ、当然ラグは0というわけではないので一応頭の片隅にとめておいてください。

注意点

agentRef.Child("Place").ValueChanged()のようなValueChanged()は、最初の初期化時でも呼ばれてしまいます。
Firebaseから拾ってきた値で初期化するには便利ですが、そういう処理がいらない場合はifで制御しましょう。
何か良いやり方があれば教えてください。

あとUnityEditor上では動作しますが、これをWindows向けにビルドしてもFirebaseは動きません。
FirebaseForUnityはあくまでAndroid/iOS向けなので注意。