17
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-13

2020年7月9日更新:実装変更でメソッド名や使い方の紹介などの修正を行いました。

前書き

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プレハブを通信するシーンのヒエラルキーにドラッグ&ドロップする。
  2. Assets/Umbrella/Database/DatabaseSettingsスクリタブルオブジェクトのインスペクターから、App URLのフィールドに先ほどコピーしたGASのウェブアプリケーションURLをペーストする。
  3. Default Sheet Nameフィールドに使いたいGoogleスプレッドシートのデフォルトシート名を入力する。
  4. 任意のスクリプトで、データを送信したい場合はDatabaseManager.Instance.SendDataAsync(data, sheetName)を呼び、データを取得したい場合はDatabaseManager.Instance.GetDataAsync(keys, responseHandler, sheetName)を呼ぶ。sheetNameを省略した場合は、Default Sheet Nameが使われる。
  5. うち、responseHandlerはGAS側からのレスポンスを処理するコールバックで、通信が終わってから呼ばれる。GAS側のレスポンスはobject型のリストに変換され、それの表示処理などは自分で実装できる。
  6. また、DatabaseManager.Instance.SendDataAsync(data, sheetName)DatabaseManager.Instance.GetDataAsync(keys, responseHandler, sheetName)の前にyield returnを付ければresponseHandlerが実行完了するまで待つことができる。
  7. 具体的な使い方はサンプルシーンとスクリプトを参照してください。

デモ

  • 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.SaveDataMethod){
    return saveData(request[CONST.Payload]);
  }else if(method == CONST.GetDataMethod) {
    return getData(request[CONST.Payload]);
  }
  
  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, IDictionary<string, object> data, Action<object> responseHandler = null)
{
    var strData = Json.Serialize(data);

    var formData = new List<IMultipartFormSection>();
    formData.Add(new MultipartFormDataSection(Const.Method, methodName));
    formData.Add(new MultipartFormDataSection(Const.Payload, strData));

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

    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> responseHandler = 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 responseHandler?.Invoke(response);
        }
        catch (InvalidCastException e)
        {
            Debug.LogError($"<color=blue>[GSSDataService]</color> Parsing result from Google Sheets failed. Error: {e.Message}");
        }
    }

    updateStatus(true);
}

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

後書き

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

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

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

おまけ

Umbrellaにランキング機能も付いているので、それの使い方も紹介します。

  1. Assets/Umbrella/Ranking/RankingManagerプレハブをランキングを表示したいシーンのヒエラルキーに置いておく。
  2. Assets/Umbrella/Ranking/RankingSettingsスクリタブルオブジェクトのインスペクターから、App URLフィールドにGASのウェブアプリケーションURLをコピーする。
  3. Default Ranking Request Dataにデフォルトのランキング取得設定を入力する。
    • Ranking Name:ランキングの名前(Googleシートのシート名でもある)。
    • Top Ranking List Settings
      • Take Number:上位何位までデータを取得する。ゼロならランキングリストがソートされず、空のリストが返ってくる。
      • Order By:昇順(ASC)あるいは降順(DESC)にソートする。
    • Around Me Ranking List Settings
      • Take Number:自分の順位の周りの何個データを取得する(自分のデータが出来る限り中心にされる)。ゼロならランキングリストがソートされず、空のリストが返ってくる。
      • Order By:昇順(ASC)あるいは降順(DESC)にソートする。
  4. スコアを送信したい時は、まずSendScoreRequestData というオブジェクトを作る必要がある。このオブジェクトでは送信するスコアとランキング取得設定をプロパティとして持つことになっている。ちなみに、RankingManager.Instance.CreateDefaultSendScoreRequest(score)を呼ぶことで、デフォルトのランキング取得設定でオブジェクトを作ることができる。ランキング取得時も同様で、RankingRequestDataというオブジェクトを先に作っておく必要がある。あとはRankingManager.Instance.SendScoresAsync(requestDataList, responseHandler)でスコアを送信し、RankingManager.Instance.GetRankingListsAsync(requestDataList, responseHandler)でランキングリストを取得する。
  5. うち、responseHandlerはGAS側からのレスポンスを処理するコールバックで、通信が終わってから呼ばれる。GAS側のレスポンスはRankingResponseData 型のリストに変換され、それの表示処理などは自分で実装できる。
  6. こちらもRankingManager.Instance.SendScoresAsync(requestDataList, responseHandler)RankingManager.Instance.GetRankingListsAsync(requestDataList, responseHandler)の前にyield returnを付ければresponseHandlerの実行完了を待つことができる。
  7. 具体的な使い方はサンプルシーンとスクリプトを参照してください。
  • Googleスプレッドシートにスコア送信します。
    send_score.gif

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

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

参考

  1. UnityのWebGL出力に簡単に無料でグローバルランキングを実装できる仕組みを考えてみた
  2. GAS で「一部のスコープへのアクセス権限がありません」と怒られたときの対処法
17
7
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?