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

# GS2-Limit

횟수 제한 기능




플레이어가 행동할 수 있는 횟수를 제한하기 위한 구조입니다.
"하루에 5번까지 가챠를 뽑을 수 있다", "일주일에 한 번만 받을 수 있는 보상" 등, 게임 내에서 자주 등장하는 횟수 제한을 일괄적으로 관리할 수 있습니다.

```mermaid
graph LR
  Action["플레이어의 행동"] --> CountUp["카운트업 (maxValue 지정)"]
  CountUp -- 상한에 도달하지 않음 --> Success["처리를 허가"]
  CountUp -- 상한에 도달함 --> Failure["처리를 거부"]
  Reset["리셋 주기"] --> Counter["카운터를 0으로 되돌림"]
```

## 카운터

플레이어의 행동 횟수를 표현하기 위한 엔티티로,
카운터의 값을 증가시킬 때 허용 가능한 최댓값을 지정하여 카운트업을 시도함으로써, 최댓값을 초과하는 경우에는 카운트업이 실패하고 후속 처리도 실패시킬 수 있는 메커니즘으로 횟수 제한을 실현합니다.
이때, 카운터 자체에 최댓값이 존재하는 것이 아니라 카운트업 액션에 최댓값을 설정할 수 있다는 점이 특징입니다.

예를 들어, 스태미나 회복 처리를 예로 생각해 봅시다.
많은 게임에서는 하루에 스태미나를 회복할 수 있는 횟수에 상한이 설정되어 있습니다.
그리고 회복을 하면 할수록 회복에 필요한 금액이 상승합니다.

이러한 사양은 다음과 같이 표현할 수 있습니다.

| 티어 | 회복에 필요한 비용 | 실행 가능한 횟수 |
| ------- | --------------- | ------------ |
| Tier.1 | 5 | 10 |
| Tier.2 | 10 | 10 |
| Tier.3 | 20 | 10 |
| Tier.4 | 40 | 10 |

그리고 이를 횟수 제한 카운트업 액션과 회복에 필요한 비용으로 파악한 것이 다음과 같습니다.

| 티어 | 카운터 이름 | 카운터 상승량 | 카운터 최댓값 | 회복에 필요한 비용 |
| ------- | --------------- | ------------ | ------------ | ------------ |
| Tier.1 | RecoveryStaminaCounter | 1 | 10 | 5 |
| Tier.2 | RecoveryStaminaCounter | 1 | 20 | 10 |
| Tier.3 | RecoveryStaminaCounter | 1 | 30 | 20 |
| Tier.4 | RecoveryStaminaCounter | 1 | 40 | 40 |

모든 티어에서 동일한 카운터를 사용하며, 회복에 필요한 비용이 저렴할수록 카운터의 최댓값을 낮게 설정합니다.

이렇게 하면 가장 먼저 가장 저렴한 Tier.1 을 구매할 수 없게 되어 Tier.2 를 구매할 수밖에 없게 되고,
Tier.2 도 머지않아 구매할 수 없게 되어 Tier.3 을 구매할 수밖에 없게 되도록 설계할 수 있습니다.

## 카운터 리셋

카운터에는 리셋 주기를 설정할 수 있습니다.
리셋 주기에는 다음과 같은 종류가 있습니다.

| `resetType` | 설명 |
| --- | --- |
| `notReset` | 리셋하지 않음(수동으로 리셋하지 않는 한 카운트가 영구적으로 유지됨) |
| `daily` | 매일 `resetHour` 시에 리셋 |
| `weekly` | 매주 `resetDayOfWeek` 요일의 `resetHour` 시에 리셋 |
| `monthly` | 매월 `resetDayOfMonth` 일의 `resetHour` 시에 리셋 |
| `days` | 기준일(`anchorTimestamp`)로부터 `days` 일마다 리셋 |

`anchorTimestamp` 를 활용하면 이벤트 시작일로부터 N일마다 리셋하는 등 유연한 스케줄링이 가능합니다.

## 버프에 의한 보정

GS2-Buff 와 연동하면 `CountUp`/`CountUpByUserId` 의 `maxValue` 를 버프로 보정하여 상한값을 일시적으로 증감시킬 수 있습니다.
"기간 한정으로 하루에 뽑을 수 있는 가챠 횟수를 두 배로 늘린다"와 같이 이벤트에 맞춘 완화 조치를 구현할 때 유용합니다.

## 스크립트 트리거

네임스페이스에 `countUpScript` 를 설정하면 카운트업 처리 전후에 커스텀 스크립트를 실행할 수 있습니다.
트리거는 동기·비동기 실행 방식을 선택할 수 있으며, 비동기 처리에서는 GS2-Script 나 Amazon EventBridge 를 이용한 외부 연동도 가능합니다.

설정 가능한 주요 이벤트 트리거와 스크립트 설정명은 다음과 같습니다.

- `countUpScript`(완료 알림: `countUpDone`): 카운트업 처리 전후.

## 트랜잭션 액션

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

| 종류 | 액션 | 설명 |
| --- | --- | --- |
| 검증 | `Gs2Limit:VerifyCounterByUserId` | 카운터 값 검증(일치/미만/초과 등) |
| 소비 | `Gs2Limit:CountUpByUserId` | 카운터 값 가산(카운트업) |
| 입수 | `Gs2Limit:CountDownByUserId` | 카운터 값 감산(카운트다운) |
| 입수 | `Gs2Limit:DeleteCounterByUserId` | 카운터 삭제(리셋) |

"카운터 값 감산(카운트다운)"을 입수 액션으로 이용하면, 특정 아이템을 입수했을 때나 미션 달성 보상으로 제한 횟수를 회복(실질적으로 소비한 횟수를 되돌림)시키는 처리가 가능해집니다. 이를 통해 플레이어의 지속적인 플레이를 촉진하는 보상 설계를 쉽게 할 수 있습니다.

## 마스터 데이터 운용

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

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

- `LimitModel`: 리셋 주기와 상한값

다음은 마스터 데이터의 JSON 예입니다.

```json
{
  "version": "2023-09-04",
  "limitModels": [
    {
      "name": "daily",
      "metadata": "일일 제한",
      "resetType": "daily",
      "resetHour": 5
    },
    {
      "name": "weekly",
      "metadata": "주간 제한",
      "resetType": "weekly",
      "resetDayOfWeek": "monday",
      "resetHour": 5
    }
  ]
}
```

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

## 구현 예제

### 카운터 목록 조회



**Unity**
```csharp

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

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

var iterator = ez.limit.namespace_(
        "namespace-0001"
    ).me(
        game_session
    ).counters(
    )

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.Limit.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Counter(
        limitName: "daily",
        counterName: "counter1"
    ).ModelAsync();
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Limit->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Counter(
        "daily", // limitName
        "counter1" // counterName
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError())
    {
        return false;
    }
```
**Godot**
```gdscript

var domain = ez.limit.namespace_(
        "namespace-0001"
    ).me(game_session).counter(
        "daily",
        "counter1"
    )

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

var result = async_result.result

```


### 카운터 값 증가

이 API 로 카운터의 증가 처리를 수행하는 것은 권장하지 않습니다.
GS2-Exchange / GS2-Showcase / GS2-Quest 와 같이 횟수 제한을 걸고자 하는 대상 처리를 실행하기 위한 대가로서 카운터 값 증가를 설정하는 것을 권장합니다.

`maxValue` 를 초과하는 카운트업을 시도하면 `OverflowException` 이 발생합니다.
클라이언트 측에서 이 예외를 포착하여 "오늘의 상한에 도달했습니다"와 같은 UI 를 표시함으로써 횟수 제한을 표현할 수 있습니다.



**Unity**
```csharp

    try {
        var result = await gs2.Limit.Namespace(
            namespaceName: "namespace-0001"
        ).Me(
            gameSession: GameSession
        ).Counter(
            limitName: "daily",
            counterName: "counter1"
        ).CountUpAsync(
            countUpValue: 1,
            maxValue: 100
        );
    } catch (Gs2.Gs2Limit.Exception.OverflowException e) {
        // The maximum number of times limit has been reached.
    }
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Limit->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Counter(
        "daily", // limitName
        "counter1" // counterName
    );
    const auto Future = Domain->CountUp(
        1, // countUpValue
        100 // maxValue
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError())
    {
        auto e = Future->GetTask().Error();
        if (e->IsChildOf(Gs2::Limit::Error::FOverflowError::Class))
        {
            // The maximum number of times limit has been reached.
        }
        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();
```
**Godot**
```gdscript

var domain = ez.limit.namespace_(
        "namespace-0001"
    ).me(game_session).counter(
        "daily",
        "counter1"
    )

var async_result = await domain.count_up(
    1, # count_up_value
    100 # max_value
)
if async_result.error != null:
    if async_result.error.type == "OverflowException":
        # 횟수 제한 상한에 도달했습니다
        pass
    push_error(str(async_result.error))
    return

var result = async_result.result

```


### 카운터 강제 리셋

카운터의 강제 리셋은 게임 엔진용 SDK 에서는 처리할 수 없습니다.
게임 서버에서 `DeleteCounterByUserId` 를 호출하거나, 트랜잭션의 입수 액션으로 `Gs2Limit:DeleteCounterByUserId` 를 실행함으로써 카운터를 리셋할 수 있습니다.

## 상세 레퍼런스

[GS2-Limit API 레퍼런스](../../api_reference/limit)



