GS2-SkillTree

Skill tree feature

This microservice realizes the skill tree functionality commonly used as a growth element for characters and other objects. A skill tree is a feature with a tree structure such as the one shown below, in which releasing nodes improves a character’s parameters. Releasing a node requires a cost, and GS2-SkillTree allows you to set the consume actions provided by each microservice.

flowchart TD
  Base --> Node1[STR+5]
  Node1 --> Node2[DEF+5]
  Node2 --> Node3[SPD+5]

  Node3 --> Node4[STR+5]
  Node4 --> Node5[DEF+5]
  Node5 --> Node6[STR+5]
  Node6 --> Node7[DEF+5]

  Node3 --> Node10[SPD+5]
  Node10 --> Node11[DEF+5]
  Node11 --> Node12[SPD+5]
  Node12 --> Node13[DEF+5]

  Node11 --> Node30[SPD+5]
  Node30 --> Node31[STR+5]
  Node31 --> Node32[SPD+5]

  Node7 --> Node20[STR+5]
  Node13 --> Node20
  Node20 --> Node21[STR+5]
  Node21 --> Node22[STR+5]

Node Definition (NodeModel)

Nodes that make up the skill tree are defined as the master data NodeModel. Each node consists of the following fields, which express the release conditions, cost, effect, and dependencies of the node.

Field Role
name Identifier of the node
metadata Descriptive text displayed on the client
releaseVerifyActions Verify actions required for release (e.g., being at a specific rank; up to 10)
releaseConsumeActions Actions consumed at release (SP, items, gold, etc.; 1 to 10)
restrainReturnRate Cost return rate when un-releasing (restraining) (0.0 to 1.0, default 1.0)
premiseNodeNames Prerequisite nodes (up to 10)

The acquire actions returned when un-released (returnAcquireActions) are automatically generated by inverting releaseConsumeActions and multiplying by restrainReturnRate. Skill effects (such as stat increases) should be expressed by linking the resources consumed in releaseConsumeActions (such as GS2-Experience rank or GS2-Inventory skill points) with another microservice.

Prerequisite Nodes

Each node can have prerequisite nodes configured to form a tree structure. Up to 10 nodes can be set as prerequisite nodes, and a node cannot be released unless the nodes set as prerequisites are in the released state.

Note that you do not have to set all nodes leading up to that node as prerequisites; you only need to specify the one immediately preceding it to form the tree structure.

flowchart LR
  A --> B
  B --> C
  C --> D

In the example above, it is sufficient to specify only C for D’s premiseNodeNames. A and B are necessarily already released in order to release C, so they do not need to be described as dependencies.

Release Process Flow

sequenceDiagram
  participant Client
  participant SkillTree as GS2-SkillTree
  participant Other as Other microservices
  Client->>SkillTree: Release(nodeModelNames)
  SkillTree->>SkillTree: Check prerequisite nodes
  SkillTree->>SkillTree: Verify releaseVerifyActions
  SkillTree->>Other: Execute releaseConsumeActions
  SkillTree-->>Client: List of released nodes (Status)

Reverting released nodes to unreleased

You can revert each node, or all nodes, to the unreleased state. This can be used to implement a “respec” feature in which the player rebuilds their build.

Cost reimbursement

In this case, the cost consumed to release the node can be returned based on the rate specified in restrainReturnRate.

It is important to note here that transaction consume actions come in two kinds: “reversible” and “non-reversible.” “Reversible” consume actions are refunded, while “non-reversible” consume actions are not.

Please refer to the documentation for each microservice to check whether its consume actions are reversible.

Manipulating nodes in the middle of the tree

If a node that depends on this node has already been released, the node cannot be reverted to the unreleased state.

For example, with a dependency chain A → B → C, you cannot un-release B while C remains released. To implement a respec, either un-release nodes in order from the leaves of the dependency, or use ResetAsync (described later) to revert all nodes at once.

Batch release of nodes

Multiple nodes can be released at once. You do not need to specify nodes in dependency order; GS2-SkillTree resolves the dependency order and processes the release while determining whether each release is possible.

Script Triggers

Setting releaseScript and restrainScript on the namespace allows you to invoke a custom script at the timing of node release or reverting a node to unreleased.

The main configurable event triggers and script setting names are as follows:

  • releaseScript: When a node is released
  • restrainScript: When a node is reverted to unreleased

Master Data Operation

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

Master data types include the following:

  • NodeModel: Skill tree node definition

JSON example of master data

{
  "version": "2024-05-30",
  "nodeModels": [
    {
      "name": "str-001",
      "metadata": "STR+5",
      "premiseNodeNames": [],
      "releaseConsumeActions": [
        {
          "action": "Gs2Inventory:ConsumeItemSetByUserId",
          "request": "{\"namespaceName\":\"inventory-0001\",\"inventoryName\":\"skill_point\",\"itemName\":\"sp\",\"userId\":\"#{userId}\",\"consumeCount\":1}"
        }
      ],
      "restrainReturnRate": 1.0
    },
    {
      "name": "str-002",
      "metadata": "STR+10",
      "premiseNodeNames": ["str-001"],
      "releaseConsumeActions": [
        {
          "action": "Gs2Inventory:ConsumeItemSetByUserId",
          "request": "{\"namespaceName\":\"inventory-0001\",\"inventoryName\":\"skill_point\",\"itemName\":\"sp\",\"userId\":\"#{userId}\",\"consumeCount\":2}"
        }
      ],
      "restrainReturnRate": 1.0
    }
  ]
}

Master data can be registered via the Management Console, by reflecting data from GitHub, or by setting up workflows to register via CI using GS2-Deploy.

Adjustment via Buffs

Using GS2-Buff, you can adjust the releaseVerifyActions, releaseConsumeActions, and restrainReturnRate of a node model via buffs, dynamically adjusting release conditions, costs, and return rates according to events.

For example, you can implement measures such as “release is possible with just 1 skill point” or “return rate is 1.5x when respeccing” as a limited-time event, without rewriting the master data, simply by applying buffs.

Transaction Actions

GS2-SkillTree provides the following transaction actions:

  • Consume actions: mark a node as unreleased
  • Acquire actions: record a node as released

By using “record a node as released” as an acquire action, you can safely execute processes within a transaction to directly mark specific nodes in the skill tree as released as a reward for purchasing an item at a shop or clearing a quest. This enables flexible character growth, such as immediately granting special skills to players who achieve specific conditions without consuming costs.

Example Implementation

Get the release status of nodes

Status contains the list of currently released nodes (releasedNodeNames). If you want to manage separate skill trees per character, you can hold multiple skill tree states by specifying propertyId.

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

Release a node

Multiple nodes can be passed in at once. Even if you specify them including prerequisite relationships, GS2-SkillTree resolves the order and attempts to release them.

    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
    );
    var result = await domain.ReleaseAsync(
        nodeModelNames: new string[] {
            "node-0001",
        }
    );
    const auto Domain = Gs2->SkillTree->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Status(
    );
    const auto Future = Domain->Release(
        []
        {
            const auto v = MakeShared<TArray<FString>>();
            v->Add("node-0001");
            return v;
        }()
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError())
    {
        return false;
    }

Revert the release state of a node

Reverts the specified nodes to the unreleased state. Consumed actions are returned based on the rate specified in restrainReturnRate (only for reversible consume actions).

    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
    );
    var result = await domain.RestrainAsync(
        nodeModelNames: new string[] {
            "node-0001",
        }
    );
    const auto Domain = Gs2->SkillTree->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Status(
    );
    const auto Future = Domain->Restrain(
        []
        {
            const auto v = MakeShared<TArray<FString>>();
            v->Add("node-0001");
            return v;
        }()
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError())
    {
        return false;
    }

Reset the release state of nodes

Reverts all released nodes to unreleased at once. This can be used for specifications such as consuming a “skill respec item.” For all nodes, cost return is attempted in accordance with restrainReturnRate.

    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
    );
    var result = await domain.ResetAsync(
    );
    const auto Domain = Gs2->SkillTree->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Status(
    );
    const auto Future = Domain->Reset(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError())
    {
        return false;
    }

Managing skill trees per character

In cases where a single account has multiple characters that each have an independent skill tree, you can leverage the propertyId of Status. By assigning the character ID managed in GS2-Dictionary or GS2-Inventory directly as the propertyId, you can save an independent release state per character.

Detailed Reference