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

# GS2-SkillTree SDK for Game Engine API 레퍼런스

게임 엔진용 GS2-SkillTree SDK의 모델 사양과 API 레퍼런스



## 모델

### EzStatus

스킬 트리 해방 상황<br>

특정 플레이어와 프로퍼티에 대한 스킬 트리의 해방 상태를 추적하는 모델입니다.<br>
해방(언락)된 노드명의 목록을 관리합니다. 노드는 해방(목록에 추가), 구속(목록에서 제거), 초기화(전체 삭제)할 수 있습니다.<br>
이미 해방된 노드를 해방하려 하거나, 미해방 노드를 구속하려 하면 오류가 발생합니다.<br>
사용자가 처음 접근했을 때 자동으로 생성됩니다.

|  | 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 |
| --- | --- | --- | --- | --- | --- | --- |
| statusId | string |  | ※ |  |  ~ 1024자 | 상태GRN<br>※ 서버가 자동으로 설정 |
| userId | string |  | ✓ |  |  ~ 128자 | 사용자ID |
| releasedNodeNames | List&lt;string&gt; |  |  | [] | 0 ~ 1000 items | 해제된 노드 모델명 리스트<br>이 스킬트리에서 플레이어가 잠금 해제한 노드 모델명 리스트입니다.<br>해제(추가), 구속(삭제), 리셋(전체 초기화) 작업으로 갱신됩니다.<br>최대 1000건. |

**관련 메서드:**
getStatus - 플레이어의 스킬 트리 진행 상황 취득
release - 스킬 트리 노드 해방
reset - 스킬 트리 전체 초기화(전체 리스펙)
restrain - 특정 노드를 잠금 상태로 되돌리기


---

### EzNodeModel

노드 모델<br>

스킬 트리 내의 노드를 정의하는 모델로, 해방 비용·전제 조건·반환 동작을 포함합니다.<br>
각 노드에는 검증 액션(해방 전 조건 확인), 소비 액션(지불할 비용), 먼저 해방되어야 하는 전제 노드를 설정할 수 있습니다.<br>
노드를 구속(미해방 상태로 되돌림)하면, 소비한 리소스가 반환율에 따라 부분적으로 반환됩니다.<br>
반환 입수 액션은 소비 액션에 반환율을 곱하여 자동으로 계산됩니다.

|  | 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 |
| --- | --- | --- | --- | --- | --- | --- |
| name | string |  | ✓ |  |  ~ 128자 | 노드 모델명<br>노드 모델 고유의 이름. 영숫자 및 -(하이픈) _(언더스코어) .(마침표)로 지정합니다. |
| metadata | string |  |  |  |  ~ 2048자 | 메타데이터<br>메타데이터에는 임의의 값을 설정할 수 있습니다.<br>이 값들은 GS2의 동작에는 영향을 주지 않으므로, 게임 내에서 사용하는 정보를 저장하는 용도로 사용할 수 있습니다. |
| releaseVerifyActions | [List&lt;EzVerifyAction&gt;](#ezverifyaction) |  |  | [] | 0 ~ 10 items | 해방 검증 액션 목록<br>이 노드를 해방하기 전에 실행되어 조건이 충족되었는지 확인하는 검증 액션의 목록입니다.<br>예를 들어 플레이어가 특정 레벨에 도달했는지, 특정 아이템을 소지하고 있는지를 검증할 수 있습니다.<br>검증 액션 중 하나라도 실패하면 노드 해방이 거부됩니다. 최대 10개 액션. |
| releaseConsumeActions | [List&lt;EzConsumeAction&gt;](#ezconsumeaction) |  |  | [] | 1 ~ 10 items | 해방 소비 액션 목록<br>이 노드를 해방할 때 실행되는 소비 액션의 목록으로, 해방 비용을 나타냅니다.<br>이 액션들은 반환 입수 액션 계산에도 사용됩니다. 노드를 구속할 때 각 소비 액션이 반환율에 따라 역산됩니다.<br>최소 1개의 소비 액션이 필요합니다. 최대 10개 액션. |
| returnAcquireActions | [List&lt;EzAcquireAction&gt;](#ezacquireaction) |  |  |  | 0 ~ 10 items | 반환 입수 액션 목록<br>이 노드를 구속(취소)할 때 실행되는 입수 액션의 목록으로, 플레이어에게 반환되는 리소스를 나타냅니다.<br>이 필드는 해방 소비 액션에 반환율을 곱하여 자동 생성됩니다.<br>예를 들어 해방 비용이 골드 100이고 반환율이 0.8인 경우, 구속 시 골드 80이 반환됩니다.<br>최대 10개 액션. |
| restrainReturnRate | float |  |  | 1.0 | 0.0 ~ 1.0 | 반환율<br>이 노드를 구속(미해방 상태로 되돌림)했을 때 소비된 리소스가 반환되는 비율입니다.<br>1.0은 전액 반환, 0.5는 절반 반환, 0.0은 반환 없음을 의미합니다.<br>기본값은 1.0(전액 반환)입니다. 유효 범위: 0.0~1.0. |

**관련 메서드:**
getNodeModel - 특정 스킬 트리 노드의 상세 정보 취득
listNodeModels - 스킬 트리의 전체 노드 목록 조회


---

### EzConfig

컨피그 설정<br>

트랜잭션의 변수에 적용하는 설정 값

|  | 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 |
| --- | --- | --- | --- | --- | --- | --- |
| key | string |  | ✓ |  |  ~ 64자 | 이름 |
| value | string |  |  |  |  ~ 51200자 | 값 |


---

### EzAcquireAction

입수 액션

|  | 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 |
| --- | --- | --- | --- | --- | --- | --- |
| action | 문자열 열거형<br>enum {<br>}<br> |  | ✓ |  |  | 입수 액션에서 실행할 액션의 종류 |
| request | string |  | ✓ |  |  ~ 524288자 | 액션 실행 시 사용되는 요청의 JSON 문자열 |


**관련 모델:**
EzNodeModel - 노드 모델



---

### EzConsumeAction

소비 액션

|  | 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 |
| --- | --- | --- | --- | --- | --- | --- |
| action | 문자열 열거형<br>enum {<br>}<br> |  | ✓ |  |  | 소비 액션에서 실행할 액션의 종류 |
| request | string |  | ✓ |  |  ~ 524288자 | 액션 실행 시 사용되는 요청의 JSON 문자열 |


**관련 모델:**
EzNodeModel - 노드 모델



---

### EzVerifyAction

검증 액션

|  | 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 |
| --- | --- | --- | --- | --- | --- | --- |
| action | 문자열 열거형<br>enum {<br>}<br> |  | ✓ |  |  | 검증 액션에서 실행할 액션의 종류 |
| request | string |  | ✓ |  |  ~ 524288자 | 액션 실행 시 사용되는 요청의 JSON 문자열 |


**관련 모델:**
EzNodeModel - 노드 모델



---

### EzVerifyActionResult

검증 액션 실행 결과

|  | 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 |
| --- | --- | --- | --- | --- | --- | --- |
| action | 문자열 열거형<br>enum {<br>}<br> |  | ✓ |  |  | 검증 액션에서 실행할 액션의 종류 |
| verifyRequest | string |  | ✓ |  |  ~ 524288자 | 액션 실행 시 사용되는 요청의 JSON 문자열 |
| statusCode | int |  |  |  | 0 ~ 999 | 상태 코드 |
| verifyResult | string |  |  |  |  ~ 1048576자 | 결과 내용 |


**관련 모델:**
EzTransactionResult - 트랜잭션 실행 결과



---

### EzConsumeActionResult

소비 액션 실행 결과

|  | 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 |
| --- | --- | --- | --- | --- | --- | --- |
| action | 문자열 열거형<br>enum {<br>}<br> |  | ✓ |  |  | 소비 액션에서 실행할 액션의 종류 |
| consumeRequest | string |  | ✓ |  |  ~ 524288자 | 액션 실행 시 사용되는 요청의 JSON 문자열 |
| statusCode | int |  |  |  | 0 ~ 999 | 상태 코드 |
| consumeResult | string |  |  |  |  ~ 1048576자 | 결과 내용 |


**관련 모델:**
EzTransactionResult - 트랜잭션 실행 결과



---

### EzAcquireActionResult

획득 액션 실행 결과

|  | 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 |
| --- | --- | --- | --- | --- | --- | --- |
| action | 문자열 열거형<br>enum {<br>}<br> |  | ✓ |  |  | 입수 액션에서 실행할 액션의 종류 |
| acquireRequest | string |  | ✓ |  |  ~ 524288자 | 액션 실행 시 사용되는 요청의 JSON 문자열 |
| statusCode | int |  |  |  | 0 ~ 999 | 상태 코드 |
| acquireResult | string |  |  |  |  ~ 1048576자 | 결과 내용 |


**관련 모델:**
EzTransactionResult - 트랜잭션 실행 결과



---

### EzTransactionResult

트랜잭션 실행 결과<br>

서버 사이드에서 트랜잭션 자동 실행 기능을 이용하여 실행된 트랜잭션의 실행 결과

|  | 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 |
| --- | --- | --- | --- | --- | --- | --- |
| transactionId | string |  | ✓ |  | 36 ~ 36자 | 트랜잭션 ID |
| verifyResults | [List&lt;EzVerifyActionResult&gt;](#ezverifyactionresult) |  |  |  | 0 ~ 10 items | 검증 액션의 실행 결과 목록 |
| consumeResults | [List&lt;EzConsumeActionResult&gt;](#ezconsumeactionresult) |  |  | [] | 0 ~ 10 items | 소비 액션의 실행 결과 목록 |
| acquireResults | [List&lt;EzAcquireActionResult&gt;](#ezacquireactionresult) |  |  | [] | 0 ~ 100 items | 획득 액션 실행 결과 리스트 |

**관련 메서드:**
release - 스킬 트리 노드 해방
reset - 스킬 트리 전체 초기화(전체 리스펙)
restrain - 특정 노드를 잠금 상태로 되돌리기


---

## 메서드

### getNodeModel

특정 스킬 트리 노드의 상세 정보 취득<br>

노드 이름을 지정하여 전제 조건, 해방 비용, 반환 설정을 포함한 상세 정보를 취득합니다.<br>

플레이어가 스킬 트리에서 노드를 탭했을 때 상세 정보를 표시하는 데 사용합니다. 예를 들어:<br>
- "파워 스트라이크" — 필요: 기본 공격(해방됨). 비용: 스킬 포인트 3. 효과: 공격력 +50%<br>
- 노드의 구속 반환율이 80%라면, 이 노드를 되돌릴 때 비용의 80%가 반환됩니다<br>

응답에는 다음이 포함됩니다:<br>
- 전제 노드명: 먼저 해방되어야 하는 노드 목록<br>
- 해방 시 소비 액션: 해방을 위해 필요한 비용(예: 스킬 포인트 3 소비)<br>
- 해방 시 검증 액션: 해방을 허용하기 전에 확인되는 조건(예: 플레이어 레벨 >= 10)<br>
- 구속 반환율: 노드를 되돌릴 때 반환되는 리소스의 비율(0.0~1.0)

#### Request

|  | 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 |
| --- | --- | --- | --- | --- | --- | --- |
| namespaceName | string |  | ✓|  |  ~ 128자 | 네임스페이스 이름<br>네임스페이스 고유의 이름입니다. 영숫자 및 -(하이픈) _(언더스코어) .(마침표)로 지정합니다. |
| nodeModelName | string |  | ✓|  |  ~ 128자 | 노드 모델명<br>노드 모델 고유의 이름. 영숫자 및 -(하이픈) _(언더스코어) .(마침표)로 지정합니다. |

#### Result

|  | 타입 | 설명 |
| --- | --- | --- |
| item | [EzNodeModel](#eznodemodel) | 노드 모델|

#### 구현 예제




**Unity (UniTask)**
```csharp
    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).NodeModel(
        nodeModelName: "node-0001"
    );
    var item = await domain.ModelAsync();

```

**Unity (Vanilla)**
```cs
    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).NodeModel(
        nodeModelName: "node-0001"
    );
    var future = domain.ModelFuture();
    yield return future;
    var item = future.Result;

```

**Unreal Engine 5**
```cpp
    const auto Domain = Gs2->SkillTree->Namespace(
        "namespace-0001" // namespaceName
    )->NodeModel(
        "node-0001" // nodeModelName
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError())
    {
        return false;
    }

```

**Godot**
```gdscript

var domain = ez.skill_tree.namespace_(
        "namespace-0001"
    ).node_model(
        "node-0001"
    )

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

var result = async_result.result

```


##### 값 변경 이벤트 핸들링




**Unity (UniTask)**
```csharp
    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).NodeModel(
        nodeModelName: "node-0001"
    );
    
    // 이벤트 핸들링 시작
    var callbackId = domain.Subscribe(
        value => {
            // 값이 변화했을 때 호출됨
            // value에는 변경 후의 값이 전달됨
        }
    );

    // 이벤트 핸들링 정지
    domain.Unsubscribe(callbackId);

```

**Unity (Vanilla)**
```cs
    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).NodeModel(
        nodeModelName: "node-0001"
    );
    
    // 이벤트 핸들링 시작
    var callbackId = domain.Subscribe(
        value => {
            // 값이 변화했을 때 호출됨
            // value에는 변경 후의 값이 전달됨
        }
    );

    // 이벤트 핸들링 정지
    domain.Unsubscribe(callbackId);

```

**Unreal Engine 5**
```cpp
    const auto Domain = Gs2->SkillTree->Namespace(
        "namespace-0001" // namespaceName
    )->NodeModel(
        "node-0001" // nodeModelName
    );
    
    // 이벤트 핸들링 시작
    const auto CallbackId = Domain->Subscribe(
        [](TSharedPtr<Gs2::SkillTree::Model::FNodeModel> value) {
            // 값이 변화했을 때 호출됨
            // value에는 변경 후의 값이 전달됨
        }
    );

    // 이벤트 핸들링 정지
    Domain->Unsubscribe(CallbackId);

```

**Godot**
```gdscript

var domain = ez.skill_tree.namespace_(
        "namespace-0001"
    ).node_model(
        "node-0001"
    )

# 이벤트 핸들링 시작
var callback_id = domain.subscribe_model(func(value):
    # 값이 변화했을 때 호출됨
    # value에는 변경 후의 값이 전달됩니다
    pass
)

# 이벤트 핸들링 정지
domain.unsubscribe_model(callback_id)

```


**⚠️ Warning**

이 이벤트는 SDK가 가진 로컬 캐시의 값이 변경되었을 때 호출됩니다.

로컬 캐시는 SDK가 가진 API의 실행, 또는 GS2-Gateway의 알림을 활성화한 GS2-Distributor를 통한 스탬프 시트의 실행, 또는 GS2-Gateway의 알림을 활성화한 GS2-JobQueue의 실행에 의해 변화한 것만이 대상이 됩니다.

따라서 이 방법 이외로 값이 변경된 경우 콜백은 호출되지 않습니다.

---

### listNodeModels

스킬 트리의 전체 노드 목록 조회<br>

스킬 트리의 모든 노드 정의를 조회합니다.<br>
각 노드는 플레이어가 리소스를 소비하여 해방(잠금 해제)할 수 있는 스킬, 능력, 또는 강화를 나타냅니다.<br>

스킬 트리는 분기 구조이며, 노드에는 전제 조건이 있습니다. 이후 노드를 해방하려면 이전 노드를 먼저 해방해 두어야 합니다.<br>
예를 들어 "일반 공격" → "파워 스트라이크" → "크리티컬 슬래시"처럼, 각 노드의 해방에는 이전 노드가 필요합니다.<br>

각 노드 정의에는 다음이 포함됩니다:<br>
- 전제 노드: 이 노드를 해방하기 전에 해방되어 있어야 하는 다른 노드<br>
- 해방 비용: 노드를 해방하기 위해 플레이어가 소비해야 하는 리소스(예: 스킬 포인트, 골드, 재료)<br>
- 해방 조건: 추가로 충족해야 하는 요건(예: 최소 레벨)<br>
- 구속 반환율: 플레이어가 나중에 이 노드를 되돌릴(잠글) 때 반환되는 리소스의 비율<br>

스킬 트리 UI를 구축할 때 사용합니다. 모든 노드를 그리고, 전제 조건에 따라 연결하며, 어떤 노드가 해방 가능한지 표시할 수 있습니다.

#### Request

|  | 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 |
| --- | --- | --- | --- | --- | --- | --- |
| namespaceName | string |  | ✓|  |  ~ 128자 | 네임스페이스 이름<br>네임스페이스 고유의 이름입니다. 영숫자 및 -(하이픈) _(언더스코어) .(마침표)로 지정합니다. |

#### Result

|  | 타입 | 설명 |
| --- | --- | --- |
| items | [List&lt;EzNodeModel&gt;](#eznodemodel) | 노드 모델 리스트|

#### 구현 예제




**Unity (UniTask)**
```csharp
    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    );
    var items = await domain.NodeModelsAsync(
    ).ToListAsync();

```

**Unity (Vanilla)**
```cs
    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    );
    var it = domain.NodeModels(
    );
    List<EzNodeModel> items = new List<EzNodeModel>();
    while (it.HasNext())
    {
        yield return it.Next();
        if (it.Error != null)
        {
            onError.Invoke(it.Error, null);
            break;
        }
        if (it.Current != null)
        {
            items.Add(it.Current);
        }
        else
        {
            break;
        }
    }

```

**Unreal Engine 5**
```cpp
    const auto Domain = Gs2->SkillTree->Namespace(
        "namespace-0001" // namespaceName
    );
    const auto It = Domain->NodeModels(
    );
    TArray<Gs2::UE5::SkillTree::Model::FEzNodeModelPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }

```


##### 값 변경 이벤트 핸들링




**Unity (UniTask)**
```csharp
    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    );
    
    // 이벤트 핸들링 시작
    var callbackId = domain.SubscribeNodeModels(
        () => {
            // 리스트의 요소가 변화했을 때 호출됨
        }
    );

    // 이벤트 핸들링 정지
    domain.UnsubscribeNodeModels(callbackId);

```

**Unity (Vanilla)**
```cs
    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    );
    
    // 이벤트 핸들링 시작
    var callbackId = domain.SubscribeNodeModels(
        () => {
            // 리스트의 요소가 변화했을 때 호출됨
        }
    );

    // 이벤트 핸들링 정지
    domain.UnsubscribeNodeModels(callbackId);

```

**Unreal Engine 5**
```cpp
    const auto Domain = Gs2->SkillTree->Namespace(
        "namespace-0001" // namespaceName
    );
    
    // 이벤트 핸들링 시작
    const auto CallbackId = Domain->SubscribeNodeModels(
        []() {
            // 리스트의 요소가 변화했을 때 호출됨
        }
    );

    // 이벤트 핸들링 정지
    Domain->UnsubscribeNodeModels(CallbackId);

```


**⚠️ Warning**

이 이벤트는 SDK가 가진 로컬 캐시의 값이 변경되었을 때 호출됩니다.

로컬 캐시는 SDK가 가진 API의 실행, 또는 GS2-Gateway의 알림을 활성화한 GS2-Distributor를 통한 스탬프 시트의 실행, 또는 GS2-Gateway의 알림을 활성화한 GS2-JobQueue의 실행에 의해 변화한 것만이 대상이 됩니다.

따라서 이 방법 이외로 값이 변경된 경우 콜백은 호출되지 않습니다.

---

### getStatus

플레이어의 스킬 트리 진행 상황 취득<br>

지정된 프로퍼티 ID의 스킬 트리에서 플레이어가 어떤 노드를 해방(언락)했는지를 취득합니다.<br>

propertyId는 어떤 스킬 트리 인스턴스를 참조할지를 특정합니다. 게임이 캐릭터별로 하나의 스킬 트리를 가지는 경우, propertyId로 캐릭터 ID를 사용할 수 있습니다.<br>
예를 들어 전사 캐릭터와 마법사 캐릭터는 각각 다른 propertyId로 별도의 스킬 트리 진행 상황이 관리됩니다.<br>

스킬 트리 화면을 렌더링할 때 사용합니다. ListNodeModels로 트리 전체 구조를 취득한 후, 플레이어의 해방 상태를 겹쳐서 다음과 같이 표시합니다:<br>
- 해방된 노드(언락됨, 하이라이트 표시)<br>
- 해방 가능한 노드(전제 조건을 충족함, 다음에 해방 가능)<br>
- 잠긴 노드(전제 조건을 아직 충족하지 못함, 회색 처리)

#### Request

|  | 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 |
| --- | --- | --- | --- | --- | --- | --- |
| namespaceName | string |  | ✓|  |  ~ 128자 | 네임스페이스 이름<br>네임스페이스 고유의 이름입니다. 영숫자 및 -(하이픈) _(언더스코어) .(마침표)로 지정합니다. |
| gameSession | GameSession | | ✓|  |  | GameSession |
| propertyId | string |  | ✓|  |  ~ 1024자 | 프로퍼티 ID<br>사용자마다 여러 개의 독립된 스킬트리 인스턴스를 가지기 위한 식별자입니다.<br>동일한 네임스페이스 내에서 플레이어가 서로 다른 캐릭터나 컨텍스트에 대해 별도의 스킬트리를 가지는 시나리오를 구현합니다.<br>최대 1024자. |

#### Result

|  | 타입 | 설명 |
| --- | --- | --- |
| item | [EzStatus](#ezstatus) | 상태|

#### 구현 예제




**Unity (UniTask)**
```csharp
    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        propertyId: "property-0001"
    );
    var item = await domain.ModelAsync();

```

**Unity (Vanilla)**
```cs
    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        propertyId: "property-0001"
    );
    var future = domain.ModelFuture();
    yield return future;
    var item = future.Result;

```

**Unreal Engine 5**
```cpp
    const auto Domain = Gs2->SkillTree->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        GameSession
    )->Status(
        "property-0001" // propertyId
    );
    const auto Future = Domain->Model();
    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.model()
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


##### 값 변경 이벤트 핸들링




**Unity (UniTask)**
```csharp
    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        propertyId: "property-0001"
    );
    
    // 이벤트 핸들링 시작
    var callbackId = domain.Subscribe(
        value => {
            // 값이 변화했을 때 호출됨
            // value에는 변경 후의 값이 전달됨
        }
    );

    // 이벤트 핸들링 정지
    domain.Unsubscribe(callbackId);

```

**Unity (Vanilla)**
```cs
    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        propertyId: "property-0001"
    );
    
    // 이벤트 핸들링 시작
    var callbackId = domain.Subscribe(
        value => {
            // 값이 변화했을 때 호출됨
            // value에는 변경 후의 값이 전달됨
        }
    );

    // 이벤트 핸들링 정지
    domain.Unsubscribe(callbackId);

```

**Unreal Engine 5**
```cpp
    const auto Domain = Gs2->SkillTree->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        GameSession
    )->Status(
        "property-0001" // propertyId
    );
    
    // 이벤트 핸들링 시작
    const auto CallbackId = Domain->Subscribe(
        [](TSharedPtr<Gs2::SkillTree::Model::FStatus> value) {
            // 값이 변화했을 때 호출됨
            // value에는 변경 후의 값이 전달됨
        }
    );

    // 이벤트 핸들링 정지
    Domain->Unsubscribe(CallbackId);

```

**Godot**
```gdscript

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

# 이벤트 핸들링 시작
var callback_id = domain.subscribe_model(func(value):
    # 값이 변화했을 때 호출됨
    # value에는 변경 후의 값이 전달됩니다
    pass
)

# 이벤트 핸들링 정지
domain.unsubscribe_model(callback_id)

```


**⚠️ Warning**

이 이벤트는 SDK가 가진 로컬 캐시의 값이 변경되었을 때 호출됩니다.

로컬 캐시는 SDK가 가진 API의 실행, 또는 GS2-Gateway의 알림을 활성화한 GS2-Distributor를 통한 스탬프 시트의 실행, 또는 GS2-Gateway의 알림을 활성화한 GS2-JobQueue의 실행에 의해 변화한 것만이 대상이 됩니다.

따라서 이 방법 이외로 값이 변경된 경우 콜백은 호출되지 않습니다.

---

### release

스킬 트리 노드 해방<br>

스킬 트리의 하나 이상의 노드를 해방(언락)합니다. 플레이어는 필요한 리소스를 소비하고 노드가 활성화됩니다.<br>

스킬 트리 UI의 "스킬 습득" 또는 "언락" 버튼에 해당합니다. 플레이어가 해방하려는 노드를 탭하면:<br>
1. 모든 전제 노드가 이미 해방되었는지 확인합니다<br>
2. 해방 조건(검증 액션)을 확인합니다 — 예: 최소 플레이어 레벨<br>
3. 해방 비용(소비 액션)을 차감합니다 — 예: 스킬 포인트 3 소비<br>
4. 노드가 해방됨으로 표시됩니다<br>

여러 노드 이름을 전달하여 한 번에 여러 노드를 해방할 수 있습니다. 각 노드에 대해 모든 전제 조건과 비용이 검증됩니다.<br>

전제 조건이 충족되지 않았거나 플레이어의 리소스가 부족한 경우, 해방이 실패하며 리소스는 소비되지 않습니다.

#### Request

|  | 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 |
| --- | --- | --- | --- | --- | --- | --- |
| namespaceName | string |  | ✓|  |  ~ 128자 | 네임스페이스 이름<br>네임스페이스 고유의 이름입니다. 영숫자 및 -(하이픈) _(언더스코어) .(마침표)로 지정합니다. |
| gameSession | GameSession | | ✓|  |  | GameSession |
| nodeModelNames | List&lt;string&gt; |  | ✓|  | 1 ~ 1000 items | 노드 모델 이름 목록 |
| propertyId | string |  | ✓|  |  ~ 1024자 | 프로퍼티 ID<br>사용자마다 여러 개의 독립된 스킬트리 인스턴스를 가지기 위한 식별자입니다.<br>동일한 네임스페이스 내에서 플레이어가 서로 다른 캐릭터나 컨텍스트에 대해 별도의 스킬트리를 가지는 시나리오를 구현합니다.<br>최대 1024자. |

#### Result

|  | 타입 | 설명 |
| --- | --- | --- |
| item | [EzStatus](#ezstatus) | 상태|
| transactionId | string | 발행된 트랜잭션 ID|
| stampSheet | string | 해방 처리 실행에 사용하는 스탬프 시트|
| stampSheetEncryptionKeyId | string | 스탬프 시트의 서명 계산에 사용한 암호화 키 GRN|
| autoRunStampSheet | bool | 트랜잭션 자동 실행이 활성화되어 있는지 여부|
| atomicCommit | bool | 트랜잭션을 원자적으로 커밋할지 여부|
| transaction | string | 발행된 트랜잭션|
| transactionResult | [EzTransactionResult](#eztransactionresult) | 트랜잭션 실행 결과|

#### 구현 예제




**Unity (UniTask)**
```csharp
    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        propertyId: "property-0001"
    );
    var result = await domain.ReleaseAsync(
        nodeModelNames: new List<string> {
            "node-0001",
        }
    );
    // New Experience에서는 스탬프 시트가 SDK 레벨에서 자동으로 실행됩니다.
    // 에러가 발생하면 TransactionException이 발생합니다.
    // TransactionException::Retry()로 재시도할 수 있습니다.

```

**Unity (Vanilla)**
```cs
    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        propertyId: "property-0001"
    );
    var future = domain.ReleaseFuture(
        nodeModelNames: new List<string> {
            "node-0001",
        }
    );
    yield return future;
    if (future.Error != null)
    {
        onError.Invoke(future.Error, null);
        yield break;
    }
    // New Experience에서는 스탬프 시트가 SDK 레벨에서 자동으로 실행됩니다.
    // 에러가 발생하면 TransactionException이 발생합니다.
    // TransactionException::Retry()로 재시도할 수 있습니다.

```

**Unreal Engine 5**
```cpp
    const auto Domain = Gs2->SkillTree->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        GameSession
    )->Status(
        "property-0001" // propertyId
    );
    const auto Future = Domain->Release(
        []
        {
            auto v = TOptional<TArray<FString>>();
            v->Add("node-0001");
            return v;
        }() // nodeModelNames
    );
    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

```


---

### reset

스킬 트리 전체 초기화(전체 리스펙)<br>

스킬 트리의 모든 해방된 노드를 한 번에 미해방 상태로 되돌립니다. 각 노드의 구속 반환율에 따라 리소스가 반환됩니다.<br>

"전체 스킬 초기화" 또는 "전체 리스펙" 버튼에 해당합니다. 플레이어는 스킬 트리를 처음부터 다시 시작합니다.<br>

예를 들어 플레이어가 다양한 비용으로 10개의 노드를 해방한 경우, 10개 모두가 되돌려지고 설정된 반환율에 따라 각각 부분 환불이 이루어집니다.<br>

주요 사용 사례:<br>
- 플레이어가 다른 전략으로 스킬 트리를 다시 구성할 수 있는 "리스펙" 기능<br>
- 모든 플레이어가 처음부터 다시 시작하는 시즌 리셋<br>
- 게임 밸런스 변경으로 현재 빌드가 최적이 아니게 되었을 때 플레이어가 재조정할 수 있도록 하는 경우<br>

Restrain(특정 노드를 선택적으로 되돌림)과 달리, Reset은 의존 관계와 관계없이 트리 전체에 영향을 줍니다.

#### Request

|  | 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 |
| --- | --- | --- | --- | --- | --- | --- |
| namespaceName | string |  | ✓|  |  ~ 128자 | 네임스페이스 이름<br>네임스페이스 고유의 이름입니다. 영숫자 및 -(하이픈) _(언더스코어) .(마침표)로 지정합니다. |
| gameSession | GameSession | | ✓|  |  | GameSession |
| propertyId | string |  | ✓|  |  ~ 1024자 | 프로퍼티 ID<br>사용자마다 여러 개의 독립된 스킬트리 인스턴스를 가지기 위한 식별자입니다.<br>동일한 네임스페이스 내에서 플레이어가 서로 다른 캐릭터나 컨텍스트에 대해 별도의 스킬트리를 가지는 시나리오를 구현합니다.<br>최대 1024자. |

#### Result

|  | 타입 | 설명 |
| --- | --- | --- |
| item | [EzStatus](#ezstatus) | 상태|
| transactionId | string | 발행된 트랜잭션 ID|
| stampSheet | string | 초기화 처리 실행에 사용하는 스탬프 시트|
| stampSheetEncryptionKeyId | string | 스탬프 시트의 서명 계산에 사용한 암호화 키 GRN|
| autoRunStampSheet | bool | 트랜잭션 자동 실행이 활성화되어 있는지 여부|
| atomicCommit | bool | 트랜잭션을 원자적으로 커밋할지 여부|
| transaction | string | 발행된 트랜잭션|
| transactionResult | [EzTransactionResult](#eztransactionresult) | 트랜잭션 실행 결과|

#### 구현 예제




**Unity (UniTask)**
```csharp
    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        propertyId: "property-0001"
    );
    var result = await domain.ResetAsync(
    );
    // New Experience에서는 스탬프 시트가 SDK 레벨에서 자동으로 실행됩니다.
    // 에러가 발생하면 TransactionException이 발생합니다.
    // TransactionException::Retry()로 재시도할 수 있습니다.

```

**Unity (Vanilla)**
```cs
    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        propertyId: "property-0001"
    );
    var future = domain.ResetFuture(
    );
    yield return future;
    if (future.Error != null)
    {
        onError.Invoke(future.Error, null);
        yield break;
    }
    // New Experience에서는 스탬프 시트가 SDK 레벨에서 자동으로 실행됩니다.
    // 에러가 발생하면 TransactionException이 발생합니다.
    // TransactionException::Retry()로 재시도할 수 있습니다.

```

**Unreal Engine 5**
```cpp
    const auto Domain = Gs2->SkillTree->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        GameSession
    )->Status(
        "property-0001" // propertyId
    );
    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

```


---

### restrain

특정 노드를 잠금 상태로 되돌리기<br>

해방된 하나 이상의 노드를 미해방 상태로 되돌립니다. 각 노드의 구속 반환율에 따라 해방 시 소비한 리소스의 일부가 반환됩니다.<br>

"부분적인 리스펙" 기능입니다. 스킬 트리 전체를 초기화하는 대신, 특정 노드만 선택하여 취소할 수 있습니다.<br>

예를 들어 노드 해방에 골드 100이 필요하고 구속 반환율이 0.8(80%)인 경우, 구속 시 골드 80이 반환됩니다.<br>

중요한 규칙:<br>
- 다른 해방된 노드가 의존하고 있는 노드(전제 조건으로 참조되는 노드)는 구속할 수 없습니다<br>
- 다른 해방된 노드의 전제가 되는 노드를 구속하려면, 먼저 의존하는 노드를 구속해야 합니다<br>
- 이를 통해 스킬 트리가 항상 유효한 상태로 유지됩니다(고립된 노드가 발생하지 않습니다)<br>

스킬 트리 UI의 "스킬 취소" 또는 "되돌리기" 기능으로 사용합니다.

#### Request

|  | 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 |
| --- | --- | --- | --- | --- | --- | --- |
| namespaceName | string |  | ✓|  |  ~ 128자 | 네임스페이스 이름<br>네임스페이스 고유의 이름입니다. 영숫자 및 -(하이픈) _(언더스코어) .(마침표)로 지정합니다. |
| gameSession | GameSession | | ✓|  |  | GameSession |
| nodeModelNames | List&lt;string&gt; |  | ✓|  | 1 ~ 1000 items | 노드 모델 이름 목록 |
| propertyId | string |  | ✓|  |  ~ 1024자 | 프로퍼티 ID<br>사용자마다 여러 개의 독립된 스킬트리 인스턴스를 가지기 위한 식별자입니다.<br>동일한 네임스페이스 내에서 플레이어가 서로 다른 캐릭터나 컨텍스트에 대해 별도의 스킬트리를 가지는 시나리오를 구현합니다.<br>최대 1024자. |

#### Result

|  | 타입 | 설명 |
| --- | --- | --- |
| item | [EzStatus](#ezstatus) | 상태|
| transactionId | string | 발행된 트랜잭션 ID|
| stampSheet | string | 미해방 상태로 되돌리는 처리 실행에 사용하는 스탬프 시트|
| stampSheetEncryptionKeyId | string | 스탬프 시트의 서명 계산에 사용한 암호화 키 GRN|
| autoRunStampSheet | bool | 트랜잭션 자동 실행이 활성화되어 있는지 여부|
| atomicCommit | bool | 트랜잭션을 원자적으로 커밋할지 여부|
| transaction | string | 발행된 트랜잭션|
| transactionResult | [EzTransactionResult](#eztransactionresult) | 트랜잭션 실행 결과|

#### 구현 예제




**Unity (UniTask)**
```csharp
    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        propertyId: "property-0001"
    );
    var result = await domain.RestrainAsync(
        nodeModelNames: new List<string> {
            "node-0001",
        }
    );
    // New Experience에서는 스탬프 시트가 SDK 레벨에서 자동으로 실행됩니다.
    // 에러가 발생하면 TransactionException이 발생합니다.
    // TransactionException::Retry()로 재시도할 수 있습니다.

```

**Unity (Vanilla)**
```cs
    var domain = gs2.SkillTree.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        propertyId: "property-0001"
    );
    var future = domain.RestrainFuture(
        nodeModelNames: new List<string> {
            "node-0001",
        }
    );
    yield return future;
    if (future.Error != null)
    {
        onError.Invoke(future.Error, null);
        yield break;
    }
    // New Experience에서는 스탬프 시트가 SDK 레벨에서 자동으로 실행됩니다.
    // 에러가 발생하면 TransactionException이 발생합니다.
    // TransactionException::Retry()로 재시도할 수 있습니다.

```

**Unreal Engine 5**
```cpp
    const auto Domain = Gs2->SkillTree->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        GameSession
    )->Status(
        "property-0001" // propertyId
    );
    const auto Future = Domain->Restrain(
        []
        {
            auto v = TOptional<TArray<FString>>();
            v->Add("node-0001");
            return v;
        }() // nodeModelNames
    );
    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

```


---



