GS2-Formation

パーティ・装備編成機能

所有しているリソースを組み合わせて1つの何かを編成するという仕様は一般的です。 複数のキャラクターを編成してパーティを作ったり、武器・防具といったアイテムを編成して装備するといったものです。

GS2-Formation は「どんなスロットが存在するか」「各スロットに何を装着できるか」「同じ種類の編成をいくつまで保持できるか」をマスターデータで定義し、プレイヤーごとの編成内容を管理するマイクロサービスです。

用語

graph LR
  MoldModel --> FormModel
  FormModel --> SlotModel
  PropertyFormModel --> SlotModel2[SlotModel]

  Mold --> Form
  Form --> Slot
  PropertyForm --> Slot2[Slot]
用語 説明
MoldModel 同種の編成(パーティ・装備セットなど)を複数保有するための型。容量(保存枠)の概念を持つ
FormModel Mold に紐づく Form の構成定義。どのようなスロットを持つかを定義する
PropertyFormModel プロパティIDで参照する単独の編成定義。Mold を介さず1キャラクター1編成のような形態で使う
SlotModel スロットの定義。スロットに装着できるプロパティを正規表現で制限できる
Mold プレイヤーごとの Mold 実体。capacity を持ち最大容量まで Form を作成可能
Form Mold 内の編成インスタンス。index で識別される
PropertyForm プロパティID単位の編成インスタンス
Slot プレイヤーが実際に装着しているリソースを保持する

フォーム

キャラクターの装備機能を実現するにあたって、武器・兜・籠手・胴・脚・足といった複数のスロットを用意し、 各スロットに適合するアイテムのみを装備できるように設定が可能です。

Form Model では、どのようなスロットが存在するのか、各スロットにはどのようなアイテムを編成できるのかをマスターデータとして定義します。

Mold と PropertyForm の使い分け

flowchart LR
  subgraph Mold
    M1["Form #0"]
    M2["Form #1"]
    M3["Form #2 (空き枠)"]
  end
  subgraph PropertyForm
    P1["character-001 の編成"]
    P2["character-002 の編成"]
  end
  • Mold を使うケース: パーティ編成1〜N のように、編成セットを番号(index)で管理し、最大保有数(capacity)を成長要素として拡張したい場合
  • PropertyForm を使うケース: キャラクターごと・装備セットごとなど、外部のリソースID(propertyId)に1対1で結びつく編成

MoldMoldModelinitialMaxCapacity(初期容量)と maxCapacity(拡張上限)を持ち、プレイヤーの成長やマネタイズ施策に応じてキャパシティを増減できます。

スロットへの装着対象

Form / PropertyForm の Slot には、以下のリソースを装着できます。装着時には GS2 が発行する「所有証明署名」を SlotWithSignature として渡します。

propertyType 装着対象
gs2_inventory GS2-Inventory で管理する ItemSet
gs2_simple_inventory GS2-Inventory(Simple)で管理する SimpleItem
gs2_dictionary GS2-Dictionary で管理する Entry

スロットモデル(SlotModel)の propertyRegex に正規表現を設定することで、特定のスロットに装着可能なプロパティIDを制限できます。たとえば「武器スロットには weapon-* の Item のみ装着可能」といった制限を実現できます。

マスターデータ運用

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

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

  • MoldModel: Mold の保存枠構成
  • FormModel: Mold 配下の Form のスロット構成
  • PropertyFormModel: PropertyForm のスロット構成

マスターデータの JSON 例

{
  "version": "2019-09-09",
  "moldModels": [
    {
      "name": "party",
      "metadata": "パーティ編成",
      "initialMaxCapacity": 3,
      "maxCapacity": 10,
      "formModel": {
        "name": "party",
        "slots": [
          { "name": "leader", "propertyRegex": "character-.*" },
          { "name": "member-1", "propertyRegex": "character-.*" },
          { "name": "member-2", "propertyRegex": "character-.*" }
        ]
      }
    }
  ],
  "propertyFormModels": [
    {
      "name": "equipment",
      "metadata": "キャラクターの装備",
      "slots": [
        { "name": "weapon", "propertyRegex": "weapon-.*" },
        { "name": "armor",  "propertyRegex": "armor-.*" }
      ]
    }
  ]
}

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

スクリプトトリガー

ネームスペースに updateMoldScript updateFormScript updatePropertyFormScript を設定すると、編成データの更新前後でカスタムスクリプトを実行できます。 スクリプトは同期・非同期の実行方式を選択でき、非同期では GS2-Script や Amazon EventBridge を介した外部連携にも対応します。

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

  • updateMoldScript(完了通知: updateMoldDone): Mold 更新の前後
  • updateFormScript(完了通知: updateFormDone): Form 更新の前後
  • updatePropertyFormScript(完了通知: updatePropertyFormDone): PropertyForm 更新の前後

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

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

  • 検証アクション: キャパシティ(保存数量上限)の検証
  • 消費アクション: キャパシティの減算
  • 入手アクション: キャパシティの加算、キャパシティの設定、編成内容(Form / PropertyForm)の設定、編成されているリソースに対する入手アクションの適用

「編成されているリソースに対する入手アクションの適用」を入手アクションとして利用することで、特定のパーティスロットに編成されているキャラクターに対して直接経験値を加算するといった処理が可能になります。また、「キャパシティの加算」を報酬として設定することで、特定のミッション達成時に編成枠を自動的に拡張するといった運用も可能です。

実装例

Mold での編成の永続化

Form の Slot には GS2-Inventory で管理する ItemSet / SimpleItem か、GS2-Dictionary で管理する Entry を登録できます。 それぞれ設定するためには、所有証明署名 を付加する必要があります。

所有証明署名 の取得方法については各サービスの説明を確認してください。

propertyType の種類

  • gs2_inventory – GS2-Inventory で管理する ItemSet を装着
  • gs2_simple_inventory – GS2-Inventory で管理する SimpleItem を装着
  • gs2_dictionary – GS2-Dictionary で管理する Entry を装着
    var result = await gs2.Formation.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Mold(
        moldName: "mold-0001"
    ).Form(
        index: 0
    ).SetFormAsync(
        slots: new [] {
            new Gs2.Unity.Gs2Formation.Model.EzSlotWithSignature
            {
                Name = "slot-0001",
                PropertyType = "gs2_dictionary",
                Body = "body",
                Signature = "signature",
            },
        },
        keyId: "key-0001"
    );
    const auto Domain = Gs2->Formation->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Mold(
        "mold-0001" // moldName
    )->Form(
        0 // index
    );
    const auto Future = Domain->SetForm(
        []
        {
            const auto v = MakeShared<TArray<TSharedPtr<Gs2::Formation::Model::FSlotWithSignature>>>();
            v->Add({'name': 'slot-0001', 'propertyType': 'gs2_dictionary', 'body': 'body', 'signature': 'signature'});
            return v;
        }(),
        "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();

Mold の キャパシティの引き上げ

キャパシティ(保存数量上限)の引き上げはゲームエンジン用の SDK では直接処理できません。 クライアント主導でのキャパシティ操作を許可すると不正改ざんの対象になり得るため、容量変更はトランザクションアクション経由で実施する設計になっています。

GS2-Exchange で課金通貨との交換などの報酬としてキャパシティを引き上げるようにしてください。 GS2-Exchange の acquireActions に Formation のキャパシティ加算アクションを設定することで、課金アイテムやミッション報酬の付与の延長としてキャパシティ拡張を実現できます。

Mold 情報の取得

Mold 単体の情報(現在のキャパシティ)を取得します。

    var item = await gs2.Formation.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Mold(
        moldName: "mold-0001"
    ).ModelAsync();
    const auto Domain = Gs2->Formation->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Mold(
        "mold-0001" // moldName
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Item = Future->GetTask().Result();

Mold での編成内容の一覧を取得

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

Mold での編成内容を取得

    var item = await gs2.Formation.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Mold(
        moldName: "mold-0001"
    ).Form(
        index: 0
    ).ModelAsync();
    const auto Domain = Gs2->Formation->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Mold(
        "mold-0001" // moldName
    )->Form(
        0 // index
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Item = Future->GetTask().Result();

Form の削除

不要になった編成内容を削除します。Mold のキャパシティ自体は維持され、index のスロットが空きとして再利用可能になります。

    var result = await gs2.Formation.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Mold(
        moldName: "mold-0001"
    ).Form(
        index: 0
    ).DeleteFormAsync(
    );
    const auto Future = Gs2->Formation->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Mold(
        "mold-0001" // moldName
    )->Form(
        0 // index
    )->DeleteForm(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

Property Form での編成の永続化

Form の Slot には GS2-Inventory で管理する ItemSet か、GS2-Dictionary で管理する Entry を登録できます。 それぞれ設定するためには、所有証明署名 を付加する必要があります。

所有証明署名 の取得方法については各サービスの説明を確認してください。

    var result = await gs2.Formation.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).PropertyForm(
        formModelName: "form-0001",
        propertyId: "property-0001"
    ).SetPropertyFormAsync(
        slots: new [] {
            new Gs2.Unity.Gs2Formation.Model.EzSlotWithSignature
            {
                Name = "slot-0001",
                PropertyType = "gs2_dictionary",
                Body = "body",
                Signature = "signature",
            },
        },
        keyId: "key-0001"
    );
    const auto Domain = Gs2->Formation->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->PropertyForm(
        "form-0001", // formModelName
        "property-0001" // propertyId
    );
    const auto Future = Domain->SetPropertyForm(
        []
        {
            const auto v = MakeShared<TArray<TSharedPtr<Gs2::Formation::Model::FSlotWithSignature>>>();
            v->Add({'name': 'slot-0001', 'propertyType': 'gs2_dictionary', 'body': 'body', 'signature': 'signature'});
            return v;
        }(),
        "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();

Property Form での編成内容を取得

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

Property Form の削除

    var result = await gs2.Formation.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).PropertyForm(
        formModelName: "form-0001",
        propertyId: "property-0001"
    ).DeletePropertyFormAsync(
    );
    const auto Future = Gs2->Formation->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->PropertyForm(
        "form-0001", // formModelName
        "property-0001" // propertyId
    )->DeletePropertyForm(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

編成情報への署名取得

Form / PropertyForm の内容を改ざんされていないことを保証した形で他システムへ受け渡したい場合、署名付きの編成情報を取得できます。 バトル開始時にサーバーへ編成内容を送るユースケースなどで利用できます。

    var domain = await gs2.Formation.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Mold(
        moldName: "mold-0001"
    ).Form(
        index: 0
    ).GetFormWithSignatureAsync(
        keyId: "key-0001"
    );
    const auto Future = Gs2->Formation->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Mold(
        "mold-0001" // moldName
    )->Form(
        0 // index
    )->GetFormWithSignature(
        "key-0001" // keyId
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

編成リソースの管理と注意事項

プロパティの指定方法

フォームのスロットに編成するプロパティはプロパティIDで指定します。 スロットモデルの propertyRegex でプロパティIDの正規表現を指定することで、そのスロットに編成できる値を限定することができます。

所有証明署名と編成後の注意

GS2-Inventory で管理するアイテムをスロットに設定する場合、GS2-Inventory が発行する署名付きアイテムセット(所有証明署名)を使って編成対象に指定できます。 署名付きアイテムセットが保証する「所有していること」は編成時点での保証であり、編成後にそのアイテムを売却・消費してもフォームのデータはそのままになります。 このため、GS2-Inventory でアイテムを消費・売却する場合は、事前に GS2-Formation の編成で使用されていないかをクライアントまたはスクリプトで確認する必要があります。

ItemSet.referenceOf による編成中アイテムの削除防止

スタンダードインベントリ(GS2-Inventory)を使用している場合は、ItemSetreferenceOf フィールドを利用することで、編成中のアイテムが誤って消費・削除されることを防ぐことができます。

フォームのスロットにアイテムをセットする際に、そのスロットを識別する文字列(GRN など)を ItemSet.referenceOf に設定します。 スタンダードインベントリのインベントリモデル設定には「referenceOf に値が存在する場合は消費を禁止する」オプションがあります。 このオプションを有効にすると、ItemSet.referenceOf が空でないアイテムは消費・売却できなくなるため、編成中のアイテムに対してシステムレベルで削除の制限をかけることができます。

詳細なリファレンス