Documentation index for AI agents

GS2-Limit

횟수 제한 기능

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

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

카운터

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

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

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

티어회복에 필요한 비용실행 가능한 횟수
Tier.1510
Tier.21010
Tier.32010
Tier.44010

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

티어카운터 이름카운터 상승량카운터 최댓값회복에 필요한 비용
Tier.1RecoveryStaminaCounter1105
Tier.2RecoveryStaminaCounter12010
Tier.3RecoveryStaminaCounter13020
Tier.4RecoveryStaminaCounter14040

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

이렇게 하면 가장 먼저 가장 저렴한 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/CountUpByUserIdmaxValue 를 버프로 보정하여 상한값을 일시적으로 증감시킬 수 있습니다. “기간 한정으로 하루에 뽑을 수 있는 가챠 횟수를 두 배로 늘린다"와 같이 이벤트에 맞춘 완화 조치를 구현할 때 유용합니다.

스크립트 트리거

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

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

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

트랜잭션 액션

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

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

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

마스터 데이터 운용

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

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

  • LimitModel: 리셋 주기와 상한값

다음은 마스터 데이터의 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 에서 등록하는 워크플로우를 구성할 수 있습니다.

구현 예제

카운터 목록 조회

    var items = await gs2.Limit.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).CountersAsync(
    ).ToListAsync();
    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());
    }
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

카운터 상태 조회

    var item = await gs2.Limit.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Counter(
        limitName: "daily",
        counterName: "counter1"
    ).ModelAsync();
    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;
    }
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 를 표시함으로써 횟수 제한을 표현할 수 있습니다.

    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.
    }
    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();
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 를 실행함으로써 카운터를 리셋할 수 있습니다.

상세 레퍼런스