GS2-Enhance

Enhancement feature

This feature may be completely incomprehensible to some developers.

However, it is a feature that is always present in the de facto standard game systems of mobile games, starting with those in Japan. This game mechanic itself should be useful knowledge for realizing Game as a Service, so for developers who do not understand it we add an explanation of the game mechanism.

If you already have sufficient knowledge of the game mechanics of the enhancement feature, you can safely skip the rest of this explanation section.

Game mechanics of the enhancement feature

Enhancement itself is a very simple game system in which material items can be consumed to add experience to a target. In addition to the method of gaining experience by participating in battle, characters and equipment can be grown in a variety of ways. (Equipment also has levels.)

Now let us explain why this game system plays a role in realizing Game as a Service.

In a nutshell, it is used to pad play time.

You all surely understand that the speed of game play is overwhelmingly faster than the speed of game development. A game that we game developers spent three years building, players finish in 10 hours. But Game as a Service cannot be sustained if this gap is not closed. In other words, it is necessary to debuff the rate at which players consume content. That magic is the enhancement game system.

A typical Game as a Service game holds an event once per month. In each event there is some kind of boss, and the player keeps defeating that boss repeatedly throughout the event period. The bosses come in multiple difficulties matched to the player’s growth stage, and defeating them yields character and equipment enhancement materials.

Using the enhancement materials they have gathered, players convert them into experience for characters and equipment to grow them. As characters and equipment grow, they can challenge higher difficulties of the boss.

In this way, while all players participate in the event, they obtain enhancement materials suited to their character’s growth stage and enjoy the gameplay of growing characters and equipment.

In reality, bosses rarely drop enhancement materials directly. Instead, there may be a gacha that can only be challenged during the event period and players collect points to draw it and obtain materials through the gacha, or there may be a shop open only during the event period where players collect in-game currency from defeating bosses and purchase enhancement materials from the shop. There are various designs that let players decide where to prioritize their growth, but ultimately what is happening is that the time a player needs to grow their character is being stretched to pad play time.

What remains consistent throughout the event is that the player’s character becomes stronger. Once the event ends, the stronger character is used to enjoy higher-difficulty permanent content, leading into the next form of fun.

graph TD
  BossBattle["Boss Battle"] -- Acquire Event Point --> Shop
  Shop -- Buy Enhance Materials --> Enhance["Enhance Character"]
  Enhance -- More Formidable --> BossBattle

Architecture

Enhancement is realized by GS2-Enhance operating on GS2-Inventory, which manages the items used as materials, and either GS2-Inventory or GS2-Dictionary, which manages the target character or equipment, plus GS2-Experience, which manages the experience and levels of that character or equipment.

In the current GS2, enhancement via DirectEnhance, which completes the consumption and experience addition in a single API request, is recommended. Previously, because automatic transaction execution was not available, a multi-step enhancement process (Progress) divided into preparation, execution, and completion reporting was recommended to prevent acts such as cutting off communication and retrying until a great success appeared. Now that automatic transaction execution and atomic commit mechanisms are in place, that concern has been resolved.

actor Player
participant "GS2-Enhance#Namespace"
participant "GS2-Inventory#ItemSet(Material)"
participant "GS2-Experience#Status"
Player -> "GS2-Enhance#Namespace" : Direct Enhance(Materials/Target Character)
"GS2-Enhance#Namespace" -> "GS2-Inventory#ItemSet(Material)" : Get experience value from metadata
"GS2-Enhance#Namespace" -> "GS2-Inventory#ItemSet(Material)" : Consume
"GS2-Enhance#Namespace" -> "GS2-Experience#Status": Add experience(Key: Target Character/Equipment Id)
"GS2-Enhance#Namespace" -> Player : Enhance result

You call the enhancement execution API (DirectEnhance) on GS2-Enhance with the parameters of “enhancement target” and “materials used for enhancement”.

GS2-Enhance then retrieves the master data of the material item from GS2-Inventory and obtains, from its metadata, the amount of experience to gain when used as material. Once the amount of experience is determined, the item is consumed and the experience-addition process is executed on GS2-Experience.

Although an enhancement target is specified, it is only used as the key for the experience managed by GS2-Experience; the enhancement target’s information itself is not used directly.

Differences between DirectEnhance and Progress

GS2-Enhance provides two enhancement flows.

ItemDirectEnhanceProgress (Start / End)
Number of API calls12 (start / end)
Recommended useAll enhancement processingWhen you want to obtain a draw result in advance to reflect outcomes such as great success in your effects
AtomicityConsumption and experience addition complete within the requestThe draw result is held on the server and confirmed by a subsequent End
Abuse via disconnectNoneAn in-progress Progress can only be held one at a time; retries are only possible via DeleteProgress

Unless there is a special requirement, we recommend using DirectEnhance.

sequenceDiagram
  participant Player
  participant Enhance as GS2-Enhance
  participant Inventory as GS2-Inventory
  participant Experience as GS2-Experience
  Player ->> Enhance: DirectEnhance(rateName, targetItemSetId, materials)
  Enhance ->> Inventory: Obtain experience amount from metadata
  Enhance ->> Inventory: Consume material items
  Enhance ->> Experience: Add experience
  Enhance -->> Player: Added experience and great-success multiplier

Enhancement Rate

To limit the materials that can be used for enhancement and the enhancement target, the enhancement rate must be set as master data.

Master data records information such as the GS2-Inventory namespace and inventory name of items that can be used as materials, and the GS2-Inventory namespace and inventory name of items that can be enhancement targets.

Master data is managed in JSON format.

Here is an example of setting metadata for an ItemModel in JSON format such as { "experience": 50 }.

Define the JSON key (experience in the example above) in the acquireExperienceHierarchy of the RateModel. acquireExperienceHierarchy can also define a hierarchical structure. For example, if you want to set metadata with a data structure such as { "aaa": { "bbb": { "experienceValue": 100 } } }, specify [ "aaa", "bbb", "experienceValue" ] in acquireExperienceHierarchy.

Main configuration items of RateModel

ItemDescription
nameEnhancement rate name
targetInventoryModelIdInventory model ID of the GS2-Inventory holding items to be enhanced
materialInventoryModelIdInventory model ID of the GS2-Inventory holding material items
acquireExperienceHierarchyJSON path used to extract the experience amount from the material item’s metadata
acquireExperienceSuffixSuffix appended to the experience model name key (e.g. :level)
experienceModelIdExperience model ID of the GS2-Experience to which experience is added
bonusRatesMultipliers and weights for great-success draws

GS2-Enhance RateModel master data — example of setting experience in metadata:


{
  "version": "2020-08-22",
  "rateModels": [
    {
      "name": "enhanceRate",
      "description": "",
      "metadata": "",
      "targetInventoryModelId": "grn:gs2:ap-northeast-1:YourOwnerId:inventory:enhance-inventory:model:character",
      "acquireExperienceSuffix": ":level",
      "materialInventoryModelId": "grn:gs2:ap-northeast-1:YourOwnerId:inventory:enhance-inventory:model:material",
      "acquireExperienceHierarchy": [
        "experience"
      ],
      "experienceModelId": "grn:gs2:ap-northeast-1:YourOwnerId:experience:enhance-experience:model:character",
      "bonusRates": [
        {
          "rate": 2,
          "weight": 1
        },
        {
          "rate": 1,
          "weight": 1
        }
      ]
    }
  ]
}

bonusRates can hold multiple entries; each entry has a draw weight (weight) and multiplier (rate). In the example above, “2x experience” and “1x experience” are drawn at the same weight, so a great success (2x) occurs roughly 50% of the time.

GS2-Experience ExperienceModel master data example:

{
  "version": "2019-01-11",
  "experienceModels": [
    {
      "name": "character",
      "metadata": "CHARACTER",
      "defaultExperience": 0,
      "defaultRankCap": 50,
      "maxRankCap": 80,
      "rankThreshold": {
        "metadata": "RANK_THRESHOLD",
        "values": [
          100,
          300,
          500,
          1000
        ]
      }
    }
  ]
}

GS2-Inventory ItemModel master data — example of setting experience in metadata:

{
  "version": "2019-02-05",
  "inventoryModels": [
    {
      "name": "character",
      "initialCapacity": 1,
      "maxCapacity": 1,
      "protectReferencedItem": false,
      "itemModels": [
        {
          "name": "character-0001",
          "stackingLimit": 1,
          "allowMultipleStacks": false,
          "sortValue": 0
        }
      ]
    },
    {
      "name": "material",
      "metadata": "",
      "initialCapacity": 10,
      "maxCapacity": 10,
      "protectReferencedItem": false,
      "itemModels": [
        {
          "name": "material-0001",
          "metadata": "{\"experience\":50}",
          "stackingLimit": 99,
          "allowMultipleStacks": false,
          "sortValue": 0
        }
      ]
    }
  ]
}

Limit Break (Unleash)

A “limit break” feature is also provided that consumes items such as duplicates of the same kind as materials to raise the target’s level cap. It raises the grade managed by GS2-Grade by one, which in turn raises the rankCap managed by GS2-Experience. It is configured through the master data of UnleashRateModel with the following:

  • The inventory model that is the limit-break target (targetInventoryModelId)
  • The associated grade model (GS2-Grade gradeModelId)
  • The materials required to reach each grade (gradeEntries)

This makes it possible to implement game mechanics such as “collect 4 of the same character to limit-break” or “merge equipment by consuming either equipment of the same slot or a dedicated material”.

The gradeValue of each entry in gradeEntries is the grade reached by the limit break. If the target’s current grade is 2, the conditions of the entry whose gradeValue is 3 are used. GS2-Grade grades start at 1 (a target that matches none of the defaultGrades starts at grade 1).

Types of material conditions

Each entry chooses how its material conditions are written with type. Different grades can use different types.

typeDescription
simple (default)Consumes needCount items of the same kind as the target. The item sets to consume are specified with materials at execution time. Whether an item is of the same kind is determined by gradeUpPropertyIdRegex of GS2-Grade.
recipeThe player chooses one of the recipes defined in recipes and executes it. All the materials of the chosen recipe are consumed.

Recipes

Each recipe material chooses how it is consumed with materialType, and holds the setting for that type.

materialTypeHow it is consumedSetting
individualConsumes item sets chosen by the player (such as duplicate equipment), one each. The materials must belong to the same inventory model as the target.individualSetting
quantityConsumes a quantity of an item decided by the setting (such as a general-purpose enhancement material). The player does not choose the item. Both standard inventories and simple inventories can be used.quantitySetting

The fields of individualSetting are as follows.

FieldDescription
matchTypesameItem: only the same item model as the target. sameGroup: item models with the same group key as the target.
gradeConditionany: the material’s grade is not checked. sameAsTarget: the material’s grade must equal the target’s current grade. equal: the material’s grade must equal gradeValue. The material’s grade is verified by GS2-Grade within the transaction.
countThe number of item sets to consume

The fields of quantitySetting are as follows.

FieldDescription
matchTypespecified: consumes the item specified by itemModelId. sameGroup: consumes the item in the materialInventoryModelId inventory that has the same group key as the target (exactly one item model must match).
itemModelId / materialInventoryModelIdThe item model to consume / the inventory model to search for the material. Simple inventory GRNs can also be specified.
countThe quantity to consume

When targetGroupKeys of a recipe is specified, the recipe can be used only when the target’s group key is included in it. When it is empty, the recipe can be used for any target.

Group keys

Classifications such as “the same slot” are expressed with a group key written in the item model’s metadata (JSON). groupKeyHierarchy of UnleashRateModel specifies at which level of the metadata the group key is stored. The value must be a string.

For example, if the item model’s metadata is as follows, specify ["unleash", "group"] for groupKeyHierarchy.

{ "unleash": { "group": "weapon" } }

If the item models of slot-specific general-purpose materials also carry a group key in the same form, sameGroup of quantitySetting can express “the general-purpose material for the target’s slot” with a single recipe.

Master data example

An example where reaching grade 2 consumes one copy of the same equipment, and reaching grade 3 consumes either equipment of the same slot (grade 1) or two slot-specific general-purpose materials.

{
  "name": "equipment",
  "targetInventoryModelId": "grn:gs2:{region}:{ownerId}:inventory:namespace-0001:model:equipment",
  "gradeModelId": "grn:gs2:{region}:{ownerId}:grade:namespace-0001:model:equipment",
  "groupKeyHierarchy": ["unleash", "group"],
  "gradeEntries": [
    {
      "gradeValue": 2,
      "type": "recipe",
      "recipes": [
        {
          "name": "merge",
          "materials": [
            {
              "name": "same",
              "materialType": "individual",
              "individualSetting": { "matchType": "sameItem", "gradeCondition": "sameAsTarget", "count": 1 }
            }
          ]
        }
      ]
    },
    {
      "gradeValue": 3,
      "type": "recipe",
      "recipes": [
        {
          "name": "fodder",
          "materials": [
            {
              "name": "fodder",
              "materialType": "individual",
              "individualSetting": { "matchType": "sameGroup", "gradeCondition": "equal", "gradeValue": 1, "count": 1 }
            }
          ]
        },
        {
          "name": "material",
          "targetGroupKeys": ["weapon", "armor"],
          "materials": [
            {
              "name": "material",
              "materialType": "quantity",
              "quantitySetting": {
                "matchType": "sameGroup",
                "materialInventoryModelId": "grn:gs2:{region}:{ownerId}:inventory:namespace-0001:simple:model:unleash-material",
                "count": 2
              }
            }
          ]
        }
      ]
    }
  ]
}

Parameters at execution time

For an entry whose type is recipe, specify the recipe with recipeName when executing the limit break, and specify the item sets to consume for each individual material with recipeMaterials. quantity materials do not need to be specified.

{
  "targetItemSetId": "grn:...:item:sword-0001:itemSet:target",
  "recipeName": "merge",
  "recipeMaterials": [
    { "name": "same", "itemSetIds": ["grn:...:item:sword-0001:itemSet:material"] }
  ]
}

An error is returned before the transaction is issued in the following cases.

  • The target itself is included in the materials, or the same item set is specified more than once
  • The number of materials does not match the recipe’s count, or a material name not in the recipe is specified
  • A material belongs to an inventory model other than the target’s, or does not satisfy the matchType condition
  • The target’s group key is not included in targetGroupKeys, or the target’s group key cannot be read from the metadata
  • For sameGroup of quantitySetting, zero or two or more item models match

The experience or grade held by item sets consumed as materials is not carried over to the target.

Script Triggers

By configuring enhanceScript on the namespace, you can call custom scripts before and after enhancement execution. You can allow/deny the process or override the gained experience, and choose synchronous or asynchronous execution. Asynchronous execution also supports external integration via GS2-Script or Amazon EventBridge.

Main event triggers and script setting names are:

  • enhanceScript (completion notification: enhanceDone): before and after enhancement execution

In the script you can reference the enhancement target, the material items, and the multiplier being drawn. This is useful for purposes such as game balance adjustment or changing the boost multiplier during an event period.

Master data operations

Registering master data allows you to configure data and behaviors available to the microservice.

Master data types include:

  • RateModel: Definition of enhancement materials and targets
  • UnleashRateModel: Rate definitions for limit break

Master data can be registered via the management console, imported from GitHub, or registered from CI using GS2-Deploy or various language CDKs.

Transaction Actions

GS2-Enhance provides the following transaction actions:

  • Consume action: Delete the in-progress enhancement progress information (Progress)
  • Acquire action: Execute immediate enhancement (DirectEnhance), execute limit break (Unleash), start enhancement (Progress)

By using “Execute immediate enhancement (DirectEnhance)” as an acquire action, you can perform operations such as directly granting experience to characters or equipment as a reward for clearing a specific quest or purchasing an item from a shop. Also, by setting “Limit break (Unleash)” as a reward, you can implement operations such as automatically raising the level cap when a specific mission is completed.

Example implementation

Getting a list of enhancement rates

    var items = await gs2.Enhance.Namespace(
        namespaceName: "namespace-0001"
    ).RateModelsAsync(
    ).ToListAsync();
    const auto It = Gs2->Enhance->Namespace(
        "namespace-0001" // namespaceName
    )->RateModels();
    TArray<Gs2::UE5::Enhance::Model::FEzRateModelPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }
var iterator = ez.enhance.namespace_(
        "namespace-0001"
    ).rate_models(
    )

var async_result = await iterator.load()
if async_result.error != null:
    # Handle the error
    push_error(str(async_result.error))
    return

var items = async_result.result

Executing enhancement (DirectEnhance)

Specify the ItemSetId and quantity of the material items to consume in materials. Pass the ItemSetId of the character/equipment you want to enhance as targetItemSetId.

    var result = await gs2.Enhance.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Enhance(
    ).EnhanceAsync(
        rateName: "rate-0001",
        targetItemSetId: "item-set-0001",
        materials: new [] {
            new Gs2.Unity.Gs2Enhance.Model.EzMaterial
            {
                MaterialItemSetId = "material-0001",
                Count = 1,
            },
        }
    );

    var transaction = result;
    await transaction.WaitAsync();
    const auto Domain = Gs2->Enhance->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Enhance(
    );
    const auto Future = Domain->Enhance(
        "rate-0001",
        "item-set-0001",
        []
        {
            const auto v = MakeShared<TArray<TSharedPtr<Gs2::Enhance::Model::FMaterial>>>();
            v->Add(MakeShared<Gs2::Enhance::Model::FMaterial>()
                ->WithMaterialItemSetId(TOptional<FString>("material-0001"))
                ->WithCount(TOptional<int32>(1)));
            return v;
        }()
    );
    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;
var domain = ez.enhance.namespace_(
        "namespace-0001"
    ).me(game_session).enhance(
    )

var async_result = await domain.enhance(
    "rate-0001", # rate_name
    "item-set-0001", # target_item_set_id
    [
        Gs2EnhanceEzMaterial.new()
            .with_material_item_set_id("material-0001")
            .with_count(1),
    ], # materials
    null # config
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

Starting enhancement (Progress)

This is the flow when you want to obtain the draw result in advance. Only one in-progress Progress can be held per user.

    var result = await gs2.Enhance.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Progress(
    ).StartAsync(
        rateName: "rate-0001",
        targetItemSetId: "item-set-0001",
        materials: new [] {
            new Gs2.Unity.Gs2Enhance.Model.EzMaterial
            {
                MaterialItemSetId = "material-0001",
                Count = 1,
            },
        }
    );
    const auto Future = Gs2->Enhance->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Progress(
    )->Start(
        "rate-0001", // rateName
        "item-set-0001", // targetItemSetId
        []
        {
            const auto v = MakeShared<TArray<TSharedPtr<Gs2::Enhance::Model::FMaterial>>>();
            v->Add(MakeShared<Gs2::Enhance::Model::FMaterial>()
                ->WithMaterialItemSetId(TOptional<FString>("material-0001"))
                ->WithCount(TOptional<int32>(1)));
            return v;
        }()
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
var domain = ez.enhance.namespace_(
        "namespace-0001"
    ).me(game_session).progress(
    )

var async_result = await domain.start(
    "character-level", # rate_name
    "item-set-0001", # target_item_set_id
    [
        Gs2EnhanceEzMaterial.new()
            .with_material_item_set_id("material-0001")
            .with_count(1),
    ], # materials
    null, # force
    null # config
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

Getting in-progress enhancement information

You can obtain the drawn multiplier (great-success flag) and the amount of experience gained, and use them in enhancement effects.

    var item = await gs2.Enhance.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Progress(
    ).ModelAsync();

    var experienceValue = item.ExperienceValue;
    var rate = item.Rate;
    const auto Domain = Gs2->Enhance->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Progress(
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

    const auto Result = Future->GetTask().Result();
    const auto ExperienceValue = Result->GetExperienceValue();
    const auto Rate = Result->GetRate();
var domain = ez.enhance.namespace_(
        "namespace-0001"
    ).me(game_session).progress(
    )

var async_result = await domain.model()
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

Completing enhancement (Progress)

Finalize the enhancement started by Start with End, executing consumption and experience addition.

    var result = await gs2.Enhance.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Progress(
    ).EndAsync();

    await result.WaitAsync();
    const auto Future = Gs2->Enhance->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Progress(
    )->End();
    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;
var domain = ez.enhance.namespace_(
        "namespace-0001"
    ).me(game_session).progress(
    )

var async_result = await domain.end(
    null # config
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

Discarding in-progress enhancement information

Used for recovery in cases such as when communication is interrupted and the completion report cannot be made.

    await gs2.Enhance.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Progress(
    ).DeleteProgressAsync();
    const auto Future = Gs2->Enhance->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Progress(
    )->DeleteProgress();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
var domain = ez.enhance.namespace_(
        "namespace-0001"
    ).me(game_session).progress(
    )

var async_result = await domain.delete_progress(
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

Other features

Great success in enhancement

In some enhancement specifications, a “great success” occurs at a certain probability, multiplying the gained experience by 1.5x or 2x. With GS2-Enhance you can configure the probability of great success and the experience multiplier in that case as part of the enhancement rate.

The ratio of each entry’s weight to the total of bonusRates weight is the probability of that multiplier being drawn.

Passing parameters to scripts using Config

EnhanceAsync / StartAsync accept a config parameter, which lets you pass arbitrary key-value pairs at script trigger time. You can convey information such as an in-game event boost state or the enhancement mode the player selected to the script.

Speculative Execute

When the speculativeExecute argument (default true) is enabled, the client-side cache is updated before waiting for the API response, allowing the UI to immediately display the post-experience-addition state. This delivers a snappy operational feel that is less affected by communication latency.

Detailed Reference