Pages - Menu

2019年3月2日土曜日

[Unity, Firebase] Firebaseを使ってオンラインランキングボードを実装する

ゲームにオンラインランキング機能を付けようって思っても、ランキングサーバーを用意するっていうのはちょっと大変なので
BaaSであるところのFirebaseさんにやってもらおうという記事です。Firebaseは初めて使ったのですが、思ったより簡単にできてしまったのでFirebaseすげーなーって感想です。
記事は長いので実際に実装しながらゆっくり読むと良いですよ。対象はUnityはチョットデキルけどFirebaseはわからんってひと。JavaScriptがなんとなくわかると吉。あと自分用のメモ。

Table of Contents

環境

ぼくの環境です。Windowsじゃない人は適当に読み替えてください。

  • Windows 10 Home / PowerShell 5.1.17134.590
  • .NET Framework 4.7.03056
  • chocolatey 0.10.11
  • Unity 2018.3.0f2

つくるもの

オンラインランキング機能です。まずは単純にユーザー名とスコアを登録/表示するものをつくります。FirebaseのSDKを使って正式にサポートされるのはモバイル向けのみですがデスクトップでもなんやかんやで動きます(公式にはワークフロー用つまりテストとか検証目的ってことになってるんでちょっと微妙ですが)
capture.PNG (34.8 kB)

ソースコードはここに置いています。(Firebaseの設定ファイルとかは入ってません)
https://github.com/HassakuTb/RankingBoardSample

最終的にはこのような構成になります。
cmpRankingBoard.png (22.7 kB)

詳しくは下記で解説しますが、FireStoreがデータベースで、Functionsに実装したAPI経由で
データベースの更新と取得を行います。Authが存在するのはノリですが、
Authを使うとTwitterと連携したりも簡単にできるようになるので、とりあえず入れておけば楽しい!

Firebase

BaaSということで、バックエンドの機能を提供しているサービスです。
サーバーに必要な大体の機能の実装をサーバーを自前で立てずにできてしまうすげーやつです。
データベースとか認証とかストレージとかホスティングとかできます(つまり大体何でもできます)。しかも今回ぐらいの小規模構成なら余裕をもって無料でできてしまいます。最強かよ。母体はGoogle。

コンソールのUIがシンプル+公式ドキュメントが読みやすい ので、これから使うぞって方はとりあえず公式のガイドを読んでみて実際に遊んでみると良いと思います。

Firebase Cloud FireStore

FireStoreはデータベースです。2019年2月現在ベータ版です。
FireStoreの他にRealtime DataBaseというデータベースサービスがありますが、FireStoreの方がいろいろと高機能らしいので今回はFireStoreを使います。Googleさんもお勧めしているし。FireStoreはUnity用のSDKが存在しないため、下記のCloud Functions経由で操作することにしました。(ちなみにRealtime DatabaseにはUnity向けのSDKが存在します)

Firebase Cloud Functions

何かがあったことをトリガーに何かするための機能です。認証に成功したときとかストレージにファイルがアップロードされたりしたタイミングで特定の処理を行います。今回はFunctionsに実装された機能をアプリケーションから直接呼び出すように実装さいます。処理内容はNode.jsで動いているので、Node.jsで実装します。

Firebase Authentication

アカウント認証機能。アカウントの管理とかできます。Twitterでログインとか一瞬でできてしまうやべーやつ。
今回はランキングの登録/取得にいちいち認証するのは(ユーザー的に)めんどくさいので匿名認証を利用しています。

つくる

基本的にここに書いてあることは公式のガイドにも書いてあることがほとんどなので、
一度公式ガイドを読んだ方がいいです。読みやすいです。

Firebaseプロジェクトを作成する

何はともあれFirebaseにプロジェクトを作成します。
コンソールの「プロジェクトを追加」を押すとプロジェクト名を決めてプロジェクトを作成できます。
image.png (72.6 kB)
ちなみに、リージョンは「asia-northeast1」が東京です。
ちょっと待つとプロジェクトができます! 終わり!

ランキング保存用のFireStoreを作成する

ランキングを保存するのにデータベースが必要なのでプロジェクトにFireStoreを追加します。
左側の「開発」の「Database」をクリックするとCloud Firestoreが出てくるので「データベースの作成」をクリックします。
image.png (443.2 kB)
セキュリティルール聞いてきますが、全部非公開にしておきます。(Functions経由ならAdminSDKを通して完全非公開でもアクセスできるため)
image.png (61.2 kB)

一旦これでFireStoreを作成する作業は終わりです。

が、今後作ろうとするデータ構造についてここで話しておきます。
image.png (25.8 kB)

FireStoreは以下の要素でデータが構成されます。

  • コレクション
    • コレクションはドキュメントの集合です
  • ドキュメント
    • ドキュメントはひとつのデータを表すっぽいです
    • 複数のフィールドとコレクションを持てます
  • フィールド
    • データの内容
    • KeyValue
    • いくつかの型があります(stringとかtimestampとか)

今回のランキングボードではentriesという名前のコレクションにnameとscoreという名前のフィールドを持つ単純な構造にします。

entries
├ドキュメント1(IDは自動採番)
│  ├name : "ほげ"
│  └score : 12345
├ドキュメント2(IDは自動採番)
│  ├name : "ふが"
│  └score : 6789
│...

Functionsを作成する

FireStoreは一旦置いておいて次はFunctionsにAPIを作成します。

Node.jsのインストール

作成するにはNode環境(v6かv8)が必要なので、Nodeをインストールします。
Nodeのインストールはnvm(Node Version Manager)を使うと良いです。公式ガイドもそう言ってる。nvmは複数のNode.jsのバージョンを切り替えて使うためのコマンドラインツールです。
nvmのWindows実装はchocolatey 1からインストールするのが簡単でおすすめです。

PowerShell(管理者)
> choco install nvm

入ったら

PowerShell
> nvm list

でインストール確認。
https://nodejs.org/download/release/でパッケージリストを確認してインストール。
Functionsの実装はv6かv8じゃないとダメです。

PowerShell
> nvm install 6.16.0

そしてインストールしたバージョンに切替

PowerShell
> nvm use 6.16.0

これでNode.jsが使えます。

Firebase CLIのインストールと初期化

npm経由でFirebase CLIをインストールします。

PowerShell
> npm install -g firebase-tools

インストール確認

PowerShell
> firebase --version

インストールが確認出来たら認証を通します。

PowerShell
> firebase login

ブラウザが展開するので認証します。

次にFirebaseFunctionプロジェクトを初期化します。
適当な場所にプロジェクトフォルダを作成して、カレントディレクトリを移動します。 2
firebaseプロジェクトを初期化します

PowerShell
> firebase init functions

これを実行するとFIREBASE!
image.png (3.7 kB)
いろいろ聞かれるので答えていきます。

  • Firebaseプロジェクトは新しく作ったプロジェクトを選択します。
  • 言語はJavaScriptとTypeScriptが選べるようです。僕はとりあえずJavaScriptを選択しました。
  • ESLintも入りますが僕はOffにしました。
  • npmの依存パッケージはこの際にインストールしときましょう。

image.png (12.5 kB)
できたぞい。functionsの実装はfunctions/index.jsに実装していきます。

Functionsのデプロイ

index.jsに雛形が記述されているのでコメントアウトを外してデプロイしてみます

index.js
const functions = require('firebase-functions');

// Create and Deploy Your First Cloud Functions
// https://firebase.google.com/docs/functions/write-firebase-functions

exports.helloWorld = functions.https.onRequest((request, response) => {
  response.send("Hello from Firebase!");
});

functions.https.onRequestはHTTPリクエストがあったときに起動するFunctionsで
この実装だとresponseで文字列が返ってきます。

PowerShell
> firebase deploy --only functions

Deploy complete! したらコンソールを見てみるとデプロイされているのがわかります。
image.png (14.3 kB)

HTTPリクエストトリガーなのでブラウザで表示されているURLを叩くと実行できます。
image.png (10.9 kB)
やったぜ。

FunctionsからFireStoreを操作する

Functionsのデプロイができたことだし、これを使ってデータベース操作します。
index.jsを書き換えてレコードを追加するAPIとトップn個のレコードを取得するAPIを作成します。

index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

//  /addEntry?name=ほげ&score=1234
exports.addEntry = functions
  .region('asia-northeast1')
  .https.onRequest((req, res) =>
{
  const entry = {
    name : req.query.name,
    score : parseInt(req.query.score)
  };
  return admin.firestore().collection('entries')
    .add(entry)
    .then((snapshot) =>
  {
    return res.sendStatus(200);
  });
});

//  /getTopEntries?count=100
exports.getTopEntries = functions
  .region('asia-northeast1')
  .https.onRequest((req, res) =>
{
  const count = parseInt(req.query.count);

  return admin.firestore().collection('entries')
    .orderBy('score', 'desc')
    .limit(count)
    .get()
    .then((qSnapshot) =>
  {
    return res.status(200).json(qSnapshot.docs.map(x => x.data()));
  });
});

firebase-adminモジュールはプロジェクト内の要素にアクセスするモジュールでだいたい何でもできるやつです。
これを使ってFirebaseの読み書きをしています。他にデプロイ先のリージョンを変更したりしています。
これをfirebase deploy --only functionsしたらブラウザから叩いてみてFirestoreが更新されるか確認してみます。
image.png (24.2 kB)
image.png (24.8 kB)
addEntryを叩いてみるとドキュメントが増えているはず...!

Unityのセットアップ

APIができたところで、次はSDKを使ってUnityから直接Functionsを呼び出せるようにしていきます。3

SDKのインポート

Unity プロジェクトに Firebase を追加する  |  Firebase
からSDKが入手できます。
unitypackage形式なのでUnityプロジェクトにインポートして完了です。
今回はAuthとFunctionsのみ利用するので、それ以外のパッケージは必要ありません。

Firebaseにアプリケーションを登録する

Firebase側にアプリケーションを登録します。
image.png (115.2 kB)
Project Overviewにそれっぽいメニューがあると思うので、Unityアプリを選択して追加します。
iOSかAndroid4のどっちかもしくは両方を選択して手なりに進めると、google-services.jsonというファイルがダウンロードできるので、これをUnityプロジェクトのAsset内の適当な場所に配置します。
これでUnity側の設定は完了です。

FunctionsをHTTPS callableにする

Functionsのトリガーは現在はHTTPリクエストになっていますが、これをアプリから直接叩ける形に変更します。
index.jsを変更して再度デプロイします。

index.js

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

//  Entryを追加
exports.addEntry = functions
  .region('asia-northeast1')
  .https.onCall((data, context) =>
{
  const entry = {
    name : data.name,
    score : data.score
  };
  return admin.firestore().collection('entries')
    .add(entry)
    .then((snapshot) =>
  {
    return 'OK';
  });
});

//  トップcount件のEntryを取得
exports.getTopEntries = functions
  .region('asia-northeast1')
  .https.onCall((data, context) =>
{
  const count = data.count;

  return admin.firestore().collection('entries')
    .orderBy('score', 'desc')
    .limit(count)
    .get()
    .then((qSnapshot) =>
  {
    return {
      entries : qSnapshot.docs.map(x => x.data())
    };
  });
});

SDKから呼び出すには、functions.https.onCallを使って記述します。

UnityからFunctionを呼び出す

ではこれをUnity側のスクリプトから呼び出してみます。

FirebaseService.cs
FirebaseFunctions functions = FirebaseFunctions.GetInstance("asia-northeast1");
public async Task<object> AddEntryAsync(RankingEntry entry)
{
    object data = new Dictionary<object, object>
    {
        { "name", entry.Name },
        { "score", entry.Score },
    };

    return await functions.GetHttpsCallable("addEntry").CallAsync(data)
        .ContinueWith(task =>
        {
            return task.Result.Data;
        });
}

public void AddEntry(RankingEntry entry)
{
    service.AddEntryAsync(entry).ContinueWith(task =>
    {
        if (task.IsFaulted)
        {
                // onError
        }
        else
        {
                // onComplete
        }
    }, TaskScheduler.FromCurrentSynchronizationContext());
}

呼び出しにはFirebaseFunctions.GetHttpsCallable()で取得したCallbaleインスタンスのCallAsyncを使うわけですが、こいつはasync関数なのでContinueWithなどで待ち受けます5。Taskの結果のハンドリングはメインスレッドに戻してやらないとUnityObjectにアクセスしたときとかにログも何もなく処理が止まって頭を抱えることになるので注意です6 7(1敗)

js側でobject型で受け渡ししていると、Unity側で取得できるのはDictionary<object, object>型で取り回しが悪いのでJson文字列でやり取りした方がいいかもしれません。

これを実行してやるとFireSroreの更新を確認できるはずです。
あとはちょいちょいとUIを構成してやればオンラインランキングボードの出来上がりです。

認証を有効にする

せっかくなので認証もやりましょう。リクエストに認証ユーザーの情報が乗ってないなら処理を止めるようにします。

コンソールからAuthenticationを開いて今回は匿名認証を有効にします。設定終わり!
image.png (46.1 kB)

Functionsを認証しないと使えないようにする

Functionは今はだれでもウェルカムな状態なので、登録されているユーザー情報が載っていないと処理しないように変更します。

index.js
...

exports.addEntry = functions
  .region('asia-northeast1')
  .https.onCall((data, context) =>
{
  if(context.auth === undefined || !context.auth.uid || context.auth.uid === 0){
    throw new functions.https.HttpsError('unauthenticated');
  }
  return admin.auth().getUser(context.auth.uid)
    .then(() =>
    {
      const entry = {
        name : data.name,
        score : data.score
      };
      return admin.firestore().collection('entries').add(entry)
    })
    .then((snapshot) =>
    {
      return 'OK';
    });
});

onCallで登録する関数の第二引数のcontextに認証情報が入ってくるので、
これを見て、Authenticationに該当ユーザーがいる場合だけ処理を通すようにします。

デプロイしたら処理が通らなくなったことを確認できるはず。

Unityで匿名認証する

ではUnity側で認証処理を書きます。

FirebaseService.cs
FirebaseAuth auth = FirebaseAuth.DefaultInstance;

auth.SignInAnonymouslyAsync().ContinueWith(task =>
{
    if (!task.IsFaulted)
    {
        //  onComplete
    }
    else
    {
        //  onError
    }
}, TaskScheduler.FromCurrentSynchronizationContext());

おわり。
認証が終わった後にCallble.CallAsyncを読んでやると処理が通るようになります。すごいかんたん。

匿名認証は認証通る度にユーザーがどんどん増えるのが気持ち悪いので処理が終わったらユーザーを消す処理を追加しておきました。

FirebaseService.cs
auth.CurrentUser.DeleteAsync().ContinueWith(task =>
{
    if (!task.IsFaulted)
    {
        //  onComplete
    }
    else
    {
        //  onError
    }
}, TaskScheduler.FromCurrentSynchronizationContext());

発展させる

今回作ったランキングボードは機能的にはかなり適当なのでいろいろ発展させると楽しいかと思います。例を挙げると

  • 名前とスコア以外を記録してみる
  • 登録したレコードの周辺n件を取得して今回記録を強調表示してみる
  • 直近1週間のみの記録を乗せる
  • 匿名でない認証必須にしてひとり1レコードしか記録できないようにする
  • Twitter認証と連携してスコアボードにアイコンを表示する

などなど簡単なものから結構めんどくさそうなものまでいろいろ考えられるので、好みのランキングボードを作ると楽しそうです。

気を付けよう

Functionsは頻繁にアクセスされないとき、最初の一回は遅いです。10秒ぐらいかかるときもありましたので、この時間を考慮してゲーム内のフローを組まないとダメです。あとオンライン機能なので普通に失敗します。なので異常系処理もちゃんと組みましょうね。

UnityプロジェクトにはgoogleのAPIキーみたいなセキュアな情報が含まれることになるので、何も考えずにgithubとかに置くとかなりよろしくないです。公開するときは慎重にしましょうね。

おわり

この記事はかいふ氏が主催するVOICEゲームジャムで何か役に立ちそうな記事書けないかなーと思って書いたものなので、何か役に立ったらいいなああああああ!

  1. Windowsのパッケージマネージャー apt-getとかyumの類 便利なので超おすすめ

  2. フォルダをShift+右クリックで「PowerShellウィンドウをここに開く」が便利

  3. どこからでもアクセスされちゃうことに気を付ければHTTPリクエストを呼び出す形式でもいいと思う。HTTPリクエストのままで処理するならSDKのサポートが無いとか何も考えなくていいしね。

  4. デスクトップサポートは無いです。デスクトップで使いたい場合もどっちか選択して進めるととりあえず動きます。

  5. Unityは早くTaskをサポートして その気は無いっぽいけど

  6. ログが出ることもある

  7. ちなみにUnityのCurrentSyncronizationContextはWaitForFixedUpdateの後に待ち合わせをスケジュールするっぽい