GS2-Experience

経験値・ランク機能

Game as a Service において、成長要素は不可欠です。 キャラクターの成長をおこない、成長したキャラクターでさらに高難度のコンテンツに挑むゲームサイクルを実装するための経験値・ランク機能を提供します。

GS2-Experience は単一のキャラクターのレベルだけでなく、複数のプロパティ(キャラクター、武器、パーティ、ギルドなど)の成長を 1 つのマイクロサービスで一元管理できる柔軟な設計となっています。

ユースケース

GS2-Experience が想定する代表的なユースケースは以下の通りです。

  • プレイヤーキャラクターのレベル管理
  • 育成可能な装備品のレベル管理
  • ギルドやチームのランク管理
  • 称号・バトルパスのレベル管理
  • スキルツリーの各スキル熟練度管理
graph LR
  Player[プレイヤー] -->|経験値加算| Exp[GS2-Experience]
  Exp -->|ランク計算| Status[Status]
  Status -->|閾値超え| RankUp[ランクアップ]
  RankUp -->|報酬付与| Inventory[GS2-Inventory]
  RankUp -->|スタミナ最大値更新| Stamina[GS2-Stamina]

ランク

GS2-Experience は経験値の値から自動的にランクの値を計算します。 そのためには、マスターデータを利用してランクアップの閾値となる経験値テーブルを定義する必要があります。 Experience Model でランクアップ閾値を管理し、その下に任意の値を持つプロパティIDをぶら下げることができます。 プロパティIDごとに経験値の値は管理され、経験値の値に基づいてランクが決定します。

Experience Model とプロパティIDの関係

graph TD
  N[Namespace] --> EM1["ExperienceModel<br/>character_ssr"]
  N --> EM2["ExperienceModel<br/>character_sr"]
  N --> EM3["ExperienceModel<br/>weapon"]
  EM1 --> P11["プロパティID:<br/>character-001"]
  EM1 --> P12["プロパティID:<br/>character-002"]
  EM3 --> P31["プロパティID:<br/>weapon-001"]
  EM3 --> P32["プロパティID:<br/>weapon-002"]

たとえば「キャラクターの種類ごとに必要経験値テーブルを変えたい」「キャラクター個別にランクキャップを変えたい」といったケースで、ExperienceModel をキャラクター種別ごとに用意し、プロパティID にキャラクターの個別 ID を割り当てることで実現できます。

ランクキャップ

ランクには上限を設定可能です。 ランクの上限に達した状態で経験値を入手した場合、経験値は破棄されます。

ランクの上限は Experience Model で初期値を設定しますが、プロパティIDごとに個別に引き上げることが可能です。 これにより限界突破の強化をおこなうと、経験値の上限を引き上げるような仕様を実現することが可能です。

設定項目 説明
defaultExperience 初期経験値
defaultRankCap 初期ランクキャップ
maxRankCap ランクキャップの上限値(これを超える SetRankCap は拒否される)
rankThreshold ランクアップに必要な経験値閾値の配列

ステータス情報

Status はユーザー × ExperienceModel × プロパティID の組み合わせで一意に決まり、以下の情報を保持します。

属性 説明
experienceValue 現在の経験値
rankValue 現在のランク
rankCapValue 現在のランクキャップ
nextRankUpExperienceValue 次のランクアップに必要な経験値

nextRankUpExperienceValue をクライアントで参照することで、ランクアップまでに必要な経験値の表示や進捗バーの描画を簡単に行えます。

報酬加算テーブル

Experience Model にはランクに応じて報酬量を調整する acquireActionRates を設定できます。 倍率には通常の rates に加えて、int64 を超える値を扱える bigRates も定義でき、インフレの激しいゲームでも精度を保ったまま報酬量を制御できます。

たとえば、「ランクが高いプレイヤーには高難度クエストの報酬を多く付与する」といった調整に活用できます。 クエスト報酬としてランクに応じた倍率を入手アクションに反映する場合、GS2-Quest のクリア報酬に「ランクに応じた報酬の倍率適用」をトランザクションアクションとして組み込みます。

スクリプトトリガー

ネームスペースに各種スクリプトを設定すると、経験値変化・ランク変化・ランクキャップ変化の前後でカスタムスクリプトを呼び出せます。スクリプトは同期・非同期の実行方式を選択でき、非同期では GS2-Script や Amazon EventBridge を介した外部処理にも対応します。

設定できる主なイベントトリガーとスクリプト設定名は以下の通りです。

  • changeExperienceScript(完了通知: changeExperienceDone): 経験値変化の前後
  • changeRankScript(完了通知: changeRankDone): ランク変化の前後
  • changeRankCapScript(完了通知: changeRankCapDone): ランクキャップ変更の前後

また、以下のフィールドにはスクリプトの GRN を直接指定でき、対応するイベント発生時にスクリプトが呼び出されます。

  • rankCapScriptId: ランクキャップ取得時の処理
  • overflowExperienceScript: ランクキャップ到達後の余剰経験値(あふれ経験値)の処理

overflowExperienceScript を利用すると、ランクキャップに達したあとの余剰経験値を、たとえばゲーム内通貨として GS2-Inventory に変換するといった処理を自動化できます。

マスターデータ運用

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

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

  • ExperienceModel: ランク閾値と報酬加算テーブル

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

ExperienceModel の JSON 例

{
  "version": "2019-09-04",
  "experienceModels": [
    {
      "name": "character_ssr",
      "metadata": "SSRキャラクター",
      "defaultExperience": 0,
      "defaultRankCap": 50,
      "maxRankCap": 99,
      "rankThreshold": {
        "metadata": "SSRランクテーブル",
        "values": [
          100, 300, 600, 1000, 1500
        ]
      },
      "acquireActionRates": [
        {
          "name": "boost",
          "mode": "double",
          "rates": [1.0, 1.1, 1.2, 1.3, 1.4]
        }
      ]
    }
  ]
}

バフによる補正

GS2-Buff と連携すると、ステータスの rankCapValueAddExperienceByUserIdSubExperienceByUserId などのアクションの experienceValue をバフで補正できます。イベントやキャンペーン時に取得量やランク上限を一時的に増減させるといった調整が柔軟に行えます。

代表的なバフ対象は以下の通りです。

種別 対象 補正できる値
モデル Status rankCapValue
アクション AddExperienceByUserId experienceValue
アクション SubExperienceByUserId experienceValue
アクション AddRankCapByUserId rankCapValue

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

GS2-Experience では以下のトランザクションアクションを提供しています。

  • 検証アクション: ランクの検証、ランクキャップの検証
  • 消費アクション: 経験値の減算、ランクキャップの減算
  • 入手アクション: 経験値の加算、経験値の設定、ランクキャップの加算、ランクキャップの設定、ランクに応じた報酬の倍率適用

「経験値の加算」を入手アクションとして利用することで、ショップでの商品購入時やミッション達成時の報酬として、直接キャラクターの経験値を増やしてランクアップさせるといった処理が可能になります。また、「ランクキャップの加算」を報酬として設定することで、特定の条件を達成した際に自動的にレベル上限を解放するといった運用も可能です。

「ランクの検証」を検証アクションとして利用することで、「ランク10以上のキャラクターのみがチャレンジできるクエスト」や「ランク50以上で交換可能な強化素材」といったゲーム仕様をマスターデータの設定だけで実現できます。

実装例

経験値の加算

経験値の加算はゲームエンジン用の SDK では処理できません。

GS2-Quest のクエストのクリア報酬や、GS2-Enhance の強化報酬として経験値を加算するようにしてください。

ランクキャップの引き上げ

ランクキャップの引き上げはゲームエンジン用の SDK では処理できません。

GS2-Exchange で強化素材や、同一キャラクターとの交換の報酬としてランクキャップを引き上げるようにしてください。

経験値の一覧を取得

プレイヤーが保持するすべての Status を一括で取得します。 ホーム画面のステータス表示やキャラクター一覧画面で活用できます。

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

経験値の取得

    var item = await gs2.Experience.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        experienceName: "character_ssr",
        propertyId: "property-0001"
    ).ModelAsync();
    const auto Domain = Gs2->Experience->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Status(
        "character_ssr", // experienceName
        "property-0001" // propertyId
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Item = Future->GetTask().Result();

ExperienceModel の一覧取得

クライアントでランクアップに必要な経験値テーブルを参照する場合に利用します。

    var items = await gs2.Experience.Namespace(
        namespaceName: "namespace-0001"
    ).ExperienceModelsAsync(
    ).ToListAsync();
    const auto It = Gs2->Experience->Namespace(
        "namespace-0001" // namespaceName
    )->ExperienceModels();
    TArray<Gs2::UE5::Experience::Model::FEzExperienceModelPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }

特定の ExperienceModel の取得

    var item = await gs2.Experience.Namespace(
        namespaceName: "namespace-0001"
    ).ExperienceModel(
        experienceName: "character_ssr"
    ).ModelAsync();
    const auto Domain = Gs2->Experience->Namespace(
        "namespace-0001" // namespaceName
    )->ExperienceModel(
        "character_ssr" // experienceName
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Item = Future->GetTask().Result();

所持証明署名の取得

GS2 内の他のマイクロサービスと連携する際に、本当に GS2-Experience でランクや経験値が正しいか保証したデータを求められることがあります。

たとえば、GS2-Experience でプレイヤーのランクを管理しており、GS2-Stamina でそのランクに応じてスタミナの最大値を決定するとします。 GS2-Stamina は所持証明署名とあわせてランクを指定するよう要求します。

これによって、GS2-Stamina は裏で GS2-Experience と通信して、本当にそのランクに達しているかを判断する必要がなくなります。

sequenceDiagram
  participant Player as プレイヤー
  participant Exp as GS2-Experience
  participant Other as 他のマイクロサービス
  Player->>Exp: GetStatusWithSignature
  Exp-->>Player: Body + Signature
  Player->>Other: API + Body + Signature
  Other->>Other: 署名検証
  Other-->>Player: 処理結果
    var result = await gs2.Experience.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        experienceName: "character_ssr",
        propertyId: "property-0001"
    ).GetStatusWithSignatureAsync(
        keyId: "grn:gs2:{region}:{yourOwnerId}:key:namespace-0001:key:key-0001"
    );

    var body = result.Body;
    var signature = result.Signature;
    const auto Domain = Gs2->Experience->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Status(
        "character_ssr", // experienceName
        "property-0001" // propertyId
    );
    const auto Future = Domain->GetStatusWithSignature(
        "key-0001"
    );
    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();
    const auto Body = Result->Body;
    const auto Signature = Result->Signature;

詳細なリファレンス