【Unity】Googleドライブとスプレッドシートでお知らせ機能実装（専用サーバー不要）

# 本記事で出来るコト
お知らせ機能を「Googleドライブ」と「Googleスプレッドシート」で作ります。
![GSSInfomation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734005/33680981-a798-56f7-e8bd-a6a22a397d79.gif)


# はじめに
ゲームクリエイターのべすとまんです。
突然ですが、自分の開発するゲームに「お知らせ」を実装したくなることありませんか？
本記事では、Googleドライブとスプレッドシートを使用し、Unity製のゲームへお知らせ機能を実装する方法を紹介します。
もちろん専用のサーバーを用意する必要もありません！

# 注意点
* キャッシュせずに実行するたびに画像をダウンロードする方法のため、容量の大きい画像を指定すると通信料が大きくなる可能性があります。
* 600ユーザー以上からのアクセスがあるとアクセス制限がかかる、などのGoogle側の制限（参照： [共有ドライブの制限](https://support.google.com/a/answer/7338880?hl=ja)）があるため、大規模ゲームへの実装は非推奨となります。
* Googleへのアクセスができない環境での動作確認は行っておりません。
* たまに一定時間Googleドライブ画像へのアクセスができない（SendWebRequest();が返ってこない）ことがあります。

# 内容
## Googleスプレッドシートの対応
**1. お知らせ用のスプレッドシートを用意**
* 新しいスプレッドシートを作成します。
    * 共有設定を「リンクを知っている全員」に変更します
     ![0010_Link.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734005/7e9ceea2-9099-cfb5-f903-85eca933e475.png)

* 作成したスプレッドシートのURLよりsheetIDをメモしておきます。（※後で使います）
    * `https://docs.google.com/spreadsheets/d/＊＊＊/edit#gid=0`
    ↑URLの＊＊＊の部分がsheetIDになります。
* 表示用のsheetNameをメモしておきます。（※後で使います）
    * sheetNameとはGoogleスプレッドシートのタブの名前です。
    * 任意の名前でOKです。
        * お知らせ情報はsheetName指定により読み込みます。sheetNameを「本番環境」「ステージング」などに分けることでお知らせを出し分けることもできます。
![SheetName_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734005/c6ff2f4b-6281-09cb-621d-4511ce606af2.png)

**2. セルへデータ入力**
* 実際にお知らせを入力していきます。
* 用意するカラムは以下になります。

|Time|Title|ImageID|画像確認|Message|
|:------|:---|:--|:--|:--|
|掲載日時|タイトル|Googleドライブ画像ID|(画像確認セル)|お知らせ内容|
* 詳細
    * Time
        * お知らせ掲載日時を入力します。
    * Title
        * お知らせのタイトルを入力します。
    * ImageID
        * お知らせに載せたい画像をGoogleドライブにアップロードし、「リンクを取得」より取得したURLからImageIDをコピーして入力します。
        * `https://drive.google.com/file/d/＊＊＊/view?usp=sharing`
        ↑ドライブ上の画像URLの＊＊＊部分がImageIDになります。
※画像の共有設定は「リンクを知っている全員」に変更しておきます。
詳細は[Google Drive に保存した画像を直接呼び出せるURLの取得](https://qiita.com/codeDiver/items/0394968fa318d9309d33)を参照してください。
    * 画像確認
        * Googleスプレッドシート上で画像を確認する用のセルのため、Unityからは参照しません。`=image("http://drive.google.com/uc?export=view&id="&画像IDを乗せるセルへのセル参照（例：C2）)`と入力すると、セル内にURL＋ImageIDにより指定されたドライブ上の画像が表示されて便利です。
    * Message
        * お知らせの内容を入力します。
        

* 入力の例
![Sheet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734005/643aa661-551c-dbdc-4077-80f4b6b646f8.png)

* 注意事項
    * セル内の文字は絶対に改行しないでください。
        * セル内で改行すると行の判別が正確に行われずエラーがおきます。
        * 改行をしたい場合は改行コード（UnityC#では「\n」）を用いましょう。

## Unity側の対応
**1. お知らせ表示用の大枠UIを作る**
* `UI/Scroll View`を生成して中央に配置しましょう。
 ![0001_createUI.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734005/eacb0c29-7267-c829-67a6-cb1c537aaee4.png)
* 縦スクロールのみの場合は`Scrollbar Horizontal`は消しましょう。
 ![0003_createUI.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734005/d7e85b30-a9af-11e2-e245-8c82648da0e4.png)
* Contentには取得したお知らせがたくさん格納されるので、`Vertical Layout Group`と`Content Size Fitter`をアタッチします。そうすることで格納されたお知らせたちがいい感じに並びます。
    * `Content Size Fitter`の`Vertical Fit`を「Min Size」にします。
![0004_createUI.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734005/2f36f291-70cb-4d1d-0a01-9fb10e42df45.png)


**2. お知らせ内容表示用のUIを作る**
* 「1. お知らせ表示用の大枠UIを作る」で作った`Scroll View`の中の`Content`の中にお知らせ内容表示用のUIを生成します。
* 今回１つのお知らせに含む情報は「掲載時刻」「タイトル文字列」「画像」「内容文字列」のため、表示用のTextやImageを配置します。
![0110_createElement_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734005/55d1a7a5-36d3-8f04-f773-902dae6a572f.png)


**3. スクリプトを用意してアタッチ**
* **GSSInfomationElement.cs**
    * 各お知らせ情報を表示するためのスクリプトです。
    * [Unity超簡単！Googleスプレッドシートでパラメータ調節する方法（共同開発にオススメ）](https://qiita.com/simanezumi1989/items/32436230dadf7a123de8)を参考にしています。
    * 「2. お知らせ内容表示用のUIを作る」で作ったGameObjectのまとめ親オブジェクト（ここではImage）にスクリプトをアタッチします。
     ![0210_element.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734005/57a25aff-5929-d7f3-233a-6aa42b33c50d.png)

    * インスペクターへの入力
        * `titleText`：タイトル表示用のText
        * `timeText`：お知らせ掲載日時表示用のText
        * `image`：画像表示用のImage
        * `nowLoading`：画像読み込み中に表示するGameObject
        * `messageText`：お知らせ内容表示用のText
    * 操作が完了したらまとめ親オブジェクト（ここではImage）の名前を`InfomationElement`に変更し、プレハブ化して、Scene上からはいったん削除します。
    * （後に紹介する`GSSInfomationElement.cs`がGoogleスプレッドシートより取得したお知らせ数分の`InfomationElement`を生成します。）
    ![0220_elementPrefab.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734005/5cfb24e0-57ca-2bc0-7ed1-058dfe8c5121.png)


```C#:GSSInformationElement.cs
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class GSSInformationElement : MonoBehaviour
{
    [SerializeField] private Text titleText;
    [SerializeField] private Text timeText;
    [SerializeField] private Image image;
    [SerializeField] private GameObject nowLoading;
    [SerializeField] private Text messageText;

    Texture2D texture;

    /// <summary>
    /// おしらせ情報反映
    /// </summary>
    /// <param name="time">時間</param>
    /// <param name="title">タイトル</param>
    /// <param name="imageID">画像ID（https://drive.google.com/open?id={ID}）</param>
    /// <param name="message">メッセージ</param>
    public void SetInformation(
        DateTime time,
        string title,
        string imageID,
        string message
    )
    {
        //タイトル表示
        titleText.text = title;
        //時間表示（いい感じに整形）
        timeText.text = time.Year + "年" + time.Month + "月" + time.Day + "日 " + time.Hour + ":" + time.Minute.ToString("00") + ":" + time.Second.ToString("00");
        //メッセージ表示
        messageText.text = message.Replace(@"\n", "\n");
        messageText.rectTransform.sizeDelta = new Vector2(messageText.rectTransform.sizeDelta.x, messageText.preferredHeight);
        //imageID指定があれば画像読み込み開始
        if (imageID == "")
        {
            image.gameObject.SetActive(false);
        }
        else
        {
            StartCoroutine(GetGoogleDriveImage(imageID));
        }
    }

    /// <summary>
    /// Google上の画像を読み込んで反映
    /// </summary>
    private IEnumerator GetGoogleDriveImage(string imageID)
    {
        nowLoading.SetActive(true);
        UnityWebRequest www = UnityWebRequestTexture.GetTexture("http://drive.google.com/uc?export=view&id=" + imageID);
        yield return www.SendWebRequest();
        nowLoading.SetActive(false);
        if (www.result == UnityWebRequest.Result.ConnectionError || www.result == UnityWebRequest.Result.ProtocolError)
        //if (www.isNetworkError || www.isHttpError)
        {
            Debug.LogError(www.error);
            //Debug.Log(www.error);
            yield break;
        }
        //画像をダウンロード
        texture = ((DownloadHandlerTexture)www.downloadHandler).texture;
        //textureをSpriteに変換
        image.sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero);
    }

    private void OnDestroy()
    {
        if (texture)
        {
            Destroy(texture);
            texture = null;
        }
    }
}
```





<br>

* **GSSInformation.cs**
    * インスペクターで指定されるsheetIDとsheetNameよりGoogleスプレッドシート上の文字列を一括読み込みし、お知らせ要素を生成するためのスクリプトです。
    *  「1. お知らせ表示用の大枠UIを作る」で作った`Scroll View`にスクリプトをアタッチします。
    ![0310_Infomation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734005/42b32422-8f59-d33d-abe2-39b158602fa8.png)    
* インスペクターへの入力
        * `sheetID`、`sheetName`：先ほどの「Googleスプレッドシートの対応」よりメモしたsheetID、sheetName
        * `viewDays`：何日前までのお知らせを表示するか
        * `nowLoading`：読み込み中に表示するGameObject
        * `infomationElementPrefab`：GSSInfomationElement.csをアタッチしたお知らせ表示用プレハブ
        * `infomationElementContainer`：お知らせ数分生成するinfomationElementPrefabの格納先


```C#:GSSInfomation.cs
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using System;

public class GSSInformation : MonoBehaviour
{
    private void Start()
    {
        StartCoroutine(GetGSSInformation());
    }

    [SerializeField, Header("GSSシートID")] private string sheetID = "読み込むシートのID";
    [SerializeField, Header("GSSシート名")] private string sheetName = "読み込むシート";
    [SerializeField, Header("何日前までのお知らせを表示するか")] private int viewDays = 7;

    [SerializeField, Header("読み込み中表示")] private GameObject nowLoading;
    /// <summary>
    /// GSSからお知らせデータを取得
    /// </summary>
    IEnumerator GetGSSInformation()
    {
        nowLoading.SetActive(true);
        //スプレットシートのURL処理
        var tqx = "tqx=out:csv";
        var url = "https://docs.google.com/spreadsheets/d/" + sheetID + "/gviz/tq?" + tqx + "&sheet=" + sheetName;
        UnityWebRequest www = UnityWebRequest.Get(url);
        //リクエスト待機
        yield return www.SendWebRequest();
        nowLoading.SetActive(false);
        //エラーチェック
        var protocol_error = www.result == UnityWebRequest.Result.ProtocolError ? true : false;
        var connection_error = www.result == UnityWebRequest.Result.ConnectionError ? true : false;
        //エラーの場合は終了
        if (protocol_error || connection_error)
        {
            Debug.LogError(www.error);
            yield break;
        }

        var reader = new StringReader(www.downloadHandler.text);
        reader.ReadLine();//ヘッダー読み飛ばし

        //表示時間判定用の現在時刻
        var nowTime = System.DateTime.Now;

        //スプレットシートより受け取った文字列をお知らせへと変換
        while (reader.Peek() >= 0)
        {
            var line = reader.ReadLine();
            var elements = line.Split(',');
            for (var i = 0; i < elements.Length; i++)
            {
                elements[i] = elements[i].TrimStart('"').TrimEnd('"');
            }

            //時間判定
            var informationTime = DateTime.Parse(elements[0]);//配列０個目：お知らせ投稿時間

            //投稿時間が表示対象内ではないお知らせは表示しない
            if (informationTime < nowTime - new TimeSpan(viewDays, 0, 0, 0) || nowTime < informationTime)
                continue;

            //お知らせ要素追加
            var informationElement = CreateInformationElement();
            informationElement.SetInformation(
              time: DateTime.Parse(elements[0]),    //配列０個目：お知らせ投稿時間
              title: elements[1],                   //配列１個目：タイトル文字列
              imageID: elements[2],                 //配列２個目：Googleドライブ画像ID
                                                    //配列３個目：画像確認用の空セル
              message: elements[4]                  //配列４個目：内容文字列
            );
        }
    }

    [SerializeField, Header("お知らせ要素プレハブ")] private GSSInformationElement informationElementPrefab;
    [SerializeField, Header("お知らせ要素格納先")] private Transform informationElementContainer;
    /// <summary>
    /// お知らせ生成
    /// </summary>
    private GSSInformationElement CreateInformationElement()
    {
        var informationElement = Instantiate(informationElementPrefab);
        informationElement.transform.SetParent(informationElementContainer, false);
        return informationElement;
    }
}
```



# 実行結果
Googleスプレッドシート上に入力した情報を元に、Unity上でお知らせが展開されました。
![0410_Play.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734005/0a3111ff-b6ae-1e0b-6cc9-e753f14994bd.png)