GS2-Exchange

ゲーム内リソースの交換機能

GS2 が提供するマイクロサービスの中でも特に重用される機能です。 あらゆるマイクロサービスのリソースを、全く異なるマイクロサービスのリソースに変換する役割を担います。

ゲームの仕様にはリソースの交換に関するものが多数存在し、その度に GS2-Exchange の出番が発生します。

リソース交換の例

強化素材の変換

GS2-Inventory で管理する ★1 の強化素材 10個を、同じく GS2-Inventory で管理する ★2 の強化素材 1個と交換

6時間に1回アイテムを入手できる

GS2-Stamina で管理する6時間で1回復するスタミナ1を、GS2-Inventory で管理するアイテムと交換

アイテムを売却

GS2-Inventory で管理するアイテムを、同じく GS2-Inventory で管理するゲーム内通貨と交換

交換の種類

GS2-Exchange はマスターデータで定められた交換レートで直ちに交換できるダイレクト交換と、 交換を実行した後、現実時間で一定時間経過したのちに交換結果を得られる非同期交換、 購入する度にコストが上昇するコスト上昇型交換の3つのモードが存在します。

flowchart LR
  Verify["対価検証<br/>(verifyActions)"] --> Consume["対価消費<br/>(consumeActions)"]
  Consume -->|timingType=direct| Acquire["報酬付与<br/>(acquireActions)"]
  Consume -->|timingType=await| Await["Await オブジェクト作成"]
  Await -. lockTime 経過 .-> Acquire2["AcquireAsync で報酬受取"]

交換の挙動はネームスペース設定の enableDirectExchange / enableAwaitExchange でモードごとに有効化できます。 レートモデルの timingTypedirect または await に切り替えることで個別のレートが即時交換と非同期交換のいずれで動作するかを選択できます。

非同期交換の挙動

非同期交換を使用した場合、交換を実行したタイミングで対価は消費され、 報酬を得る代わりに Await オブジェクトが作成されます。 Await オブジェクトの作成時刻からマスターデータで定義された交換待機時間(lockTime 秒)が経過すると報酬を受け取ることができます。

状態フィールド
交換実行時刻exchangedAt
報酬受取可能時刻acquirableAt
対価として消費される数量count
スキップで短縮された秒数skipSeconds

拠点の強化

街づくり系のゲームで、拠点を成長させるために資源を消費した後 8時間経過することで実際に拠点の経験値を加算する

遠征

パーティを編成し、冒険にでたのち3時間後に冒険の結果として報酬を受け取れる

非同期交換のスキップ

非同期交換の時間経過待ちを、更なる対価を支払うことでスキップできるようにすることができます。 一般的にこのような仕様を入れる場合は、マネタイズのための機能として実装されることが多いですが、 現金で購入した GS2-Money で管理するゲーム内通貨を消費することで、待ち時間を時短可能な仕様を実現できます。

待機報酬の強制取得と削除

管理画面や API(トランザクションアクション)を通じて、待機時間を無視して報酬を強制的に取得したり、実行中の非同期交換(Await オブジェクト)を削除してキャンセルしたりすることが可能です。

コスト上昇型交換の例

強化(インフレゲーム)

強化する度に強化に必要なゴールドの消費量が増加する

スタミナ回復コストが増加

スタミナを購入する度に購入するために必要な課金通貨の消費量が上昇する。 購入回数は毎日リセットされる

コスト上昇量の計算

コストの上昇量には3つのモードが存在し、IncrementalRateModelcalculateType で指定します。

linear

baseValue + (coefficientValue * 交換回数)

baseValue = 100, coefficientValue = 50

交換回数コスト
0100
1150
2200
3250
4300

power

coefficientValue * (交換回数 + 1) ^ 2

coefficientValue = 50

交換回数コスト
050
1200
2450
3800
41250

gs2_script

GS2-Script の実行結果をもとに算出します。 複雑な条件を元にコストを計算したい場合に使用できます。

currentExchangeCount = args.currentExchangeCount
quantity = quantity

cost = 100
for i = 1 , quantity do
	cost = cost + (i + currentExchangeCount - 1) * 50
end

result = {
    cost=cost
}
交換回数コスト
0100
1150
2200
3250
4300

交換回数の管理

IncrementalRateModel では exchangeCountIdmaximumExchangeCount を指定して、GS2-Limit の回数制限モデルと連携し交換実行回数を追跡したり、特定期間内の交換上限を設けることができます。

exchangeCountId に GS2-Limit の Counter モデルを指定すると、交換回数が GS2-Limit でカウントされ、日次・週次など GS2-Limit のリセット仕様に従って交換回数を初期化できます。 maximumExchangeCount を超える回数の交換は拒否されるため、期間内の購入上限を設けたガチャや、1日に N 回まで購入可能なアイテムショップなどを実現できます。

マスターデータ運用

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

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

  • RateModel: 即時または非同期交換のレート定義
  • IncrementalRateModel: 交換回数に応じてコストが増加するレート定義

RateModel の主なフィールド

フィールド説明
nameレート名(一意)
verifyActions交換実行前に行う検証アクション
consumeActions対価として消費するアクション
acquireActions報酬として付与するアクション
timingTypedirect(即時交換)/ await(非同期交換)
lockTime非同期交換の待機秒数

IncrementalRateModel の主なフィールド

フィールド説明
nameレート名(一意)
consumeAction対価として消費するアクション(数量はコスト計算で決定)
acquireActions報酬として付与するアクション
calculateTypeコスト計算方式(linear / power / gs2_script
baseValue / coefficientValueコスト計算で使用する係数
calculateScriptIdgs2_script 時に呼び出すスクリプト
exchangeCountId交換回数を管理する GS2-Limit のカウンタ
maximumExchangeCount交換回数の上限

マスターデータの JSON 例

{
  "version": "2019-08-19",
  "rateModels": [
    {
      "name": "material_n_to_r",
      "metadata": "N -> R 強化素材交換",
      "consumeActions": [
        {
          "action": "Gs2Inventory:ConsumeItemSetByUserId",
          "request": "{\"namespaceName\":\"inventory-0001\",\"inventoryName\":\"material\",\"itemName\":\"n-material\",\"userId\":\"#{userId}\",\"consumeCount\":10}"
        }
      ],
      "timingType": "await",
      "lockTime": 3600,
      "acquireActions": [
        {
          "action": "Gs2Inventory:AcquireItemSetByUserId",
          "request": "{\"namespaceName\":\"inventory-0001\",\"inventoryName\":\"material\",\"itemName\":\"r-material\",\"userId\":\"#{userId}\",\"acquireCount\":1}"
        }
      ]
    }
  ]
}

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

スクリプトトリガー

ネームスペースに以下のスクリプト設定を追加すると、交換処理や待機報酬受取の前後でカスタムスクリプトを実行できます。

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

  • exchangeScript(完了通知: exchangeDone): 交換処理の前後
  • incrementalExchangeScript(完了通知: incrementalExchangeDone): コスト上昇型交換の前後
  • acquireAwaitScript(完了通知: acquireAwaitDone): 待機報酬受取の前後

これらのスクリプトは同期・非同期の実行方式を選択でき、非同期では GS2-Script や Amazon EventBridge を介した外部連携にも対応します。

なお、IncrementalRateModel でコスト計算方式 gs2_script を選択した場合は、レート単位で設定した calculateScriptId を呼び出してコストを算出します。

バフによる補正

GS2-Buff と連携すると、RateModellockTimeacquireActionsverifyActionsconsumeActionsIncrementalRateModelacquireActionsconsumeActionmaximumExchangeCount をコンテキストスタック経由で動的に上書きでき、イベントやキャンペーンに応じて報酬や待機時間、交換可能回数を柔軟に調整できます。

たとえば、期間限定で「強化素材交換の待機時間を半減」「ガチャの1日購入上限を増加」といった施策を、マスターデータを書き換えずにバフの適用だけで実現できます。

実装例

交換の実行(ダイレクト)

config パラメータには、交換に紐づく対価・報酬アクションのトランザクションで使用するコンテキスト値を渡すことができます。

    var result = await gs2.Exchange.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Exchange(
    ).ExchangeAsync(
        rateName: "rate-0001",
        count: 1
    );
    const auto Domain = Gs2->Exchange->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Exchange(
    );
    const auto Future = Domain->Exchange(
        "rate-0001", // rateName
        1, // count
        nullptr // config
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

非同期交換の開始

timingType=await のレートに対して ExchangeAsync を呼び出すと、対価は即座に消費され、報酬は Await オブジェクトに保留されます。

    var result = await gs2.Exchange.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Exchange(
    ).ExchangeAsync(
        rateName: "material_n_to_r",
        count: 1
    );
    const auto Domain = Gs2->Exchange->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Exchange(
    );
    const auto Future = Domain->Exchange(
        "material_n_to_r", // rateName
        1, // count
        nullptr // config
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

Await の一覧取得

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

Await 待機時間経過後の報酬受け取り

待機時間が経過した Await から報酬を受け取ります。 受け取り処理ではトランザクションが発行され、acquireActions に定義された報酬付与処理が他のマイクロサービスに対して実行されます。

    var result = await gs2.Exchange.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Await(
        rateName: "material_n_to_r",
        awaitName: "await-0001"
    ).AcquireAsync(
    );
    const auto Domain = Gs2->Exchange->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Await(
        "await-0001", // awaitName
        "material_n_to_r" // rateName
    );
    const auto Future = Domain->Acquire(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

Await の削除(キャンセル)

待機中の Await を破棄して交換をキャンセルします。 対価として消費したリソースは返却されない点に注意してください。返却が必要な場合はトランザクションアクションを利用したサーバー側スクリプトで補填してください。

    var result = await gs2.Exchange.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Await(
        rateName: "material_n_to_r",
        awaitName: "await-0001"
    ).DeleteAwaitAsync(
    );
    const auto Domain = Gs2->Exchange->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Await(
        "await-0001", // awaitName
        "material_n_to_r" // rateName
    );
    const auto Future = Domain->DeleteAwait(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

待機時間のスキップ

待機時間を追加コストの支払いでスキップする場合は、skipByConfig を用いてスキップに必要なコストを設定したうえで、サーバーサイド(GS2-JobQueue や GS2-Script 経由)の API を呼び出します。 スキップ処理はトランザクションアクションとしても利用できるため、ショップでのスキップアイテムやスキップチケットといった商品設計と組み合わせることができます。

コスト上昇型交換の実行

    var result = await gs2.Exchange.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Exchange(
    ).IncrementalExchangeAsync(
        rateName: "rate-0001",
        count: 1
    );
    const auto Domain = Gs2->Exchange->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Exchange(
    );
    const auto Future = Domain->IncrementalExchange(
        "rate-0001", // rateName
        1, // count
        nullptr // config
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

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

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

  • 消費アクション: 実行中の交換待機(Await)の削除
  • 入手アクション: 直ちに交換(Exchange)を実行、コスト上昇型交換(IncrementalExchange)を実行、交換待機(Await)の作成、待機中報酬の強制取得、待機時間のスキップ

「報酬の強制取得」を入手アクションとして利用することで、特定のアイテムを入手した際やミッション達成時の報酬として、現在進行中の建築や遠征(非同期交換)を即座に完了させるといった処理が可能になります。また、「交換待機の削除」を消費アクションとして利用することで、進行中のプロセスを中断(キャンセル)させるといった運用も可能です。

詳細なリファレンス