Documentation index for AI agents

GS2-LoginReward

로그인 보너스 기능

게임에 매일 로그인한 플레이어에게 보상을 지급하는 구조입니다. 일별 아이템 지급이나 7일/30일 주기로 반복되는 로그인 보너스, 놓친 날의 보상을 나중에 보전해 주는 기능 등, 운영 현장에서 자주 사용되는 패턴을 범용적으로 제공합니다.

모드

로그인 보너스의 제공 방식에는 《스케줄 모드》와 《스트리밍 모드》 2가지 방식이 있습니다.

graph LR
  Mode{보너스 모드} --> Schedule["스케줄 모드<br/>(mode: schedule)"]
  Mode --> Streaming["스트리밍 모드<br/>(mode: streaming)"]
  Schedule -- 날짜에 고정 --> SchEx["1일차=금화, 2일차=은화…<br/>놓치면 건너뜀"]
  Streaming -- 순서대로 소화 --> StrEx["1번째=금화, 2번째=은화…<br/>놓쳐도 다음으로 이월"]
  Streaming --> Repeat{"repeat 설정"}
  Repeat -- enabled --> Loop["끝에 도달 후 처음부터 재개"]
  Repeat -- disabled --> Stop["끝에서 정지"]

스케줄 모드

GS2-Schedule 의 이벤트와 연결하여 이용합니다. 보상으로 지급할 트랜잭션 액션을 각 일정별로 정의합니다. 이벤트 시작 일시부터 24시간마다 보상이 변경되며, 각 보상은 1회씩 받을 수 있습니다. 도중에 놓친 보상이 있을 경우 건너뜁니다.

스트리밍 모드

스트리밍 모드는 스트림에 설정된, 보상으로 지급할 트랜잭션 액션을 앞에서부터 순서대로 지급합니다. 받지 않은 날이 있더라도 건너뛰지 않고 스트림의 다음 보상을 받을 수 있습니다.

스트리밍 모드의 로그인 보너스에 GS2-Schedule 의 이벤트를 연결하면, 이벤트 시작 일시부터 24시간마다 스트림의 다음 보상을 받을 수 있습니다. 설정하지 않는 경우, 보상 종류가 바뀌는 시각을 UTC 타임존 기준 24시간 단위로 지정하여 이용합니다.

반복

스트리밍 모드의 로그인 보너스에는 반복 설정을 할 수 있습니다. 반복 설정(repeat: enabled)을 활성화하면, 스트림의 끝에 도달한 경우 다음 날에는 스트림의 처음부터 보상 수령을 다시 시작할 수 있습니다. 이 기능을 이용하면 상시 로그인 보너스를 7일마다 또는 30일마다 반복시킬 수 있습니다.

repeat: disabled 를 선택하면, 스트림의 끝까지 지급한 후에는 해당 보너스로부터의 보상 획득이 중지됩니다. 기간 한정 캠페인 등에 이용할 수 있습니다.

놓친 경우 보전

스케줄 모드에서 보상을 놓친 경우나, 스트리밍 모드에서도 이벤트 개최 기간 중 스트림의 모든 아이템을 획득할 수 없는 상태에 빠졌을 때 사용할 수 있는 기능이 놓친 경우 보전 기능입니다. 설정된 비용을 지불함으로써 놓친 아이템을 획득할 수 있습니다.

놓친 경우 보전을 이용할 수 있는 것은 GS2-Schedule 의 이벤트와 연결된 로그인 보너스뿐이며, 이벤트 시작일로부터 경과한 일수까지의 보상만 받을 수 있습니다. 즉, 미래의 로그인 보너스는 비용을 지불하더라도 받을 수 없습니다.

놓친 경우 보전의 비용으로 missedReceiveReliefConsumeActions 에 GS2-Money2 등의 소비 액션을 설정할 수 있으며, 젬을 소비하여 놓친 날을 되찾는 운영이 가능합니다. 수령 전제 조건으로 missedReceiveReliefVerifyActions 에 검증 액션을 지정할 수도 있습니다.

버프에 의한 보정

GS2-Buff 와 연동하면 보너스 모델의 acquireActionsmissedReceiveReliefConsumeActions 에 버프를 적용하여, 보상 내용이나 놓친 경우 보전 비용을 이벤트에 맞춰 동적으로 조정할 수 있습니다. 예를 들어 “캠페인 기간 중에는 로그인 보너스를 2배로 한다”, “VIP 플레이어는 놓친 경우 보전 비용을 절반으로 한다"와 같은 운영이 가능합니다.

스크립트 트리거

네임스페이스에 receiveScript 를 설정하면 로그인 보너스 수령 처리 전후로 receivereceiveDone 스크립트 훅을 호출할 수 있습니다. 스크립트는 동기·비동기 실행 방식을 선택할 수 있으며, 비동기에서는 GS2-Script 나 Amazon EventBridge 를 이용한 외부 연동에도 대응합니다.

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

  • receiveScript(완료 알림: receiveDone): 로그인 보너스 수령 전후

마스터 데이터 운영

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

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

  • BonusModel: 일일 또는 스트리밍 방식의 보상 정의
마스터 항목설명
name보너스 모델 이름
modeschedule(스케줄) 또는 streaming(스트리밍)
periodEventId연결할 GS2-Schedule 의 이벤트ID(옵션)
resetHour스트리밍 모드에서 이벤트 미연결 시 보상 전환을 수행하는 UTC 시각
repeat스트리밍 시 반복 동작(enabled / disabled)
rewards각 단계에서 지급하는 acquireActions 목록
missedReceiveRelief놓친 경우 보전 활성화(enabled / disabled)
missedReceiveReliefVerifyActions놓친 경우 보전 실행 전 검증 액션
missedReceiveReliefConsumeActions놓친 경우 보전 실행 시 소비 액션

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

{
  "version": "2020-10-19",
  "bonusModels": [
    {
      "name": "bonus-0001",
      "metadata": "7days",
      "mode": "streaming",
      "resetHour": 15,
      "repeat": "enabled",
      "rewards": [
        { "acquireActions": [ { "action": "Gs2Inventory:AcquireItemSetByUserId", "request": "..." } ] },
        { "acquireActions": [ { "action": "Gs2Inventory:AcquireItemSetByUserId", "request": "..." } ] }
      ],
      "missedReceiveRelief": "disabled"
    }
  ]
}

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

트랜잭션 액션

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

소비 액션

액션용도
Gs2LoginReward:MarkReceivedByUserId지정한 단계를 수령 완료로 표시합니다.

획득 액션

액션용도
Gs2LoginReward:DeleteReceiveStatusByUserId수령 상태를 초기화합니다. 처음부터 로그인 보너스를 다시 받게 하고 싶은 경우에 이용합니다.
Gs2LoginReward:UnmarkReceivedByUserId지정한 단계를 미수령 상태로 되돌립니다.

“수령 상태 초기화"를 획득 액션으로 이용함으로써, 특정 아이템을 획득했을 때나 이벤트의 전환점 등에서 로그인 보너스 수령 현황을 처음부터 다시 시작하게 하는 처리가 가능해집니다. 이를 통해 정기적인 캠페인 초기화나 특별한 조건 달성에 따른 보너스 재획득 기회를 플레이어에게 제공할 수 있습니다.

구현 예제

로그인 보너스 수령

ReceiveAsync 는 다음에 받을 수 있는 보너스를 자동으로 수령합니다. 지급물은 EzTransactionDomain 으로 반환되므로, 반환값에 대해 WaitAsync 등을 호출하여 결과를 반영합니다.

    var transaction = await gs2.LoginReward.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Bonus(
    ).ReceiveAsync(
        bonusModelName: "bonus-0001",
        config: null
    );
    await transaction.WaitAsync();
    const auto Future = Gs2->LoginReward->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Bonus(
    )->Receive(
        "bonus-0001", // bonusModelName
        nullptr // config
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Transaction = Future->GetTask().Result();

    const auto Future2 = Transaction->Wait();
    Future2->StartSynchronousTask();
    if (Future2->GetTask().IsError()) return false;
var domain = ez.login_reward.namespace_(
        "namespace-0001"
    ).me(game_session).bonus(
    )

var async_result = await domain.receive(
    "bonus-0001", # bonus_model_name
    null # config
)
if async_result.error != null:
    if async_result.error.type == "AlreadyReceivedException":
        # 오늘의 로그인 보너스는 이미 받았습니다.
        pass
    push_error(str(async_result.error))
    return

var result = async_result.result

놓친 날의 보상 수령(놓친 경우 보전)

MissedReceiveAsync 를 호출함으로써, missedReceiveReliefConsumeActions 에 정의된 비용을 지불하고 과거에 놓친 특정 단계를 받을 수 있습니다.

    var transaction = await gs2.LoginReward.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Bonus(
    ).MissedReceiveAsync(
        bonusModelName: "bonus-0001",
        stepNumber: 2,
        config: null
    );
    await transaction.WaitAsync();
    const auto Future = Gs2->LoginReward->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Bonus(
    )->MissedReceive(
        "bonus-0001", // bonusModelName
        2, // stepNumber
        nullptr // config
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
var domain = ez.login_reward.namespace_(
        "namespace-0001"
    ).me(game_session).bonus(
    )

var async_result = await domain.missed_receive(
    "bonus-0001", # bonus_model_name
    1, # step_number
    null # config
)
if async_result.error != null:
    if async_result.error.type == "AlreadyReceivedException":
        # 오늘의 로그인 보너스는 이미 받았습니다.
        pass
    push_error(str(async_result.error))
    return

var result = async_result.result

로그인 보너스 수령 상태 조회

ReceiveStatusReceivedSteps 에는 각 단계의 수령 완료 여부가 불리언 배열로 저장됩니다. UI 측에서 “○일차 수령 완료"와 같이 달력으로 표시할 때 등에 이용할 수 있습니다.

    var item = await gs2.LoginReward.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).ReceiveStatus(
        bonusModelName: "bonus-0001"
    ).ModelAsync();
    for (var i = 0; i < item.ReceivedSteps.Count; i++) {
        Debug.Log($"step {i + 1}: {(item.ReceivedSteps[i] ? "received" : "not yet")}");
    }
    const auto Domain = Gs2->LoginReward->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->ReceiveStatus(
        "bonus-0001" // bonusModelName
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Item = Future->GetTask().Result();
var domain = ez.login_reward.namespace_(
        "namespace-0001"
    ).me(game_session).receive_status(
        "bonus-0001"
    )

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

var result = async_result.result

로그인 보너스 내용 확인

UI에 “내일 받을 수 있는 보상”, “마지막 날에 받을 수 있는 보상"을 미리보기로 표시할 때는, BonusModelRewards 에서 각 단계의 AcquireActions 를 가져올 수 있습니다.

    var item = await gs2.LoginReward.Namespace(
        namespaceName: "namespace-0001"
    ).BonusModel(
        bonusModelName: "bonus-0001"
    ).ModelAsync();
    foreach (var reward in item.Rewards) {
        foreach (var action in reward.AcquireActions) {
            Debug.Log($"{action.Action}: {action.Request}");
        }
    }
    const auto Domain = Gs2->LoginReward->Namespace(
        "namespace-0001" // namespaceName
    )->BonusModel(
        "bonus-0001" // bonusModelName
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Item = Future->GetTask().Result();
var domain = ez.login_reward.namespace_(
        "namespace-0001"
    ).bonus_model(
        "bonus-0001"
    )

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

var result = async_result.result

상세 레퍼런스