GS2-Ranking2

ランキング機能

ゲームのスコアやクリアタイムを競うランキング機能を実現します。

GS2-Ranking2 は以下の3種類のモードを提供します。

  • グローバルランキング
  • クラスターランキング
  • 購読ランキング

ゲームが必要とするランキング機能の多くはこのいずれかのモードで要件を満たせるはずです。

ランキングモード

グローバルランキング

グローバルランキングは全てのプレイヤーと競い合うためのランキング機能を提供します。 GS2-Ranking2 では上位1000位のプレイヤーのみランキングに参加可能で、1000位以下のプレイヤーのスコアは送信したとしてもランキングへは登録されません。

以前のバージョンである GS2-Ranking が1億を超えるプレイヤーの正確な順位を返す能力を有する形で提供していましたので、以前のバージョンと比較して大きく仕様が変化しています。 GS2-Ranking では、多くのスコアを扱える代わりに、15分〜24時間の範囲で指定した集計間隔で集計が行われるまでランキングへの登録が行われない仕様で、集計の度に参加人数に応じたコストが生じていました。

GS2-Ranking を利用した開発者からのフィードバックを受けて GS2-Ranking2 は再設計されました。 具体的に以下の点を重要なフィードバックと捉えて再設計しています。

  • 多くのユースケースにおいて上位プレイヤーのみ表示できればよい
  • 順位の変化を即座にランキングへと反映したい
  • プレイヤーの増加によるランキング集計コストの増加は望ましくない

結果として、前述のように上位1000人のプレイヤーのみがランキングに参加でき、1001位以下のプレイヤーは「圏外」として処理する仕組みになりました。 その代わりスコアの登録直後にランキングに反映され、通常のAPIリクエストコスト以上の追加コストは発生しません。

クラスターランキング

概ねグローバルランキングと同じ仕様ですが、唯一異なるのはクラスターIDごとに異なるランキングが作成されることです。

クラスターIDに GS2-Guild のギルドIDを指定することで、ギルドメンバー同士で競うようなランキングを実現するために利用できます。

クラスター参加判定

クラスターランキングでは、クラスターの種類を定義しておくことでスコア登録時にクラスターに参加しているか確認してからスコア登録を実行できます。

たとえば、GS2-Guild のギルドをクラスターとしたランキングを実現する場合、ランキングモードの設定でクラスターの種類に「Gs2Guild::Guild」を指定することで スコア登録時にクラスターIDで指定されたギルドにスコアを登録しようとしているプレイヤーがメンバーとして登録されていることを確認した上でスコアを登録するようにできます。

購読ランキング

GS2-Ranking のスコープランキングに類似する仕様のランキング機能です。 他プレイヤーを購読することで、自分のランキングボードに他プレイヤーの最新のスコアを含めることが可能となります。

フレンド内ランキングのようなプレイヤー間で非対称性が強いランキングを実現するために使用します。

購読ランキングにおける反映遅延

スコアの登録を実行すると、プレイヤーを購読しているプレイヤーのランキングに非同期でスコア登録が実行されます。 この処理は通常1秒以内に実行されますが、非同期処理のためスコアが反映されるまで若干の遅延が生じます。

シーズン

各ランキングにはスコアの登録を受け付ける期間として GS2-Schedule のイベントを関連づけることが可能です。 GS2-Schedule のイベントには繰り返し設定が可能で、各ランキングはイベントが繰り返す度にリセットされます。

この機能を実現するために、GS2-Ranking2 は各ランキングに シーズン というプロパティを持っています。 ランキングの結果はシーズンごとに格納され、過去のシーズンの結果をいつでも参照することができます。

ランキング報酬

グローバルランキング・クラスターランキングではランキングの順位報酬を設定できます。 報酬を設定するには 順位閾値報酬の内容 を設定します。

閾値に 3 を指定すると、1,2,3位のプレイヤーへの報酬 続けて 10 を指定すると、4, 5, 6, 7, 8, 9, 10位プレイヤーへの報酬を設定できます。

ランキング圏外のプレイヤーへの報酬

1001 を閾値に指定すると、ランキング圏外のプレイヤーへの報酬を設定できます。 1001 を閾値とするランキング報酬の設定は任意で、未指定の場合は圏外のプレイヤーは報酬を受け取ることはできません。

過去シーズンの報酬受け取り

過去シーズンのランキング報酬は、報酬受け取りAPIに過去のシーズン番号を指定して呼び出すことで、いつでも受け取ることができます。

スコアの有効範囲

スコアとして登録を受け付ける値の範囲を設定できます。 これによって、明らかに不適切なスコアの登録があった時に登録処理を行わずに捨てることができます。

期間設定

スコアの登録可能期間

スコアの登録を受け付ける期間の設定として GS2-Schedule のイベントを関連づけることができます。 スコア受け付け期間外にスコアを送信してもスコアは捨てられます。

ランキングデータへのアクセス可能期間

ランキングデータへのアクセス可能期間として GS2-Schedule のイベントを関連づけることができます。 イベント終了後はスコアの参照もできなくする場合などに活用できます。

実装例

スコアを登録

このAPIは、利便性の観点から ApplicationAccess で呼び出せるようになっています。 しかし、任意のスコアで送信できるのは脆弱性となります。

そのため、可能であればこのAPIをクライアントから呼び出せないように設定し、信頼できる送信元からのみスコアの登録を受け付けられるようにするべきです。

たとえば、アイテムの所持数量のランキングを実現したいのであれば、GS2-Inventory のアイテム入手時にトリガーされるスクリプトでスコアとしてアイテムの所持数量を登録する方が安全に処理できます。

グローバルランキング

    var result = await gs2.Ranking2.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).PutGlobalRankingAsync(
        rankingName: "ranking-0001",
        score: 100L,
        metadata: null
    );
    var item = await result.ModelAsync();
    const auto Future = Gs2->Ranking2->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        GameSession
    )->PutGlobalRanking(
        "ranking-0001", // rankingName
        100L, // score
        TOptional<FString>() // metadata
    );
    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 Future2->GetTask().Error();
    }
    const auto Result = Future2->GetTask().Result();

クラスターランキング

    var result = await gs2.Ranking2.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).PutClusterRankingAsync(
        rankingName: "ranking-0001",
        clusterName: "cluster-0001",
        score: 100L,
        metadata: null
    );
    var item = await result.ModelAsync();
    const auto Future = Gs2->Ranking2->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        GameSession
    )->PutClusterRanking(
        "ranking-0001", // rankingName
        "cluster-0001", // clusterName
        100L, // score
        TOptional<FString>() // metadata
    );
    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 Future2->GetTask().Error();
    }
    const auto Result = Future2->GetTask().Result();

購読ランキング

    var result = await gs2.Ranking2.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).SubscribeRankingSeason(
        rankingName: "ranking-0001",
        season: null // current season
    ).PutSubscribeRankingAsync(
        score: 100L,
        metadata: null
    );
    var item = await result.ModelAsync();
    const auto Future = Gs2->Ranking2->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        GameSession
    )->SubscribeRankingSeason(
        "ranking-0001", // rankingName
        TOptional<int64>() // current season
    )->PutSubscribeRanking(
        100L, // score
        TOptional<FString>() // metadata
    );
    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 Future2->GetTask().Error();
    }
    const auto Result = Future2->GetTask().Result();

順位を取得

グローバルランキング

    var result = await gs2.Ranking2.Namespace(
        namespaceName: "namespace-0001"
    ).GlobalRankingModel(
        rankingName: "ranking-0001"
    ).GlobalRankingSeason(
        season: null // current season
    ).GlobalRankingData(
        GameSession
    ).GetGlobalRankingRankAsync(
    );
    var item = await result.ModelAsync();
    const auto Future = Gs2->Ranking2->Namespace(
        "namespace-0001" // namespaceName
    )->GlobalRankingModel(
        "ranking-0001" // rankingName
    )->GlobalRankingSeason(
        TOptional<int64>() // current season
    )->GlobalRankingData(
        GameSession
    )->GetGlobalRankingRank(
    );
    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 Future2->GetTask().Error();
    }
    const auto Result = Future2->GetTask().Result();

クラスターランキング

    var result = await gs2.Ranking2.Namespace(
        namespaceName: "namespace-0001"
    ).ClusterRankingModel(
        rankingName: "ranking-0001"
    ).ClusterRankingSeason(
        clusterName: "cluster-0001",
        season: null // current season
    ).ClusterRankingData(
        GameSession
    ).GetClusterRankingRankAsync(
    );
    var item = await result.ModelAsync();
    const auto Future = Gs2->Ranking2->Namespace(
        "namespace-0001" // namespaceName
    )->ClusterRankingModel(
        "ranking-0001" // rankingName
    )->ClusterRankingSeason(
        "cluster-0001", // clusterName
        TOptional<int64>() // current season
    )->ClusterRankingData(
        GameSession
    )->GetClusterRankingRank(
    );
    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 Future2->GetTask().Error();
    }
    const auto Result = Future2->GetTask().Result();

購読ランキング

    var result = await gs2.Ranking2.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).SubscribeRankingSeason(
        rankingName: "ranking-0001",
        season: null
    ).SubscribeRankingData(
        GameSession
    ).GetSubscribeRankingRankAsync(
    );
    var item = await result.ModelAsync();
    const auto Future = Gs2->Ranking2->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        GameSession
    )->SubscribeRankingSeason(
        "ranking-0001", // rankingName
        TOptional<int64>() // current season
    )->SubscribeRankingData(
        GameSession
    )->GetSubscribeRankingRank(
    );
    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 Future2->GetTask().Error();
    }
    const auto Result = Future2->GetTask().Result();

ランキングを取得

グローバルランキング

    var items = await gs2.Ranking2.Namespace(
        namespaceName: "namespace-0001"
    ).GlobalRankingModel(
        rankingName: "ranking-0001"
    ).GlobalRankingSeason(
        season: null
    ).GlobalRankingsAsync(
    ).ToListAsync();
    const auto It = Gs2->Ranking2->Namespace(
        "namespace-0001" // namespaceName
    )->GlobalRankingModel(
        "ranking-0001" // rankingName
    )->GlobalRankingSeason(
        nullptr // season
    )->GlobalRankings(
    );
    TArray<Gs2::UE5::Ranking2::Model::FEzGlobalRankingDataPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }

クラスターランキング

    var items = await gs2.Ranking2.Namespace(
        namespaceName: "namespace-0001"
    ).ClusterRankingModel(
        rankingName: "ranking-0001"
    ).ClusterRankingSeason(
        clusterName: "cluster-0001",
        season: null
    ).ClusterRankingsAsync(
    ).ToListAsync();
    const auto It = Gs2->Ranking2->Namespace(
        "namespace-0001" // namespaceName
    )->ClusterRankingModel(
        "ranking-0001" // rankingName
    )->ClusterRankingSeason(
        "cluster-0001", // clusterName
        nullptr // season
    )->ClusterRankings(
    );
    TArray<Gs2::UE5::Ranking2::Model::FEzClusterRankingDataPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }

購読ランキング

    var items = await gs2.Ranking2.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).SubscribeRankingSeason(
        rankingName: "ranking-0001",
        season: null
    ).SubscribeRankingsAsync(
    ).ToListAsync();
    const auto It = Gs2->Ranking2->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        GameSession
    )->SubscribeRankingSeason(
        "ranking-0001", // rankingName
        nullptr // season
    )->SubscribeRankings(
    );
    TArray<Gs2::UE5::Ranking2::Model::FEzSubscribeRankingDataPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }

詳細なリファレンス