UnityのWebGL出力に簡単に無料でKVSを使うサンプル1(Chat編)

## ChatChatChat ～シンプルなチャットを作る～

[UnityのWebGL出力に簡単に無料でグローバルランキングを実装できる仕組みを考えてみた](http://qiita.com/divideby_zero/items/50897a2695aa04bfeb69) の続きになります。


まず、 https://github.com/divide-by-zero/GSSA/ をチェックアウトし、
projects/Assets/Samples/Empty/Sample1_ChatChatChat/ChatChatChat.unity を開きます。

※こちら、ある程度作ってあるけれど重要なところが空のプロジェクトです。　完成品はCompleteフォルダ配下にあるので、チマチマと講座なんてみるより完成品見た方が早い！　というかたはそちらを見てくれればいいと思います。

## 0.下準備
- [元記事](http://qiita.com/divideby_zero/items/50897a2695aa04bfeb69)にもあったように、GoogleSpreadSheet側の準備と、WebアプリケーションとしてのURL取得、SpreadSheetSettingsのinspectorへのURLのセットが済んでいる事を前提に進めていきます。

初期のソースコードは

```csharp
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using GSSA;
using UnityEngine;
using UnityEngine.UI;

public class ChatChatChat : MonoBehaviour
{
    [SerializeField] private Text logText;
    [SerializeField] private InputField nameInputField;
    [SerializeField] private InputField messageInputField;

    void Start ()
	{
        messageInputField.onEndEdit.AddListener(SendChatMessage);
	    StartCoroutine(GetChatLogIterator());
	}

    private void SendChatMessage(string s)
    {

    }

    private IEnumerator GetChatLogIterator()
    {
        while (true)
        {

            yield return new WaitForSeconds(5.0f);
        }
    }
}
```
以上な感じで、実行しても何も起きない状態です。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/21cfa2d1-eabf-170d-69b5-5031b01faa74.png)

徐々に処理を追加していきます。

## 1.メッセージでエンターを押したらGSSA経由で保存する
NameのInputFieldとMessageのInputFieldは用意してあり```onEndEdit```に```AddListener```で```SendChatMessage```を呼ぶところまではできています。　が、中身が空なので、何もできていない状態です。
ここで、onEndEdit＝Enterキー押下 のたびに名前とメッセージをSpreadSheetに登録するよう処理を書きます。

```csharp
    private void SendChatMessage(string s)
    {
        var so = new SpreadSheetObject("Chat");//Chatシート用のSpreadSheetObject作成
        so["name"] = nameInputField.text;//"name"列に名前用InputFieldのtextをセット
        so["message"] = s;//"message"列にはメッセージ用InputFieldのtext = onEndEditで渡される文字列(s)をセット
        so.SaveAsync();//SpreadSheetへ保存
        messageInputField.text = "";//サーバーに送信したので、手元の文字はクリア
    }
```
この時点で名前を入れて、メッセージを入力してエンターを押すたびに、サーバに1行データが保存されるようになるはずです。
＜EX＞
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/e878f061-8956-0630-f490-28cf06811cdd.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/13e6857c-1a8a-e837-b05e-e6ff3b009a69.png)



## 2.一定時間間隔で、ログ取得する
すでにサーバーにはデータがたまっているので、たまったデータを取得して、画面に表示すればチャットの大枠の出来上がりです。

上の何もない空間が```TextField(logText)```なので、ここにサーバーから取得したログを表示いれてあげます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/aca9fa8c-e87e-31b9-ee4d-058d8fd8825c.png)

```csharp
    private IEnumerator GetChatLogIterator()
    {
        while (true)
        {
            var query = new SpreadSheetQuery("Chat");//Chatシート用のQueryオブジェクト作成
            query.OrderByDescending("createTime").Limit(20);//生成日時の大きい順にソートして、最新20件を取得
            yield return query.FindAsync();//取得処理(yield returnで、サーバーから返却されるまで待機する）

            logText.text = "";
            foreach (var so in query.Result.Reverse())//取得できた結果をReverseで逆転（↑で生成日時の新しい順でソートしてしまっているので）
            {
                logText.text += so["name"] + ">" + so["message"] + "\n";
            }
            yield return new WaitForSeconds(5.0f);//5秒待ってあげてから、またサーバーからデータ取得
        }
    }
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/a5d0cd3d-cf4f-3fa3-ed39-83fe0556b492.png)

キモは、queryにOrderByDescendingでcreateTimeを指定して、生成日時の大きい＝新しいデータ順にしているところ。
＋Limitに20を指定して、最新データ20だけに抑えているところです。
これをしないと、表示しきれないほどのデータを毎回サーバーから取得しようとするので、通信量がなかなかな事になりかねません。

しかし、これでもまだ無駄が多いです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/6afe014a-e244-7981-ae4c-73c46b4167c4.png)
SpreadSheetSettingの**Is Debug Log Output**にチェックを入れて、通信ログを取得するとわかるのですが
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/916619ec-3bcc-d7a4-2f75-ee055a6997ec.png)
常に最新データ上20件を取得するので、まったくデータが更新されていなくても同じデータが5秒に1回来てしまいます。
これは・・・あかんですよ・・・。

## 3.既に取得しているログはいらない
そんなわけで、↑これに尽きるんですが、どうやってすでに取得したものかどうかを判断するか。
今回の例だと、自動で生成されるcreateTimeを保持しておいて、Queryオブジェクトに条件を追加すればよさそうです。

最後に取得した時間(long)を取っておくための変数
```private long lastGetTime;```をフィールド変数として用意し、
Queryオブジェクトに、**保持しているlastGetTimeよりも新しいデータだけほしい**というWhereを追加してあげます。
```query.OrderByDescending("createTime").Limit(20);``` →```query.OrderByDescending("createTime").Where("createTime",">",lastGetTime).Limit(20);```

加えて、この変更によって、Queryで返却される件数が0の場合があります（データ更新が無い場合、lastGetTimeよりも新しいデータが無いため）
そのためのif追加や、Logを継ぎ足していくだけではなく、表示しきれない範囲分は消していかなくてはいけないため、**いきなりlogTextにログを追加するのではなく、List<string>の形で表示分の文字列リストを作成してからlogTextにセットするように**します。

```csharp
    private List<string> chatLogList = new List<string>();
    private long lastGetTime;

    private IEnumerator GetChatLogIterator()
    {
        while (true)
        {
            var query = new SpreadSheetQuery("Chat");
            query.OrderByDescending("createTime").Where("createTime",">",lastGetTime).Limit(20);//ローカルで保持していない最新データ上限20件に絞る
            yield return query.FindAsync();

            if (query.Count > 0)//有効なデータが無い場合は何もしない
            {
                foreach (var so in query.Result.Reverse())
                {
                    chatLogList.Add(so["name"] + ">" + so["message"]);//いきなりlogTextに入れるのではなく、一旦Listに追加
                    if (chatLogList.Count > 17) chatLogList.RemoveAt(0);//画面に収まりきらない部分(17以上)からは古いデータを削除
                }
                logText.text = string.Join("\n", chatLogList.ToArray());//Listを改行でjoinしてlogTextにセット
                lastGetTime = (long)query.Result.First()["createTime"];//createTimeの新しい順に並んでいるので、先頭(First)のcreateTimeを保持しておく。
            }

            yield return new WaitForSeconds(5.0f);
        }
    }
```

※要注意！：**createTime**は実際にはlongでデータが入っていますが、SpreadSheetObject上では**object型**なので、よしなにキャストしてあげる必要があります


この修正により、ローカルにあるデータは来なくなるので、通信量の削減につながります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/37184/cfecae0d-1400-375d-7ef1-93f74b0faa52.png)


## 最後に
たかが、チャット。　しかも5秒に1回しか更新しない、プル型のチャットですが、いろいろな基本が詰まっており、これが出来れば大体のことができるんじゃないかと思ってます。
次回は、実際にゲームにランキング機能を付けて行きたいと思います。