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

# GS2-Experience

경험치·랭크 기능




Game as a Service 에서 성장 요소는 필수 불가결합니다.
캐릭터를 성장시키고, 성장한 캐릭터로 더욱 높은 난이도의 콘텐츠에 도전하는 게임 사이클을 구현하기 위한 경험치·랭크 기능을 제공합니다.

GS2-Experience 는 단일 캐릭터의 레벨뿐만 아니라, 여러 프로퍼티(캐릭터, 무기, 파티, 길드 등)의 성장을 하나의 마이크로서비스로 일원 관리할 수 있는 유연한 설계로 되어 있습니다.

## 유스케이스

GS2-Experience 가 상정하는 대표적인 유스케이스는 다음과 같습니다.

- 플레이어 캐릭터의 레벨 관리
- 육성 가능한 장비품의 레벨 관리
- 길드나 팀의 랭크 관리
- 칭호·배틀 패스의 레벨 관리
- 스킬 트리의 각 스킬 숙련도 관리

```mermaid
graph LR
  Player[플레이어] -->|경험치 가산| Exp[GS2-Experience]
  Exp -->|랭크 계산| Status[Status]
  Status -->|임계값 초과| RankUp[랭크업]
  RankUp -->|보상 지급| Inventory[GS2-Inventory]
  RankUp -->|스태미나 최대값 갱신| Stamina[GS2-Stamina]
```

## 랭크

GS2-Experience 는 경험치 값으로부터 자동으로 랭크 값을 계산합니다.
이를 위해서는 마스터 데이터를 이용해 랭크업 임계값이 되는 경험치 테이블을 정의해야 합니다.
Experience Model 에서 랭크업 임계값을 관리하며, 그 아래에 임의의 값을 가지는 프로퍼티ID 를 종속시킬 수 있습니다.
프로퍼티ID 별로 경험치 값이 관리되고, 경험치 값에 따라 랭크가 결정됩니다.

### Experience Model 과 프로퍼티ID 의 관계

```mermaid
graph TD
  N[Namespace] --> EM1["ExperienceModel<br/>character_ssr"]
  N --> EM2["ExperienceModel<br/>character_sr"]
  N --> EM3["ExperienceModel<br/>weapon"]
  EM1 --> P11["프로퍼티ID:<br/>character-001"]
  EM1 --> P12["프로퍼티ID:<br/>character-002"]
  EM3 --> P31["프로퍼티ID:<br/>weapon-001"]
  EM3 --> P32["프로퍼티ID:<br/>weapon-002"]
```

예를 들어 "캐릭터 종류별로 필요 경험치 테이블을 다르게 하고 싶다", "캐릭터별로 랭크 캡을 다르게 하고 싶다" 와 같은 경우, ExperienceModel 을 캐릭터 종류별로 준비하고 프로퍼티ID 에 캐릭터의 개별 ID를 할당함으로써 구현할 수 있습니다.

## 랭크 캡

랭크에는 상한을 설정할 수 있습니다.
랭크의 상한에 도달한 상태에서 경험치를 입수한 경우, 경험치는 파기됩니다.

랭크의 상한은 Experience Model 에서 초기값을 설정하지만, 프로퍼티ID 별로 개별적으로 인상할 수 있습니다.
이를 통해 한계돌파 강화를 실시하면 랭크 캡을 인상하는 것과 같은 사양을 구현할 수 있습니다.

| 설정 항목 | 설명 |
| --- | --- |
| `defaultExperience` | 초기 경험치 |
| `defaultRankCap` | 초기 랭크 캡 |
| `maxRankCap` | 랭크 캡의 상한값(이를 초과하는 `SetRankCap` 은 거부됨) |
| `rankThreshold` | 랭크업에 필요한 경험치 임계값 배열 |

## 스테이터스 정보

Status 는 사용자 × ExperienceModel × 프로퍼티ID 의 조합으로 고유하게 결정되며, 다음 정보를 보유합니다.

| 속성 | 설명 |
| --- | --- |
| `experienceValue` | 현재 경험치 |
| `rankValue` | 현재 랭크 |
| `rankCapValue` | 현재 랭크 캡 |
| `nextRankUpExperienceValue` | 다음 랭크업에 필요한 경험치 |

`nextRankUpExperienceValue` 를 클라이언트에서 참조함으로써, 랭크업까지 필요한 경험치 표시나 진행 바 그리기를 간단하게 수행할 수 있습니다.

## 보상 가산 테이블

Experience Model 에는 랭크에 따라 보상량을 조정하는 `acquireActionRates` 를 설정할 수 있습니다.
배율에는 일반적인 `rates` 외에도, int64 를 초과하는 값을 다룰 수 있는 `bigRates` 도 정의할 수 있어, 인플레이션이 심한 게임에서도 정밀도를 유지한 채 보상량을 제어할 수 있습니다.

예를 들어, "랭크가 높은 플레이어에게는 고난도 퀘스트의 보상을 더 많이 지급한다" 와 같은 조정에 활용할 수 있습니다.
퀘스트 보상으로 랭크에 따른 배율을 입수 액션에 반영하고 싶은 경우, GS2-Quest 의 클리어 보상에 "랭크에 따른 보상 배율 적용" 을 트랜잭션 액션으로 포함시킵니다.

## 스크립트 트리거

네임스페이스에 각종 스크립트를 설정하면, 경험치 변화·랭크 변화·랭크 캡 변화의 전후에 커스텀 스크립트를 호출할 수 있습니다. 스크립트는 동기·비동기 실행 방식을 선택할 수 있으며, 비동기의 경우 GS2-Script 나 Amazon EventBridge 를 통한 외부 처리에도 대응합니다.

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

- `changeExperienceScript`(완료 알림: `changeExperienceDone`): 경험치 변화 전후
- `changeRankScript`(완료 알림: `changeRankDone`): 랭크 변화 전후
- `changeRankCapScript`(완료 알림: `changeRankCapDone`): 랭크 캡 변경 전후

또한 다음 필드에는 스크립트의 GRN 을 직접 지정할 수 있으며, 해당 이벤트가 발생했을 때 스크립트가 호출됩니다.

- `rankCapScriptId`: 랭크 캡 취득 시의 처리
- `overflowExperienceScript`: 랭크 캡 도달 후 잉여 경험치(넘친 경험치)의 처리

`overflowExperienceScript` 를 이용하면, 랭크 캡에 도달한 이후의 잉여 경험치를, 예를 들어 게임 내 통화로 GS2-Inventory 에 변환하는 처리를 자동화할 수 있습니다.

## 마스터 데이터 운용

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

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

- `ExperienceModel`: 랭크 임계값과 보상 가산 테이블

마스터 데이터의 등록은 매니지먼트 콘솔에서 등록하는 것 외에도, GitHub 에서 데이터를 반영하거나, GS2-Deploy 를 사용해 CI 에서 등록하는 등의 워크플로우를 구성하는 것이 가능합니다.

### ExperienceModel 의 JSON 예

```json
{
  "version": "2019-09-04",
  "experienceModels": [
    {
      "name": "character_ssr",
      "metadata": "SSR 캐릭터",
      "defaultExperience": 0,
      "defaultRankCap": 50,
      "maxRankCap": 99,
      "rankThreshold": {
        "metadata": "SSR 랭크 테이블",
        "values": [
          100, 300, 600, 1000, 1500
        ]
      },
      "acquireActionRates": [
        {
          "name": "boost",
          "mode": "double",
          "rates": [1.0, 1.1, 1.2, 1.3, 1.4]
        }
      ]
    }
  ]
}
```

## 버프에 의한 보정

GS2-Buff 와 연동하면, 스테이터스의 `rankCapValue` 나 `AddExperienceByUserId`·`SubExperienceByUserId` 등 액션의
`experienceValue` 를 버프로 보정할 수 있습니다. 이벤트나 캠페인 시에 획득량이나 랭크 상한을 일시적으로 증감시키는 등의 조정을 유연하게 할 수 있습니다.

대표적인 버프 대상은 다음과 같습니다.

| 종별 | 대상 | 보정 가능한 값 |
| --- | --- | --- |
| 모델 | Status | `rankCapValue` |
| 액션 | AddExperienceByUserId | `experienceValue` |
| 액션 | SubExperienceByUserId | `experienceValue` |
| 액션 | AddRankCapByUserId | `rankCapValue` |

## 트랜잭션 액션

GS2-Experience 에서는 다음과 같은 트랜잭션 액션을 제공하고 있습니다.

- 검증 액션: 랭크 검증, 랭크 캡 검증
- 소비 액션: 경험치 감산, 랭크 캡 감산
- 입수 액션: 경험치 가산, 경험치 설정, 랭크 캡 가산, 랭크 캡 설정, 랭크에 따른 보상 배율 적용

"경험치 가산" 을 입수 액션으로 이용함으로써, 상점에서 상품 구매 시나 미션 달성 시의 보상으로 직접 캐릭터의 경험치를 늘려 랭크업시키는 처리가 가능해집니다. 또한 "랭크 캡 가산" 을 보상으로 설정함으로써, 특정 조건을 달성했을 때 자동으로 레벨 상한을 해제하는 운용도 가능합니다.

"랭크 검증" 을 검증 액션으로 이용함으로써, "랭크 10 이상의 캐릭터만 도전할 수 있는 퀘스트" 나 "랭크 50 이상에서 교환 가능한 강화 소재" 와 같은 게임 사양을 마스터 데이터 설정만으로 구현할 수 있습니다.

## 구현 예제

### 경험치 가산

경험치 가산은 게임 엔진용 SDK 로는 처리할 수 없습니다.

GS2-Quest 의 퀘스트 클리어 보상이나, GS2-Enhance 의 강화 보상으로 경험치를 가산하도록 해주세요.

### 랭크 캡 인상

랭크 캡 인상은 게임 엔진용 SDK 로는 처리할 수 없습니다.

GS2-Exchange 에서 강화 소재나, 동일 캐릭터와의 교환 보상으로 랭크 캡을 인상하도록 해주세요.

### 경험치 목록 취득

플레이어가 보유한 모든 Status 를 일괄로 취득합니다.
홈 화면의 스테이터스 표시나 캐릭터 목록 화면에서 활용할 수 있습니다.



**Unity**
```csharp

    var items = await gs2.Experience.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).StatusesAsync(
    ).ToListAsync();
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Experience->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    );
    const auto It = Domain->Statuses(
    );
    TArray<Gs2::UE5::Experience::Model::FEzStatusPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }
```
**Godot**
```gdscript

var iterator = ez.experience.namespace_(
        "namespace-0001"
    ).me(
        game_session
    ).statuses(
    )

var async_result = await iterator.load()
if async_result.error != null:
    # 오류 처리
    push_error(str(async_result.error))
    return

var items = async_result.result

```


### 경험치 취득



**Unity**
```csharp

    var item = await gs2.Experience.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        experienceName: "character_ssr",
        propertyId: "property-0001"
    ).ModelAsync();
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Experience->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Status(
        "character_ssr", // experienceName
        "property-0001" // propertyId
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Item = Future->GetTask().Result();
```
**Godot**
```gdscript

var domain = ez.experience.namespace_(
        "namespace-0001"
    ).me(game_session).status(
        "character_ssr",
        "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

```


### ExperienceModel 목록 취득

클라이언트에서 랭크업에 필요한 경험치 테이블을 참조하고자 할 때 이용합니다.



**Unity**
```csharp

    var items = await gs2.Experience.Namespace(
        namespaceName: "namespace-0001"
    ).ExperienceModelsAsync(
    ).ToListAsync();
```
**Unreal Engine**
```cpp

    const auto It = Gs2->Experience->Namespace(
        "namespace-0001" // namespaceName
    )->ExperienceModels();
    TArray<Gs2::UE5::Experience::Model::FEzExperienceModelPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }
```
**Godot**
```gdscript

var iterator = ez.experience.namespace_(
        "namespace-0001"
    ).experience_models(
    )

var async_result = await iterator.load()
if async_result.error != null:
    # 오류 처리
    push_error(str(async_result.error))
    return

var items = async_result.result

```


### 특정 ExperienceModel 취득



**Unity**
```csharp

    var item = await gs2.Experience.Namespace(
        namespaceName: "namespace-0001"
    ).ExperienceModel(
        experienceName: "character_ssr"
    ).ModelAsync();
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Experience->Namespace(
        "namespace-0001" // namespaceName
    )->ExperienceModel(
        "character_ssr" // experienceName
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Item = Future->GetTask().Result();
```
**Godot**
```gdscript

var domain = ez.experience.namespace_(
        "namespace-0001"
    ).experience_model(
        "experience-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 내의 다른 마이크로서비스와 연동할 때, 실제로 GS2-Experience 에서 랭크나 경험치가 올바른지 보증하는 데이터가 요구되는 경우가 있습니다.

예를 들어, GS2-Experience 에서 플레이어의 랭크를 관리하고 있고, GS2-Stamina 에서 그 랭크에 따라 스태미나의 최대값을 결정한다고 합시다.
GS2-Stamina 는 소지 증명 서명과 함께 랭크를 지정하도록 요구합니다.

이를 통해, GS2-Stamina 는 뒤에서 GS2-Experience 와 통신하여 실제로 해당 랭크에 도달했는지를 판단할 필요가 없어집니다.

```mermaid
sequenceDiagram
  participant Player as 플레이어
  participant Exp as GS2-Experience
  participant Other as 다른 마이크로서비스
  Player->>Exp: GetStatusWithSignature
  Exp-->>Player: Body + Signature
  Player->>Other: API + Body + Signature
  Other->>Other: 서명 검증
  Other-->>Player: 처리 결과
```



**Unity**
```csharp

    var result = await gs2.Experience.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        experienceName: "character_ssr",
        propertyId: "property-0001"
    ).GetStatusWithSignatureAsync(
        keyId: "grn:gs2:{region}:{yourOwnerId}:key:namespace-0001:key:key-0001"
    );

    var body = result.Body;
    var signature = result.Signature;
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Experience->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Status(
        "character_ssr", // experienceName
        "property-0001" // propertyId
    );
    const auto Future = Domain->GetStatusWithSignature(
        "grn:gs2:{region}:{yourOwnerId}:key:namespace-0001:key:key-0001" // keyId
    );
    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();
    const auto Body = Result->Body;
    const auto Signature = Result->Signature;
```
**Godot**
```gdscript

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

var async_result = await domain.get_status_with_signature(
    "key-0001" # key_id
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


## 상세 레퍼런스

[GS2-Experience API 레퍼런스](../../api_reference/experience)



