UnityのWebGL出力に簡単に無料でKVSを使うサンプル2(ハイスコアランキング編)

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

GoogleSpreadSheetをバックエンドに使うことでWebGLでもそこそこ簡単・無料にランキング処理を実装してみるという試みです。

そのために作ったんですが、当の本人がまだランキングを実装しているサンプルを作っていないという体たらく・・・。

## 作りました
というわけで、作りました。

https://github.com/divide-by-zero/GSSA
の
Samples/Complete/Sample2_2mu2mu
になります。
**※SpreadSheetSettingのSpreadSheetUrlは自分のSpreadSheetのURLを貼ってくださいね。**

実際に動くデモとしてはhttps://unityroom.com/games/line2mu2mu になります。

既にローカルではハイスコア保持できている！
ぐらいの状態からなら・・・。　2～30分でグローバルランキングができるんじゃないかと思います。

## ざっくり解説① スコア送信

GameMain.csの50行目～ `private IEnumerator ScoreSendIterator()`が送信処理（＋送信中のステータス表示や、ボタンの有効・無効処理）です。

```csharp
    private IEnumerator ScoreSendIterator()
    {
        leaderBoardButton.interactable = false; //スコア送信する前にリーダーボードを見ても自分のスコアが表示されないので無効化

        var isHiscore = false;
        if (score > HiScore)    //所持しているHiScoreよりも今回のScoreの方が大きい場合
        {
            HiScore = score;
            isHiscore = true;
            retryButton.interactable = false;   //すぐリトライできるとスコア送信する前にCoroutineが止められてしまうので無効化
        }

        //スコア結果のパネル表示
        resultPanel.Show("Time Attack Results","HiScore " + HiScore + "\nScore " + score + (isHiscore ? "\nハイスコア更新!!" : ""), "Time Attack の結果です。");

        if (isHiscore)
        {
            resultPanel.Description = "サーバーのハイスコアを確認しています。";

            //すでにスコアが登録されているかチェック
            var hiScoreCheck = new SpreadSheetQuery();
            yield return hiScoreCheck.Where("id", "=", SpreadSheetSetting.Instance.UniqueID).FindAsync();   //"id"を検索条件に入れることで、すでにスコアが登録されているかチェック

            //既にハイスコアは登録されている
            if (hiScoreCheck.Count > 0)
            {
                resultPanel.Description = "ハイスコアの更新処理中・・・";

                //登録されている＝hiScoreCheckの戻りリストが更新対象SpreadSheetObjectになるので、そのまま使用する
                var so = hiScoreCheck.Result.First();
                so["hiscore"] = HiScore;
                yield return so.SaveAsync();
            }
            else
            {
                resultPanel.Description = "ハイスコアの新規登録中・・・";

                //登録されていなかったので、新規としてidにUniqueIDを入れて次の更新処理に備えたデータで保存する
                var so = new SpreadSheetObject();
                so["id"] = SpreadSheetSetting.Instance.UniqueID;
                so["hiscore"] = HiScore;
                yield return so.SaveAsync();
            }
            resultPanel.Description = "サーバーへのハイスコア登録処理が終了しました。";
        }

        //ハイスコア登録処理が終わったので、リトライとリーダーボードへの遷移ボタンを有効化
        leaderBoardButton.interactable = true;
        retryButton.interactable = true;
    }
```

この処理はIEnumeratorな事からも推測できる通り、StartCoroutineで呼び出される事前提にしてあります。

毎回バックエンドに登録されているハイスコアと今回のスコア突き合わしてもよいですが、通信の無駄（だし、そもそものアクセスが遅いのであまりやってられないと思う）なので、ハイスコアが更新されていると判断したときに限りハイスコアデータを送信しています。

また、その際に何も考えずにSaveAsyncしていくと、一人のユーザーが100回ハイスコア更新したらデータが100出来てしまいます。
時にそういう形式が望ましい場合もありますが、今回は微妙なユーザー管理（風）に同一ユーザーと分かった場合はバックエンドのデータは新規追加じゃなくて更新にしています。　以下、該当ソース

```csharp
            //すでにスコアが登録されているかチェック
            var hiScoreCheck = new SpreadSheetQuery();
            yield return hiScoreCheck.Where("id", "=", SpreadSheetSetting.Instance.UniqueID).FindAsync();   //"id"を検索条件に入れることで、すでにスコアが登録されているかチェック

            //既にハイスコアは登録されている
            if (hiScoreCheck.Count > 0)
```

なお、同一ユーザーかどうかのチェックにidに入れてある`SpreadSheetSetting.Instance.UniqueID`を使っています。
前回記事にも書きましたが、これはguidを保存してあるだけなので、もっと良い手があるならそれを送信すればよいです。
ただ、WebGLでは`SystemInfo.deviceUniqueIdentifier`もnullが返ってきますし、端末のmacアドレスを取得～なんてのもうまくいかなさそうなので（やってませんが）これくらいゆるくてもなんだかWebGLなら許される気がしています。WebGLの不思議。

また、作り方次第ではあるんですが、CoroutineでSave処理を走らせている間にシーン遷移が走ったりして、元のGameObjectが破棄されたりすると、Coroutineは終了してしまいます。
そのため、ハイスコアを出してもすぐRetryしてしまうと（今回のサンプルは自分自身のシーンを再読み込みする形式なので）ちゃんとスコアが登録されなかったりします。
そうならないよう、スコア登録が終わるまでリトライボタンを無効化するなどの涙ぐましい小細工等もしています。

さて。これで、ユーザー毎のハイスコア送信管理が出来ました。

## ざっくり解説① TopRanking・RivalRanking取得
次に上位スコアとライバル（自分のスコアの近傍）の取得をします。
実際に取得、表示をしているのはLeaderBoard.csの23行目～`private IEnumerator _LoadLeaderBoardIterator(int hiscore)`になります。

```csharp
    private IEnumerator _LoadLeaderBoardIterator(int hiscore)
    {
        top5rankText.text = "now loading";
        rivalRankText.text = "now loading";

        fader.Show(true);

        //まずTop5の取得
        var topRankQuery = new SpreadSheetQuery("ScoreRanking");

        //ハイスコアを降順（大きい順）にして、Limit5にすることで、TOP5を取得
        yield return topRankQuery.OrderByDescending("hiscore").Limit(5).FindAsync(); 

        //取得できたデータをうまく整形しつつ表示
        top5rankText.text = "Top5\n";
        var dispRank = 0;
        foreach (var so in topRankQuery.Result)
        {
            var text = ++dispRank + "位\t" + so["hiscore"];
            //自分のスコアを赤くするために、idでチェック
            if (so["id"] as string == SpreadSheetSetting.Instance.UniqueID)
            {
                text = "<color=red>" + text + "</color>";
            }
            top5rankText.text += text + "\n";
        }

        //近傍スコア（ライバル）の表示処理
        //まずプレイヤーの順位を取得
        var playerRankingQuery = new SpreadSheetQuery("ScoreRanking");
        yield return playerRankingQuery.Where("hiscore", ">", hiscore).CountAsync();  //自分よりスコアが高いプレイヤーが何人いるか
        var rank = playerRankingQuery.Count;    //自分のスコアのランク（-1）取得

        //自分の順位を取得できたので、そこからライバルのスコア取得
        var neigborRankingQuery = new SpreadSheetQuery("ScoreRanking");

        //TOP5同様、降順＋Limit5 に加え、自分のRank-2をSkipすることで、自分のスコアの2つ上のユーザーから取得
        dispRank = Mathf.Max(0, rank - 2);
        yield return neigborRankingQuery.OrderByDescending("hiscore").Skip(dispRank).Limit(5).FindAsync();   

        //取得できたデータをうまく整形しつつ表示
        rivalRankText.text = "your rival\n";
        foreach (var so in neigborRankingQuery.Result)
        {
            var text = ++dispRank + "位\t" + so["hiscore"];
            if (so["id"] as string == SpreadSheetSetting.Instance.UniqueID)
            {
                text = "<color=red>" + text + "</color>";
            }
            rivalRankText.text += text + "\n";
        }
    }
```

まず、単純にTOP5を取るのは簡単で、この部分です。

```csharp
        //ハイスコアを降順（大きい順）にして、Limit5にすることで、TOP5を取得
        yield return topRankQuery.OrderByDescending("hiscore").Limit(5).FindAsync(); 
```

複数行に渡って以下のように書いてもよいですが、メソッドチェイン方式で書いたほうが簡潔で見やすい場合もあるで好き好きで。

```csharp
        //ハイスコアを降順（大きい順）にして、Limit5にすることで、TOP5を取得
        topRankQuery.OrderByDescending("hiscore");
        topRankQuery.Limit(5);
        yield return topRankQuery.FindAsync(); 
```

そして今回はユーザー名などは送っていないので、どれが自分のスコアかよくわかりません。
そのため自分のスコアだけは赤くしたいと思います。
これは、uguiのTextにはRitchTextという概念があり、HTMLの様に決められたタグで挟むことでフォントサイズや色を設定する事ができるので簡単に出来ます。

```csharp
            if (so["id"] as string == SpreadSheetSetting.Instance.UniqueID)
            {
                text = "<color=red>" + text + "</color>";
            }
```

これで、TOP5の表示。かつ自分のスコアを目立たせる工夫が出来ました。
しかし、自分が必ずTOP5にいるとは限りません。　しかし、ランク外だとしても自分のスコアが惜しいのか、それとも全然足りていないのかを知るために、ライバル（自分のスコアの近傍だけ表示）を作ります。
しかし、「自分のスコアの近辺のランキング情報だけくれ」なんて都合のいいメソッドは用意してないので、1手間必要になります。

まず、自分の順位を知りませう。

```csharp
        var playerRankingQuery = new SpreadSheetQuery("ScoreRanking");
        yield return playerRankingQuery.Where("hiscore", ">", hiscore).CountAsync();  //自分よりスコアが高いプレイヤーが何人いるか
        var rank = playerRankingQuery.Count;    //自分のスコアのランク（-1）取得
```

ただ、スコアの順位を知りたいだけなら、`rank`に1足した値が順位になります(1位のスコアは自分より高いスコアが無い＝0が返却されるので）

例えば、このrank分だけ単純にSkipをしてあげると、自分のスコアが先頭になって、5人分表示ができます。
では、5人分表示の中、自分のスコアが中心（上から3番目、下から3番目）に来るようにするためには、そう。-2をする必要があるんですね。

なので、実際にライバルランクを取得するQueryは

```csharp
        //TOP5同様、降順＋Limit5 に加え、自分のRank-2をSkipすることで、自分のスコアの2つ上のユーザーから取得
        dispRank = Mathf.Max(0, rank - 2);
        yield return neigborRankingQuery.OrderByDescending("hiscore").Skip(dispRank).Limit(5).FindAsync();  
```

となっています。

## 最後に
非常に細かいところなんですが、このスコアアップロード処理やランキングデータのダウンロード処理の**進捗を画面に表示**するのが意外と大事です。
人は2秒でも画面が止まっていると不具合に思うもんです。特に、このGoogleSpreadSheetのバックエンドは性質上ものすごく速度が出ないです。
ただでさえ遅いのにライバルランク取得のように2回サーバーに情報取得が走ってたりするとより顕著に遅さが際立つので、通信中は画面に「通信中」と出すだけでも良いですし、ちょっと凝った感じで画面中心にくるくるするエフェクトを置くなど、ともかく工夫が必要になります。
細部にこそ神は宿るので気を付けてみるとよいと思います。　ではでは。