GS2-SeasonRating

シーズンレーティング機能

プレイヤースキルでプレイヤーを分類し、より近い実力のプレイヤーとのマッチメイキングを実現するための機能です。 レーティング値によるマッチメイキング機能は GS2-Matchmaking に備わっていますが、近年増えている現実時間で数週間〜数ヶ月程度のシーズンを通してプレイヤーをランクづけする仕組みには対応していませんでした。

このマイクロサービスは、定めた期間をシーズンと捉え、シーズン期間に高いティアーを目指して対戦を繰り返すゲームサイクルを実現するために利用できます。 そのため、この機能で強さを表す指標はレート値ではなく、どのティアーに属しているかによって表現されます。

graph TD
  Match["GS2-Matchmaking<br/>マッチメイキング成立"] --> Session["MatchSession 作成"]
  Session --> Ballot["各プレイヤーが投票用紙を取得<br/>(Ballot / SignedBallot)"]
  Ballot --> GamePlay["ゲームプレイ"]
  GamePlay --> Vote["Vote / VoteMultiple<br/>順位を投票"]
  Vote --> Aggregate{"多数決で結果確定"}
  Aggregate -- 確定 --> Calc["ポイント変動量を計算<br/>(TierModel に基づく)"]
  Calc --> Experience["GS2-Experience に<br/>ポイント / ランクを反映"]
  Aggregate -- 分断 --> Skip["集計せず終了"]

ティアー

ティアーは一般的にブロンズから始まり、勝ち進めることでシルバー、ゴールド、プラチナ…のように上がっていく仕様が一般的です。 ティアーを上げるには、同一ティアー内のプレイヤーと対戦し、順位によって変動する入手ポイントが閾値を超えると次のティアーに上がれます。 勝負に負けるとポイントは減ることがあり、ポイントが閾値を下回ると前のティアーに下がることがあります。

ティアーモデルの設定項目

各ティアーは TierModel として定義し、以下のパラメータでポイントの増減や昇格時の挙動を制御できます。

項目 説明
metadata ティアー名などの自由記述(ブロンズ/シルバー/ゴールド 等)
entryFee ゲーム参加時に消費するポイント。連戦連勝でなければ昇格できない難易度設計に利用
minimumChangePoint 最下位を取った時のポイント減算量
maximumChangePoint 最上位を取った時のポイント加算量
raiseRankBonus ランクアップした際に付与するボーナスポイント。直後に降格しないチャタリング防止に有効

ティアーをまたぐ対戦

レーティング戦において、マッチメイキングのルールは原則的に同一ティアーで構成するべきです。 しかし、プレイヤーが不足するなどの理由で前後のティアーのプレイヤーを含んだ状態でマッチメイキングをする場合も各プレイヤーが属しているティアーの最小・最大変動量と順位を元にポイントの変動量を決定します。 低いティアーのプレイヤーが高いティアーのプレイヤーに勝った場合にボーナスポイントを加算する仕組みやその逆のような仕組みはありません。 本来異なる強さのプレイヤー同士を対戦させて細工で工夫をするより、各ティアーに最低限ゲームプレイを成立させるだけのプレイヤーが滞留するようにティアーの閾値設計を行うことを推奨します。

ポイント

変動範囲

各ティアーごとに、最下位の時のポイント減算量、最上位の時のポイント加算量を設定できます。 中間の順位をとった場合には、報告された順位のパターン数で等分し、ポイントの加減算量を決定します。

参加料

ティアーによって、ゲームに参加する際にポイントを消費する必要があるように設定が可能です。 これによって、連戦連勝もティアーを上がるために必要となる条件を表現することが可能です。 参加料の支払いは、ゲーム結果をサーバーに報告するための投票用紙を取得する際に支払います。

ランクアップボーナス

ポイントを加算してランクアップした際にボーナスポイントを加算できます。 これによって昇格後にすぐに降格するようなチャタリングを防止することができます。

ユーザーデータ管理

GS2-SeasonRating はポイントとランクを管理しません。 実際のユーザーデータの管理は GS2-Experience を使用します。

シーズンのマスターデータとして、シーズンのポイントを管理する GS2-Experience の経験値モデルを指定し、 プロパティIDにシーズンモデルのIDを指定して、 経験値にポイント、ランクにどのティアーに属しているかを管理します。 つまり、ポイントの増減量は GS2-SeasonRating のマスターデータで管理しますが、ポイントによってランクを決定する閾値の管理や、プレイヤーがどのティアーに属しているかといったユーザーデータは GS2-Experience が管理します。 これによって、シーズン終了後に特定のランクであればアイテムがもらえるような交換処理に GS2-Experience が提供するランク値の検証機能のような高度な機能が利用できます。

graph LR
  SeasonModel["SeasonModel<br/>(GS2-SeasonRating)"] -- "experienceModelId" --> ExpModel["ExperienceModel<br/>(GS2-Experience)"]
  ExpModel -- "ランク閾値" --> Status["プレイヤーのランク状態<br/>(GS2-Experience に保存)"]
  Vote["投票結果確定"] -- "ポイント加減算" --> Status

マッチセッション

レーティング戦を行うには、まずは GS2-SeasonRating にマッチセッションリソースを作成する必要があります。 GS2-Matchmaking にはマッチメイキング成立時に、マッチメイキング成立したギャザリング名でマッチセッションを作成する連携機能があります。 特に理由がなければこの方法でマッチセッションを作成してください。

マッチセッションの有効期限

マッチセッションには有効期限を秒単位で、最大24時間の範囲で指定できます。 この期間内に結果の投票を行う必要があり、最初の投票から5分が経過して全ての投票が行われない場合はその段階で結果の集計が行われます。

結果の投票

マッチメイキングが完了したら、各プレイヤーはマッチセッションから投票用紙を取得します。 投票用紙を使用して、結果の投票を行います。 投票の内容には対戦に参加したプレイヤーのユーザーIDと順位のリストを渡します。

投票用紙の署名

Ballot を取得すると、SignedBallot として GS2 から署名された投票用紙が払い出されます。 署名は keyId に指定した GS2-Key で行われ、投票時にサーバー側で署名検証が行われるため、投票内容の改ざんを防止できます。

1試合複数投票(VoteMultiple)

シングルプレイヤー視点のゲーム(CPU との対戦結果を投票する想定)など、参加者全員分の投票用紙を1人のプレイヤーがまとめて取得して投票する必要があるケースでは VoteMultiple を使用します。 複数の SignedBallot を一括で送信できます。

投票結果の分断

サーバーが受け付けた投票の多数決を取ろうとした時に、結果が同数で最終的な結果を確定できない場合はレートの計算が行われません。 そのため、1vs1 のゲームでは正しいレート値を求めるのは困難です。 この問題を解決するためには、裏で対戦には直接関わらない3人目のプレイヤーをマッチメイキングし、そのプレイヤーに第三者視点で投票してもらうといった工夫が必要となります。

スクリプトトリガー

GS2-SeasonRating ではスクリプトトリガーを提供していません。

トランザクションアクション

GS2-SeasonRating ではトランザクションアクションを提供していません。 ポイントとランクの増減は内部的に GS2-Experience のトランザクションアクションを通じて反映されます。シーズン終了後に特定ランク以上のプレイヤーへ報酬を配布する場合は、GS2-Exchange の交換処理に GS2-Experience の VerifyRankAction を組み合わせるなどの実装が一般的です。

マスターデータ運用

マスターデータを登録することでマイクロサービスで利用可能なデータや振る舞いを設定できます。

マスターデータの種類には以下があります。

  • SeasonModel: シーズンに含まれるティアーと、紐付ける GS2-Experience の経験値モデル
  • TierModel: 各ティアーごとのポイント変動量・参加料・ランクアップボーナス

マスターデータの登録はマネージメントコンソールから登録する他、GitHubからデータを反映したり、GS2-Deployを使ってCIから登録するようなワークフローを組むことが可能です。

以下はマスターデータの JSON 例です。

{
  "version": "2023-04-05",
  "seasonModels": [
    {
      "name": "season-0001",
      "metadata": "シーズン1",
      "experienceModelId": "grn:gs2:{region}:{ownerId}:experience:experience-0001:model:season",
      "tiers": [
        {
          "metadata": "ブロンズ",
          "raiseRankBonus": 100,
          "entryFee": 0,
          "minimumChangePoint": 10,
          "maximumChangePoint": 30
        },
        {
          "metadata": "シルバー",
          "raiseRankBonus": 150,
          "entryFee": 10,
          "minimumChangePoint": 20,
          "maximumChangePoint": 40
        }
      ]
    }
  ]
}

実装例

現在のポイントやランクを取得

GS2-Experience のAPIを利用してステータスを取得してください。 「NamespaceName」「ExperienceName」 にはシーズンマスターデータに指定した値を 「PropertyId」 にはシーズンモデルIDを指定してください。

マッチセッションを作成

マッチセッションの作成はゲームエンジン用の SDK では処理できません。

GS2-Matchmaking の連携機能を使用してください。GS2-Matchmaking のネームスペース設定でマッチメイキング成立時のスクリプトに GS2-SeasonRating のセッション作成を呼び出すスクリプトを設定することで、マッチメイキング成立と同時に対応するセッションを自動生成できます。

投票用紙を取得

    var item = await gs2.SeasonRating.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Ballot(
        seasonName: "rating-0001",
        sessionName: "gathering-0001",
        numberOfPlayer: 4,
        keyId: "key-0001"
    ).ModelAsync();
    const auto Future = Gs2->SeasonRating->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        GameSession
    )->Ballot(
        "rating-0001", // seasonName
        "gathering-0001", // sessionName
        4, // numberOfPlayer
        "key-0001" // keyId
    )->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError())
    {
        return false;
    }
    const auto Result = Future->GetTask().Result();

投票を実行

    var result = await gs2.SeasonRating.Namespace(
        namespaceName: "namespace-0001"
    ).VoteAsync(
        ballotBody: "ballotBody",
        ballotSignature: "ballotSignature",
        gameResults: new List<Gs2.Unity.Gs2SeasonRating.Model.EzGameResult> {
            new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
                Rank = 1,
                UserId = "user-0001",
            },
            new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
                Rank = 2,
                UserId = "user-0002",
            },
            new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
                Rank = 2,
                UserId = "user-0003",
            },
            new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
                Rank = 3,
                UserId = "user-0004",
            },
        },
        keyId: "key-0001"
    );
    const auto Future = Gs2->SeasonRating->Namespace(
        "namespace-0001" // namespaceName
    )->Vote(
        "ballotBody", // ballotBody
        "ballotSignature", // ballotSignature
        []
        {
            auto v = MakeShared<TArray<TSharedPtr<Gs2::UE5::SeasonRating::Model::FEzGameResult>>>();
            v->Add(
                MakeShared<Gs2::UE5::SeasonRating::Model::FEzGameResult>()
                ->WithRank(TOptional<int32>(1))
                ->WithUserId(TOptional<FString>("user-0001"))
            );
            v->Add(
                MakeShared<Gs2::UE5::SeasonRating::Model::FEzGameResult>()
                ->WithRank(TOptional<int32>(2))
                ->WithUserId(TOptional<FString>("user-0002"))
            );
            v->Add(
                MakeShared<Gs2::UE5::SeasonRating::Model::FEzGameResult>()
                ->WithRank(TOptional<int32>(2))
                ->WithUserId(TOptional<FString>("user-0003"))
            );
            v->Add(
                MakeShared<Gs2::UE5::SeasonRating::Model::FEzGameResult>()
                ->WithRank(TOptional<int32>(3))
                ->WithUserId(TOptional<FString>("user-0004"))
            );
            return v;
        }(), // gameResults
        "key-0001" // keyId
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError())
    {
        return false;
    }

複数の投票用紙をまとめて投票

CPU 戦の結果や、参加者全員の投票用紙を1人のプレイヤーが代表して投じるケースで利用します。

    var result = await gs2.SeasonRating.Namespace(
        namespaceName: "namespace-0001"
    ).VoteMultipleAsync(
        signedBallots: new [] {
            signedBallot1,
            signedBallot2,
        },
        gameResults: new [] {
            new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
                Rank = 1,
                UserId = "user-0001",
            },
            new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
                Rank = 2,
                UserId = "user-0002",
            },
        },
        keyId: "key-0001"
    );
    const auto Future = Gs2->SeasonRating->Namespace(
        "namespace-0001" // namespaceName
    )->VoteMultiple(
        SignedBallots,
        GameResults,
        "key-0001" // keyId
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError())
    {
        return false;
    }

詳細なリファレンス