UnityでFirebaseのRealtimeDatabaseとデータのやり取りをする - UnityでFirebaseを使ったオンラインランキングシステムを作るvol3

この記事はシリーズ物です。
シリーズの記事は以下を参照ください。
特に本稿は、以下の2つの記事を読み終えている事前提で記載していますのでご注意ください。

www.project-unknown.jp www.project-unknown.jp www.project-unknown.jp

https://assetstore.unityhttps://assetstore.unity.com/packages/3d/environments/fantasy/alchemist-s-house-interior-47318.com/packages/3d/environments/fantasy/alchemist-s-house-interior-47318

はじめに

今回は、FirebaseのRealtimeDatabaseとのデータのやり取りを行います。
これまで、Twitter認証・Firebase認証とやってきて、やっとランキングのデータのやり取りを行います。
試行錯誤した結果を記載していますので、もし手順などの漏れで動かない等御座いましたら、@YuwUnknownまでご連絡ください。

注意

アクセス制限について

今回の記事では、Debug・実装のやりやすさから、FirebaseのRealtimeDatabaseに対しての読み書きの制限を行いません。
実際に運用する際には、セキュリティの観点からも、読み書きの制限を行うことを強くお勧めします
また、読み書きの制限については、次の回で説明する予定です。

データ通信のやり取りについて

今回の記事は、Firebaseの無料の範囲内で記載しています。
無料で出来る範囲は、公式の料金表に詳細が記載されています。

料金表に載っていない突っ込んだ情報は、以下の記事で実際にヒアリングした結果をレポートしていますので、ご確認ください。

www.project-unknown.jp

今回のランキングシステムは、データ量もさほど多くないですし、スコアを見る時だけ接続する等を実施すれば無料の範囲内で十分に使えるはずです。
またFirebaseのRealtimeDatabaseはローカルにキャッシュし、データ更新があれば実際にデータのやり取りを行う優れた機能ですので、そこまで気にする必要は無いかもしれません。

本稿の構成

以下で進めます。

  • FirebaseのRealtimeDatabaseにスタブデータ(自前で用意したデータ)を登録して、Unityから参照できるようにする
  • UnityからRealtimeDatabaseに書き込みをする

では行きます。

FirebaseのRealtimeDatabaseにスタブデータ(自前で用意したデータ)を登録して、Unityから参照できるようにする

Firebaseコンソールにスタブデータを登録し、ルールを編集して簡単にアクセスできるようにする

FirebaseのRealtimeDatabaseを見てみる

まずは、実際にランキングデータを格納することになる、FirebaseのRealtimeDatabaseを見てみます。
Firebase Consoleのプロジェクトに移動して、以下のキャプチャが示す所を見てください。

f:id:project-unknown:20170830222145p:plain

これが実際にデータを格納する所です。最初のデータが無い状態だとrootを指し示すものだけが表示されていると思います。
今開いているのが、データを弄るエディタにもなっているので、実際にデータを手入力で入れてみます。

RealtimeDatabaseにスタブデータを登録する

rootノードをマウスオーバーすると、「+」が出て来るのでクリックします。

f:id:project-unknown:20170831230219p:plain

すると、子要素を入れれるようになるので、ここにデータを追加していきます。

f:id:project-unknown:20170831230304p:plain

ここでは、子要素を更に入れ子にしたいので、「名前」のところだけ入力して、更に「+」ボタンを押します。

f:id:project-unknown:20170831230433p:plain

最終的に、以下のように入力していきます。

f:id:project-unknown:20170831230734p:plain

それぞれの要素の説明は以下です。

  • UnitySample
    • Firebaseでは複数のProjectをぶら下げる事が出来ますし、見て分かるように階層ごとにデータの意味合いをもたらして管理することも出来ます。ここではUnitySampleという名前のProjectという意味での識別子としています
  • Ranking
    • Rankingデータをこの要素の以下にぶら下げるように作っています
  • uid
    • ユーザを一意に特定する為のものです。TwitterのIDでも良いのですが、ここではFirebaseのAuthenticationが発行するuidを入れる用途にしています。
    • 上述の通り、このuidはユーザ毎のrankingデータを格納する為に用意しています
  • name
    • ユーザ名です。Twitterから取得します
  • id
    • ユーザIDです。Twitter IDを入れます
  • score
    • ユーザのScoreです。
  • updatedate
    • データの更新日を入れておきます。

これで、スタブデータを入れることが出来ました。
ここまでデータを入れてきておわかりの通り、FirebaseのRealtimeDatabaseはJSONライクのNoSQLっぽいデータの取扱となります。
SQL周りを知らなくてもなんとかなると言うのが気軽に使えて便利ですね。
ただ、その分DBMSでよく使うリレーションシップ周りのデータ管理をしようと思うと、自力でデータ構造を設計してあーだこーだ考えないといけなくなるので、そこはデメリットになるかも…。

RealtimeDatabaseのルールを編集する。

冒頭で記載しました通り、Firebaseへのアクセスに制限を加えることができます。
デフォルトでは、Authenticationの認証が終わっていないとアクセスする事ができないので、今回の説明を行う上でちょっと不便となるので、アクセス制限を開放します。
ルールについては、次回に詳細を書く予定ですので、本番リリースする際は、確実に適切なルール設定をお願いします
というのと、アクセス制限を開放するのは、テスト時であっても危険ですので、出来ることなら、開発が一段落したらルールを設定して制限するなど自衛を行うように出来ると良いですね。

まず、Database > ルールでルール画面を開きます。

f:id:project-unknown:20170831232437p:plain

恐らくデフォルトでは、以下が設定されていると思います。

{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null"
  }
}

この状態だと、Firebaseの認証しているユーザのみ読み書きが出来る状態となっています。
これを以下に置き換えてください

{
  "rules": {
    ".read": true,
    ".write": true
  }
}

これで、全てのユーザが読み書き出来るようになります。

UnityからFirebaseのRealtimeDatabaseとデータのやり取りを行う

UnityからFirebaseのRealtimeDatabaseのデータを参照する

前提として、FirebaseSDKがimportされている必要があります。
まだの方はVol2の記事に導入の仕方を載せてますので、importをお願いします。

さぁいよいよRealtimeDatabaseとUnityとを接続させます!

ここでの例は、Firebase Realtime Databaseにあるscoreが一番高いデータを取得する。を目標に実装を行います。

UnityからFirebaseへ接続する設定は、ここまでで全部終わっているので、コードをゴリゴリ書くだけです。

説明は後ほど記載しますので、まずは完成形のコードを記載します。

前回のFirebaseへTwitterアカウントでログインする際のコードを拡張します。

gist.github.com

では、以下に今回追加分になったコードの説明を記載していきます。

コードの詳細

フィールド

フィールドには、以下を追加しています。

// Firebase
private DatabaseReference _FirebaseDB;
private Firebase.Auth.FirebaseUser _FirebaseUser;
  • _FirebaseDB
    • Firebase RealtimeDatabaseへ参照を持ったObjectを保持します。(参照の仕方は後述)
    • 以後は、このObjectに対して読み書きの処理を行えば、適切なタイミングでデータを引っ張り出してくれたり、書き込みの通信を行ってくれるようになります。
  • _FirebaseUser
    • 前回の記事に載せるのを失念していたのですが、Firebaseに接続した際の認証情報を保持し、Firebaseとユーザ認証周りでやり取りする際には、このObjectに保持されている情報を利用します
Startメソッド

Startメソッドには、以下を追加しています。

// Firebase RealtimeDatabase接続初期設定
FirebaseApp.DefaultInstance.SetEditorDatabaseUrl("Firebase Realtime Database URL");

Firebaseの接続設定を行います。
中身まで読み取っていないですが、シングルトン設計になっているっぽくて、引数に参照先のURL(Firebase Realtime Database URL)を記載する事で、以後RealtimeDatabaseの参照先はそのURLを見に行くようになります。
URLは、以下のキャプチャの場所を参照して入力してください。

f:id:project-unknown:20170902125359p:plain

// Databaseの参照先設定
_FirebaseDB = FirebaseDatabase.DefaultInstance.GetReference("UnitySample/Ranking");

フィールドにある_FirebaseDBに、どの要素を参照するのかを保持します。
冒頭で作成したスタブデータの、

f:id:project-unknown:20170831230734p:plain

Ranking以下を参照して行きたいので、「UnitySample/Ranking」と記載します。
このように階層は「/」区切りで指定していけば、該当の要素まで一気に絞って参照することができます。

もちろん、「UnitySample/Ranking」ではなく「UnitySample」としても良いのですが、
そうすると、今回必要なのはRanking以下のデータなのに、UnitySampleにぶら下がった全てのデータを取得するようになり、データ通信的に無駄になりますし、後の処理も一々Ranking以下を参照するコードを書かないといけなくなり冗長となってしまいます。
この例の用に、参照する要素が特定出来るのであれば、参照先は一気にその場所まで指定してしまった方が良いです。

FirebaseLoginメソッド

このメソッドは殆ど変更はありません。
認証が完了した後の、以下2行を追加しています。

_FirebaseUser = task.Result;

// 認証完了したらデータを読み込む
this.GetRankingData();

最初の_FirebaseUserに、task.Resultを突っ込んでいるのは、後ほどFirebase認証情報を利用したいので、保持するようにしています。
二つ目の、this.GetRankingData()が、後述するランキングデータを読み込むメソッドを認証が完了した際に実行しようとしています。

GetRankingDataメソッド

今回の肝となる部分です。
ここでは、先程Firebase参照Objectである、_FirebaseDBを用いて実際にデータを取得しています。

_FirebaseDB.OrderByChild("score").LimitToLast(1).GetValueAsync().ContinueWith(task => {
});

ここで、Firebaseからscoreが1番高いユーザのデータを1件だけ取得する。
事を実現しています。

GetValueAsync

GetValueAsyncを使う事で、指定されたPath(UnitySample/Ranking)を1度読み込む事が出来ます。
読み込んだデータはFirebaseでスナップショットと呼ばれます。
なので、データを取得するだけであれば、以下でも十分です。

_FirebaseDB.GetValueAsync().ContinueWith(task => {
});

ただ、これだと「UnitySample/Ranking」パス以下のデータを全て取得してしまうので、不要なデータも受け取ってしまい通信量が増えますし、その後の処理も要るデータ/要らないデータとを分けて処理を行う必要が出てきてしまうため、非常に無駄な事をしてしまいます。
ですので、後述にある通り、ある程度必要なデータを絞って取得するようにします。

今回の例では、1度読み込むだけにしていますが、Realtime Databaseの旨味である、データに変更があったら通信を行うことも出来ます。
これについては別途記事にします。

OrderByChild

SQLを利用した事がある方だとなんとなく意味が理解できそうですが、これで結果を並べ替えることが出来ます。

  • OrderByChildは指定した子キーの値で結果を並べ替える事ができます。
    • また、子キーは指定したPath直下じゃなくても、子の子でも指定できます。
      • 今回の場合は、「UnitySample/Ranking」配下の「uid/score」で並べ替えを行います。
  • OrderByChild("score")とすることで、scoreを昇順に並び替えます。
  • OrderByChildは降順には並び替えることが出来ません。
    ただ、今回はランキングが一番高いユーザ情報を取得したいので、降順なデータが欲しい所。
    そんなときは、以下の、LimitToLastを使って降順なデータ取得を実現します。

データの並び替えには、他にも以下の機能がサポートされています。

  • OrderByKey()
    • 子キーで結果を並べ替える
  • OrderByValue()
    • 子の値で結果を並べ替えます
LimitToLast

これは、OrderByChild等で並び替えられたデータを、末尾から返却する最大数を設定します。
なので、OrderByChildで降順に並び替えを行い、LimitToLastで最後の方のデータを取得する事で降順が実現できます。
以下は具体例です。

// 元のデータの並び
1,3,2,6,5,4,8,7,9

// OrderByChildを掛けた後
1,2,3,4,5,6,7,8,9

// LimitToLast(2)で最後から2番目まで取得
8,9

// これをローカルで大きい順に並び替える
9,8

本当だったら、OrderByChildDeskとかあればいいんですけどね…。
LimitToLastと似た機能として、以下が提供されています。

  • LimitToFirst()
    • LimitToLastの反対で最初の方からデータを取得します
  • StartAt()
    • 指定したキーまたは値以上のデータを取得します
  • EndAt()
    • 指定したキーまたた値以下のデータを取得します
  • EqualTo()
    • 指定してキーまたは値に等しいデータを取得します
まとめると
_FirebaseDB.OrderByChild("score").LimitToLast(1).GetValueAsync().ContinueWith(task => {

これは、
scoreを降順で並び替えたデータの最後から1つ目までのデータを取得する
という事を行っています。

DataSnapshot snapshot = task.Result;

上述していますが、Firebaseで取得したデータをshapshotと呼称しており、
取得した結果(task.Result)をDataSnapshot型のオブジェクトに突っ込みます。

IEnumerator result = snapshot.Children.GetEnumerator();

snapshotに含まれる結果セットをIEnumeratorに一度突っ込み、後のデータを取り出しやすくします。

while (result.MoveNext())

ここで、取得した結果セットをあるだけwhileを回し、データの中にアクセスします。
このwhileの中で、Rankingの子要素にアクセスして、データを受け取っています。

駆け足でしたが、ここまででデータの取得処理が完了です。
一度実行してみましょう。

UnityEditor上だと、Firebaseの接続がうまくいかない事があるので、実機で確認します。
以下は、XCode上からiPhoneで起動してDebugLogの中身を表示しています。

f:id:project-unknown:20170902211227p:plain

無事にデータを取得する事が出来ています。
ただ、スタブデータを1件しか登録していなかったので、OrderByChildやLimitToLastが効いているかわかりにくいので、もう1つデータを登録します。

以下のように、uid2にscoreが200のユーザを追加します。

f:id:project-unknown:20170902211519p:plain

これで再度実行してみましょう。

f:id:project-unknown:20170902211624p:plain

無事取れていますねヽ(=´▽`=)ノ

さて、ここまででFirebase Realtime Database上にあるデータを取得するところまで出来ました。
次は、スタブデータでは無くて、実際にデータを登録します。

UnityからRealtimeDatabaseに書き込みをする

データ取得と同様、Firebaseの設定は終わっているので、コードを書いていきます。
これまでのコードに処理を書き足します。
まずは、最初に完成形のコードをお見せします。

gist.github.com

処理の流れを以下の通りに変えています。

before

  1. Teritterログイン
  2. Firebaseログイン
  3. Realtime Databaseからデータ取得

after

  1. Twitterログイン
  2. Firebaseログイン
  3. Realtime Databaseへデータ書き込み
  4. Realtime Databaseからデータ取得

細かい処理の流れの変更点は上記コードを見てもらうこととして、今回の肝であるデータを追加しているロジックを見ていきます。

WriteNewScoreメソッド

引数に渡されたScoreをFirebase Realtime Databaseへ書き込みに行く用途のメソッドです。
データはDictionary形式でデータ構造を作成します。

しつこいかもしれませんが、今回作成するデータ構造は以下の通りでしたね。

f:id:project-unknown:20170831230734p:plain

「_FirebaseDB」に「UnitySample/Ranking」までのPathを参照するようになっているので、それ以下のデータを作成していきます。

まず、最下層に位置する「name」「id」「score」「updatedate」のデータセットを作成します。

Dictionary<string, object> itemMap = new Dictionary<string, object>();
itemMap.Add("name",       _FirebaseUser.DisplayName);
itemMap.Add("id",         _UserName);
itemMap.Add("score",      score);
itemMap.Add("updatedate", System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"));

前回の記事でも書きましたが、Display NameはTwitter Kit for Unityではサポートしていないので、Firebase認証情報から取得するんでしたね。

次に、「uid」部分のデータ・セットを造ります。

Dictionary<string, object> map = new Dictionary<string, object>();
map.Add(_FirebaseUser.UserId, itemMap);

冒頭でも記載しましたが、uidには、FirebaseのAuthenticationが発行するuidを登録したいので、「_FirebaseUser.UserId」からデータを取得しています。
そして、uidをkeyに作成した「name」「id」「score」「updatedate」を纏めたitemMapをvalueに登録します。

最後に

_FirebaseDB.UpdateChildrenAsync(map);

上記、Firebaseへ参照を持ったオブジェクトの、UpdateChildrenAsyscメソッドに、作成したデータをセットすれば、「UnitySample/Ranking」以下にデータを登録できます。

余談

ここまで記載した通り、Dictionary形式でデータセットを作成し、階層構造のデータをつくる分には分かりやすいのですが、毎回このような書き方をすると非常に面倒だったりする時があります。
この場合便利な記載方法があります。

例えば、もう既にuidまで登録されていて、そのuidに紐づくscoreを書き直したいんだと言う場合は、以下のようにも書けます。

Dictionary<string, object> map = new Dictionary<string, object>();
map.Add(_FirebaseUser.UserId + "/score/", itemMap);

_FirebaseDB.UpdateChildrenAsync(map);

このように、書き直したいデータまでKeyに一気にパスを貼ってしまって、それに該当するデータだけセットすれば、データ登録を行うことが出来ます。
極論で言うと、以下のような書き方も出来ます。

Dictionary<string, object> map = new Dictionary<string, object>();
map.Add("/Ranking/" + _FirebaseUser.UserId + "/score/", itemMap);

FirebaseDatabase.DefaultInstance.GetReference("UnitySample").UpdateChildrenAsync(map);

上記の例は、Firebaseへの参照を「UnitySample」までしか貼っていないケースで、RankingからscoreまでのPathを一気に貼る例です。

余談2

Sampleの以下のコードの説明がねーよって思っている人がいるかもしれません、

string key = FirebaseDatabase.DefaultInstance.GetReference("UnitySample").Child("Ranking").Push().Key;

今回の事例では利用していないのですが、Firebaseのデータ管理の意味で重要で便利な意味合いを持つ所なので、今回ご紹介しています。

この部分は、UnitySample/Ranking以下に1つユニークな文字列を作成する
これをどういう時に利用するのか?と言うと、
例えば、今回の例では、ユーザ毎にデータを登録して、更新があれば上書きする仕組みにしていますが、例えばスコアを登録する度に、Updateでは無く追加する場合に大いに役に立ちます。
具体的に言うと、

今回の例

  • 1位 : ゆう@あんのうん
  • 2位 : ぽぽた
  • 3位:どうまず

Push().Keyを使った具体例

  • 1位:ゆう@あんのうん
  • 2位:ゆう@あんのうん
  • 3位:ゆう@あんのうん

上記の用に、ユーザ名でユニークなデータ構造ではなくて、毎回スコア更新する度に別なデータとして登録する事で、まさに昔のゲームセンターにハイスコアを一人のユーザで埋め尽くすような機能も実装出来ます。
ただ、この場合問題となるのが、ユーザ名でユニーク化をしないので、何かしらデータを特定出来るユニークなKeyが必要となり、この例の様なKeyを使うことで同一ユーザでも別なデータとして取り扱うことが出来るようになります。

ただ、Keyは発行する度に異なりますので、自前で何処かに保存しておかないと、自分自身ですら分からないことになるので、取扱注意です。(Firebase上でこれを解決する場合は、「UnitySample/Ranking」とは別に「UnitySample/User/uid/」以下にそのユーザが発行したKeyをぶら下げると言う管理の仕方もありですね。

実行する

余談のほうが長いんじゃないかのレベルで記載してきましたが、さぁ実行してみましょう。
今回は、実際にデータを登録しているので、実機ログではなくて、Firebase Consoleのデータを見てみます。

f:id:project-unknown:20170902224140p:plain

(uid部分は私の実際にuidが入り込んでいるのでモザイクにしています。実際に見てもらえば分かるのですが、英数字の文字列が羅列されていると思います)

これでデータの登録も出来るようになりましたね!

また、例えば、以下の

this.WriteNewScore(100);

をスコア更新の意味で、

this.WriteNewScore(300);

に変更して、再度実行してみましょう。
新規にデータが登録されるんじゃなくて、既にあるユーザのScoreが上書きされたかと思います。

自分自身のデータを取得する

これまではランキングの上位10件を取得する機能を作って来ましたが、自分自身のデータが欲しい時があります。

再掲ですが、以下のデータから、自分自身のuserIDにぶら下がる情報を取得します。

https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20170831/20170831230734.png

欲しい情報は、Ranking直下にある、自分自身のUIDの情報です。
このような時は、Childメソッドを使って自分自身のデータを取得します。

_FirebaseDB.Child(_FirebaseUser.UserId).GetValueAsync().ContinueWith(task => {
    if (task.IsFaulted) {
        // 失敗時の処理
    } else if (task.IsCompleted) {
        // 成功時の処理
    }
});

_FirebaseDBは、

_FirebaseDB = FirebaseDatabase.DefaultInstance.GetReference("UnitySample/Ranking");

UnitySample/RankingまでのPathが貼られているので、その直下にある、自分自身のUserIDを指定し取得します。

まとめ

実際にFirebase Realtime Databaseへデータの読み書きを行う所まで行いました。
今回の所で、オンラインランキングシステムが実現できるようになりましたが、何度も記載している通り、今のままだとセキュリティリスクが非常に高いですので、次回はセキュリティ対策の意味を込めた「ルール」造りをしていきます。

参考