初心者でもわかる？Unityランキング機能の実装（オフライン/オンライン両方実装するの巻）

## はじめに
この記事は[K3 Advent Calender 2024](https://adventar.org/calendars/10132 "K3 Advent Calender 2024")の19日目です☆

あと、少しでクリスマスらしいですよ。
皆様はクリスマス、どう過ごす予定ですか？
私は研究です。~~ゴミですね~~　楽しみだな！

余談はさておき、この記事ではUnityの`オフラインランキング`と`オンラインランキング`の処理について解説していきます。他の方のAdvent Calenderは技術のさわりだけ解説してる人もいましたが、私の記事はガッツリ実装手法についてお話をする予定です。**「Unityの操作方法とC#の書き方がなんとな～く分かる人に100理解してもらう」** くらいの気持ちで書いてるのでバカなげぇです。

右側の`目次`で見たい所だけ適宜見てください。

それでは皆さん、Unityを開きましょう。

## オフラインランキングを実装する
`オフラインランキング`を実装する手法は様々ありますが、今回は**PlayerPrefs**という機能を使います。まずはそれの解説からさせていただきます。
### PlayerPrefsって何？
簡単に言ってしまうと、Unityが提供している`セーブ機能`の一種です。
通常、ゲームを終了させるとデータは消えてしまいますが、PlayerPrefsを利用するとゲームを閉じても`データを保存させることが可能`です。

以下がPlayerPrefsの関数の一覧になります。

#### セーブに関する関数
```c#:PlayerPrefsの関数集その1
// SampleKey1 という名前のキーに float 型の値 10.0f を保存する
PlayerPrefs.SetFloat("SampleKey1", 10.0f);

// SampleKey2 という名前のキーに int 型の値 20 を保存する
PlayerPrefs.SetInt("SampleKey2", 20);

// SampleKey3 という名前のキーに string 型の値 "Data" を保存する
PlayerPrefs.SetString("SampleKey3", "Data");
```
:::note info
**PlayerPrefsのキーについて**
今回はSampleKey1/2/3というキーを使っていますが、キーの名前は何でもOKです。
おすすめはしませんが「さんぷるきー」とかでも動くはずです。
:::
#### ロードに関する関数
```c#:PlayerPrefsの関数集その2
// SampleKey1 という名前のキーに保存されている値をGetFloatに代入する
// 保存されている値が無ければ 30.0f を代入する
float GetFloat = PlayerPrefs.GetFloat("SampleKey1", 30.0f);

// SampleKey2 という名前のキーに保存されている値をGetIntに代入する
// 保存されている値が無ければ 40 を代入する
int GetInt = PlayerPrefs.GetInt("SampleKey2", 40);

// SampleKey3 という名前のキーに保存されている値をGetStringに代入する
// 保存されている値が無ければ "DefaultData" を代入する
string GetString = PlayerPrefs.GetString("SampleKey3", "DefaultData");
```
:::note info
**さらに詳しく！**
`PlayerPrefs.Get〇〇("〇〇");` と、右側の値を省略して書くこともできます。
保存されている値がない状態でこの書き方をした場合、`GetFloat`と`GetInt`だったら`0`、`GetString`だったら`空文字`が代入されます。
:::
#### 削除に関する関数
```c#:PlayerPrefsの関数集その3
// 全てのキーとそのデータを削除
PlayerPrefs.DeleteAll();

// SampleKey という名前のキーとそのデータを削除
PlayerPrefs.DeleteKey("SampleKey");
```
以上8つしかありません。めちゃめちゃ少ないんですよね。
#### PlayerPrefsの注意点
PlayerPrefsには**同じキーは使用できない**という、大きな注意点があります。
例えば、以下のプログラムを実行したとします。
```C#:サンプルプログラム
// sample という名前のキーに float 型の値 10.0f を保存する
PlayerPrefs.SetFloat("sample", 10.0f);

// sample という名前のキーに int 型の値 20 を保存する
PlayerPrefs.SetInt("sample", 20);
```

この場合、`SetFloat`と`SetInt`で同じキーを使用しているため、`PlayerPrefs.SetInt("sample", 20);`を実行した段階で、`SetFloat`で保存された`10.0f`という値は消滅します(上書きされます)。
違う型であっても同じキーを使うのは控えるようにしましょう。

PlayerPrefsの説明は以上です。オフラインランキングではSetStringとGetStringを利用してランキングを作成します。
* * *
座学の時間はここまでだ。今からガッツリプログラミングです。覚悟を決めてください。
### オフラインランキングの概要
基本的には、ランキング情報をまとめた文字列をSetStringで保存する形になります。
ランキングの更新が行われる度にSetStringを呼び出し、ランキングを取得する度にGetStringを呼び出す感じです。

<img width = "500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/8f0c2e16-c1b4-ca2c-10fa-b9951ab34b9b.png">

では、実際にプログラミングの方に取り掛かりたいと思います。

### 実装

ランキングの更新に関する実装と取得に関する実装に分けて解説します。

#### ランキング更新の実装

ランキングデータは次のような string 型の文字列で保存します。
`"500,400,300,200,100"`
1位の人のスコアが500、2位の人が400、... 、5位の人が100という感じです。タイムアタックの様にデータを昇順で並べることも可能です。実装に合わせてsortメソッド等を使い、並び替えを行ってください。

更新に使うプログラムは次の様になります。

```C#:ランキング更新メソッド
void RankingUpdate(string rankingKey, int newScore)
{
    // rankingText にランキングデータを代入する
    // 今回は初期値として "500,400,300,200,100" を代入する
    string rankingText = PlayerPrefs.GetString(rankingKey, "500,400,300,200,100");

    // splitメソッドを使い、"," で区切られている値を rankingArr 配列に代入する
    // rankingArrの中身：["500","400","300","200","100"]
    string[] rankingTextArr = rankingText.Split(',');

    // ランキングの人数を topNScores に代入する
    int topNScores = rankingTextArr.Length;

    // ランキングより 1 大きいランキング配列を作成する
    // サイズが 1 大きいランキングを作る理由は後述
    int[] rankingIntArr = new int[topNScores + 1];

    // rankingIntArr 配列に int 変換したスコアを代入する
    for (int i = 0; i < topNScores; i++)
    {
        rankingIntArr[i] = int.Parse(rankingTextArr[i]);
    }

    // rankingIntArrの一番後ろに新しいスコアを代入する
    rankingIntArr[topNScores] = newScore;

    // rankingIntArrを昇順ソート
    Array.Sort(rankingIntArr);
    // rankingIntArrの順序を反転させる (降順のランキングを作る場合は Array.Reverse(); は不要)
    Array.Reverse(rankingIntArr);

    // ランキング保存用テキストの初期化
    string uploadRankingText = "";

    // ランキング保存用テキストの作成
    for (int i = 0; i < topNScores; i++)
    {
        // rankingIntArr配列を保存用のテキストに代入していく（最後だけ , を外す）
        if (i < topNScores - 1)
        {
            uploadRankingText += rankingIntArr[i].ToString() + ",";
        } else
        {
            uploadRankingText += rankingIntArr[i].ToString();
        }
    }

    // ランキングの保存
    PlayerPrefs.SetString(rankingKey, uploadRankingText);

    return;
}
```

上記のコードをコピペしていただければランキング機能は完成です。
PlayerPrefsで扱うキーを常に変更できるように、キーと更新用のスコアを引数にしています。

主な解説はコード内のコメントの通りです。`rankingIntArr`配列を作る時に「1」大きいサイズの配列を作っている理由ですが、例えば、上位5人のランキングを作成する場合、

サイズ6の配列を作る
↓
0番目から4番目に現状のスコアを代入する
↓
5番目に新しいスコアを代入する
↓
ソート（昇順や降順に並び替える）をする
↓
0番目~4番目が更新後のスコア、5番目が不要なスコアになる

という理由です。このような処理をするおかげで、自分でソートのメソッドを作ったりする必要がなくなるため、非常に楽な更新作業ができます。

<img width = "500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/44d594ad-810a-044f-38c1-f87d89293422.png">

このような仕組みを取ることのメリットは、**新しいスコアが出るたびにとりあえず `RankingUpdate`メソッドを使っておけば、常に新しいランキングになる**ということです。
いちいちランキングを取得して、今回のスコアがランキング圏内か確認したりする必要はありません。とりあえず結果が出るたびに`RankingUpdate`関数を使っておけばいいのです。
非常に楽で良いですね。

#### ランキング取得の実装

次はランキングの取得（ランキング表とかに使う）に関するメソッドについてです。

```C#:ランキング取得メソッド
void RankingLoad(string rankingKey)
{
    // rankingText にランキングデータを代入する
    // 今回は初期値として "500,400,300,200,100" を代入する
    string rankingText = PlayerPrefs.GetString(rankingKey, "500,400,300,200,100");

    // splitメソッドを使い、"," で区切られている値を rankingArr 配列に代入する
    // rankingArrの中身：["500","400","300","200","100"]
    string[] rankingTextArr = rankingText.Split(',');

    // ランキングの人数を topNScores に代入する
    int topNScores = rankingTextArr.Length;

    // ランキングを出力する（今回は Debug.Log(); で仮出力）
    for (int i = 0; i < topNScores; i++)
    {
        Debug.Log($"{i + 1}位：{rankingTextArr[i]});
    }

    return;
}
```

基本的には、ランキングの更新で作成したメソッドと同じです。
今回は取得した値を`Debug.Log();`に出力させてますが、実際の運用の時はこの部分を`TextMeshPro`等に代入してあげる形になります。

#### オフラインランキング全体の実装

以上でオフラインランキングに必要なメソッド作成は終わりです。
仮のプログラムとして以下のようなコードを作成して動かしてみましょう。

```c#:オフラインランキングの全体コード
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// <summary>
// PlayerPrefsを使った、オフラインランキング処理を行うクラス
// </summary>

public class OffLineRanking : MonoBehaviour
{

    // =============================================
    // ランキング更新・取得処理
    // =============================================
    
    void Start()
    {
        // 初期のランキングの出力
        RankingLoad("testRanking");

        // ランキング更新
        RankingUpdate("testRanking", 1000);

        // 更新後のランキングの出力
        RankingLoad("testRanking");
    }

    // =============================================
    // ランキング処理に関わるメソッド
    // =============================================

    // ランキング更新メソッド
    void RankingUpdate(string rankingKey, int newScore)
    {
        // rankingText にランキングデータを代入する
        // 今回は初期値として "500,400,300,200,100" を代入する
        string rankingText = PlayerPrefs.GetString(rankingKey, "500,400,300,200,100");

        // splitメソッドを使い、"," で区切られている値を rankingArr 配列に代入する
        // rankingArrの中身：["500","400","300","200","100"]
        string[] rankingTextArr = rankingText.Split(',');

        // ランキングの人数を topNScores に代入する
        int topNScores = rankingTextArr.Length;

        // ランキングより 1 大きいランキング配列を作成する
        // サイズが 1 大きいランキングを作る理由は後述
        int[] rankingIntArr = new int[topNScores + 1];

        // rankingIntArr 配列に int 変換したスコアを代入する
        for (int i = 0; i < topNScores; i++)
        {
            rankingIntArr[i] = int.Parse(rankingTextArr[i]);
        }

        // rankingIntArrの一番後ろに新しいスコアを代入する
        rankingIntArr[topNScores] = newScore;

        // rankingIntArrを昇順ソート
        Array.Sort(rankingIntArr);
        // rankingIntArrの順序を反転させる (降順のランキングを作る場合は Array.Reverse(); は不要)
        Array.Reverse(rankingIntArr);

        // 保存用のランキングテキストの初期化
        string uploadRankingText = "";

        for (int i = 0; i < topNScores; i++)
        {
            // rankingIntArr配列を保存用のテキストに代入していく（最後だけ , を外す）
            if (i < topNScores - 1)
            {
                uploadRankingText += rankingIntArr[i].ToString() + ",";
            } else
            {
                uploadRankingText += rankingIntArr[i].ToString();
            }
        }

        // ランキングの保存
        PlayerPrefs.SetString(rankingKey, uploadRankingText);

        return;
    }

    // ランキング取得メソッド
    void RankingLoad(string rankingKey)
    {
        // rankingText にランキングデータを代入する
        // 今回は初期値として "500,400,300,200,100" を代入する
        string rankingText = PlayerPrefs.GetString(rankingKey, "500,400,300,200,100");

        // splitメソッドを使い、"," で区切られている値を rankingArr 配列に代入する
        // rankingArrの中身：["500","400","300","200","100"]
        string[] rankingTextArr = rankingText.Split(',');

        // ランキングの人数を topNScores に代入する
        int topNScores = rankingTextArr.Length;

        // ランキングを出力する（今回は Debug.Log(); で仮出力）
        for (int i = 0; i < topNScores; i++)
        {
            Debug.Log($"{i + 1}位：{rankingTextArr[i]});
        }

        return;
    }
}

```

`Start`メソッド に`RankingLoad("testRanking");`、`RankingUpdate("testRanking", 1000);`、`RankingLoad("testRanking");`を順に置くことで、ランキングがしっかり更新されていることを確認します。

以下が出力結果です。

<img width = "500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/d4202270-63c0-8b6b-f706-f240c214fbd3.png">

ちゃんと、最初のランキングでは`500,400,300,200,100`のスコアが、更新後のランキングでは`1000,500,400,300,200`のスコアが代入されていることが確認できましたね。

後はお好みでUI等を作成してみてください。

以上でオフラインランキングの実装は終わりです。

## オンラインランキングを実装する
オフラインランキングの実装お疲れ様でした。休憩したいですか？
残念、まだかかります。覚悟を決めましょう。

オンラインランキングの実装には`Microsoft`が提供している`PlayFab`というクラウドベースのバックエンドサービスを使っていきます。
まぁ、無料で使えるオンラインのデータベースみたいなものだと考えて頂ければそれでOKです。

無料版と有料版がありますが、大きな違いは以下です。

**無料版**
累計10万ユーザーまで登録可能
**有料版**
月10万ユーザーまで登録可能

なので、小中規模のゲームであれば無料版で全く問題ないと思います。

今回はUnityで実装しますが、PlayFab自体はUnreal Engineでも使えるので、気になる方は調べてみてください。~~というかUnreal Engineのやり方は知らないです~~

まずは`PlayFab`を使う準備から始めていきましょう。

### PlayFabの準備
PlayFabを使うためにはアカウントを作る必要があります。
[こちら](https://developer.playfab.com/en-us/sign-up)からアカウントの作成ができます。
作成に当たって、`Microsoftアカウント`が必要になるので、予め準備しておいてください。

<img width = "500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/4413e5d7-9e1e-6fcd-f638-474d8df422f7.png">

必要事項を記入すると、上のような画面に遷移するはずです。
これは、PlayFabの管理画面のようなものです。

<img width = "500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/8acdf4ee-209b-d7d1-3604-4218a06d4b92.png">

しばらくポチポチしていきます。
赤枠の`New title`を選択してください。

<img width = "500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/25c30482-8285-b017-71a5-1abd6320d9f0.png">

`New title`を選択すると以下のような画面に遷移すると思います。
`Name`を入力したら`Create title`を選択してください。
今回は例として`TestRanking`を`Name`にしています。
他2つに関しては記入しなくて大丈夫です。

<img width = "500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/48e4be2c-c5e6-ea07-f77d-06c441a425d0.png">

`Create title`を押すと、上の様に`My Game Studio`の欄に`TestRanking`が追加されているはずです。では、`TestRanking`の設定を行っていきましょう。

<img width = "500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/c2d9754e-e4aa-3962-3167-bd9c859f62b6.png">

`TestRanking`を開くと下のような画面に遷移します。`Settings`を押しましょう。

<img width = "500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/ad7585a3-9afa-76ba-5247-93c04f46bd75.png">

上の欄にある`API Features`を選択すると上のような画面に遷移します。

<img width = "500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/57c8faa0-bcf5-e009-0626-6ce6f99bca1b.png">

そのまま下にスクロールしたら`ENABLE AIP FEATURES`が出てくると思うので、そこにある`Allow client to post player statistics`にチェックを入れてください。
これは、プレイヤーが統計情報（スコア等）を送信することを許可しますよ～という内容です。PlayFabでは、チート対策とかを兼ねてクライアントが統計情報を送信することをデフォルトで禁止してます。
個人開発のゲームなのでまぁ良いでしょう。
ここにチェックを入れたら`Save`を押して保存しましょう。

<img width ="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/0ccb3b3f-58f4-249d-db50-f87141f51a03.png">

上に戻って、今度は`General`を選択してください。
`PLAYER ACCOUNT FEATURES`欄の`Allow non-unique player display names`にチェックを入れましょう。
これは、Playerの名前がかぶってもOKだよ～という意味です。
PlayFabではプレイヤーごとにスコアだけでなく、名前を設定することができます。もちろん、プレイヤー名が被るのを防ぐ場合はここにチェックを入れる必要はありません。ただ名前が被った場合エラーが発生するので、各々対応するようにお願いします。

後は、`Default Language`を選択して`Save`を押せば完成です。

<img width = "500" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/fa687291-441d-6476-e82c-9df9ab80598b.png">

次にランキングの保存先を作っていきます。左の欄から`Progression`を選択してください。選択すると、上のような画面になるはずです。
上の欄から`Leaderboards(Legacy)`を選択し、`New leaderboard`を押してください。

:::note info
**LeaderboardsとLeaderboards(Legacy)って何？**
(Legacy)は古いやり方です。（現在だと非推奨のやり方になってる）
ただ最新版のLeaderboardsの資料が少なく、エラーが多発したため、旧式の(Legacy)のやり方を採用しています。
旧式のやり方でもランキング機能は正しく機能します。
:::

<img width = "500" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/8909536c-9932-ccd6-0d86-6c584e5adb0f.png">

`New leaderboard`を選択すると、上のような画面に遷移します。
それぞれ必要事項を記入していきます。

<dl>
  <dt>Statistic name</dt>
  <dd>ランキングの名前です。今回はTestRankingという名前にしてます</dd>
  <dt>Reset frequency</dt>
  <dd>ランキングをリセットする頻度を決められます。「Manually(手動) / Hourly(毎時) / Daily(毎日) / Weekly(毎月) / Monthly(毎月)」から選べます。今回はManually(手動)を採用してます。</dd>
  <dt>Aggregation method</dt>
  <dd>スコアの採用方法を決めます。「Last(最新のスコア) / Minimum(最小のスコア) / Maximum(最大のスコア) / Sum(合計)」から選べます。昇順ランキング、降順ランキング関係なくMaximumを採用するようにして下さい。</dd>
</dl>

昇順ランキングでも`Maximum`を採用する理由は、`Leaderboards (Legacy)`の構造上、**昇順ランキングを作成する場合に特殊な処理が必要になる**ためです。この点については、実装編で詳しく解説します。なお、プログラムコードは昇順ランキング用に作成していますが、降順ランキングのコードの書き方についても解説する予定です。

各々の目的に合った項目を設定してください。

最後に`Save`を押したら完成です。

<img width = "500" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/64d63a24-7da7-bbeb-ff7a-548bc96f3305.png">

ここまでやると、上のように`Progression`の`Leaderboards(Legacy)`に`TestRanking`が追加されています。
以上でPlayFab側の設定は終わりです。

* * *

### Unityの準備

次にUnity側の準備をしていきます。
まずは、PlayFabの機能をUnityで使えるようにするため、PlayFabのGitHubから`UnityPackage`をダウンロードします。

[こちら](https://github.com/PlayFab/UnitySDK?tab=readme-ov-file)がPlayFabのGitHubになります。

<img width = "500" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/c6bf80b7-8dff-9091-0a88-d1ef17079fdc.png">

下にスクロールし、`UnitySDKE README`にある、`PlayFab Unity Editor Extensions`をダウンロードしてください。

<img width = "500" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/ffa33e28-bb84-d385-d41b-e45938e638bb.png">

ダウンロードしたPackageをインポートすると、左の赤枠のような物が出てくると思います。もし出てこなかったら、一回Unityを再起動してください。
正常に表示されているのが確認出来たら、赤枠の中の`LOG IN`を押してください。**この時、表示されているEMAIL / PASSWORD / CONFIRM PASSWORD / STUDIO NAME は新しくPlayFabのアカウントを作る時に入力する項目なので、無記入のまま`LOG IN`を押してください。**

また、下の矢印の様にエラーが表示されていると思います。このエラーは`PlayFabを動かすためのファイルがない`という内容のエラーになります。Unityエディターの中でPlayFabにログインした際、必要なファイルをダウンロードすることができるので、このエラーは無視して頂いて構いません。

<img width = "500" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/e155aacb-65f6-2ee4-3be2-39c14810cd14.png">

恐らくですが、初期のUIだと`CREATE AN ACCOUNT`と`LOG IN`のボタンが重なっていることが多いと思うので、Unityエディターをいじって上のように調節してあげてください。

`Microsoftアカウント`でPlayFabを作成した方は`EMAIL`と`PASSWORD`に無記入のまま`LOG IN WITH MICROSOFT`を押してください。そうで無い方は、`EMAIL`と`PASSWORD`を記入し、`LOG IN`を押してください。

<img width = "500" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/c77a8bdf-9815-c9a2-80f0-7b7cb06a6c8a.png">

ログインに成功すると上のような画面になります。ここでは`Install PlayFab SDK`を押してください。これで、PlayFabを扱うために必要なファイルが全てインストールされるので、先ほどのエラーも解消します。

もし間違えてインストールをしなかった場合でも、先ほどのGitHubから同内容のPackageをインポートできるので、そちらも活用してみてください。

<img width = "500" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/0ab9775d-85ad-338f-d99f-3c176d108d2e.png">

このような画面が出力されたら成功です。
次に先ほど用意したランキングの保存先とUnityを同期させる作業を行います。

<img width = "500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/aeca0591-4c30-9464-9290-7fae18f8fb26.png">

`SETTINGS`・`PROJECT`を選び、`STUDIO`と`TITLE ID`を先ほどの物に設定してください。
ここ↓で作ったやつですね。
<img width = "400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/48e4be2c-c5e6-ea07-f77d-06c441a425d0.png">
今回だと、`STUDIO`が`My Game Studio`、`TITLE ID`が`(A14E9) TestRanking`になります。プルダウンで表示されるはずなので、`TITLE ID`等を覚える必要はないです。

以上でUnityの準備が終わりです。

* * *
### 実装
下準備お疲れ様です。

ではここからプログラミングコードの実装について解説していきます。
疲れました？まだあるんで気合い入れていきましょう。

#### ログイン処理
まずはログイン処理を解説していきます。ログインメソッドは以下の様になります。

```c#:ログイン処理
void Login()
{
    // ログインに使うデータの代入
    var request = new LoginWithCustomIDRequest { CustomId = "testID", CreateAccount = true};
    
    // ログイン処理の実行
    // ログインに成功したら「OnLoginSuccess」、失敗したら「OnLoginFailure」を実行する
    PlayFabClientAPI.LoginWithCustomID(request, OnLoginSuccess, OnLoginFailure);

    //ログイン成功時の処理
    private void OnLoginSuccess(LoginResult result)
    {
        Debug.Log("ログインに成功しました");
    }

    //ログイン失敗時の処理
    private void OnLoginFailure(PlayFabError error)
    {
        Debug.Log("ログインに失敗しました");
    }
}
```

`request` 変数にログインで使うデータを代入します。`CustomId` はいわゆる`ログインID`です。ユーザーごとに固有のIDを振り分ける必要があります。やり方は後述します。`CreateAccount = true`は`アカウントが存在しない場合は新規作成する`という意味です。

`PlayFabClientAPI.LoginWithCustomID`がログイン処理、`request`がログイン情報、`OnLoginSuccess`がログイン成功時の処理、`OnLoginFailure`がログイン失敗時の処理となっております。

ログイン状態が切れるタイミングは主に4点あります。

<dl>
  <dt>セッションの有効期限切れ</dt>
  <dd>一定時間経過するとセッションが無効化されます。デフォルトだと24時間に設定されています。</dd>
  <dt>新しいログインが発生した場合</dt>
  <dd>同じアカウントで別デバイスからログインすると、古いセッションが無効化される場合があります。</dd>
  <dt>ユーザーアクションによる明示的な切断</dt>
  <dd>明示的にログイン状態をリセットしたり、別のユーザーでログインすると、現在のセッションを終了します。</dd>
  <dt>通信エラー</dt>
  <dd>ネット環境が不安定な場合や高頻度でPlayFabへリクエスト(ログイン処理やスコア送信)をすると失敗することがあります。リクエスト過多に関しては、10秒程度待ってから再試行すると直ることが多いです。</dd>
</dl>

このままでは、`CustomId`が`"testID"`となっているため、すべてのユーザーが同じログインIDでログインするというカスのゲームが出来上がります。
ポケモンを放置してたら兄弟に勝手にプレイされて、いつの間にかチャンピオンが倒されてるような感覚です。許せませんね。

このような悲しい事件を防ぐために、ユーザーごとに一意の`CustomId`を決定するプログラムを書いていきます。

とても長くなるので、全体のプログラムを先に見せます。

```c#:CustomIdを一意に決定するログイン用プログラム
using PlayFab.ClientModels;
using PlayFab;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Text;

// <summary>
// PlayFabで一意なcustomIDのログイン処理を行うクラス
// </summary>

public class PlayFabLogin : MonoBehaviour
{
    // アカウントを新規作成するかどうか
    bool shouldCreateAccount;

    // customIDを代入しておく変数
    string customID;

    // ==============================================================
    // ログイン処理
    // ==============================================================

    void Start()
    {
        // ログイン処理の実行
        Login();
    }

    // ログインメソッド
    void Login()
    {
        // customIDを読み込む
        customID = LoadCustomID();

        // ログイン情報の代入
        var request = new LoginWithCustomIDRequest { CustomId = customID, CreateAccount = shouldCreateAccount };

        // ログイン処理
        PlayFabClientAPI.LoginWithCustomID(request, OnLoginSuccess, OnLoginFailure);
    }

    // ログイン成功時の処理
    void OnLoginSuccess(LoginResult result)
    {
        // 新規でアカウントを作成しようとしたが、既にcustomIDが使われていた場合
        if (shouldCreateAccount && !result.NewlyCreated)
        {
            // 再度ログインし直す
            Login();

            return;
        }

        // 新規でアカウントを作成した場合
        if (result.NewlyCreated)
        {
            // デバイスにcustomIDを保存する
            SaveCustomID();
        }

        Debug.Log("ログインに成功しました");
        Debug.Log($"CustomID：{customID}");
    }

    // ログイン失敗時の処理
    void OnLoginFailure(PlayFabError error)
    {
        Debug.Log("ログインに失敗しました");
    }

    // ==============================================================
    // customIDの取得
    // ==============================================================

    // デバイスにcustomIDを保存する場合に使うキーの設定
    // PlayerPrefsを使って保存をcustomIDの保存を行うので、そのキーの設定です
    // 詳しくは「オフラインランキングを実装する」の「PlayerPrefsって何？」をご覧ください
    static readonly string CUSTOM_ID_SAVE_KEY = "TEST_RANKING_SAVE_KEY";

    // 自分のIDを取得するメソッド
    string LoadCustomID()
    {
        // PlayerPrefsを使って、customIDを取得する
        // もし保存されていない場合は空文字を返す
        string id = PlayerPrefs.GetString(CUSTOM_ID_SAVE_KEY);

        // もしidが空文字だったらshouldCreateAccountにtrueを代入、そうでないならfalseを代入する
        shouldCreateAccount = string.IsNullOrEmpty(id);

        // shouldCreateAccountがtrueならcustomIDを新規作成、falseなら保存されていたcustomIDを返す
        return shouldCreateAccount ? GenerateCustomID() : id;
    }

    // customIDをデバイスに保存するメソッド
    void SaveCustomID()
    {
        // PlayerPrefsを使って、customIDを保存する
        PlayerPrefs.SetString(CUSTOM_ID_SAVE_KEY, customID);
    }

    // ==============================================================
    // customIDの生成
    // ==============================================================

    // customIDに使用する文字一覧（好きに設定してOKです）
    static readonly string ID_CHARACTERS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    
    // customIDを生成するメソッド
    string GenerateCustomID()
    {
        // customIDの長さ
        int idLength = 32;

        // 生成したcustomIDを代入する変数の初期化
        StringBuilder stringBuilder = new StringBuilder(idLength);

        // customIDをランダム出力するために乱数を使う
        var random = new System.Random();

        // customIDの生成
        for (int i = 0; i < idLength; i++)
        {
            // 乱数を使ってランダムに文字列を代入する
            // 代入された文字列をcustomIDにする
            stringBuilder.Append(ID_CHARACTERS[random.Next(ID_CHARACTERS.Length)]);
        }

        // 生成したcustomIDを返す
        return stringBuilder.ToString();
    }

    
}
```

なっがいですね。一応、コピペでも動きはするはずです。各メソッドや変数がどのような役割をしているかはコメントで書いているので、それを参考にしてください。

:::note info
**エラーが起きる場合**
恐らくですが、`using PlayFab;`と`using PlayFab.ClientModels;`が抜けているのではないかと思います。基本的にPlayFabをインポートしたら勝手に書いてくれるんですが、たまに書かれないときがあるので、その際は自分で書いてあげてください。
:::

全体の処理の流れをフローチャートで表すと以下の様になります。
<img width = "500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/16d27d3b-7d11-c3ac-f4a0-282b70ae998c.png">

それでは実際に動かしてみましょう。

<img width = "500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/4850840c-9a21-b2de-e66b-ba927b791d84.png">

コンソールを見てあげると実際に動いたことが確認できると思います。また、2回以上動かしても同じ`CustomID`が使われていることが分かるはずです。

:::note info
**どうしても失敗してしまう**
まずはネット接続環境を確認してみましょう。オンライン実装なのでネット環境が整っていない場合、処理に失敗します。
それでも直らない場合は`OnLoginFailure`の所で`Debug.LogError(error.GenerateErrorReport()}");`を実行してみて下さい。これをすることでエラーの内容が出力されます。
:::

PlayFabの方も確認してみましょう。

<img widt = "500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/b9fa869c-eb5d-9115-2049-260094d63c50.png">

このように、赤枠の部分の数字が0から1に増えていると思います。
同じ`CustomID`を使っているため、2回以上実行してもユーザー数が1から2に増えることはありません。`CustomID`を別のものにしたら、ユーザー数は増えます。

以上でログイン処理の実装は終わりです。
プレイヤー名を設定する場合や、スコアの送信等を行う場合はログインが必須になるので、ゲームの最初で組み込むようにしましょう。

#### プレイヤー名の設定
次にプレイヤー名の設定方法について解説していきます。
ここから先はログイン処理と比べるとコードが少ないので多少楽です。頑張りましょう。

以下がプレイヤー名設定に関するコードになります。

```c#:プレイヤー名を設定するメソッド
// 変更後のプレイヤー名を引数にする
void SetUserName(string name)
{
    // ユーザー名を設定するリクエストを作る
    var request = new UpdateUserTitleDisplayNameRequest
    {
        // ユーザー名の設定
        DisplayName = name
    };

    // リクエストをPlayFabに送信する
    PlayFabClientAPI.UpdateUserTitleDisplayName(request, OnSetUserNameSuccess, OnSetUserNameFailure);

    // 送信成功時の処理
    void OnSetUserNameSuccess(UpdateUserTitleDisplayNameResult result)
    {
        Debug.Log("プレイヤー名の変更に成功しました");
    }

    // 送信失敗時の処理
    void OnSetUserNameFailure(PlayFabError error)
    {
        Debug.Log("プレイヤー名の変更に失敗しました");
    }
}
```

各処理が何をしているかはコメントを確認してください。これを行うことで、「1位：〇〇、スコア：××」のように名前とスコアを出力することが可能です。PlayFab側でも登録されたプレイヤー名の確認ができます。

PlayFabの設定をしている時に、`プレイヤー名が被る事を考慮するかどうか`のチェックがあったと思います。もし、同じプレイヤー名を受け付けない設定にしているなら、ユーザーが誰かと同じプレイヤー名を設定しようとした際、変更に失敗するはずです。その場合は`OnSetUserNameFailure`にそれ専用の処理を記述するようにして下さい。

#### スコアの送信
いよいよスコアの送信に関するコードを書いていきます。
降順のランキングを作成する際は問題ないのですが、**昇順のランキングを作成する際は少し特殊な操作をする必要**があります。

以下が**昇順ランキング**のスコア送信のコードになります。
```c#:昇順ランキングのスコア送信メソッド
// 送信するスコアを引数にする
void SubmitScore(int score)
{
    // 昇順ランキング用にスコアを修正する
    // 降順ランキングの場合はこの作業は省略する
    // 処理の内容と理由は後述
    int modifiedScore = int.MaxValue - score;

    // 送信する内容を作る
    var statisticUpdate = new StatisticUpdate
    {
        // 統計情報名の指定（LeaderBoards(Legace)で設定した名前）
        StatisticName = "TestRanking",
        // 送信するスコアの代入
        Value = modifiedScore,
    };

    // 作った内容をリクエストにまとめる
    var request = new UpdatePlayerStatisticsRequest
    {
        Statistics = new List<StatisticUpdate>
        {
            statisticUpdate
        }
    };

    // リクエストをPlayFabに送信する        
    PlayFabClientAPI.UpdatePlayerStatistics(request, OnSubmitScoreSuccess, OnSubmitScoreFailure);

    // 送信成功時の処理
    void OnSubmitScoreSuccess(UpdatePlayerStatisticsResult result)
    {
        Debug.Log("スコアの送信に成功しました");
    }

    // 送信失敗時の処理
    void OnSubmitScoreFailure(PlayFabError error)
    {
        Debug.Log("スコアの送信に失敗しました");
    }
}
```

PlayFabに送信する用のリクエストを作成して、送信する点はプレイヤー名の登録処理やログイン処理と同じです。

異なる点は`int modifiedScore = int.MaxValue - score;`です。この点について解説していきます。
まず`int.MaxValue`ですが、ここは int 型の最大値を取得しています。int 型の最大値から送信するスコアを引いた値を`modifiedScore`に代入し、PlayFabに送信しています。
このような処理をする理由ですが、**PlayFabのLeaderboards(Legace)では統計情報を降順でしか取得できない**のが理由となっています。そのため、int 型の最大値から送信するスコアを引くことで、スコアが小さければ小さいほどPlayFabに大きい値が送信される、という状況を作って無理やり昇順ランキングを作成しています。
Leaderboradsの構造上、最下位から統計情報を取得するのも難しいため、このような特殊な処理を採用しています。

降順ランキングを作成する際はこのような作業をせず、単純に`score`の値をPlayFabに送信するでOKです。

:::note warn
**送信する時は int 型に！**
PlayFabのLeaderboardは int 型以外を認めていません。そのため、string 型だけでなく、float 型や double 型なども受け付けていません。
もし、小数点以下の値もスコアとして扱いたい場合は、1000倍して無理やり int 型にする等の修正を行ってください。

送信時に1000倍し、ランキング取得時に1000で割れば元の形式を保つことができます。
:::

#### ランキングの取得
いよいよ最後です。これさえ終わればすべての実装が終わります。もう少し頑張りましょう。

昇順ランキングでスコアを送信する際に特殊な処理をしたため、ランキングの取得でも同じ様な処理を行います。

以下が**昇順ランキング**のスコア取得のコードになります。

```c#:昇順ランキングのスコア取得メソッド
void GetRanking()
{
    // PlayFabに送信するリクエストを作成する
    var request = new GetLeaderboardRequest
    {
        // 統計情報名の指定（LeaderBoards(Legace)で設定した名前）
        StatisticName = "TestRanking",
        // 何位以降のランキングを取得するか指定する
        // 1位から取得する場合は0を代入
        StartPosition = 0,
        // 何件分のランキングデータを取得するか指定する
        // 最大は100
        MaxResultsCount = 10
    };

    // PlayFabにリクエストを送信する
    PlayFabClientAPI.GetLeaderboard(request, OnGetRankingSuccess, OnGetRankingFailure);

    // 送信成功時の処理
    void OnGetRankingSuccess(GetLeaderboardResult leaderboardResult)
    {
        // ランキングを表示する仮コード
        foreach (var item in leaderboardResult.Leaderboard)
        {
            // Positionは順位。0から始まるので+1して表示する
            // intの最大値から取得したスコアを引いて、本来のスコアを出力する
            Debug.Log($"{item.Position + 1}位　プレイヤー名：{item.DisplayName}　スコア：{int.MaxValue - item.StatValue}");
        }
    }

    // 送信失敗時の処理
    void OnGetRankingFailure(PlayFabError error)
    {
        Debug.Log("ランキングの取得に失敗しました");
    }
}
```

以上がランキング取得のコードになります。
スコアを出力する際、送信時と同じように int の最大値から取得したスコアを引くことで、本来のスコアを出力しています。

また、`GetLeaderboard`ではなく`GetLeaderboardAroundPlayer`を使うことで、上位のプレイヤーの順位だけでなく、ユーザーごとの現在の順位を取得することが可能です。
参考程度に`GetLeaderboardAroundPlayer`のコードも掲載しておきます。

```c#:ユーザー周辺の昇順ランキングのスコア取得メソッド
void GetRankingAroundPlayer()
{
    // PlayFabに送信するリクエストを作成する
    var request = new GetLeaderboardAroundPlayerRequest
    {
        // 統計情報名の指定（LeaderBoards(Legace)で設定した名前）
        StatisticName = "TestRanking",
        // 自分と+-5位をあわせて合計11件を取得
        MaxResultsCount = 11 
    };

    // PlayFabにリクエストを送信する
    PlayFabClientAPI.GetLeaderboardAroundPlayer(request, OnGetRankingAroundPlayerSuccess, OnGetRankingAroundPlayerFailure);

    // 送信成功時の処理
    void OnGetRankingAroundPlayerSuccess(GetLeaderboardAroundPlayerResult leaderboardResult)
    {
        // ランキングを表示する仮コード
        foreach (var item in leaderboardResult.Leaderboard)
        {
            // Positionは順位。0から始まるので+1して表示する
            // intの最大値から取得したスコアを引いて、本来のスコアを出力する
            Debug.Log($"{item.Position + 1}位　プレイヤー名：{item.DisplayName}　スコア：{int.MaxValue - item.StatValue}");
        }
    }

    // 送信失敗時の処理
    void OnGetRankingAroundPlayerFailure(PlayFabError error)
    {
        Debug.Log("ランキングの取得に失敗しました");
    }
}
```

以上で全てのメソッドの実装は終わりです。

#### オンラインランキング全体の実装
最後にこれらを組み合わせて動作するか確認します。
以下がテスト用のコードになります。

```c#:オンラインランキングの全体コード
using PlayFab.ClientModels;
using PlayFab;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// <summary>
// PlayFabでオンラインランキング処理を行うクラス
// </summary>
public class Ranking : MonoBehaviour
{
    // ==============================================================
    // テストプログラム
    // ==============================================================

    // テスト用の仮プログラム
    // ランキングの送受信を行う
    void Update()
    {
        // キーボードで「A」が入力されたらプレイヤー名を「菅田将暉」にする
        if (Input.GetKeyDown(KeyCode.A))
        {
            SetUserName("菅田将暉");
        }

        // キーボードで「B」が入力されたらスコアとして1000を送信する
        if (Input.GetKeyDown(KeyCode.B))
        {
            SubmitScore(1000);
        }

        // キーボードで「C」が入力されたらランキングを取得する
        if (Input.GetKeyDown(KeyCode.C))
        {
            GetRanking();
        }
    }

    // ==============================================================
    // ランキング更新メソッド
    // ==============================================================

    // プレイヤー名を設定するメソッド
    void SetUserName(string name)
    {
        // ユーザー名を設定するリクエストを作る
        var request = new UpdateUserTitleDisplayNameRequest
        {
            // ユーザー名の設定
            DisplayName = name
        };

        // リクエストをPlayFabに送信する
        PlayFabClientAPI.UpdateUserTitleDisplayName(request, OnSetUserNameSuccess, OnSetUserNameFailure);

        // 送信成功時の処理
        void OnSetUserNameSuccess(UpdateUserTitleDisplayNameResult result)
        {
            Debug.Log("プレイヤー名の変更に成功しました");
        }

        // 送信失敗時の処理
        void OnSetUserNameFailure(PlayFabError error)
        {
            Debug.Log("プレイヤー名の変更に失敗しました");
        }
    }

    // 昇順ランキングのスコア送信メソッド
    void SubmitScore(int score)
    {
        // 昇順ランキング用にスコアを修正する
        // 降順ランキングの場合はこの作業は省略する
        // 処理の内容と理由は後述
        int modifiedScore = int.MaxValue - score;

        // 送信する内容を作る
        var statisticUpdate = new StatisticUpdate
        {
            // 統計情報名の指定（LeaderBoards(Legace)で設定した名前）
            StatisticName = "TestRanking",
            // 送信するスコアの代入
            Value = modifiedScore,
        };

        // 作った内容をリクエストにまとめる
        var request = new UpdatePlayerStatisticsRequest
        {
            Statistics = new List<StatisticUpdate>
            {
                statisticUpdate
            }
        };

        // リクエストをPlayFabに送信する        
        PlayFabClientAPI.UpdatePlayerStatistics(request, OnSubmitScoreSuccess, OnSubmitScoreFailure);

        // 送信成功時の処理
        void OnSubmitScoreSuccess(UpdatePlayerStatisticsResult result)
        {
            Debug.Log("スコアの送信に成功しました");
        }

        // 送信失敗時の処理
        void OnSubmitScoreFailure(PlayFabError error)
        {
            Debug.Log("スコアの送信に失敗しました");
        }
    }

    // ==============================================================
    // ランキング取得メソッド
    // ==============================================================

    // 昇順ランキングのスコア取得メソッド
    void GetRanking()
    {
        // PlayFabに送信するリクエストを作成する
        var request = new GetLeaderboardRequest
        {
            // 統計情報名の指定（LeaderBoards(Legace)で設定した名前）
            StatisticName = "TestRanking",
            // 何位以降のランキングを取得するか指定する
            // 1位から取得する場合は0を代入
            StartPosition = 0,
            // 何件分のランキングデータを取得するか指定する
            // 最大は100
            MaxResultsCount = 10
        };

        // PlayFabにリクエストを送信する
        PlayFabClientAPI.GetLeaderboard(request, OnGetRankingSuccess, OnGetRankingFailure);

        // 送信成功時の処理
        void OnGetRankingSuccess(GetLeaderboardResult leaderboardResult)
        {
            // ランキングを表示する仮コード
            foreach (var item in leaderboardResult.Leaderboard)
            {
                // Positionは順位。0から始まるので+1して表示する
                // intの最大値から取得したスコアを引いて、本来のスコアを出力する
                Debug.Log($"{item.Position + 1}位　プレイヤー名：{item.DisplayName}　スコア：{int.MaxValue - item.StatValue}");
            }
        }

        // 送信失敗時の処理
        void OnGetRankingFailure(PlayFabError error)
        {
            Debug.Log("ランキングの取得に失敗しました");
        }
    }

    // ユーザー周辺の昇順ランキングのスコア取得メソッド
    void GetRankingAroundPlayer()
    {
        // PlayFabに送信するリクエストを作成する
        var request = new GetLeaderboardAroundPlayerRequest
        {
            // 統計情報名の指定（LeaderBoards(Legace)で設定した名前）
            StatisticName = "TestRanking",
            // 自分と+-5位をあわせて合計11件を取得
            MaxResultsCount = 11
        };

        // PlayFabにリクエストを送信する
        PlayFabClientAPI.GetLeaderboardAroundPlayer(request, OnGetRankingAroundPlayerSuccess, OnGetRankingAroundPlayerFailure);

        // 送信成功時の処理
        void OnGetRankingAroundPlayerSuccess(GetLeaderboardAroundPlayerResult leaderboardResult)
        {
            // ランキングを表示する仮コード
            foreach (var item in leaderboardResult.Leaderboard)
            {
                // Positionは順位。0から始まるので+1して表示する
                // intの最大値から取得したスコアを引いて、本来のスコアを出力する
                Debug.Log($"{item.Position + 1}位　プレイヤー名：{item.DisplayName}　スコア：{int.MaxValue - item.StatValue}");
            }
        }

        // 送信失敗時の処理
        void OnGetRankingAroundPlayerFailure(PlayFabError error)
        {
            Debug.Log("ランキングの取得に失敗しました");
        }
    }
}
```

使用しているメソッドは先ほど解説したものと全く同じものです。テストコードを動かすために、プレイヤー名を「菅田将暉」にしてます。菅田将暉が私のゲームをプレイしてくれてたら嬉しいので。

Aを押したら名前変更、Bを押したらスコア送信、Cを押したらランキング表示というコードとなっております。デバッグ開始後、`A→B→C`の順でキーボードを操作することで、プログラムが正常に動いている事を確認できると思います。

実行結果は以下です。

<img width = "500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/25b48a69-0bb6-9dd0-136a-ba2c8f186e2b.png">

無事、菅田将暉がゲームを遊んでくれてます。
やりましたね。

`CustomID`をいじって、正常に昇順ランキングができているかも確認します。

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/06669190-d567-5bbc-d3e8-b8a781c4c891.png">

無事、昇順ランキングも機能していることが確認できました。
山崎賢人も来てくれてます。嬉しいですね。

<img width = "500" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/47132062-ac7b-6fa4-0c88-7fecaec06669.png">

念の為、より順位が高くなるスコアを送った場合の動作も確認しましたが、無事動作確認が取れました。

<img width = "500" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3962140/c6de9608-eca6-0039-66df-0840aca57313.png">

PlayFabの`TestRanking`を見てあげると、このようにランキングに追加されているのも確認できます。

以上となります。お疲れさまでした。

## まとめ
今回はUnityでオフラインとオンラインのランキング処理の仕方についてまとめました。勉強不足で間違っている箇所もあるかもしれませんが、温かい目で見ていただけると嬉しいです。
非常に長くなってしまいましたが、ここまで読んで頂き誠にありがとうございます。皆様の助けになったら幸いです。

## 参考文献

https://qiita.com/riekure/items/3fd4526b13d8e89a7fc6

https://kan-kikuchi.hatenablog.com/entry/PlayFabLogin

https://qiita.com/_y_minami/items/9143502f465ad11ff2ca