GS2-Ranking
ゲームのスコアやクリアタイムを競うランキング機能を実現します。
ランキングには2種類存在し「参加者全員が同じボード上で競い合うもの」と「購読したプレイヤーのスコアと競い合うもの」があります。 前者はグローバルランキング、後者はスコープランキングと呼びます。
グローバルランキング
グローバルランキングは全てのプレイヤーと競い合うためのランキング機能を提供します。
GS2-Ranking では、数億人以上のプレイヤーが参加するような大規模なランキングを実現できます。
その代わり、ランキングの集計はリアルタイムではなく、事前に設定した周期で集計処理が行われ その集計結果をベースに順位計算などを行います。
カテゴリ
ランキングの種類を設定します。 順位づけをするにあたって、スコアが大きい方が優れているのか、小さい方が優れているのかを設定する必要があります。
集計間隔
ランキングの集計を行う間隔を設定します。最小15分、最大24時間の範囲で指定ができます。
集計間隔の設定で気をつけなければならないのは、ここで設定する間隔は前回の集計開始時刻からの間隔ではなく、前回の集計終了時刻からの間隔であることです。 集計処理に5分要する状況で、00:00 に初回の集計が動いた場合について考えてみます。
集計開始時刻 | 集計終了時刻 |
---|---|
00:00 | 00:05 |
00:20 | 00:25 |
00:40 | 00:45 |
固定集計時刻
集計間隔を24時間に設定した際に、その集計される時刻を固定したいというニーズがあります。 このようなニーズに対応するために、指定時刻になったら集計周期になっていなくても集計を実行する機能を提供しています。
集計間隔に24時間、固定集計時刻に AM5時 を設定したとします。
集計開始時刻 | 集計終了時刻 | |
---|---|---|
2020-01-01 05:00 | 2020-01-01 05:05 | |
2020-01-02 05:00 | 2020-01-02 05:05 | ← 23時間55分しか経過していないが、固定集計時刻になったため集計する |
2020-01-03 05:00 | 2020-01-03 05:05 | ← 23時間55分しか経過していないが、固定集計時刻になったため集計する |
スコアの有効範囲
スコアとして登録を受け付ける値の範囲を設定できます。 これによって、明らかに不適切なスコアの登録があった時に登録処理を行わずに捨てることができます。
この場合、不正なスコアの境界を調べるのを困難にするため、クライアントにエラーは返りません。
スコアの登録可能期間
スコアの登録を受け付ける期間の設定として GS2-Schedule のイベントを関連づけることができます。 スコア受け付け期間外にスコアを送信してもスコアは捨てられます。
スコアの登録可能期間外は集計処理は行われないため、集計にまつわる費用は発生しませんが、 スケジュールの判定のために GS2-Schedule のAPIコールは発生します。 そのため、明らかに2度と参照しないランキングに関してはマスターデータから削除することを推奨します。
ランキングデータへのアクセス可能期間
ランキングデータへのアクセス可能期間として GS2-Schedule のイベントを関連づけることができます。 イベント終了後はスコアの参照もできなくする場合などに活用できます。
ランキングの世代
カテゴリごとに世代を設定可能です。 世代を変更することで、カテゴリ名を変更せずにランキングの登録内容をリセットすることができます。
スコアの更新
スコアを送信する際に、サーバーでは最後に登録されたスコアを有効なスコアとして処理します。 そのため、最も優れたスコアをランキングとして記録したい場合、クライアントでスコアの優劣を判定し、スコアを送信するかしないかを判断する必要があります。
順位の取得
指定したユーザーIDのプレイヤーの順位を取得できます。 この処理では、なるべく最新の状況に近い順位を応答しようとします。
事前に集計された集計結果の内容の中で、最新のスコアの場合何位になるかを計算し、その順位を応答します。 そのため、スコア更新直後に順位を取得した場合も、集計時刻を迎えていなくても最新のスコアを使用した順位に相当する値を得ることができます。
指定したスコア周辺のランキングを取得
スコアを指定してその周辺のランキングを取得できます。 同一のスコアが大量に存在する場合、リストの中心に指定したスコアを設定されないことがあります。
スコープランキング
スコープランキングはフレンド内のランキングといったごく一部のプレイヤー内でのランキングを実現します。
この機能を実現するために、各プレイヤーにスコアバケットを用意し プレイヤーがスコアを更新した際に、自分のスコアを購読しているプレイヤーのバケットのデータも更新することで 各プレイヤーは自分のバケット内のスコアを使用してランキングを計算することで実現しています。
実装例
スコアを登録
このAPIは、利便性の観点から ApplicationAccess で呼び出せるようになっています。 しかし、任意のスコアで送信できるのは脆弱性となります。
そのため、可能であればこのAPIをクライアントから呼び出せないように設定し、信頼できる送信元からのみスコアの登録を受け付けられるようにするべきです。
たとえば、アイテムの所持数量のランキングを実現したいのであれば、GS2-Inventory のアイテム入手時にトリガーされるスクリプトでスコアとしてアイテムの所持数量を登録する方が安全に処理できます。
var result = await gs2.Ranking.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Ranking(
categoryName: "category-0001"
).PutScoreAsync(
score: 1000L,
metadata: null
);
var item = await result.ModelAsync();
const auto Domain = Gs2->Ranking->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Ranking(
"category-0001" // categoryName
);
const auto Future = Domain->PutScore(
1000L,
nullptr // metadata
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
順位を取得(グローバル)
var item = await gs2.Ranking.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Ranking(
categoryName: "category-0001"
).ModelAsync(
scorerUserId : "user-0001"
);
const auto Domain = Gs2->Ranking->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Ranking(
"category-0001" // categoryName
);
const auto item = Domain.ModelAsync(
"user-0001" // scorerUserId
);
ランキングを取得
var items = await gs2.Ranking.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).RankingsAsync(
).ToListAsync();
const auto Domain = Gs2->Ranking->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
);
const auto It = Domain->Rankings( // categoryName
);
TArray<Gs2::UE5::Ranking::Model::FEzRankingPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}
他プレイヤーを購読(スコープ)
var result = await gs2.Ranking.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).SubscribeAsync(
categoryName: "category-0001",
targetUserId: "user-0002"
);
var item = await result.ModelAsync();
const auto Domain = Gs2->Ranking->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
);
const auto Future = Domain->Subscribe(
"category-0001",
"user-0002"
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
// obtain changed values / result values
const auto Future2 = Future->GetTask().Result()->Model();
Future2->StartSynchronousTask();
if (Future2->GetTask().IsError()) return false;
const auto Result = Future2->GetTask().Result();
他プレイヤーの購読を解除(スコープ)
var result = await gs2.Ranking.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).SubscribeUser(
categoryName: "category-0001",
targetUserId: "user-0002"
).UnsubscribeAsync(
);
const auto Domain = Gs2->Ranking->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->SubscribeUser(
"category-0001", // categoryName
"user-0002" // targetUserId
);
const auto Future = Domain->Unsubscribe(
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
購読中のプレイヤーリストを取得
var items = await gs2.Ranking.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).SubscribeUsersAsync(
).ToListAsync();
const auto Domain = Gs2->Ranking->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
);
const auto It = Domain->SubscribeUsers(
"category-0001", // categoryName
);
TArray<Gs2::UE5::Ranking::Model::FEzSubscribeUserPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}