> For the complete documentation index, see [llms.txt](/llms.txt)

# GS2-SkillTree

스킬 트리 기능



캐릭터 등의 성장 요소로 일반적으로 사용되는 스킬 트리 기능을 구현하기 위한 마이크로서비스입니다.
스킬 트리란 아래와 같은 트리 구조를 가지며, 노드를 해제함으로써 캐릭터의 파라미터를 향상시킬 수 있는 기능을 가리킵니다.
노드 해제에는 비용이 필요하며, GS2-SkillTree에서는 각 마이크로서비스가 제공하는 《소비 액션》을 설정할 수 있습니다.

```mermaid
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]
```

## 노드 정의(NodeModel)

스킬 트리를 구성하는 노드는 마스터 데이터 `NodeModel`로 정의합니다.
각 노드는 다음 필드로 구성되며, 이를 통해 노드의 해제 조건·비용·효과·의존 관계를 표현합니다.

| 필드 | 역할 |
| --- | --- |
| `name` | 노드의 식별자 |
| `metadata` | 클라이언트에서 표시할 설명용 텍스트 |
| `releaseVerifyActions` | 해제에 필요한 검증 액션(특정 랭크일 것 등. 최대 10건) |
| `releaseConsumeActions` | 해제 시 소비되는 액션(SP·아이템·골드 등. 1~10건) |
| `restrainReturnRate` | 미해제화(리스트레인) 시 비용 반환율(0.0~1.0, 기본값 1.0) |
| `premiseNodeNames` | 전제 노드(최대 10개) |

미해제화(리스트레인) 시 반환되는 입수 액션(`returnAcquireActions`)은 `releaseConsumeActions`를 반전시키고 `restrainReturnRate`를 곱한 내용으로 자동 생성됩니다.
스킬의 효과(스탯 가산 등)는 `releaseConsumeActions`에서 소비하는 리소스(GS2-Experience의 랭크나 GS2-Inventory의 스킬 포인트 등)를 다른 마이크로서비스와 연동하는 형태로 표현하십시오.

## 전제 노드

각 노드에는 트리 구조를 만들기 위해 《전제 노드》를 설정할 수 있습니다.
전제 노드에는 최대 10개의 노드를 설정할 수 있으며, 전제 노드로 설정된 노드가 해제 상태가 아니면 해당 노드는 해제할 수 없습니다.

전제 노드에는 그 노드에 이르기까지의 모든 노드를 설정할 필요는 없으며, 바로 앞의 노드 하나만 설정해도 트리 구조를 만들 수 있다는 점에 유의하십시오.

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

위 예시에서는 D의 `premiseNodeNames`에 C만 지정하면 충분합니다.
A·B는 C를 해제하기 위해 필연적으로 이미 해제되어 있어야 하므로, 의존 관계로 별도 기술할 필요가 없습니다.

## 해제 처리 흐름

```mermaid
sequenceDiagram
  participant Client
  participant SkillTree as GS2-SkillTree
  participant Other as 다른 마이크로서비스
  Client->>SkillTree: Release(nodeModelNames)
  SkillTree->>SkillTree: 전제 노드 판정
  SkillTree->>SkillTree: releaseVerifyActions 검증
  SkillTree->>Other: releaseConsumeActions 실행
  SkillTree-->>Client: 해제된 노드 목록 (Status)
```

## 해제한 노드를 미해제 상태로 되돌리기

각 노드 단위로, 또는 모든 노드를 미해제 상태로 되돌릴 수 있습니다.
플레이어가 빌드를 다시 짜는 "재분배" 기능을 구현하는 데 활용할 수 있습니다.

### 비용 반환

이때 노드를 해제하는 데 소비한 비용을 `restrainReturnRate`로 지정한 비율에 따라 반환할 수 있습니다.

여기서 주의해야 할 점은 트랜잭션의 소비 액션에는 "반전 가능"과 "반전 불가능"의 두 가지 종류가 존재한다는 것입니다.
"반전 가능"한 소비 액션에 대해서는 반환이 이루어지지만, "반전 불가능"한 소비 액션에 대해서는 반환이 이루어지지 않습니다.

소비 액션이 "반전 가능"한지 여부는 각 마이크로서비스 레퍼런스의 기재 내용을 확인하십시오.

### 트리 중간에 있는 노드 조작

노드에 의존하고 있는 노드가 이미 해제된 상태라면, 해당 노드를 미해제 상태로 되돌릴 수 없습니다.

예를 들어 A → B → C의 의존 관계가 있는 경우, C를 해제한 상태로 둔 채 B만 미해제로 만들 수는 없습니다.
재분배를 구현할 때는 의존 관계의 말단부터 순서대로 미해제화하거나, 후술할 `ResetAsync`로 모든 노드를 일괄 미해제 상태로 되돌리십시오.

### 노드의 일괄 해제

노드 해제는 여러 노드를 일괄로 해제할 수 있습니다.
이때 의존 관계에 주의하며 노드를 지정할 필요는 없으며, GS2-SkillTree 쪽에서 의존 관계의 순서를 고려하면서 해제 처리를 수행하고 해제 가능 여부를 판정합니다.

## 스크립트 트리거

네임스페이스에 `releaseScript`·`restrainScript`를 설정하면 노드 해제 및 미해제 복원 처리 시점에 커스텀 스크립트를 호출할 수 있습니다.

설정할 수 있는 주요 이벤트 트리거와 스크립트 설정명은 다음과 같습니다.

- `releaseScript`: 노드 해제 시
- `restrainScript`: 노드를 미해제 상태로 되돌릴 때

## 마스터 데이터 운용
마스터 데이터를 등록함으로써 마이크로서비스에서 이용 가능한 데이터와 동작을 설정할 수 있습니다.

마스터 데이터의 종류에는 다음이 있습니다.

- `NodeModel`: 스킬 트리의 노드 정의

### 마스터 데이터 JSON 예시

```json
{
  "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
    }
  ]
}
```

마스터 데이터 등록은 관리 콘솔에서 등록하는 방법 외에도, GitHub에서 데이터를 반영하거나 GS2-Deploy를 사용하여 CI에서 등록하는 워크플로우를 구성할 수 있습니다.

## 버프에 의한 보정

GS2-Buff를 이용하면 노드 모델의 `releaseVerifyActions`·`releaseConsumeActions`·`restrainReturnRate`를 버프로 보정하여, 해제 조건이나 비용, 반환율을 이벤트에 맞게 조정할 수 있습니다.

예를 들어 기간 한정 이벤트로 "스킬 포인트 1개만으로도 해제 가능"하게 하거나 "재분배 시 반환율을 1.5배로 한다"와 같은 시책을, 마스터 데이터를 다시 쓰지 않고 버프 적용만으로 실현할 수 있습니다.

## 트랜잭션 액션

GS2-SkillTree에서는 다음과 같은 트랜잭션 액션을 제공합니다.

- 소비 액션: 노드의 미해제화
- 입수 액션: 노드의 해제 완료 기록

"노드의 해제 완료 기록"을 입수 액션으로 이용함으로써, 상점에서 상품을 구입할 때나 퀘스트 클리어 시의 보상으로 스킬 트리의 특정 노드를 직접 해제 완료 상태로 만드는 처리를, 트랜잭션 내에서 안전하게 실행할 수 있습니다. 이를 통해 특정 조건을 달성한 플레이어에게 비용을 소비시키지 않고 특별한 스킬을 즉시 부여하는 등, 유연한 캐릭터 성장 연출이 가능해집니다.

## 구현 예제

### 노드의 해제 상태 취득

`Status`에는 현재 해제 완료된 노드 목록(`releasedNodeNames`)이 저장되어 있습니다.
캐릭터별로 스킬 트리를 나누어 관리하고 싶은 경우에는 `propertyId`를 지정함으로써 여러 개의 스킬 트리 상태를 유지할 수 있습니다.



**Unity**
```csharp

    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
    );
    var item = await domain.ModelAsync();
```
**Unreal Engine**
```cpp

    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();
```
**Godot**
```gdscript

var domain = ez.skill_tree.namespace_(
        "namespace-0001"
    ).me(game_session).status(
        "property-0001"
    )

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

var result = async_result.result

```


### 노드 해제

여러 노드를 일괄로 전달할 수 있습니다. 전제 관계를 포함하여 지정해도 GS2-SkillTree 쪽에서 순서를 해결하여 해제를 시도합니다.



**Unity**
```csharp

    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
    );
    var result = await domain.ReleaseAsync(
        nodeModelNames: new string[] {
            "node-0001",
        }
    );
```
**Unreal Engine**
```cpp

    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;
    }
```
**Godot**
```gdscript

var domain = ez.skill_tree.namespace_(
        "namespace-0001"
    ).me(game_session).status(
        "property-0001"
    )

var async_result = await domain.release(
    [
        "node-0001",
    ] # node_model_names
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


### 노드의 해제 상태를 원래대로 되돌리기

지정한 노드를 미해제 상태로 되돌립니다. `restrainReturnRate`로 지정된 비율에 따라 소비한 액션이 반환됩니다(반전 가능한 소비 액션만 해당).



**Unity**
```csharp

    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
    );
    var result = await domain.RestrainAsync(
        nodeModelNames: new string[] {
            "node-0001",
        }
    );
```
**Unreal Engine**
```cpp

    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;
    }
```
**Godot**
```gdscript

var domain = ez.skill_tree.namespace_(
        "namespace-0001"
    ).me(game_session).status(
        "property-0001"
    )

var async_result = await domain.restrain(
    [
        "node-0001",
    ] # node_model_names
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


### 노드의 해제 상태를 리셋하기

해제 완료된 노드를 일괄로 미해제 상태로 되돌립니다. "스킬 재분배 아이템"을 소비하는 사양 등에서 활용할 수 있습니다.
모든 노드에 대해 `restrainReturnRate`에 따른 비용 반환이 시도됩니다.



**Unity**
```csharp

    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
    );
    var result = await domain.ResetAsync(
    );
```
**Unreal Engine**
```cpp

    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;
    }
```
**Godot**
```gdscript

var domain = ez.skill_tree.namespace_(
        "namespace-0001"
    ).me(game_session).status(
        "property-0001"
    )

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

var result = async_result.result

```


## 캐릭터 단위 스킬 트리 관리하기

하나의 계정 안에서 여러 캐릭터가 각각 독립된 스킬 트리를 가지는 경우에는 `Status`의 `propertyId`를 활용합니다.
GS2-Dictionary나 GS2-Inventory에서 관리하고 있는 캐릭터ID를 그대로 `propertyId`로 할당함으로써, 캐릭터별로 독립된 해제 상태를 저장할 수 있습니다.

## 상세 레퍼런스

[GS2-SkillTree API 레퍼런스](../../api_reference/skill_tree)



