GS2-Ranking

Ranking feature

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

There are two types of rankings: “those where all participants compete on the same board” and “those where players compete against the scores of players they have subscribed to.” The former is called a global ranking, and the latter is called a scope ranking.

Global Ranking

Global ranking provides a ranking feature for competing against all players.

GS2-Ranking can realize large-scale rankings involving hundreds of millions of players.

Instead, ranking aggregation is not performed in real time; aggregation runs on a preconfigured cycle, and ranking is calculated based on those aggregated results.

Category

Set the type of ranking. For determining ranks, you must specify whether higher scores are better or lower scores are better.

Aggregation Interval

Set the interval at which ranking aggregation is performed. You can specify a minimum of 15 minutes and a maximum of 24 hours.

A point to watch out for is that the interval you set here is not the interval from the start time of the previous aggregation, but the interval from the end time of the previous aggregation. Consider the case where aggregation takes 5 minutes and the first aggregation runs at 00:00.

Aggregation start time Aggregation end time
00:00 00:05
00:20 00:25
00:40 00:45

Fixed Aggregation Time

When the aggregation interval is set to 24 hours, there is a need to fix the time at which aggregation runs. To meet this need, a feature is provided that runs aggregation when a specified time is reached even if the aggregation cycle has not arrived.

Suppose you set 24 hours for the aggregation interval and 5 AM for the fixed aggregation time.

Aggregation start time Aggregation end time
2020-01-01 05:00 2020-01-01 05:05
2020-01-02 05:00 2020-01-02 05:05 <- Only 23 hours 55 minutes have elapsed, but aggregation runs because the fixed time has been reached
2020-01-03 05:00 2020-01-03 05:05 <- Only 23 hours 55 minutes have elapsed, but aggregation runs because the fixed time has been reached

Valid Score Range

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

In this case, no error is returned to the client in order to make it harder to investigate the boundary of invalid scores.

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.

Aggregation does not run outside the score registration period, so there are no aggregation-related costs, but GS2-Schedule API calls do occur for the schedule check. Therefore, for rankings that will clearly never be referenced again, it is recommended to delete them from the master data.

Ranking Data Access Period

You can associate a GS2-Schedule event as the period during which ranking data can be accessed. This can be used in cases such as disabling score references after an event ends.

Ranking Generations

You can set a generation per category. By changing the generation, you can reset the registered content of the ranking without changing the category name.

Score Updates

When submitting a score, the server treats the most recently registered score as the valid score. Therefore, if you want to record only the best score in the ranking, the client must determine which score is better and decide whether to send the score or not.

Retrieving Ranks

You can retrieve the rank of a player with the specified user ID. This process attempts to return a rank that reflects the most recent state as much as possible.

It computes the rank the player’s latest score would have within the latest aggregated results and returns that rank. Therefore, even if you obtain the rank immediately after a score update, you can get a value corresponding to the rank using the latest score even though the aggregation time has not arrived yet.

Retrieving Rankings Around a Specified Score

You can specify a score and retrieve the ranking around it. If there are a large number of identical scores, the specified score may not be placed exactly in the center of the list.

Scope Ranking

Scope ranking realizes rankings within a small set of players, such as a friends ranking.

To realize this feature, a score bucket is prepared for each player. When a player updates their score, the data in the buckets of players who have subscribed to that player’s score is also updated. Each player computes the ranking using the scores in their own bucket.

Master Data Management

Ranking categories are defined in master data.

  • CategoryModel: Ranking category settings (order, aggregation interval, fixed aggregation time, valid score range, registration period, viewable period, generation, score retention method, etc.)

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.

Script Triggers

GS2-Ranking does not provide script triggers.

Transaction Actions

GS2-Ranking does not provide transaction actions.

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.

    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
    )->RankingCategory(
        "category-0001" // categoryName
    );
    const auto Future = Domain->PutScore(
        1000L,
        nullptr // metadata
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

Retrieving the Rank (Global)

    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
    )->RankingCategory(
        "category-0001" // categoryName
    )->Ranking(
        "user-0001" // scorerUserId
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Item = Future->GetTask().Result();

Retrieving the Ranking

    var items = await gs2.Ranking.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).RankingCategory(
        categoryName: "category-0001"
    ).RankingsAsync(
    ).ToListAsync();
    const auto Domain = Gs2->Ranking->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->RankingCategory(
        "category-0001" // categoryName
    );
    const auto It = Domain->Rankings(
    );
    TArray<Gs2::UE5::Ranking::Model::FEzRankingPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }

Subscribing to Another Player (Scope)

    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
    )->RankingCategory(
        "category-0001" // categoryName
    );
    const auto Future = Domain->Subscribe(
        "user-0002" // targetUserId
    );
    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();

Unsubscribing from Another Player (Scope)

    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
    )->RankingCategory(
        "category-0001" // categoryName
    )->SubscribeUser(
        "user-0002" // targetUserId
    );
    const auto Future = Domain->Unsubscribe(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

Retrieving the List of Subscribed Players

    var items = await gs2.Ranking.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).RankingCategory(
        categoryName: "category-0001"
    ).SubscribeUsersAsync(
    ).ToListAsync();
    const auto Domain = Gs2->Ranking->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->RankingCategory(
        "category-0001" // categoryName
    );
    const auto It = Domain->SubscribeUsers(
    );
    TArray<Gs2::UE5::Ranking::Model::FEzSubscribeUserPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }

Score Retention Settings

For both global ranking and scope ranking, you can configure whether to hold a single score per user ID or multiple scores. You can choose whether only the most recently registered score is included in the ranking, or whether all registered scores are included.

If all scores are included, the data volume tends to grow, which also affects GS2 usage fees. Unless there is a significant reason in your game system, we recommend configuring it so that only the most recently registered score is included in the ranking.

Detailed Reference