GS2-Limit
플레이어가 행동할 수 있는 횟수를 제한하기 위한 구조입니다. “하루에 5번까지 가챠를 뽑을 수 있다”, “일주일에 한 번만 받을 수 있는 보상” 등, 게임 내에서 자주 등장하는 횟수 제한을 일괄적으로 관리할 수 있습니다.
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 예입니다.
{
"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 를 실행함으로써 카운터를 리셋할 수 있습니다.