20200113のC#に関する記事は1件です。

Unity+Googleスプレッドシート+GASでサーバーレスのデータベースシステムを実現する?

前書き

unity1weekをきっかけに、Unity+Googleスプレッドシート+GASで簡易ランキング機能を作ってみました。この仕組みで、ランキングだけではなく、任意のデータをアップロード・ダウンロードできれば、一応汎用的なデータベースとして使えるのではないかと思いました。例えばゲームのマスタデータをGoogleスプレッドシートに保存することで、アプリのバージョン更新なしでゲームの調整ができてまうとか、ゲームの最新バージョンをGoogleスプレッドシートに書き込んで、アプリ起動時にそれを取得して古ければ強制アップデートポップアップを出すとか、さらにチャットやユーザー情報の管理もGoogleスプレッドシートでやりとりするなど、ユースケースがどんどん湧いてきます。
ということで、実装してみました。

使い方

プロジェクトはUmbrella(トトロなので)という名でGithubに公開しました。最新パッケージのダウンロードはこちら。ちなみに、Umbrellaには単純なデータ通信管理システムDatabase以外に、簡易ランキングシステムRankingも含まれています。興味ある方はぜひ合わせていじってみてください。

GAS側

  1. 新しいGoogleスプレッドシートを作成する。
  2. メニューのツールからスクリプト エディタをクリックする。
  3. Assets/Umbrella/Database/Database.gsの内容をコード.gsにコピーする。
  4. メニューのファイル > 保存でプロジェクトに名前をつけて保存する(保存にちょっと2、3秒ぐらい掛かるかも)。
  5. メニューの公開 > ウェブアプリケーションとして導入...をクリックする。
  6. ウェブアプリケーションとして導入のポップアップにて、次のユーザーとしてアプリケーションを実行に自分のアカウントを、アプリケーションにアクセスできるユーザー全員(匿名ユーザーを含む)を設定する。
  7. 導入をクリックして、現在のウェブアプリケーションのURLの下に書いてあるURLをコピーする。
  8. 「認証が必要です」のポップアップが出たら@zk_phiさんの記事を参考にして認証を行ってください。

Unity側

  1. Assets/Umbrella/Database/DatabaseManager.prefabをデータをやりとりしたいシーンのヒエラルキーにドラッグ&ドロップする。
  2. プレハブインスタンスのインスペクターから、App URLのフィールドに先ほどコピーしたGASのウェブアプリケーションURLをペーストする。
  3. Default Sheetフィールドに使いたいGoogleスプレッドシートのデフォルトシート名を入力する。
  4. スクリプト内で、データを送信したい場合はDatabaseManager.Instance.SendDataAsync(data, handleResponseCallback, sheetName)を呼び、データを取得したい場合はDatabaseManager.Instance.GetDataAsync(key, handleResponseCallback, sheetName)を呼ぶ。また、メソッドの前にyield returnを付ければデータ取得後の処理(handleResponseCallback)の実行完了まで待つことができる。
  5. 具体的な使い方はサンプルシーンとスクリプトを参照してください。

デモ

  • Googleスプレッドシートにデータを送信します。デモではデータ名と値のペアで複数データを送信しています。
    send_data.gif

  • Googleスプレッドシートにあるデータを更新します。デモではデータ名を指定して新しい値を送信しています。
    update_data.gif

  • 他のクライアントとしてデータを送信します。Umbrellaではクライアントを識別するユニークIDをUnity側で生成してPlayerPrefsに保存しているため、PlayerPrefsをクリアしないまま送信すると既存のデータを上書きすることになります。
    send_another_data.gif

  • Googleスプレッドシートにあるデータを取得します。デモではデータ名のリストで複数のデータを取得しています。
    get_data.gif

  • セル参照で範囲内のデータを一気に取得する方法もあります。
    get_data_by_cell.gif

実装の抜粋

GAS側の処理

GASはUnityから送ってきたデータに基づき、スプレッドシートの中身を更新します。

function doPost(e) {
  var request = e.parameter;
  var method = request[CONST.Method];

  if(method == CONST.SaveData){
    return saveData(request);
  }else if(method == CONST.GetData) {
    return getData(request);
  }

  return ContentService.createTextOutput("Error: Invalid method");
}

saveDatagetDataの実装詳細はここでは省略しますが、GASのAPIコールはコスト高いため、処理速度を高めにはできる限りAPIコールの回数を減らす必要があります。例えばセルデータの取得で、各セルでsheet.getRange().getValue()の代わりに、予めvar data = sheet.getDataRange().getValues()で指定範囲のセルデータを一括で配列に格納し、後で配列から値を取るなどの策が考えられます。また、データの書き込みがある場合、排他処理(ロック)を入れる必要がありますが、一行のデータをまとめて一括でappendRow()を使えばセル単位でロックをかける手間をなくすテクニックもあります。appendRow()は不可分操作(Atomic Operation)なので排他処理が要らないからです。

UnityからGASへの送信

簡易のデータベース機能なので、特に通信の仕様とかは決めなく(暗号化??)、完全にJSON形式で送信しています。JSON解析は軽量のMiniJsonを導入しています。

public CustomYieldInstruction SendDataAsync(MonoBehaviour context, string methodName, string sheetName, Dictionary<string, object> data, Action<object> handleResponse = null)
{
    var strData = Json.Serialize(data);

    var formData = new List<IMultipartFormSection>();
    formData.Add(new MultipartFormDataSection("method", methodName));
    formData.Add(new MultipartFormDataSection("sheet", sheetName));
    formData.Add(new MultipartFormDataSection("data", strData));

    bool complete = false;
    context.StartCoroutine(CT_SendData(formData, status => complete = status, handleResponse));

    return new WaitUntil(() => complete);
}

WWWFormがLegacyになったので、IMultipartFormSectionでフォームデータを作成しています。フォームデータにGAS側で呼び出したいメソッド名、使いたいシート名とデータ内容を入れています。また、外部でyield returnをつけて待たせられるように、返り値のタイプをCustomYieldInstructionにしてcompleteがtrueになるまで処理を止めることを可能にしています。

UnityWebRequestPostメソッドでフォームデータを送信しています。

private IEnumerator CT_SendData(List<IMultipartFormSection> formData, Action<bool> updateStatus, Action<object> handleResponse = null)
{
    updateStatus(false);

    var www = UnityWebRequest.Post(_appURL, formData);

    Debug.Log("<color=blue>[GSSDataService]</color> Start sending data to Google Sheets.");

    yield return www.SendWebRequest();

    if (www.isNetworkError || www.isHttpError)
    {
        Debug.LogError($"<color=blue>[GSSDataService]</color> Sending data to Google Sheets failed. Error: {www.error}");
    }
    else
    {
        Debug.Log("<color=blue>[GSSDataService]</color> Sending data to Google Sheets completed");
        try
        {
            var response = Json.Deserialize(www.downloadHandler.text);
            string message = response as string;
            if (message != null && message.Contains("Error")) Debug.LogError($"<color=blue>[GSSDataService]</color> Getting data from Google Sheets failed. {message}");
            else handleResponse?.Invoke(response);
        }
        catch (InvalidCastException e)
        {
            Debug.LogError($"<color=blue>[GSSDataService]</color> Parsing result from Google Sheets failed. Error: {e.Message}");
        }

    }

    updateStatus(true);
}

返ってきた結果にError文字列(GAS側で入れている)が含まれたらエラーログを書き出し、なければ結果を処理するhandleResponseメソッドを呼び出します。

後書き

Unity+Googleスプレッドシート+GASでサーバーレスの簡易データベース機能を作ってみました。当然いくつか問題もあります。

  • 完全JSON形式でデータのやりとりをしているため、複雑のデータ構造に対応できない(自分でシリアライザとデシリアライザを書くなどの工夫が要る)。
  • 安全性一切考えていない(Google神がいい感じにしてくれるはず)。
  • 負荷検証や処理速度を計測・比較していないので不明(使った肌感だと耐えられレベルの遅延)。
  • そもそもGoogleのサービスに制限がある。URL Fetch callsだと、無料のGmailアカウントで1日2万回までしか呼べないので、大規模や非常に頻繁な通信に向いていない。

しかしながら個人プロジェクトレベルのものとしては十分機能できるのではないかと思います。何より、Googleスプレッドシートならではの機能が使えて、直接データを一目瞭然で見たり、気軽にデータを修正したりすることができる点から、普通のSQLデータベースよりも便利かもしれません?(?)

おまけ

Umbrellaにランキング機能もついているので、それの使い方も紹介します。
1. Assets/Umbrella/Ranking/RankingManager.prefabをランキングを表示したいシーンのヒエラルキーに置いておく。
2. Assets/Umbrella/Ranking/RankingSettings.assetのインスペクターから、App URLフィールドにGASのウェブアプリケーションURLをコピーする。
3. Ranking Request Settingsフィールドに、ランキングの種類ごとで配列に要素を入れていく。Ranking Nameはランキングの名前で、Ranking Numberは上位何位まで取得するかを指定し、Order Byは昇順(ASC)か降順(DESC)を選べる。
4. スクリプト内でRankingManager.Instance.SendScoreAsync(playerName, score, handleResponseCallback, rankingRequestIndex)を呼んでスコアを送信し、RankingManager.Instance.GetRankingListAsync(handleResponseCallback, rankingRequestIndex)でランキングリストを取得できる。同様に、メソッドの前にyield returnを付ければデータ取得後の処理(handleResponseCallback)の実行完了まで待つことができる。
5. 具体的な使い方はサンプルシーンとスクリプトを参照してください。

  • Googleスプレッドシートにスコア送信します。
    send_score.gif

  • Googleスプレッドシートにあるスコアを更新します。
    update_score.gif

  • Googleスプレッドシートのランキングリストを取得します。
    get_ranking.gif

参考

  1. UnityのWebGL出力に簡単に無料でグローバルランキングを実装できる仕組みを考えてみた
  2. GAS で「一部のスコープへのアクセス権限がありません」と怒られたときの対処法
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む