GS2-Distributor

トランザクション処理機能

GS2-Distributor は、GS2 の各マイクロサービスをまたいだトランザクション処理を実現するための中核となるサービスです。 プレイヤーが「アイテムを消費して報酬を得る」「スタミナを消費してクエストを開始する」といった、複数のマイクロサービスをまたいだ一連の処理を安全に実行するための基盤を提供します。

GS2 では、プレイヤーにとってデメリットとなる操作を《消費アクション》、メリットとなる操作を《入手アクション》と呼びます。
GS2-Distributor は、これらのアクションをまとめた《トランザクション》を受け取り、適切なマイクロサービスに転送・実行することで、ゲームサイクルを構成する全ての処理を一貫した形で扱えるようにします。

トランザクションの仕組みについて詳しくは トランザクション を参照してください。

トランザクションを構成するアクション

GS2 のトランザクションは、以下の3種類のアクションから構成されます。

graph LR
  Issue["トランザクション発行<br/>(ストア・クエスト・ミッション など)"] --> Verify["検証アクション"]
  Verify --> Consume["消費アクション"]
  Consume --> Acquire["入手アクション"]
  Acquire --> Done["完了"]

検証アクション (VerifyAction)

トランザクションの実行を開始する前に、消費アクションや入手アクションを実行できる状態にあるかを事前にチェックするアクションです。
例えば「特定のアイテムを所持していること」「ランクが一定値以上であること」「特定のクエストをクリア済みであること」などの条件を、消費を発生させる前に確認できます。

検証に失敗した場合、消費アクション・入手アクション は実行されません。

消費アクション (ConsumeAction)

プレイヤーにとってデメリットとなる処理を表すアクションです。
アイテムの消費、通貨の消費、スタミナの消費、回数制限カウンターの増加などが該当します。

消費アクションが実行されると、各マイクロサービスは「実行済みであること」を証明する署名を発行します。 この署名は次の入手アクションの実行時に検証され、消費アクションを全て通過していなければ入手アクションは実行できない仕組みです。

入手アクション (AcquireAction)

プレイヤーにとってメリットとなる処理を表すアクションです。
アイテムの入手、通貨の入手、経験値の入手、クエストの開始処理などが該当します。

入手アクションは、関連する全ての消費アクションが正常終了したことを示す署名が揃ったときにのみ実行されます。

トランザクションの構造

GS2 のトランザクションは「複数の消費アクション + 1つの入手アクション」という構造で発行されます。
トランザクションは GS2-Showcase / GS2-Quest / GS2-Mission など、各マイクロサービスのトランザクション発行 API の応答として受け取り、ゲームクライアントは取得したトランザクションを GS2-Distributor 経由で実行します。

なお、GS2 SDK にはトランザクションの実行を自動化する仕組みが組み込まれており、多くの場合、ゲーム側でトランザクションを意識的に実行する必要はありません。
発行 API の結果に含まれる TransactionDomainWaitAsync を呼び出すだけで、消費アクション・入手アクションが正しい順番で実行され、結果を取得できます。

DistributorModel

DistributorModel は、ネームスペース内に登録するリソース配布の設定単位です。

DistributorModel に対しては以下を設定できます。

  • inboxNamespaceId: 配布物がオーバーフローした場合の自動転送先となる GS2-Inbox のネームスペース GRN
  • whiteListTargetIds: トランザクション経由で実行を許可するアクションのホワイトリスト

複数の DistributorModel を用意することで、ストア用・クエスト用・ミッション用などの用途別に異なる転送ルールを使い分けることが可能です。

配布物のオーバーフロー処理

入手アクションを実行する際、プレイヤーの保有数上限を超過するなどの理由で「その場では受け取れない」状態となる場合があります。
このようなケースに備えて、DistributorModel の inboxNamespaceId を設定しておくと、受け取れなかった配布物を GS2-Inbox にメッセージとして自動転送し、プレイヤーが後から受け取れるように退避できます。

これにより、報酬を受け取れずに失われてしまう状況を回避できます。

graph TD
  Acquire["入手アクション実行"] --> Check{"プレイヤーに付与可能?"}
  Check -- Yes --> Granted["プレイヤーに付与"]
  Check -- No (上限超過など) --> Inbox["GS2-Inbox に転送"]
  Inbox --> Receive["プレイヤーは後から受信"]

バッチリクエスト

GS2-Distributor は、複数の GS2 API 呼び出しを1つのリクエストにまとめて実行する《バッチリクエスト》機能を提供します。

複数のサービスへの問い合わせを1回の API 呼び出しで完了させられるため、ネットワーク往復回数を削減してレスポンスタイムを改善できます。 ゲーム起動時に複数のマイクロサービスから初期データをまとめて取得するようなケースで特に有用です。

バッチリクエストに含められる各リクエストは、独立して処理され、それぞれに応答が返却されます。

マスターデータ管理

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

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

  • DistributorModel: トランザクションの転送ルールとオーバーフロー時の転送先

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

{
  "version": "2019-09-09",
  "distributorModels": [
    {
      "name": "default",
      "metadata": "default distributor",
      "inboxNamespaceId": "grn:gs2:{region}:{ownerId}:inbox:namespace-0001"
    }
  ]
}

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

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

GS2-Distributor は他のサービスから発行されたトランザクションを実行する側のサービスであり、自身は消費アクション・入手アクションを発行しません。

実装例

トランザクションの実行

GS2 SDK では、各サービスのトランザクション発行 API の応答 (TransactionDomain) に対して WaitAsync を呼び出すことで、自動的にトランザクションが GS2-Distributor 経由で実行されます。
以下は GS2-Mission の報酬受け取り処理を例にした、トランザクションの実行例です。

    var transaction = await gs2.Mission.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Complete(
        missionGroupName: "group-0001"
    ).ReceiveRewardsAsync(
        missionTaskName: "task-0001"
    );

    await transaction.WaitAsync();
    const auto Future = Gs2->Mission->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        GameSession
    )->Complete(
        "group-0001" // missionGroupName
    )->ReceiveRewards(
        "task-0001" // missionTaskName
    );
    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;

バッチリクエストの実行

複数の GS2 API 呼び出しを1度の通信でまとめて実行します。

    var result = await gs2.Distributor.Namespace(
        namespaceName: "namespace-0001"
    ).BatchExecuteApiAsync(
        requestPayloads: new [] {
            new Gs2.Unity.Gs2Distributor.Model.EzBatchRequestPayload
            {
                RequestId = "1",
                Service = "inventory",
                MethodName = "describe_inventories",
                Parameter = "{\"namespaceName\":\"namespace-0001\"}",
            },
            new Gs2.Unity.Gs2Distributor.Model.EzBatchRequestPayload
            {
                RequestId = "2",
                Service = "experience",
                MethodName = "describe_statuses",
                Parameter = "{\"namespaceName\":\"namespace-0001\"}",
            },
        }
    );
    const auto Future = Gs2->Distributor->Namespace(
        "namespace-0001" // namespaceName
    )->BatchExecuteApi(
        []
        {
            const auto v = MakeShared<TArray<Gs2::UE5::Distributor::Model::FEzBatchRequestPayloadPtr>>();
            v->Add(MakeShared<Gs2::UE5::Distributor::Model::FEzBatchRequestPayload>()
                ->WithRequestId(TOptional<FString>("1"))
                ->WithService(TOptional<FString>("inventory"))
                ->WithMethodName(TOptional<FString>("describe_inventories"))
                ->WithParameter(TOptional<FString>("{\"namespaceName\":\"namespace-0001\"}")));
            v->Add(MakeShared<Gs2::UE5::Distributor::Model::FEzBatchRequestPayload>()
                ->WithRequestId(TOptional<FString>("2"))
                ->WithService(TOptional<FString>("experience"))
                ->WithMethodName(TOptional<FString>("describe_statuses"))
                ->WithParameter(TOptional<FString>("{\"namespaceName\":\"namespace-0001\"}")));
            return v;
        }()
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

マスターデータの凍結 (Freeze)

トランザクションが発行された後にマスターデータが更新されると、発行済みのトランザクションと現在のマスターデータの間に矛盾が生じる可能性があります。
GS2-Distributor の FreezeMasterData を呼び出すことで、ログイン中のプレイヤーに対して使用するマスターデータのバージョンを固定し、ゲームプレイ中のマスターデータ切り替えによる不整合を回避できます。

    await gs2.Distributor.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).FreezeMasterDataAsync(
    );
    // Unreal Engine 向け SDK には FreezeMasterData は提供されていません。
    // マネジメントコンソール / GS2 CLI / 各種言語向け一般 SDK (C#/Go/Python/TypeScript/PHP/Java) を利用してください。

詳細なリファレンス