Documentation index for AI agents

GS2-Exchange

게임 내 리소스 교환 기능

GS2 가 제공하는 마이크로서비스 중에서도 특히 널리 사용되고 있는 기능입니다. 모든 마이크로서비스의 리소스를 전혀 다른 마이크로서비스의 리소스로 변환하는 역할을 담당합니다.

게임 사양에는 리소스 교환에 관한 것이 다수 존재하며, 그때마다 GS2-Exchange 가 활약하게 됩니다.

리소스 교환의 예

강화 소재 변환

GS2-Inventory 로 관리하는 ★1 강화 소재 10개를, 마찬가지로 GS2-Inventory 로 관리하는 ★2 강화 소재 1개와 교환

6시간에 1회 아이템을 입수할 수 있다

GS2-Stamina 로 관리하는 6시간마다 1 회복하는 스태미나 1을, GS2-Inventory 로 관리하는 아이템과 교환

아이템 매각

GS2-Inventory 로 관리하는 아이템을, 마찬가지로 GS2-Inventory 로 관리하는 게임 내 통화와 교환

교환의 종류

GS2-Exchange 는 마스터 데이터에 정의된 교환 비율로 즉시 교환할 수 있는 다이렉트 교환과, 교환을 실행한 후 실제 시간으로 일정 시간이 경과한 뒤 교환 결과를 얻을 수 있는 비동기 교환, 구매할 때마다 비용이 상승하는 비용 상승형 교환의 3가지 모드가 존재합니다.

flowchart LR
  Verify["대가 검증<br/>(verifyActions)"] --> Consume["대가 소비<br/>(consumeActions)"]
  Consume -->|timingType=direct| Acquire["보상 부여<br/>(acquireActions)"]
  Consume -->|timingType=await| Await["Await 오브젝트 생성"]
  Await -. lockTime 경과 .-> Acquire2["AcquireAsync 로 보상 수취"]

교환 동작은 네임스페이스 설정의 enableDirectExchange / enableAwaitExchange 로 모드별로 활성화할 수 있습니다. 레이트 모델의 timingTypedirect 또는 await 로 전환함으로써 개별 레이트가 즉시 교환과 비동기 교환 중 어느 쪽으로 동작할지를 선택할 수 있습니다.

비동기 교환의 동작

비동기 교환을 사용한 경우, 교환을 실행한 시점에 대가가 소비되고, 보상을 받는 대신 Await 오브젝트가 생성됩니다. Await 오브젝트의 생성 시각으로부터 마스터 데이터에 정의된 교환 대기 시간(lockTime 초)이 경과하면 보상을 수취할 수 있습니다.

상태필드
교환 실행 시각exchangedAt
보상 수취 가능 시각acquirableAt
대가로 소비되는 수량count
스킵으로 단축된 초수skipSeconds

거점 강화

마을 건설계 게임에서 거점을 성장시키기 위해 자원을 소비한 후 8시간이 경과하면 실제로 거점의 경험치를 가산한다

원정

파티를 편성하여 모험을 떠난 뒤 3시간 후 모험의 결과로 보상을 수취할 수 있다

비동기 교환의 스킵

비동기 교환의 시간 경과 대기를, 추가 대가를 지불함으로써 스킵할 수 있도록 할 수 있습니다. 일반적으로 이러한 사양을 넣는 경우 수익화를 위한 기능으로 구현되는 경우가 많지만, 현금으로 구매한 GS2-Money 로 관리하는 게임 내 통화를 소비함으로써 대기 시간을 단축할 수 있는 사양을 실현할 수 있습니다.

대기 중 보상의 강제 취득과 삭제

관리 화면이나 API(트랜잭션 액션)를 통해 대기 시간을 무시하고 보상을 강제로 취득하거나, 실행 중인 비동기 교환(Await 오브젝트)을 삭제하여 취소하는 것이 가능합니다.

비용 상승형 교환의 예

강화(인플레 게임)

강화할 때마다 강화에 필요한 골드 소비량이 증가한다

스태미나 회복 비용 증가

스태미나를 구매할 때마다 구매에 필요한 과금 통화 소비량이 상승한다. 구매 횟수는 매일 리셋된다

비용 상승량의 계산

비용 상승량에는 3가지 모드가 존재하며, IncrementalRateModelcalculateType 으로 지정합니다.

linear

baseValue + (coefficientValue * 교환 횟수)

baseValue = 100, coefficientValue = 50

교환 횟수비용
0100
1150
2200
3250
4300

power

coefficientValue * (교환 횟수 + 1) ^ 2

coefficientValue = 50

교환 횟수비용
050
1200
2450
3800
41250

gs2_script

GS2-Script 의 실행 결과를 바탕으로 산출합니다. 복잡한 조건을 바탕으로 비용을 계산하고 싶은 경우에 사용할 수 있습니다.

currentExchangeCount = args.currentExchangeCount
quantity = quantity

cost = 100
for i = 1 , quantity do
	cost = cost + (i + currentExchangeCount - 1) * 50
end

result = {
    cost=cost
}
교환 횟수비용
0100
1150
2200
3250
4300

교환 횟수 관리

IncrementalRateModel 에서는 exchangeCountIdmaximumExchangeCount 를 지정하여, GS2-Limit 의 횟수 제한 모델과 연동해 교환 실행 횟수를 추적하거나, 특정 기간 내의 교환 상한을 설정할 수 있습니다.

exchangeCountId 에 GS2-Limit 의 Counter 모델을 지정하면 교환 횟수가 GS2-Limit 에서 카운트되어, 일 단위·주 단위 등 GS2-Limit 의 리셋 사양에 따라 교환 횟수를 초기화할 수 있습니다. maximumExchangeCount 를 초과하는 횟수의 교환은 거부되므로, 기간 내 구매 상한을 두는 가챠나, 하루에 N 회까지 구매 가능한 아이템 상점 등을 실현할 수 있습니다.

마스터 데이터 운용

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

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

  • RateModel: 즉시 또는 비동기 교환의 레이트 정의
  • IncrementalRateModel: 교환 횟수에 따라 비용이 증가하는 레이트 정의

RateModel 의 주요 필드

필드설명
name레이트 이름(고유)
verifyActions교환 실행 전에 수행하는 검증 액션
consumeActions대가로 소비하는 액션
acquireActions보상으로 부여하는 액션
timingTypedirect(즉시 교환) / await(비동기 교환)
lockTime비동기 교환의 대기 초수

IncrementalRateModel 의 주요 필드

필드설명
name레이트 이름(고유)
consumeAction대가로 소비하는 액션(수량은 비용 계산으로 결정)
acquireActions보상으로 부여하는 액션
calculateType비용 계산 방식(linear / power / gs2_script)
baseValue / coefficientValue비용 계산에서 사용하는 계수
calculateScriptIdgs2_script 사용 시 호출하는 스크립트
exchangeCountId교환 횟수를 관리하는 GS2-Limit 의 카운터
maximumExchangeCount교환 횟수의 상한

마스터 데이터의 JSON 예

{
  "version": "2019-08-19",
  "rateModels": [
    {
      "name": "material_n_to_r",
      "metadata": "N -> R 강화 소재 교환",
      "consumeActions": [
        {
          "action": "Gs2Inventory:ConsumeItemSetByUserId",
          "request": "{\"namespaceName\":\"inventory-0001\",\"inventoryName\":\"material\",\"itemName\":\"n-material\",\"userId\":\"#{userId}\",\"consumeCount\":10}"
        }
      ],
      "timingType": "await",
      "lockTime": 3600,
      "acquireActions": [
        {
          "action": "Gs2Inventory:AcquireItemSetByUserId",
          "request": "{\"namespaceName\":\"inventory-0001\",\"inventoryName\":\"material\",\"itemName\":\"r-material\",\"userId\":\"#{userId}\",\"acquireCount\":1}"
        }
      ]
    }
  ]
}

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

스크립트 트리거

네임스페이스에 다음의 스크립트 설정을 추가하면 교환 처리나 대기 중 보상 수취 전후에 커스텀 스크립트를 실행할 수 있습니다.

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

  • exchangeScript(완료 알림: exchangeDone): 교환 처리 전후
  • incrementalExchangeScript(완료 알림: incrementalExchangeDone): 비용 상승형 교환 전후
  • acquireAwaitScript(완료 알림: acquireAwaitDone): 대기 중 보상 수취 전후

이 스크립트들은 동기·비동기 실행 방식을 선택할 수 있으며, 비동기의 경우 GS2-Script 나 Amazon EventBridge 를 통한 외부 연동에도 대응합니다.

또한 IncrementalRateModel 에서 비용 계산 방식으로 gs2_script 를 선택한 경우, 레이트 단위로 설정한 calculateScriptId 를 호출하여 비용을 산출합니다.

버프에 의한 보정

GS2-Buff 와 연동하면 RateModellockTime 이나 acquireActions·verifyActions·consumeActions, IncrementalRateModelacquireActions·consumeAction·maximumExchangeCount 를 컨텍스트 스택을 통해 동적으로 오버라이드할 수 있어, 이벤트나 캠페인에 맞게 보상이나 대기 시간, 교환 가능 횟수를 유연하게 조정할 수 있습니다.

예를 들어, 기간 한정으로 “강화 소재 교환의 대기 시간을 절반으로 단축”, “가챠의 1일 구매 상한을 증가” 와 같은 시책을, 마스터 데이터를 수정하지 않고 버프 적용만으로 실현할 수 있습니다.

구현 예제

교환 실행(다이렉트)

config 파라미터에는 교환에 연동된 대가·보상 액션의 트랜잭션에서 사용되는 컨텍스트 값을 전달할 수 있습니다.

    var result = await gs2.Exchange.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Exchange(
    ).ExchangeAsync(
        rateName: "rate-0001",
        count: 1
    );
    const auto Domain = Gs2->Exchange->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Exchange(
    );
    const auto Future = Domain->Exchange(
        "rate-0001", // rateName
        1, // count
        nullptr // config
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
var domain = ez.exchange.namespace_(
        "namespace-0001"
    ).me(game_session).exchange(
    )

var async_result = await domain.exchange(
    "rate-0001", # rate_name
    1, # count
    null # config
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

비동기 교환 시작

timingType=await 인 레이트에 대해 ExchangeAsync 를 호출하면, 대가는 즉시 소비되고 보상은 Await 오브젝트에 보류됩니다.

    var result = await gs2.Exchange.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Exchange(
    ).ExchangeAsync(
        rateName: "material_n_to_r",
        count: 1
    );
    const auto Domain = Gs2->Exchange->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Exchange(
    );
    const auto Future = Domain->Exchange(
        "material_n_to_r", // rateName
        1, // count
        nullptr // config
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
var domain = ez.exchange.namespace_(
        "namespace-0001"
    ).me(game_session).exchange(
    )

var async_result = await domain.exchange(
    "rate-0001", # rate_name
    1, # count
    null # config
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

Await 목록 취득

    var items = await gs2.Exchange.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).AwaitsAsync(
    ).ToListAsync();
    const auto Domain = Gs2->Exchange->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    );
    const auto It = Domain->Awaits(
    );
    TArray<Gs2::UE5::Exchange::Model::FEzAwaitPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }
var iterator = ez.exchange.namespace_(
        "namespace-0001"
    ).me(
        game_session
    ).awaits(
    )

var async_result = await iterator.load()
if async_result.error != null:
    # 오류 처리
    push_error(str(async_result.error))
    return

var items = async_result.result

Await 대기 시간 경과 후 보상 수취

대기 시간이 경과한 Await 로부터 보상을 수취합니다. 수취 처리에서는 트랜잭션이 발행되어, acquireActions 에 정의된 보상 부여 처리가 다른 마이크로서비스에 대해 실행됩니다.

    var result = await gs2.Exchange.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Await(
        rateName: "material_n_to_r",
        awaitName: "await-0001"
    ).AcquireAsync(
    );
    const auto Domain = Gs2->Exchange->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Await(
        "await-0001", // awaitName
        "material_n_to_r" // rateName
    );
    const auto Future = Domain->Acquire(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
var domain = ez.exchange.namespace_(
        "namespace-0001"
    ).me(game_session).await_(
        "await-0001"
    )

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

var result = async_result.result

Await 삭제(취소)

대기 중인 Await 를 파기하여 교환을 취소합니다. 대가로 소비한 리소스는 반환되지 않는다는 점에 주의하세요. 반환이 필요한 경우에는 트랜잭션 액션을 이용한 서버 측 스크립트로 보완하세요.

    var result = await gs2.Exchange.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Await(
        rateName: "material_n_to_r",
        awaitName: "await-0001"
    ).DeleteAwaitAsync(
    );
    const auto Domain = Gs2->Exchange->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Await(
        "await-0001", // awaitName
        "material_n_to_r" // rateName
    );
    const auto Future = Domain->DeleteAwait(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
var domain = ez.exchange.namespace_(
        "namespace-0001"
    ).me(game_session).await_(
        "await-0001"
    )

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

var result = async_result.result

대기 시간 스킵

대기 시간을 추가 비용 지불로 스킵하려면, skipByConfig 를 이용해 스킵에 필요한 비용을 설정한 다음, 서버 사이드(GS2-JobQueue 나 GS2-Script 경유)의 API 를 호출합니다. 스킵 처리는 트랜잭션 액션으로도 이용할 수 있으므로, 상점의 스킵 아이템이나 스킵 티켓 같은 상품 설계와 조합할 수 있습니다.

비용 상승형 교환의 실행

    var result = await gs2.Exchange.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Exchange(
    ).IncrementalExchangeAsync(
        rateName: "rate-0001",
        count: 1
    );
    const auto Domain = Gs2->Exchange->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Exchange(
    );
    const auto Future = Domain->IncrementalExchange(
        "rate-0001", // rateName
        1, // count
        nullptr // config
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
var domain = ez.exchange.namespace_(
        "namespace-0001"
    ).me(game_session).exchange(
    )

var async_result = await domain.incremental_exchange(
    "rate-0001", # rate_name
    1, # count
    null # config
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

트랜잭션 액션

GS2-Exchange 에서는 다음과 같은 트랜잭션 액션을 제공하고 있습니다.

  • 소비 액션: 실행 중인 교환 대기(Await)의 삭제
  • 입수 액션: 즉시 교환(Exchange) 실행, 비용 상승형 교환(IncrementalExchange) 실행, 교환 대기(Await) 생성, 대기 중 보상의 강제 취득, 대기 시간 스킵

“보상 강제 취득"을 입수 액션으로 이용함으로써, 특정 아이템을 입수했을 때나 미션 달성 시의 보상으로, 현재 진행 중인 건축이나 원정(비동기 교환)을 즉시 완료시키는 처리가 가능해집니다. 또한 “교환 대기 삭제"를 소비 액션으로 이용함으로써, 진행 중인 프로세스를 중단(취소)시키는 운용도 가능합니다.

상세 레퍼런스