GS2-Showcase
게임 내에서 상품을 판매할 때 사용합니다.
GS2-Exchange와의 차이점은 진열대가 존재한다는 점입니다. 진열대에는 DisplayItem을 진열할 수 있으며, DisplayItem을 구매하기 위해 필요한 대가와 상품을 구매했을 때 얻을 수 있는 보상을 설정할 수 있습니다.
진열대에는 2종류가 있으며, 고정된 DisplayItem이 진열되는 《스탠다드 진열대》와, 일정 간격으로 진열 내용이 랜덤으로 추첨되는 《랜덤 진열대》가 있습니다.
graph TD Master["마스터 데이터"] --> Showcase["스탠다드 진열대<br/>(ShowcaseModel)"] Master --> RandomShowcase["랜덤 진열대<br/>(RandomShowcaseModel)"] Showcase --> DisplayItem["DisplayItem<br/>(SalesItem / SalesItemGroup)"] RandomShowcase --> RandomDisplayItem["RandomDisplayItem<br/>(stock / weight)"] Player["플레이어"] -- Buy --> DisplayItem Player -- RandomShowcaseBuy --> RandomDisplayItem DisplayItem -- "consumeActions / acquireActions" --> Transaction["GS2-Distributor<br/>트랜잭션 실행"] RandomDisplayItem -- "consumeActions / acquireActions" --> Transaction
스탠다드 진열대
스탠다드 진열대는 지정한 DisplayItem이 모두 진열됩니다. DisplayItem에는 2종류가 있으며 《SalesItem》과 《SalesItemGroup》이 있습니다.
SalesItem
SalesItem에는 구매하기 위해 필요한 대가와, 구매했을 때 얻을 수 있는 보상을 설정할 수 있습니다.
SalesItem은 다음 3종류의 액션으로 동작을 표현합니다.
| 항목 | 설명 |
|---|---|
verifyActions | 구매 조건 검증 액션. 지정한 마이크로서비스의 상태를 검증하여 조건을 충족하지 않으면 구매를 거부합니다. |
consumeActions | 구매 시 소비하는 액션. GS2-Money2의 통화나 GS2-Inventory의 아이템 등을 대가로 소비합니다. |
acquireActions | 구매 시 입수하는 액션. GS2-Inventory·GS2-Experience·GS2-Lottery 등, 임의의 마이크로서비스에 보상으로 배포합니다. |
SalesItemGroup
SalesItemGroup은 구매 횟수에 따라 판매되는 SalesItem이 변화하는 구조를 구현합니다.
SalesItemGroup에는 여러 SalesItem을 포함시킬 수 있으며, 목록의 마지막 상품 이외에는 GS2-Limit의 카운터 상승을 대가로 설정해야 합니다. SalesItemGroup을 진열대에 진열할 때는 내부의 SalesItem이 구매 가능한지를 판정하여, 가장 먼저 구매 가능하다고 판정된 상품이 진열됩니다.
이 기능을 이용하면, 첫 구매에 한해 반값으로 상품을 판매하거나, 구매할 때마다 가격이 올라가는 상품을 구현하거나, 10번째 구매에는 덤을 붙이는 등의 상품을 구현할 수 있습니다.
graph LR
Buy["구매 요청"] --> Check1{"SalesItem 1<br/>(첫 구매 한정)"}
Check1 -- 구매 가능 --> Sell1["SalesItem 1 판매"]
Check1 -- 구매 완료 --> Check2{"SalesItem 2<br/>(2~9번째)"}
Check2 -- 구매 가능 --> Sell2["SalesItem 2 판매"]
Check2 -- 상한 도달 --> Check3{"SalesItem 3<br/>(10번째 특전)"}
Check3 -- 구매 가능 --> Sell3["SalesItem 3 판매"]랜덤 진열대
랜덤 진열대는 마스터 데이터에 지정한 DisplayItem 중 지정한 개수가 랜덤으로 추첨되어 진열됩니다.
랜덤 진열대는 다음 파라미터로 동작을 제어합니다.
| 항목 | 설명 |
|---|---|
maximumNumberOfChoice | 한 번의 추첨으로 진열되는 DisplayItem의 최대 개수 |
displayItems | 추첨 대상 RandomDisplayItem 목록. 각 아이템은 weight(추첨 가중치)와 stock(재고 수)을 가집니다 |
baseTimestamp / resetIntervalHours | 진열 내용의 재추첨 간격. 지정 시각을 기점으로 일정 시간마다 진열 내용이 갱신됩니다 |
salesPeriodEventId | GS2-Schedule의 이벤트ID. 판매 가능 기간을 제한하고 싶은 경우에 지정합니다 |
추첨 결과는 플레이어별로 RandomShowcaseStatus로 저장되며, 구매 시 재고가 소비됩니다. 재추첨 간격이 도래하면 다음 접속 시 진열 내용이 갱신됩니다.
버프에 의한 보정
GS2-Buff와 연동하면 DisplayItem 및 RandomDisplayItemModel의 acquireActions·verifyActions·consumeActions를 동적으로 보정할 수 있으며, 랜덤 진열 상품에서는 stock의 덮어쓰기도 가능합니다. 이벤트나 캠페인에 맞춰 보상이나 필요 대가, 재고 수를 유연하게 변경할 수 있습니다.
스크립트 트리거
네임스페이스에 buyScript를 설정하면, 상품 구매 시점에 커스텀 스크립트를 호출할 수 있습니다.
설정할 수 있는 주요 이벤트 트리거와 스크립트 설정명은 다음과 같습니다.
buyScript: 상품 구매 시
이를 활용하면, 구매 처리에 독자적인 검증이나 감사 로그 출력, KPI 집계 등을 끼워 넣을 수 있습니다.
트랜잭션 액션
GS2-Showcase에서는 다음과 같은 트랜잭션 액션을 제공하고 있습니다.
- 소비 액션: 구매 횟수 증가
- 입수 액션: 구매 횟수 감소, 랜덤 진열대의 재추첨
“랜덤 진열대의 재추첨"을 입수 액션으로 이용하면, 특정 아이템을 입수했을 때나 퀘스트 클리어 시의 보상으로서 상점의 진열 내용을 강제로 갱신시키는 등의 처리를, 트랜잭션 내에서 안전하게 실행할 수 있습니다. 이를 통해 플레이어의 진행 상황에 맞춘 시기적절한 상품 라인업 제공이 가능해집니다. 또한 “구매 횟수 감소"를 이용하면, 상품의 구매 제한(한정 상품의 재판매 등)을 개별적으로 회복시키는 등의 운영도 쉬워집니다.
마스터 데이터 운용
마스터 데이터를 등록하면 마이크로서비스에서 사용 가능한 데이터나 동작을 설정할 수 있습니다.
마스터 데이터의 종류에는 다음과 같은 것이 있습니다.
ShowcaseModel: 스탠다드 진열대의 정의.DisplayItem으로서SalesItem또는SalesItemGroup을 진열할 수 있습니다.RandomShowcaseModel: 랜덤 진열대의 정의.RandomDisplayItemModel목록에서maximumNumberOfChoice건을 추첨하여 진열합니다.SalesItem/SalesItemMaster: 단일 판매 상품의 정의.SalesItemGroup/SalesItemGroupMaster: 구매 상황에 따라 진열 내용이 변화하는 상품 그룹의 정의.
마스터 데이터 등록은 관리 콘솔에서 등록하는 방법 외에도, GitHub에서 데이터를 반영하거나, GS2-Deploy를 사용해 CI에서 등록하는 등의 워크플로우를 구성할 수 있습니다.
다음은 랜덤 진열대를 포함한 마스터 데이터의 JSON 예시입니다.
{
"version": "2019-09-13",
"showcases": [
{
"name": "showcase-0001",
"metadata": "일반 상점",
"salesPeriodEventId": null,
"displayItems": [
{
"displayItemId": "display-item-0001",
"type": "salesItem",
"salesItemName": "item-0001"
}
]
}
],
"randomShowcases": [
{
"name": "random-showcase-0001",
"metadata": "일일 상점",
"maximumNumberOfChoice": 4,
"baseTimestamp": 1700000000000,
"resetIntervalHours": 24,
"displayItems": [
{
"name": "display-item-0001",
"weight": 10,
"stock": 3,
"consumeActions": [],
"acquireActions": []
}
]
}
]
}구현 예제
스탠다드 진열대
진열대 가져오기
var item = await gs2.Showcase.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Showcase(
showcaseName: "showcase-0001"
).ModelAsync(); const auto Domain = Gs2->Showcase->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Showcase(
"showcase-0001" // showcaseName
);
const auto Future = Domain->Model();
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
const auto Result = Future->GetTask().Result();var domain = ez.showcase.namespace_(
"namespace-0001"
).me(game_session).showcase(
"showcase-0001"
)
var async_result = await domain.model()
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result진열대 목록 가져오기
진열대 목록을 가져오는 API는 게임 엔진용 SDK에는 제공되지 않습니다. 가져와야 하는 경우, 관리 콘솔 / GS2 CLI / 각종 언어용 범용 SDK(C# / Go / Python / TypeScript / PHP / Java)를 이용해 주세요.
상품 구매
var result = await gs2.Showcase.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Showcase(
showcaseName: "showcase-0001"
).DisplayItem(
displayItemId: "display-item-0001"
).BuyAsync(
quantity: 1,
config: null
);
await result.WaitAsync(); const auto Domain = Gs2->Showcase->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Showcase(
"showcase-0001" // showcaseName
)->DisplayItem(
"display-item-0001" // displayItemId
);
const auto Future = Domain->Buy(
1, // quantity
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.showcase.namespace_(
"namespace-0001"
).me(game_session).showcase(
"showcase-0001"
).display_item(
"display-item-0001"
)
var async_result = await domain.buy(
null, # quantity
null # config
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.resultBuy는 GS2-Distributor를 경유한 트랜잭션 처리로 실행됩니다. 트랜잭션 결과를 반영하려면, 반환값인 EzTransactionDomain의 WaitAsync / Wait를 호출하여 트랜잭션 완료를 대기해 주세요.
랜덤 진열대
진열대 가져오기
var items = await gs2.Showcase.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).RandomShowcase(
showcaseName: "showcase-0001"
).RandomDisplayItemsAsync(
).ToListAsync(); const auto It = Gs2->Showcase->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->RandomShowcase(
"showcase-0001" // showcaseName
)->RandomDisplayItems(
);
TArray<Gs2::UE5::Showcase::Model::FEzRandomDisplayItemPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}var iterator = ez.showcase.namespace_(
"namespace-0001"
).me(
game_session
).random_showcase(
"showcase-0001"
).random_display_items(
)
var async_result = await iterator.load()
if async_result.error != null:
# 오류를 처리
push_error(str(async_result.error))
return
var items = async_result.result상품 구매
var result = await gs2.Showcase.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).RandomShowcase(
showcaseName: "showcase-0001"
).RandomDisplayItem(
displayItemName: "display-item-0001"
).RandomShowcaseBuyAsync(
quantity: 1,
config: null
);
await result.WaitAsync(); const auto Future = Gs2->Showcase->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->RandomShowcase(
"showcase-0001" // showcaseName
)->RandomDisplayItem(
"display-item-0001" // displayItemName
)->RandomShowcaseBuy(
1, // quantity
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.showcase.namespace_(
"namespace-0001"
).me(game_session).random_showcase(
"showcase-0001"
).random_display_item(
"display-item-0001"
)
var async_result = await domain.random_showcase_buy(
1, # quantity
null # config
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result랜덤 진열대 상태 가져오기
랜덤 진열대는 플레이어별로 추첨 결과와 구매 횟수를 RandomShowcaseStatus로 유지하고 있습니다.
다음 재추첨 시각이나 구매 완료 수를 확인하고 싶은 경우에 가져옵니다.
var item = await gs2.Showcase.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).RandomShowcaseStatus(
showcaseName: "showcase-0001"
).ModelAsync(); const auto Domain = Gs2->Showcase->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->RandomShowcaseStatus(
"showcase-0001" // showcaseName
);
const auto Future = Domain->Model();
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
const auto Result = Future->GetTask().Result();var domain = ez.showcase.namespace_(
"namespace-0001"
).me(game_session).random_showcase_status(
"showcase-0001"
)
var async_result = await domain.model()
if async_result.error != null:
push_error(str(async_result.error))
return
var item = async_result.result