📃

Unityからスプレッドシートに直接アクセスしてデータ調整を高速化できた話

2023/12/05に公開

「Happy Elements Advent Calendar 2023」 12月5日の記事です。

はじめに

Happy Elements株式会社のカカリアスタジオでゲームエンジニアをしているtommyです。今年、カカリアスタジオからカジュアル恋愛ノベルゲーム「六ツ獄恋いろは」をリリースしました。その開発において私はメインエンジニアを務めました。

「六ツ獄恋いろは」は、スマホでカジュアルにあやかしたちとの学園での恋愛を楽しめる恋愛ノベルゲームです。
https://mutsugoku-koiiroha.jp/

ノベルゲームという性質上「六ツ獄恋いろは」は大量のシナリオを含んでおり、全てに演出を付けていく必要がありました。「六ツ獄恋いろは」では主にスクリプト専任担当とライターの二人で演出付けが行われており、少人数で大量のシナリオデータ化作業を捌くためにできるだけ効率よく行う必要があると考えました。

そこで、Unity上や開発用アプリから直接スプレッドシートを読み込めるようにすることで、スプレッドシートで編集した内容が即時にアプリに反映されて動作確認できる仕組みを開発しました。

この仕組みはデータ作成担当から非常に好評で、実際に効率よくデータワークを進めることができました。

ゲームの開発過程ではデータを色々触りながらバランス調整や演出調整を行うケースは多いですが、直接スプレッドシートのデータを取得してゲームを動かせるようにしておくことで、データインポートやエンジニアのビルド等を介さずに非エンジニアでも調整を容易に行うことができるようになるため、効率化できるケースは多いのではないかと思います。

ということで、本記事ではUnityからスプレッドシートを直接読み込む方法についてご紹介します。

Unityからスプレッドシートを読み込む

この手の話をネットで検索すると、

  • SpreadSheetを共有設定してAPIを叩く
  • GASでAPIを公開して取得する

という方法が出てきたりもしますが、クライアントから何の認証もなしにデータが取れてしまう方法はセキュリティ上の懸念があるため組織における開発では採用しづらいところです。

なので、今回はGCPでサービスアカウントを作成してAPIを叩く方法で実装を行いました。

設定周りは以下の.NETのガイドも参考にしていただければと思います。
https://developers.google.com/api-client-library/dotnet/get_started?hl=ja

GCPの設定

まずGCPのプロジェクトを作成し、必要なAPIを有効化します。
今回はSheets APIとDrive APIを有効化しています。

次に、認証情報からサービスアカウントを作成します。
必要な項目を入力して作成します。

後はアクセス用のキーを作成してjsonをDLしておきます。
このキーファイルの扱いは気を付けてください。

Google Driveへのアクセス設定

Google Driveの共有設定に作成したサービスアカウントを追加します。
アクセスしたいフォルダの共有設定で、作成したサービスアカウントに読み取り権限を付与しました。

これでサービスアカウントの設定は完了です。

UnityにGoogle APIプラグインを取り込む

.NET向けのプラグインが提供されているので、そちらをUnityに取り込みます。

ということでNuGetから必要なパッケージを探します。
https://www.nuget.org/profiles/google-apis-packages

今回は全て手動でDLしました。右のDownload packageからダウンロードして、.nupkgを.zipにファイル名を変更してから展開します。

今回は以下をダウンロードしました。

  • google.apis.1.57.0
  • google.apis.auth.1.57.0
  • google.apis.core.1.57.0
  • google.apis.drive.v3.1.57.0.2684
  • google.apis.sheets.v4.1.57.0.2657
  • newtonsoft.json.13.0.2-beta1

展開したらプロジェクトに合わせてdllファイルを探してください。
今回はnetstandard2.0のものを使用しています。

そしてこれらをUnityにインポートします。

これで準備は完了です。

認証処理

Streaming Assetsから取得する例です。

using System.IO;
using Google.Apis.Auth.OAuth2;
using UnityEngine;

public static class GoogleAuthService
{
    static string KeyPath => Path.Combine(Application.streamingAssetsPath, "path-to-key-json.json");

    private static ICredential _credential;

    public static ICredential GetCredential(string[] scopes)
    {
        if (_credential != null)
        {
            return _credential;
        }

        using (var stream = new FileStream(KeyPath, FileMode.Open, FileAccess.Read))
        {
            _credential = GoogleCredential.FromStream(stream)
                .CreateScoped(scopes).UnderlyingCredential;
        }

        return _credential;
    }
}

Google Driveのデータ取得

※UniTaskを使用しています。

using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using Google.Apis.Drive.v3;
using Google.Apis.Drive.v3.Data;
using Google.Apis.Services;

public static class GoogleDriveService
{
    private static readonly string[] Scopes = new [] { DriveService.Scope.DriveReadonly };
    const string ApplicationName = "任意のアプリケーション名";

    private class PageToken
    {
        public string Token { get; set; } = "";
    }

    public static async UniTask<List<File>> GetFolderList(string folderId)
    {
        var credential = GoogleAuthService.GetCredential(Scopes);
        var driveService = new DriveService(new BaseClientService.Initializer
        {
            HttpClientInitializer = credential,
            ApplicationName = ApplicationName
        });

        var query = $"('{folderId}' in parents) and (mimeType = 'application/vnd.google-apps.folder') and (trashed = false)";
        var pageToken = new PageToken();
        var files = new List<File>();
        files.AddRange(await RequestFiles(driveService, query, pageToken));
        while (!string.IsNullOrEmpty(pageToken.Token))
        {
            files.AddRange(await RequestFiles(driveService, query, pageToken));
        }

        return files;
    }

    public static async UniTask<List<File>> GetSpreadSheetList(string folderId)
    {
        var credential = GoogleAuthService.GetCredential(Scopes);
        var driveService = new DriveService(new BaseClientService.Initializer
        {
            HttpClientInitializer = credential,
            ApplicationName = ApplicationName
        });

        var query = $"('{folderId}' in parents) and (mimeType = 'application/vnd.google-apps.spreadsheet') and (trashed = false)";
        var pageToken = new PageToken();
        var files = new List<File>();
        files.AddRange(await RequestFiles(driveService, query, pageToken));
        while (!string.IsNullOrEmpty(pageToken.Token))
        {
            files.AddRange(await RequestFiles(driveService, query, pageToken));
        }

        return files;
	// 戻り値のFileオブジェクトの以下を利用するなどします
	// file.Id : スプレッドシートの場合スプレッドシートのID
	// file.Name : ドライブ上の表示名
    }

    private static async UniTask<IList<File>> RequestFiles(DriveService service, string query, PageToken pageToken)
    {
        var listRequest = service.Files.List();
        listRequest.PageSize = 200;
        listRequest.Fields = "nextPageToken, files(id, name)";
        listRequest.PageToken = pageToken.Token;
        listRequest.Q = query;

        var response = await listRequest.ExecuteAsync();
        if (response.Files != null && response.Files.Count > 0)
        {
            pageToken.Token = response.NextPageToken;
            return response.Files;
        }

        pageToken.Token = null;
        return new List<File>();
    }
}

GoogleDriveのAPIは何も指定しないと全ファイル・フォルダが返ってきます。
なのでqueryで指定する必要があります。
あるIDのドライブ以下のファイル・フォルダ情報が欲しければ

'{folderId}' in parents

という指定をします。
ファイル(フォルダ)のタイプを指定したければ

mimeType = 'application/vnd.google-apps.spreadsheet'

という指定をします。

今回は

  • このサービスアカウントがアクセスできるフォルダ一覧を取得
  • 特定のフォルダ以下のスプレッドシート一覧を取得

というメソッドを作成しています。

Google SpreadSheetのデータを取得

Google Driveのフォルダ以下のシート一覧を取得できるところまできました。
これを開発用ページなどでリストで表示して、ファイルを選択してロードするような形で利用しています。

using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using Google.Apis.Services;
using Google.Apis.Sheets.v4;

public static class GoogleSpreadSheetService
{
    private static readonly string[] Scopes = new [] { SheetsService.Scope.SpreadsheetsReadonly };

    public static async UniTask<IList<IList<object>>> GetSheet(string sheetId, string sheetRange = "Sheet!A:Z")
    {
        var credential = GoogleAuthService.GetCredential(Scopes);
        if (credential == null)
        {
            return null;
        }

        var sheetService = new SheetsService(new BaseClientService.Initializer
        {
            HttpClientInitializer = credential
        });

        // sheetRangeで指定されたセルのデータを取得
        var result = await sheetService.Spreadsheets.Values.Get(sheetId, sheetRange).ExecuteAsync();
        return result.Values;
    }
}

IList<IList<object>>が戻り値で返って来るのですが、自分の環境では全てstringで返ってきているようでした。スプレッドシート側のセルの設定によるのかもしれません。

運用方法

以上の仕組みによって、スプレッドシートでデータ入力されたシナリオ演出データをゲーム内で扱えるようにパースするようなプログラムを書いて、そのままシナリオを再生して動作確認ができるようにしています。

開発環境では、Unity上・開発ビルドから直接Google APIを利用してシートで入力した内容を動作確認できるようになっています。
リリース状態としては、一度スプレッドシートで入力されたデータをMessagePackでシリアライズしてファイルを作成し、そちらを読み込むようになっています。

おわりに

今回は「六ツ獄恋いろは」の開発の中でエンジニアの力でデータワークを効率化できた事例についてご紹介させていただきました。

こちらの記事がUnityからGoogle Drive・Google SpreadSheetのAPIを利用したい方の助けとなったり、ゲーム開発のデータワーク効率化のアイデアとなれば幸いです。

Happy Elements

Discussion