LoginSignup
5

More than 1 year has passed since last update.

posted at

updated at

Organization

Unity WebGLからGoogle スプレッドシートのデータを取得する

本記事はQualiArts アドベントカレンダー15日目の記事になります。

昨日は@yuuyatu090さんのGoogle Cloud Storage(GCS)にIP制限をかける with Terraformでした

はじめに

ちょっとしたデータの管理にGoogleのスプレッドシートは便利ですよね。
今回はそのスプレッドシートのデータをUnity WebGLから取得する方法を記述します。

スプレッドシートからデータを取るにあたってGoogle APIのOAuth2認証を行なっています。
その際、Google公式のC#ライブラリを使用してみるとどうやらWebGLでビルドした場合に使えないようでしたので(後述)、JS側のライブラリで認証->アクセストークンをC#側に渡してUnityWebRequestでの取得を試みたのが本記事になります。

環境

  • Unity: 2020.1.15f1

必須ライブラリとか

他ライブラリ(個人的に使ってる。無くてもかける)

実装

処理は特に拘らず、 公式のquickstartのコードを組み合わせて実装します。

認証キーの作成

認証はブラウザでやるのでJS側のライブラリで行います。
まず認証キーとかを作らないといけないので、公式のQuickstartから「Enable the Google Sheets Api」をクリックし、プロジェクトとSheet用のApiを有効にします。
Browser Quickstart  |  Sheets API  |  Google Developers

enable
name
enable
①は後から使います。次のプラットフォーム上からも確認できます。

「Create API Key」を押すともう1個プロジェクトが増えそうなので、Google Cloud Platformプラットフォーム上からAPI Keyを追加します。
Google Cloud Platform
プロジェクトは先ほど作成したQuickStartを選択
プロジェクト

左上の「認証情報を作成」から「APIキー」を選択し

作成した後、右下の「キーの制限」から必要分のみの設定を行います

WebGLからのアクセスを想定してますので「HTTP リファラー」を選択し、ローカル確認ように「localhost:8000」を追加します。(実際に使う際は自分のサイトのアドレスを追加しましょう。

APIの制限では、今回スプレッドシートからのデータ取得のみなので「Google Sheets API」を選択します。

保存を押したらAPIキー作成完了です。

WebGLテンプレートの作成

今回C#とJSを記述する必要があるので、生成されるindex.htmlに適当にJavaScriptを書き加えてます
(本当はちゃんとした書き方をした方がいいのですが、記事主の時間的都合から雑に書いてます。綺麗な書き方誰か教えてください🙇‍♂️

テンプレートについての説明は省略します
Using WebGL templates - Unity マニュアル

まずは適当にビルド設定してWebGLビルドを行い、基準となるindex.htmlを作成します。
WebGLプラットフォームで2Dプラットフォームとかで作ったプロジェクトを想定しています。

  • Edit -> PlayerSettings -> WebGL
    • Other Settings -> Api Compatibility Level を「.Net 4.x」に
      • UniRxとか使わないなら実はいらないかも
    • Publishing Settings -> Decompression FallbackをチェックON
      • いつからかCompression Formatがデフォルト「Gzip」になってるので、ローカル実行でエラーがでないようにチェックが必要です。

設定が完了したら一度ビルドします。
ビルドが出来たら、生成されたindex.htmlをテンプレートに使います
WebGL テンプレートの使用 - Unity マニュアル

UnityプロジェクトのAssets配下に「WebGLTemplates」フォルダを作成し、その中に好きな名前のフォルダを作成、最後にその中に生成されたindex.htmlを入れます
今回はSampleGoogleSheetという名前で作ってます

これでPlayerSettingsのResolution and Presentationから選択出来るようになりました
(画像は準備してないのでアレですが

JS記述

認証キー作成で使用したquickstartのサンプルの記述をいくつかテンプレートのindex.htmlに移します。

index.html
    // ~~ 上省略 </script>の手前に追記します 
    document.body.appendChild(script);

    // ここから追記

    // 認証キーたち
    var CLIENT_ID = '<YOUR_CLIENT_ID>';   // ①
    var API_KEY = '<YOUR_API_KEY>';     // ②

    // サンプルからそのまま
    var DISCOVERY_DOCS = ["https://sheets.googleapis.com/$discovery/rest?version=v4"];
    var SCOPES = "https://www.googleapis.com/auth/spreadsheets.readonly";

    /**
     *  ライブラリ読み込み時に呼ばれる
     */
    function handleClientLoad() {
        gapi.load('client:auth2', initClient);
    }

    /**
     *  APIライブラリの初期化
     */
    function initClient() {
        gapi.client.init({
            apiKey: API_KEY,
            clientId: CLIENT_ID,
            discoveryDocs: DISCOVERY_DOCS,
            scope: SCOPES
        }).then(function () {
            // Listen for sign-in state changes.
            gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);

            // Handle the initial sign-in state.
            updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
        }, function (error) {
            console.log(error);
        });
    }

    /**
     *  認証成功し、ログイン状態が変わった時に呼ばれる
     */
    function updateSigninStatus(isSignedIn) {
        if (isSignedIn) {
            // C#を呼び出すコードを書く
        } else {
            // C#を呼び出すコードを書く      
        }
    }

    /**
     *  ログイン状態フラグ C#から呼ぶ(サンプルでは使わない
     */
    function isSignedIn() {
        return gapi.auth2.getAuthInstance().isSignedIn.get();
    }

    /**
     *  認証 C#側から呼ぶ
     */
    function auth() {
        gapi.auth2.getAuthInstance().signIn();
    }

    /**
     *  サインアウト C#側から呼ぶ
     */
    function signOut() {
        gapi.auth2.getAuthInstance().signOut();
    }

</script>

<!-- // ライブラリの読み込み処理 -->
<script async defer src="https://apis.google.com/js/api.js"
        onload="this.onload=function(){};handleClientLoad()"
        onreadystatechange="if (this.readyState === 'complete') this.onload()">
<!-- // 追記終わり -->
</script>
</body>
</html>

C#からJSを呼べるように

C#からJSの関数を呼べるようにjslibを作成します。
以下のサイトが参考になると思います
公式:WebGL: ブラウザースクリプトとの相互作用 - Unity マニュアル
参考サイト:Unity(WebGL)のC#とブラウザのJavaScriptの連携|npaka|note

生成したjslibはPluginsフォルダ配下におきましょう
本記事では Assets > Plugins > Jslib > GoogleApi.jslib として作りました

GoogleApi.jslib
mergeInto(LibraryManager.library, {
    IsGoogleSignedIn: function () {
        return isSignedIn();
    },
    GoogleAuth: function () {
        auth();
    },
    GoogleSignOut: function () {
        signOut();
    }
});

Jslibと連携したC#のコードも作ります

GoogleSheetManager
using System.Runtime.InteropServices;
using UnityEngine;

namespace AdventCalendar2020
{
    public class GoogleSheetManager : MonoBehaviour
    {
#if UNITY_WEBGL
        /// <summary>
        /// jslibと名前を合わせる
        /// </summary>
        [DllImport("__Internal")]
        private static extern bool IsGoogleSignedIn();

        [DllImport("__Internal")]
        private static extern void GoogleAuth();

        [DllImport("__Internal")]
        private static extern void GoogleSignOut();
#endif
        /// <summary>
        /// google認証
        /// </summary>
        public void Auth()
        {
#if UNITY_WEBGL && !UNITY_EDITOR
            GoogleAuth();
#endif
        }

        /// <summary>
        /// ログアウト
        /// </summary>
        public void SignOut()
        {
#if UNITY_WEBGL && !UNITY_EDITOR
            GoogleSignOut();
#endif
        }
    }
}

JSからC#を呼べるように

Unity側

JS側で認証成功時にアクセストークンを送ってこれるように処理を追加します
今回はトークンの更新をReactivePropertyで通知できるようにしました
UniRxライブラリを導入:Releases · neuecc/UniRx · GitHub

GoogleSheetManagerはScene上に同じ名前のオブジェクトを作り、アタッチしましょう。

GoogleSheetManager
// ~~ 
using UniRx;

namespace AdventCalendar2020
{
    public class GoogleSheetManager : MonoBehaviour
    {
        // ~~ 省略 ~~

        /// <summary>
        /// JS側から渡されるアクセストークン
        /// </summary>
        private string _accessToken;

        private readonly ReactiveProperty<bool> _isSignedInProperty = new ReactiveProperty<bool>();
        public IReactiveProperty<bool> IsSignedInProperty => _isSignedInProperty;
        public bool IsSignedIn => IsSignedInProperty.Value;

        // ~~ 省略 ~~
        private void OnDestroy()
        {
            _isSignedInProperty.Dispose();
        }

        #region From JavaScript

        public void SetAccessToken(string token)
        {
            _accessToken = token;
            IsSignedInProperty.Value = string.IsNullOrEmpty(_accessToken) == false;
        }

        #endregion
    }
}

index.html側

index.html
// ~~ 省略 ~~
// gapiで使用できるように
var globalUnityInstance; // 追記

var script = document.createElement("script");
script.src = loaderUrl;
script.onload = () => {
    createUnityInstance(canvas, config, (progress) => {
        progressBarFull.style.width = 100 * progress + "%";
    }).then((unityInstance) => {
        globalUnityInstance = unityInstance; // 追記

        loadingBar.style.display = "none";
        fullscreenButton.onclick = () => {
            unityInstance.SetFullscreen(1);
        };
    }).catch((message) => {
        alert(message);
    });
};
document.body.appendChild(script);

// ~~ 省略 ~~

/**
 *  認証成功し、ログイン状態が変わった時に呼ばれる
 */
function updateSigninStatus(isSignedIn) {
    // gapi初期化時にUnityInstanceがなければ止める
    if (!globalUnityInstance) return; // 追記

    if (isSignedIn) {
        // 追記
        // サインインしてる場合はアクセストークンを渡す
        var user = gapi.auth2.getAuthInstance().currentUser.get();
        var accessToken = user.getAuthResponse().access_token;

        // GoogleSheetManagerのSetAccessTokenを呼ぶ
        globalUnityInstance.SendMessage("GoogleSheetManager", "SetAccessToken", accessToken);
    } else {
        // 追記
        // してない場合は空文字
        console.log("未認証");

        globalUnityInstance.SendMessage("GoogleSheetManager", "SetAccessToken", "");
    }
}

スプレッドシートデータの取得

お世話になってるquickstart にあるスプシデータの読み込みを参考にc#側に記述します。
使用するAPIはこちら:Method: spreadsheets.values.get  |  Sheets API  |  Google Developers

https://sheets.googleapis.com/v4/spreadsheets
の後ろに
/{スプレッドシートID}/values/{範囲}
でそのスプシの範囲のデータが配列で返ってきます。
最後にクエリで
?access_token={取得してきたアクセストークン}
を追加してプライベートなスプシでも取ってこれるようにします。

また、返ってくるJsonデータですが、公式のJson.NET だとWebGL上でエラーが出たのでUnity用の無料のアセットを使います
JSON .NET For Unity
Package Managerからインポートできます

また、本記事では個人的にリクエストの待機にUniTaskを使用してます(なくても書けます
UniTaskライブラリ:Releases · Cysharp/UniTask · GitHub

GoogleSheetManager
public class GoogleSheetManager : MonoBehaviour
{
    /// <summary>
    /// Jsonの変換用クラス
    /// </summary>
    public class ValueRange
    {
        public string range;

        public string majorDimension;

        public string[][] values;
    }

    private const string GoogleSpreadsheetURL = "https://sheets.googleapis.com/v4/spreadsheets";

    // ~~ 省略 ~~ // 

    /// <summary>
    /// スプレッドシートのレンジを取得
    /// </summary>
    /// <param name="spreadsheetId">スプレッドシートのユニークID</param>
    /// <param name="range">レンジ Ex: "シート1!A1:B2" はシート1のA1からB2までという意味</param>
    public async UniTask<ValueRange> GetSpreadsheetAsync(string spreadsheetId, string range, CancellationToken ct)
    {
        var url = $"{GoogleSpreadsheetURL}/{spreadsheetId}/values/{range}";
        try
        {
            var responseText = await SendGetRequestAsync(url, ct);
            Debug.Log(responseText);
            return JsonConvert.DeserializeObject<ValueRange>(responseText);
        }
        catch (UnauthorizedAccessException)
        {
            Debug.Log("未認証");
            return null;
        }
        catch
        {
            throw;
        }
    }

    /// <summary>
    /// UnityWebRequest.Getで送信する
    /// </summary>
    private async UniTask<string> SendGetRequestAsync(string url, CancellationToken ct)
    {
        var finalUrl = AppendAccessTokenToURL(url, _accessToken);
        using (var uwr = UnityWebRequest.Get(finalUrl))
        {
            await uwr.SendWebRequest().WithCancellation(ct);

            if (uwr.error != null)
            {
                var errorDefinition = new
                {
                    error = new
                    {
                        status = ""
                    }
                };
                var errorData = JsonConvert.DeserializeAnonymousType(uwr.downloadHandler.text, errorDefinition);
                // 未認証エラー
                if (errorData.error.status == "UNAUTHENTICATED") throw new UnauthorizedAccessException();
                // その他エラー
                throw new Exception(uwr.downloadHandler.text);
            }

            return uwr.downloadHandler.text;
        }
    }

    /// <summary>
    /// URLの最後にアクセストークンクエリを追加
    /// </summary>
    private string AppendAccessTokenToURL(string url, string token)
    {
        var delimiter = url.Contains("?") ? "&" : "?";
        return $"{url}{delimiter}access_token={token}";
    }

    private void OnDestroy()
    {
        _isSignedInProperty.Dispose();
    }

    #region From JavaScript

    public void SetAccessToken(string token)
    {
        _accessToken = token;
        IsSignedInProperty.Value = string.IsNullOrEmpty(_accessToken) == false;
    }

    #endregion
}

実行用クラスの作成と画面

最後に認証orサインインボタンとゲットボタンを作ってコードを呼びます

コード

SampleGoogleSheet.cs
using System.Threading;
using Cysharp.Threading.Tasks;
using UniRx;
using UnityEngine;
using UnityEngine.UI;

namespace AdventCalendar2020
{
    public class SampleGoogleSheet : MonoBehaviour
    {
        [SerializeField]
        private GoogleSheetManager _sheetManager;

        [SerializeField]
        private Button _authButton;

        [SerializeField]
        private Button _singOutButton;

        [SerializeField]
        private Button _getDataButton;

        private void Start()
        {
            // 最初は何も表示しない
            _authButton.gameObject.SetActive(false);
            _singOutButton.gameObject.SetActive(false);
            _getDataButton.gameObject.SetActive(false);
            Bind();
        }

        private void Bind()
        {
            // サインイン状態が更新されたら検知
            _sheetManager.IsSignedInProperty
                .Subscribe(UpdateButtonActive) // isSigned => UpdateButtonActive(isSigned);
                .AddTo(gameObject);
        }

        /// <summary>
        /// サインイン状態に応じてボタンの表示切り替え
        /// </summary>
        private void UpdateButtonActive(bool isSigned)
        {
            _authButton.gameObject.SetActive(isSigned == false);
            _singOutButton.gameObject.SetActive(isSigned);
            _getDataButton.gameObject.SetActive(isSigned);
        }

        /// <summary>
        /// AuthButtonのEventから呼ぶ
        /// </summary>
        public void GoogleAuth() => _sheetManager.Auth();

        /// <summary>
        /// SignOutButtonのEventから呼ぶ
        /// </summary>
        public void GoogleSignOut() => _sheetManager.SignOut();

        /// <summary>
        /// GetDataButtonのEventから呼ぶ
        /// </summary>
        public void GetSheetData() => GetSheetDataAsync(gameObject.GetCancellationTokenOnDestroy()).Forget();

        /// <summary>
        /// スプシのデータをとりコンソールに出力
        /// </summary>
        private async UniTask GetSheetDataAsync(CancellationToken ct)
        {
            if (_sheetManager.IsSignedIn == false) return;

            // プライベートなスプシIDでも可能(認証した際のアカウントが見れるなら取れる
            // 今回はサンプルそのままを書きます
            // https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit
            var spreadsheetId = "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms";
            var range = "Class Data!A2:E";

            var data = await _sheetManager.GetSpreadsheetAsync(spreadsheetId, range, ct);
            if (data != null && data.values.Length > 0)
            {
                foreach (var row in data.values)
                {
                    Debug.Log($"{row[0]}, {row[4]}");
                }
            }
            else
            {
                Debug.Log("No data found.");
            }
        }
    }
}

Editor上


CanvasScalerはScale With Screen Sizeで x: 960 y: 600
AuthとSingOutは適当にy:120に、GetDataはy:-120です。
ボタンサイズはx: 320 y: 80

実行

ビルドして実行し、動作を確認します
この時、Build And Runをするとポート番号がランダムになるので認証する際に失敗します。
そのため、ビルド成果物のindex.htmlのある階層でコンソールからPythonを使って8000ポートのローカルサーバーを立てて実行しましょう
$ python -m SimpleHTTPServer 8000
ブラウザを開き localhost:8000 にアクセスすると開けます

Authボタンを押すと認証用ブラウザが開きます

認証後、GetDataボタンでログが流れていたら成功です。
GetSheetData.gif

お疲れ様でした。

その他

公式ライブラリを使えなかった理由

  1. Google公式ライブラリだと内部でGoogle認証時のリダイレクト用ローカルサーバーを立てようとするため、ブラウザのWebGLだと失敗するようです。
  2. Jsonの変換で失敗します(多次元配列がダメっぽい)。エディタ上だと実行できるので、WebGLだけ何かコンパイル時に変わってるのかもしれません(詳しくは調べてない)

最終コード

Unity

SampleGoogleSheet
using System.Threading;
using Cysharp.Threading.Tasks;
using UniRx;
using UnityEngine;
using UnityEngine.UI;

namespace AdventCalendar2020
{
    public class SampleGoogleSheet : MonoBehaviour
    {
        [SerializeField]
        private GoogleSheetManager _sheetManager;

        [SerializeField]
        private Button _authButton;

        [SerializeField]
        private Button _singOutButton;

        [SerializeField]
        private Button _getDataButton;

        private void Start()
        {
            // 最初は何も表示しない
            _authButton.gameObject.SetActive(false);
            _singOutButton.gameObject.SetActive(false);
            _getDataButton.gameObject.SetActive(false);
            Bind();
        }

        private void Bind()
        {
            // サインイン状態が更新されたら検知
            _sheetManager.IsSignedInProperty
                .Subscribe(UpdateButtonActive) // isSigned => UpdateButtonActive(isSigned);
                .AddTo(gameObject);
        }

        /// <summary>
        /// サインイン状態に応じてボタンの表示切り替え
        /// </summary>
        private void UpdateButtonActive(bool isSigned)
        {
            _authButton.gameObject.SetActive(isSigned == false);
            _singOutButton.gameObject.SetActive(isSigned);
            _getDataButton.gameObject.SetActive(isSigned);
        }

        /// <summary>
        /// AuthButtonのEventから呼ぶ
        /// </summary>
        public void GoogleAuth() => _sheetManager.Auth();

        /// <summary>
        /// SignOutButtonのEventから呼ぶ
        /// </summary>
        public void GoogleSignOut() => _sheetManager.SignOut();

        /// <summary>
        /// GetDataButtonのEventから呼ぶ
        /// </summary>
        public void GetSheetData() => GetSheetDataAsync(gameObject.GetCancellationTokenOnDestroy()).Forget();

        /// <summary>
        /// スプシのデータをとりコンソールに出力
        /// </summary>
        private async UniTask GetSheetDataAsync(CancellationToken ct)
        {
            if (_sheetManager.IsSignedIn == false) return;

            // プライベートなスプシIDでも可能(認証した際のアカウントが見れるなら取れる
            // 今回はサンプルそのままを書きます
            // https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit
            var spreadsheetId = "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms";
            var range = "Class Data!A2:E";

            var data = await _sheetManager.GetSpreadsheetAsync(spreadsheetId, range, ct);
            if (data != null && data.values.Length > 0)
            {
                foreach (var row in data.values)
                {
                    Debug.Log($"{row[0]}, {row[4]}");
                }
            }
            else
            {
                Debug.Log("No data found.");
            }
        }
    }
}
GoogleSheetManager
using System;
using System.Runtime.InteropServices;
using System.Threading;
using Cysharp.Threading.Tasks;
using Newtonsoft.Json;
using UniRx;
using UnityEngine;
using UnityEngine.Networking;

namespace AdventCalendar2020
{
    public class GoogleSheetManager : MonoBehaviour
    {
        /// <summary>
        /// Jsonの変換用クラス
        /// </summary>
        public class ValueRange
        {
            public string range;

            public string majorDimension;

            public string[][] values;
        }

        private const string GoogleSpreadsheetURL = "https://sheets.googleapis.com/v4/spreadsheets";

#if UNITY_WEBGL
        /// <summary>
        /// jslibと名前を合わせる
        /// </summary>
        [DllImport("__Internal")]
        private static extern bool IsGoogleSignedIn();

        [DllImport("__Internal")]
        private static extern void GoogleAuth();

        [DllImport("__Internal")]
        private static extern void GoogleSignOut();
#endif

        /// <summary>
        /// JS側から渡されるアクセストークン
        /// </summary>
        private string _accessToken;

        private readonly ReactiveProperty<bool> _isSignedInProperty = new ReactiveProperty<bool>();
        public IReactiveProperty<bool> IsSignedInProperty => _isSignedInProperty;
        public bool IsSignedIn => IsSignedInProperty.Value;

        /// <summary>
        /// google認証
        /// </summary>
        public void Auth()
        {
#if UNITY_WEBGL && !UNITY_EDITOR
            GoogleAuth();
#endif
        }

        /// <summary>
        /// ログアウト
        /// </summary>
        public void SignOut()
        {
#if UNITY_WEBGL && !UNITY_EDITOR
            GoogleSignOut();
#endif
        }

        /// <summary>
        /// スプレッドシートのレンジを取得
        /// </summary>
        /// <param name="spreadsheetId">スプレッドシートのユニークID</param>
        /// <param name="range">レンジ Ex: "シート1!A1:B2" はシート1のA1からB2までという意味</param>
        public async UniTask<ValueRange> GetSpreadsheetAsync(string spreadsheetId, string range, CancellationToken ct)
        {
            var url = $"{GoogleSpreadsheetURL}/{spreadsheetId}/values/{range}";
            try
            {
                var responseText = await SendGetRequestAsync(url, ct);
                Debug.Log(responseText);
                return JsonConvert.DeserializeObject<ValueRange>(responseText);
            }
            catch (UnauthorizedAccessException)
            {
                Debug.Log("未認証");
                return null;
            }
            catch
            {
                throw;
            }
        }

        /// <summary>
        /// UnityWebRequest.Getで送信する
        /// </summary>
        private async UniTask<string> SendGetRequestAsync(string url, CancellationToken ct)
        {
            var finalUrl = AppendAccessTokenToURL(url, _accessToken);
            using (var uwr = UnityWebRequest.Get(finalUrl))
            {
                await uwr.SendWebRequest().WithCancellation(ct);

                if (uwr.error != null)
                {
                    var errorDefinition = new
                    {
                        error = new
                        {
                            status = ""
                        }
                    };
                    var errorData = JsonConvert.DeserializeAnonymousType(uwr.downloadHandler.text, errorDefinition);
                    // 未認証エラー
                    if (errorData.error.status == "UNAUTHENTICATED") throw new UnauthorizedAccessException();
                    // その他エラー
                    throw new Exception(uwr.downloadHandler.text);
                }

                return uwr.downloadHandler.text;
            }
        }

        /// <summary>
        /// URLの最後にクエリを追加
        /// </summary>
        private string AppendAccessTokenToURL(string url, string token)
        {
            var delimiter = url.Contains("?") ? "&" : "?";
            return $"{url}{delimiter}access_token={token}";
        }

        private void OnDestroy()
        {
            _isSignedInProperty.Dispose();
        }

        #region From JavaScript

        public void SetAccessToken(string token)
        {
            _accessToken = token;
            IsSignedInProperty.Value = string.IsNullOrEmpty(_accessToken) == false;
        }

        #endregion
    }
}

jslib

GoogleApi.jslib
mergeInto(LibraryManager.library, {
    IsGoogleSignedIn: function () {
        return isSignedIn();
    },
    GoogleAuth: function () {
        auth();
    },
    GoogleSignOut: function () {
        signOut();
    }
});

html, js

index.html
<!DOCTYPE html>
<html lang="en-us">
<head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Unity WebGL Player | advent-calendar2020</title>
    <link rel="shortcut icon" href="TemplateData/favicon.ico">
    <link rel="stylesheet" href="TemplateData/style.css">
</head>
<body>
<div id="unity-container" class="unity-desktop">
    <canvas id="unity-canvas"></canvas>
    <div id="unity-loading-bar">
        <div id="unity-logo"></div>
        <div id="unity-progress-bar-empty">
            <div id="unity-progress-bar-full"></div>
        </div>
    </div>
    <div id="unity-footer">
        <div id="unity-webgl-logo"></div>
        <div id="unity-fullscreen-button"></div>
        <div id="unity-build-title">advent-calendar2020</div>
    </div>
</div>
<script>
    var buildUrl = "Build";
    var loaderUrl = buildUrl + "/WebGL.loader.js";
    var config = {
        dataUrl: buildUrl + "/WebGL.data.unityweb",
        frameworkUrl: buildUrl + "/WebGL.framework.js.unityweb",
        codeUrl: buildUrl + "/WebGL.wasm.unityweb",
        streamingAssetsUrl: "StreamingAssets",
        companyName: "DefaultCompany",
        productName: "advent-calendar2020",
        productVersion: "1.0",
    };

    var container = document.querySelector("#unity-container");
    var canvas = document.querySelector("#unity-canvas");
    var loadingBar = document.querySelector("#unity-loading-bar");
    var progressBarFull = document.querySelector("#unity-progress-bar-full");
    var fullscreenButton = document.querySelector("#unity-fullscreen-button");

    if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
        container.className = "unity-mobile";
        config.devicePixelRatio = 1;
    } else {
        canvas.style.width = "960px";
        canvas.style.height = "600px";
    }
    loadingBar.style.display = "block";

    // gapiで使用できるように
    var globalUnityInstance;

    var script = document.createElement("script");
    script.src = loaderUrl;
    script.onload = () => {
        createUnityInstance(canvas, config, (progress) => {
            progressBarFull.style.width = 100 * progress + "%";
        }).then((unityInstance) => {
            globalUnityInstance = unityInstance;
            loadingBar.style.display = "none";
            fullscreenButton.onclick = () => {
                unityInstance.SetFullscreen(1);
            };
        }).catch((message) => {
            alert(message);
        });
    };
    document.body.appendChild(script);


    // 認証キーたち
    var CLIENT_ID = '<YOUR_CLIENT_ID>';   // ①
    var API_KEY = '<YOUR_API_KEY>';     // ②

    // サンプルからそのまま
    var DISCOVERY_DOCS = ["https://sheets.googleapis.com/$discovery/rest?version=v4"];
    var SCOPES = "https://www.googleapis.com/auth/spreadsheets.readonly";

    /**
     *  ライブラリ読み込み時に呼ばれる
     */
    function handleClientLoad() {
        gapi.load('client:auth2', initClient);
    }

    /**
     *  APIライブラリの初期化
     */
    function initClient() {
        gapi.client.init({
            apiKey: API_KEY,
            clientId: CLIENT_ID,
            discoveryDocs: DISCOVERY_DOCS,
            scope: SCOPES
        }).then(function () {
            // Listen for sign-in state changes.
            gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);

            // Handle the initial sign-in state.
            updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
        }, function (error) {
            console.log(error);
        });
    }

    /**
     *  認証成功し、ログイン状態が変わった時に呼ばれる
     */
    function updateSigninStatus(isSignedIn) {
        // gapi初期化時にUnityInstanceがなければ止める
        if (!globalUnityInstance) return;

        if (isSignedIn) {
            // サインインしてる場合はアクセストークンを渡す
            var user = gapi.auth2.getAuthInstance().currentUser.get();
            var accessToken = user.getAuthResponse().access_token;

            globalUnityInstance.SendMessage("GoogleSheetManager", "SetAccessToken", accessToken);
        } else {
            // してない場合は空文字
            console.log("未認証");
            globalUnityInstance.SendMessage("GoogleSheetManager", "SetAccessToken", "");
        }
    }

    /**
     *  ログイン状態フラグ C#から呼ぶ(サンプルでは使わない
     */
    function isSignedIn() {
        return gapi.auth2.getAuthInstance().isSignedIn.get();
    }

    /**
     *  認証 C#側から呼ぶ
     */
    function auth() {
        gapi.auth2.getAuthInstance().signIn();
    }

    /**
     *  サインアウト C#側から呼ぶ
     */
    function signOut() {
        gapi.auth2.getAuthInstance().signOut();
    }

</script>

<!-- // ライブラリの読み込み処理 -->
<script async defer src="https://apis.google.com/js/api.js"
        onload="this.onload=function(){};handleClientLoad()"
        onreadystatechange="if (this.readyState === 'complete') this.onload()">
</script>

</body>
</html>

まとめ

UnityEditorは公式のライブラリを使用することで比較的楽に取得できましたが、WebGLだといろいろな箇所でエラーが発生して結構ドツボにはまりました。。
予想より時間がかかり、担当日ももう少し後だと思ってたのでちょっと説明不足なところもあるかもです:bow:

応用すればスプシに限らずドライブのファイルや画像なども取ってこれるようになるので何かと便利になると思います。
少しでも参考になりましたら幸いです。

明日は@mitsumitsu1128さんです!

参考サイト様

【Unity】UnityエディタでGoogle APIのOAuth2認証をする(ライブラリを使う版)
【Unity】UnityエディタでGoogle APIのOAuth2認証をする(ライブラリ使わない版)
JSON .NET For Unity
Unity(WebGL)のC#とブラウザのJavaScriptの連携

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
What you can do with signing up
5