PlayFabで登録したハイスコア(Statistics)がランキング(Leaderboard)にすぐ反映されない原因と対策

# 前提と問題
Unityゲームのバックエンドに[PlayFab](https://playfab.com)を使って、スコアランキング機能の実装を試みた。

* [Using Player Statistics](https://api.playfab.com/docs/tutorials/landing-players/player-statistics)
* [Using Resettable Statistics and Leaderboards](https://api.playfab.com/docs/tutorials/landing-tournaments/using-resettable-statistics-and-leaderboards)

この辺を見て、1ゲームが終わった直後に`UpdatePlayerStatistics()`でハイスコア(PlayerStatistics)を更新して、そのレスポンスが返ってきた後に`GetLeaderboard()`でランキング(Leaderboard)を取得したのだけど、今送信したばかりのスコアがランキングに反映されていない。
しかし、暫くしてから`GetLeaderboard()`したら反映されているし、PlayFab管理画面でも更新されている。

## Unity仮想コード
```c#
void Start()
{
    PlayFabClientAPI.UpdatePlayerStatistics(
        new UpdatePlayerStatisticsRequest() {
            Statistics = new List<StatisticUpdate>() {
                new StatisticUpdate() {
                    StatisticName = myStatisticName,
                    Value = myScore
                }
            }
        }, OnUpdatePlayerStatisticsSuccess, OnFailure);
}

void OnUpdatePlayerStatisticsSuccess(UpdatePlayerStatisticsResult result)
{
    GetLeaderboard();
}

void GetLeaderboard()
{
    PlayFabClientAPI.GetLeaderboard(
        new GetLeaderboardRequest() {
            StatisticName = myStatisticName,
        }, OnGetLeaderboardSuccess, OnFailure);
}

void OnGetLeaderboardSuccess(GetLeaderboardResult result)
{
    var entries = result.Leaderboard; // これにmyScoreが反映されていないことがある

    // entries を画面に表示する処理など
}
```

## 環境
* Unity 2018.2.19f1
* PlayFab

# forumを検索してみる（英語）
* [UpdatePlayerStatistics is an asynchronously call?](https://community.playfab.com/questions/10906/updateplayerstatistics-is-an-asynchronously-call.html)

StatisticsをUpdateしてからLeaderboardが最新の状態になるまでに1秒かそれ以上かかるので、Cloud ScriptでStatisticsをUpdateしつつLeaderboardのUpdateを行って、Cloud Scriptが返ってきた後でLeaderboardのクライアントクエリを取得するのがオススメとのこと。
けどserverのAPIリファレンス見てもLeaderboardをUpdateするメソッドが見つからない。

* [Leaderboard update time](https://community.playfab.com/questions/18183/leaderboard-update-time.html)
* [Realtime leaderboard solution?](https://community.playfab.com/questions/20182/realtime-leaderboard-solution.html)
* [Safe time between statstics update and leaderboard download](https://community.playfab.com/questions/11350/safe-time-between-statstics-update-and-leaderboard.html)

この辺には数秒（具体的な数字としては2秒）待つと書いてある。
えー。

# 対策

* スコア確定次第なるはやで`UpdatePlayerStatistics()`を呼び出し、`GetLeaderboard()`まで時間が空くような画面、UXデザインにする
* 以下のいずれかの場合、未反映状態とみなして（少し時間を空けてから）取得し直す
  * 取得したLeaderboardの範囲内に自身のPlayFabIdのLeaderboardEntryがある、かつスコアが更新されているはずなのに更新されていない
  * 取得範囲内に自身のLeaderboardEntryがないが、LeaderboardEntryの個数が取得Request数に満たない（反映済みなら自身のLeaderboardが含まれるはず）
  * 取得範囲内に自身のLeaderboardがないが、自身のスコアが範囲内の最低点より高い（同上）
* 指定PlayFabId（大体は自分）を中心にしたLeaderboardを返す`GetLeaderboardAroundPlayer()`なんてのもあり、こちらを使う場合は自身のLeaderboardEntryが必ず含まれるはずなので、前者の条件だけで良い

## Unity仮想コード
さっきと同じ処理のところは割愛

```c#
void OnUpdatePlayerStatisticsSuccess(UpdatePlayerStatisticsResult result)
{
    StartCoroutine("WaitAndGetLeaderboard", 2f);
}

void WaitAndGetLeaderboard(float waitSeconds)
{
    yield return new WaitForSeconds(waitSeconds);
    GetLeaderboard();
}

void OnGetLeaderboardSuccess(GetLeaderboardResult result)
{
    var entries = result.Leaderboard;
    var myEntry = entries.Find(_entry => _entry.PlayFabId == myPlayFabId);
    if (myEntry != null && myEntry.StatValue < myScore
        || myEntry == null && entries.Count < requestedEntriesCount
        || myEntry == null && entries[entries.Count - 1].StatValue < myScore)
    {
        StartCoroutine("WaitAndGetLeaderboard", 1f);
        return;
    }

    // entries を画面に表示する処理など
}
```

## Cloud Scriptで対処する場合
本来は、クライアントでのチートを防止するため、`UpdatePlayerStatistics()`ではなくCloud ScriptでStatisticsをUpdateするのが良いそうで（そもそも意図的に`Allow client to post player statistic`を設定しないと`UpdatePlayerStatistics()`のリクエストは無効）
それを踏まえると、1個目のforum検索結果の
> Cloud ScriptでStatisticsをUpdateしつつLeaderboardのUpdateを行って、Cloud Scriptが返ってきた後でLeaderboardのクライアントクエリを取得するのがオススメとのこと。

を「StatisticsをUpdateしてからLeaderboardのUpdateを**待って**」と読み替えてCloud Scriptを実装すると良いような気が。
APIの単位としてはそれが正解だと思うのですが、レスポンスが返ってくるのに毎度2秒以上待つのも変なので…悩ましい。

もっと良い手があれば誰か教えてください！

# せっかくなので
そんなハイスコア機能を組み込んだゲーム、WebGL版をunityroomさんで、Android版をGoogle Play Storeで公開しているので、良かったら遊んでってください。
unityroomさんのunity1週間ゲームジャムに投稿後、今回の手法でハイスコア機能を追加したものとなります。
<img width="100" alt="icon2.png" src="https://qiita-image-store.s3.amazonaws.com/0/191129/5c4800f7-763e-00ff-400f-46919191ab70.png">
https://unityroom.com/games/10to10
https://play.google.com/store/apps/details?id=com.zurachu.TenToTen