GS2-Idle
게임을 플레이하지 않은 기간에 따라 보상을 지급하는 구조를 구현합니다. 스마트폰 대상 게임에서는 “오랜만에 앱을 실행했더니 대량의 보상을 획득할 수 있었다"는 디자인이 플레이어의 재방문 촉진에 크게 기여합니다. GS2-Idle은 이러한 방치 보상을 간단하게 도입하기 위한 마이크로서비스입니다.
graph LR Start["대기 시작"] --> Idle["방치 시간 경과"] Idle --> Prediction["획득 예정 보상 조회"] Prediction --> Receive["보상 수령"] Receive --> Start
카테고리
방치 보상은 여러 개를 준비할 수 있습니다. 플레이어는 카테고리별로 1개의 대기 시간을 가질 수 있습니다.
카테고리는 “캐릭터의 스태미나 자연 회복”, “채굴장의 자동 채굴”, “소재의 자동 생성” 등, 각각 독립된 방치 보상의 단위로 활용할 수 있습니다.
대기 시간
카테고리에는 대기 시간(몇 분마다 보상을 얻을 수 있는지)과 대기 시간 최댓값의 초깃값을 설정할 수 있습니다.
대기 시간의 최댓값은 플레이어별로 늘릴 수 있습니다. 보상 수령 후에 미소진된 대기 시간을 초기화할지 다음으로 이월할지는 rewardResetMode로 선택할 수 있습니다.
rewardResetMode | 동작 |
|---|---|
Reset | 보상 수령 시 경과 시간이 0으로 초기화됩니다. 획득 타이밍의 나머지 시간은 폐기됩니다. |
CarryOver | 보상 수령 시 획득한 인터벌만큼 경과 시간에서 차감됩니다. 나머지 시간은 다음 대기에도 이어집니다. |
방치 보상
방치 보상에는 대기 시간이 일정 시간 경과하면 얻을 수 있는 아이템 목록을 정의합니다. 지급하는 보상은 “경험치+아이템"처럼 여러 개(최대 10개)를 설정할 수 있습니다.
또한, 대기 시간 내에서 보상 내용에 변화를 줄 수 있도록 여러 개의 보상 목록을 설정할 수 있습니다.
예를 들어, 10분 대기할 때마다 “경험치+10"과 “강화 소재 Lv.1 x 1"을 얻을 수 있습니다. 단, 60분마다인 타이밍에는 위 보상 대신 “경험치+20"과 “강화 소재 Lv.2 x 1"을 얻을 수 있습니다. 위와 같은 예시를 생각해 봅시다.
이 경우, 방치 보상에는 다음과 같은 테이블을 설정합니다.
| - | 보상1 | 보상2 |
|---|---|---|
| 1 | 경험치+10 | 강화 소재 Lv.1 x 1 |
| 2 | 경험치+10 | 강화 소재 Lv.1 x 1 |
| 3 | 경험치+10 | 강화 소재 Lv.1 x 1 |
| 4 | 경험치+10 | 강화 소재 Lv.1 x 1 |
| 5 | 경험치+10 | 강화 소재 Lv.1 x 1 |
| 6 | 경험치+20 | 강화 소재 Lv.2 x 1 |
이렇게 하면 경과 시간에 따라 보상 아이템 내용을 변경할 수 있습니다. 보상 내용은 순환하므로, 2시간 경과 후에는 “1,2,3,4,5,6,1,2,3,4,5,6” 순서의 아이템을 얻게 됩니다.
방치 보상의 랜덤 추첨
보상 내용에 좀 더 무작위성을 부여하고 싶은 경우가 있습니다. 그러한 경우에는 보상에 GS2-Lottery의 추첨 처리를 설정해 주세요.
기존의 GS2-Lottery에서는 추첨을 수행할 때까지 결과가 정해지지 않지만, GS2-Idle을 사용하는 경우에는 대기 시작 시점에 난수 시드를 생성하고 보상 계산 시 그 난수 시드를 이용해 추첨을 수행함으로써, 대기 도중에도 무작위로 추첨된 아이템 내용을 플레이어에게 제시할 수 있으며, 내용은 불변이 됩니다. (GS2-Lottery의 경품 테이블을 변경하면 이 전제는 무너집니다)
대기 시간의 스케줄 관리
이벤트와 연동한 방치 보상을 구현할 수 있도록, 카테고리별로 GS2-Schedule의 이벤트와 연결할 수 있습니다.
이벤트 개최 기간을 2023-01-01 00:00 ~ 2023-02-01 00:00으로 하고, 2023-01-31 23:00부터 대기를 시작했다고 가정합시다. 이 경우, 2023-02-01 00:00이 되는 시점에 방치 시간 카운트가 정지합니다.
따라서 2023-02-01 01:00에 보상을 수령하는 경우도, 2023-02-01 09:00에 보상을 수령하는 경우도 내용은 동일합니다.
이벤트에 반복 설정이 있는 경우, 반복 횟수가 바뀌는 시점에 방치 시간이 초기화됩니다. 예를 들어 매주 월요일 00:00 ~ 화요일 00:00에 반복되는 이벤트의 경우, 다음 주 월요일 00:00이 되는 순간 전주의 보상을 수령하지 않았더라도 대기 시간은 초기화됩니다.
대기 시간의 스케줄과는 별도로, 보상 수령 가능 기간을 설정할 수 있습니다. 수령 가능 기간 외에도 Prediction으로 예정 보상을 가져올 수는 있지만, 수령 API를 호출하면 오류가 발생합니다.
상태
플레이어는 카테고리별로 1개의 Status를 보유합니다.
Status에는 다음과 같은 정보가 포함됩니다.
| 필드 | 설명 |
|---|---|
idleStartedAt | 대기 시작 시각 |
idleMinutes | 누적 방치 시간(분) |
nextRewardsAt | 다음 보상 획득 타이밍 |
maximumIdleMinutes | 이 플레이어에게 적용되는 방치 시간의 상한 |
randomSeed | GS2-Lottery와 결합한 추첨용 고정 시드 |
구현 예제
대기 시작
처음으로 대기 시간 정보를 가져올 때 대기가 시작됩니다.
var item = await gs2.Idle.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Status(
category: "category-0001"
).ModelAsync(); const auto Future = Gs2->Idle->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Status(
"category-0001" // categoryName
)->Model();
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;var domain = ez.idle.namespace_(
"namespace-0001"
).me(game_session).status(
"category-0001"
)
var async_result = await domain.model()
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result상태(대기 중 정보) 목록 가져오기
var items = await gs2.Idle.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).StatusesAsync(
).ToListAsync(); const auto It = Gs2->Idle->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Statuses(
);
TArray<Gs2::UE5::Idle::Model::FEzStatusPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}var iterator = ez.idle.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보상 내용 확인
획득 예정 보상을 수령 전에 확인하기 위해 Prediction API를 사용합니다.
플레이어에게 “앞으로 N분 기다리면 이만큼의 아이템을 얻을 수 있다"와 같은 사전 안내를 하고 싶은 경우에 활용할 수 있습니다.
var items = await gs2.Idle.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Status(
category: "category-0001"
).PredictionAsync(); const auto Domain = Gs2->Idle->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Status(
"category-0001" // categoryName
);
const auto Future = Domain->Prediction(
);
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();var domain = ez.idle.namespace_(
"namespace-0001"
).me(game_session).status(
"category-0001"
)
var async_result = await domain.prediction(
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result보상 수령
보상을 수령하면 대기 시간이 초기화됩니다.
또한, 대기 시간에 보상 획득 타이밍까지의 나머지가 존재하는 경우도 0으로 초기화됩니다(rewardResetMode에 CarryOver를 지정한 경우에는 나머지 시간이 다음으로 이월됩니다).
await gs2.Idle.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Status(
category: "category-0001"
).ReceiveAsync(); const auto Future = Gs2->Idle->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Status(
"category-0001" // categoryName
)->Receive();
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;var domain = ez.idle.namespace_(
"namespace-0001"
).me(game_session).status(
"category-0001"
)
var async_result = await domain.receive(
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result스크립트 트리거
네임스페이스에 overrideAcquireActionsScriptId나 receiveScript를 설정하면, 보상 산출이나 수령 처리 전후에 커스텀 스크립트를 실행할 수 있습니다.
트리거는 동기·비동기 실행 방식을 선택할 수 있으며, 비동기 처리에서는 GS2-Script나 Amazon EventBridge를 이용한 외부 연계도 가능합니다.
설정할 수 있는 주요 이벤트 트리거와 스크립트 설정명은 다음과 같습니다.
overrideAcquireActionsScriptId: 보상 산출 시 실행되는 동기 스크립트. 동적으로 보상 목록을 덮어쓸 수 있습니다. 플레이어 상태에 따른 보상 교체 등에 활용할 수 있습니다.receiveScript(완료 통지:receiveDone): 보상 수령 전후. 동기 실행으로 수령 허가·거부나 배율 보정, 비동기 실행으로 Amazon EventBridge를 통한 외부 연계가 가능합니다.
트랜잭션 액션
GS2-Idle에서는 다음과 같은 트랜잭션 액션을 제공하고 있습니다.
| 유형 | 액션 | 설명 |
|---|---|---|
| 소비 | Gs2Idle:DecreaseMaximumIdleMinutesByUserId | 최대 방치 시간 감소 |
| 입수 | Gs2Idle:IncreaseMaximumIdleMinutesByUserId | 최대 방치 시간 증가 |
| 입수 | Gs2Idle:SetMaximumIdleMinutesByUserId | 최대 방치 시간 설정 |
| 입수 | Gs2Idle:ReceiveByUserId | 보상 수령 |
“최대 방치 시간 증가"를 입수 액션으로 이용하면, 특정 아이템을 입수했을 때나 플레이어 랭크가 상승했을 때 등, 자동으로 방치 보상을 쌓을 수 있는 상한 시간을 확장하는 처리가 가능해집니다. 이를 통해 플레이어의 성장에 맞춰 더 많은 방치 보상을 축적할 수 있게 되어, 플레이 경험 향상으로 이어질 수 있습니다.
버프에 의한 보정
GS2-Buff를 이용하면, 카테고리 모델의 acquireActions나 플레이어별 maximumIdleMinutes를 버프로 보정하여, 이벤트나 캠페인에 따라 대기 중 보상 내용이나 상한 시간을 동적으로 조정할 수 있습니다.
예를 들어 “주말에는 방치 보상 1.5배”, “특정 칭호를 가진 플레이어는 최대 방치 시간 +60분"과 같은 운영이 가능합니다.
마스터 데이터 운용
마스터 데이터를 등록하면 마이크로서비스에서 사용 가능한 데이터나 동작을 설정할 수 있습니다.
마스터 데이터의 종류에는 다음과 같은 것이 있습니다.
CategoryModel: 대기 시간이나 보상 테이블 설정
다음은 마스터 데이터의 JSON 예시입니다.
{
"version": "2024-04-25",
"categoryModels": [
{
"name": "category-0001",
"metadata": "Stamina",
"rewardIntervalMinutes": 10,
"defaultMaximumIdleMinutes": 360,
"rewardResetMode": "CarryOver",
"acquireActions": [
{
"acquireActions": [
{
"action": "Gs2Experience:AddExperienceByUserId",
"request": "{\"experienceValue\": 10}"
}
]
}
]
}
]
}마스터 데이터 등록은 관리 콘솔에서 등록하는 방법 외에도, GitHub에서 데이터를 반영하거나, GS2-Deploy를 사용해 CI에서 등록하는 등의 워크플로우를 구성할 수 있습니다.