GS2-Ranking2

Ranking feature

Implements a ranking feature for competing on game scores and clear times.

GS2-Ranking2 provides the following three modes.

  • Global ranking
  • Cluster ranking
  • Subscription ranking

Most of the ranking requirements a game needs should be satisfiable by one of these modes.

Ranking Modes

Global Ranking

Global ranking provides a ranking feature for competing against all players. In GS2-Ranking2, only the top 1,000 players can participate in the ranking; the scores of players ranked 1,001 or lower are not registered in the ranking even when submitted.

This is a significant change from the previous version, GS2-Ranking, which was provided with the ability to return accurate ranks among more than 100 million players. GS2-Ranking could handle large numbers of scores, but registration to the ranking did not occur until aggregation ran on a configurable interval between 15 minutes and 24 hours, and a cost proportional to the number of participants was incurred each time aggregation ran.

GS2-Ranking2 was redesigned based on feedback from developers who used GS2-Ranking. Specifically, the following points were taken as important feedback for the redesign.

  • In many use cases, it is sufficient to display only the top players
  • Changes in rank should be reflected in the ranking immediately
  • Ranking aggregation cost should not grow as the number of players grows

As a result, as described above, only the top 1,000 players can participate in the ranking and players ranked 1,001 or lower are treated as “out of range”. In exchange, scores are reflected in the ranking immediately after they are registered, and no additional costs are incurred beyond the normal API request costs.

Cluster Ranking

Roughly the same as global ranking, with the only difference being that a separate ranking is created per cluster ID.

By specifying a GS2-Guild guild ID as the cluster ID, you can implement rankings where guild members compete against each other.

Cluster Participation Check

In cluster ranking, by defining a cluster type, you can verify whether the player is a participant of the cluster before executing score registration.

For example, when implementing a ranking that uses GS2-Guild guilds as clusters, by specifying Gs2Guild::Guild as the cluster type in the ranking mode settings, you can configure that when registering a score, the system first confirms that the player attempting to register a score to the guild specified by the cluster ID is registered as a member of that guild before performing the registration.

Subscription Ranking

A ranking feature whose specification is similar to GS2-Ranking’s scope ranking. By subscribing to other players, you can include the latest scores of those players in your own ranking board.

This is used to realize rankings with strong inter-player asymmetry, such as friend rankings.

Reflection Delay in Subscription Ranking

When a score is registered, the score is asynchronously registered into the rankings of players subscribed to that player. This process usually completes within one second, but because it is asynchronous, there is a slight delay before the score is reflected.

Seasons

Each ranking can have a GS2-Schedule event associated with it as the period during which score registration is accepted. GS2-Schedule events can be configured to repeat, and each ranking is reset every time the event repeats.

To realize this feature, GS2-Ranking2 has a season property on each ranking. Ranking results are stored per season, and past seasons’ results can be referenced at any time.

Ranking Rewards

In global ranking and cluster ranking, you can configure rank-based rewards. To configure a reward, set a rank threshold and reward content.

If you specify 3 as the threshold, you can configure the reward for players ranked 1, 2, and 3. If you then specify 10, you can configure the reward for players ranked 4, 5, 6, 7, 8, 9, and 10.

Rewards for Players Outside the Ranking

By specifying 1001 as the threshold, you can configure rewards for players who are out of range. Configuring a ranking reward with threshold 1001 is optional; if not specified, out-of-range players cannot receive rewards.

Receiving Rewards for Past Seasons

Past seasons’ ranking rewards can be received at any time by calling the reward receipt API and specifying a past season number.

Valid Score Range

You can configure the range of values accepted as scores. This allows you to discard clearly inappropriate scores without performing registration.

Period Settings

Score Registration Period

You can associate a GS2-Schedule event as the period during which score registration is accepted. Scores submitted outside the acceptance period are discarded.

Ranking Data Access Period

You can associate a GS2-Schedule event as the period during which ranking data can be accessed. This is useful when you want to disable score references after an event ends.

Transaction Actions

GS2-Ranking2 provides the following transaction actions.

  • Verify action: Verify scores for global, cluster, and subscription rankings
  • Consume action: Record ranking reward receipt history

By using “Verify ranking score” as a verify action, you can safely set, within a transaction, restrictions such as products only purchasable by players who have recorded a score above a certain level, or quests that can only be attempted by such players. This makes it possible to reliably realize events such as special reward acquisition events limited to top-ranked players.

Master Data Management

By registering master data, you can configure the data and behaviors available in the microservice.

The types of master data are as follows.

  • RankingModel: Settings for the ranking mode and reward thresholds

In addition to registering master data from the management console, you can also reflect data from GitHub or build a workflow that registers it from CI using GS2-Deploy.

Implementation Examples

Registering a Score

For convenience, this API can be called via ApplicationAccess. However, allowing arbitrary scores to be sent is a vulnerability.

Therefore, if possible, configure this API so that it cannot be called from the client, and only accept score registration from trusted sources.

For example, if you want to implement a ranking based on the number of items owned, it is safer to register the item ownership count as a score from a script that is triggered when an item is acquired in GS2-Inventory.

Global Ranking

    var result = await gs2.Ranking2.Namespace(
        namespaceName: "namespace-0001"
    ).GlobalRankingModel(
        rankingName: "ranking-0001"
    ).GlobalRankingSeason(
        gameSession: GameSession,
        season: null // current season
    ).PutGlobalRankingAsync(
        score: 100L,
        metadata: null
    );
    var item = await result.ModelAsync();
    const auto Future = Gs2->Ranking2->Namespace(
        "namespace-0001" // namespaceName
    )->GlobalRankingModel(
        "ranking-0001" // rankingName
    )->GlobalRankingSeason(
        GameSession,
        TOptional<int64>() // current season
    )->PutGlobalRanking(
        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();

Cluster Ranking

    var result = await gs2.Ranking2.Namespace(
        namespaceName: "namespace-0001"
    ).ClusterRankingModel(
        rankingName: "ranking-0001"
    ).ClusterRankingSeason(
        clusterName: "cluster-0001",
        gameSession: GameSession,
        season: null // current season
    ).PutClusterRankingAsync(
        score: 100L,
        metadata: null
    );
    var item = await result.ModelAsync();
    const auto Future = Gs2->Ranking2->Namespace(
        "namespace-0001" // namespaceName
    )->ClusterRankingModel(
        "ranking-0001" // rankingName
    )->ClusterRankingSeason(
        "cluster-0001", // clusterName
        GameSession,
        TOptional<int64>() // current season
    )->PutClusterRanking(
        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();

Subscription Ranking

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

Retrieving the Rank

Global Ranking

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

Cluster Ranking

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

Subscription Ranking

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

Retrieving the Ranking

Global Ranking

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

Cluster Ranking

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

Subscription Ranking

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

Receiving Ranking Rewards

Ranking rewards can be received by players who fall within the configured reward threshold by calling the API. The receipt is executed as a transaction, and the configured acquire actions are evaluated in order.

To receive rewards for a past season, specify the target season number in season.

Global Ranking

    var transaction = await gs2.Ranking2.Namespace(
        namespaceName: "namespace-0001"
    ).GlobalRankingModel(
        rankingName: "ranking-0001"
    ).GlobalRankingSeason(
        gameSession: GameSession,
        season: null // current season
    ).GlobalRankingReceivedReward(
    ).ReceiveGlobalRankingRewardAsync(
    );
    await transaction.WaitAsync();
    const auto Future = Gs2->Ranking2->Namespace(
        "namespace-0001" // namespaceName
    )->GlobalRankingModel(
        "ranking-0001" // rankingName
    )->GlobalRankingSeason(
        GameSession,
        TOptional<int64>() // current season
    )->GlobalRankingReceivedReward(
    )->ReceiveGlobalRankingReward(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

    const auto Transaction = Future->GetTask().Result();
    const auto Future2 = Transaction->Wait();
    Future2->StartSynchronousTask();
    if (Future2->GetTask().IsError()) return false;

Cluster Ranking

    var transaction = await gs2.Ranking2.Namespace(
        namespaceName: "namespace-0001"
    ).ClusterRankingModel(
        rankingName: "ranking-0001"
    ).ClusterRankingSeason(
        clusterName: "cluster-0001",
        gameSession: GameSession,
        season: null // current season
    ).ClusterRankingReceivedReward(
    ).ReceiveClusterRankingRewardAsync(
    );
    await transaction.WaitAsync();
    const auto Future = Gs2->Ranking2->Namespace(
        "namespace-0001" // namespaceName
    )->ClusterRankingModel(
        "ranking-0001" // rankingName
    )->ClusterRankingSeason(
        "cluster-0001", // clusterName
        GameSession,
        TOptional<int64>() // current season
    )->ClusterRankingReceivedReward(
    )->ReceiveClusterRankingReward(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

    const auto Transaction = Future->GetTask().Result();
    const auto Future2 = Transaction->Wait();
    Future2->StartSynchronousTask();
    if (Future2->GetTask().IsError()) return false;

Detailed Reference