GS2-Lottery
가챠를 구현하기 위한 시스템입니다. GS2-Lottery는 일반 가챠와 박스 가챠를 지원합니다.
graph TD Trigger["추첨 트리거<br/>(GS2-Showcase의 구매 보상 / 퀘스트 클리어 보상 등)"] --> LotteryModel["LotteryModel<br/>(추첨 모델)"] LotteryModel -- "mode = normal" --> Normal["일반 모드 추첨"] LotteryModel -- "mode = box" --> Box["박스 모드 추첨"] Normal --> PrizeTable["경품 테이블<br/>(PrizeTable)"] Box --> BoxData["플레이어 전용 박스"] PrizeTable --> Prize["Prize<br/>(acquireActions)"] BoxData --> Prize Prize --> Acquire["GS2-Distributor<br/>보상 배포"]
일반 가챠
일반 가챠는 지정된 확률로 순수한 추첨을 진행합니다.
경품 테이블
경품 테이블이란 가챠를 실행했을 때 나오는 경품의 배출 확률을 정의한 것입니다. 마스터 데이터로 정의하게 됩니다.
가중치
경품 테이블에서 확률 설정은 경품별 가중치를 설정합니다. 구체적으로는 다음과 같은 테이블을 작성하게 됩니다.
| 가중치 | |
|---|---|
| 경품A | 1 |
| 경품B | 2 |
| 경품C | 4 |
이를 백분율 확률로 해석하면 다음과 같이 해석할 수 있습니다.
| 가중치 | 확률 | |
|---|---|---|
| 경품A | 1 | 14.285% |
| 경품B | 2 | 28.571% |
| 경품C | 4 | 57.143% |
백분율 기준으로 확률을 설정해야 할 경우 모든 값을 더해서 100%가 되도록 고려해야 하지만 가중치 기준으로 확률을 설정할 경우에는 그런 부담을 질 필요가 없습니다.
경품 테이블의 중첩
경품 테이블은 최대 5단계까지 중첩할 수 있습니다.
구체적인 사용 방법으로는
- SSR 캐릭터의 배출 확률을 3%
- SR 캐릭터의 배출 확률을 7%
- R 캐릭터의 배출 확률을 90%
라는 기본 방침에 따라 배출 캐릭터를 설정하고 싶을 때 유용합니다. 이 경우 2단계로, 4개의 경품 테이블을 정의하게 됩니다.
레어도 추첨용 경품 테이블
| 가중치 | 경품 종류 | 추첨 테이블 이름 | |
|---|---|---|---|
| SSR | 3 | 경품 테이블의 중첩 | SSR 캐릭터 추첨용 경품 테이블 |
| SR | 7 | 경품 테이블의 중첩 | SR 캐릭터 추첨용 경품 테이블 |
| R | 90 | 경품 테이블의 중첩 | R 캐릭터 추첨용 경품 테이블 |
SSR 캐릭터 추첨용 경품 테이블
| 가중치 | 경품 종류 | 추첨 테이블 이름 | |
|---|---|---|---|
| SSR-0001 | 1 | 입수 처리 | SSR-0001을 GS2-Dictionary에 기록 |
| SSR-0002 | 1 | 입수 처리 | SSR-0002를 GS2-Dictionary에 기록 |
| SSR-0003 | 1 | 입수 처리 | SSR-0003을 GS2-Dictionary에 기록 |
SR 캐릭터 추첨용 경품 테이블
| 가중치 | 경품 종류 | 추첨 테이블 이름 | |
|---|---|---|---|
| SR-0001 | 1 | 입수 처리 | SR-0001을 GS2-Dictionary에 기록 |
| SR-0002 | 1 | 입수 처리 | SR-0002를 GS2-Dictionary에 기록 |
| SR-0003 | 1 | 입수 처리 | SR-0003을 GS2-Dictionary에 기록 |
R 캐릭터 추첨용 경품 테이블
| 가중치 | 경품 종류 | 추첨 테이블 이름 | |
|---|---|---|---|
| R-0001 | 1 | 입수 처리 | R-0001을 GS2-Dictionary에 기록 |
| R-0002 | 1 | 입수 처리 | R-0002를 GS2-Dictionary에 기록 |
| R-0003 | 1 | 입수 처리 | R-0003을 GS2-Dictionary에 기록 |
박스 가챠
박스 가챠는 마스터 데이터로 정의된 경품을 지정된 수량만큼 박스에 투입하고 추첨 처리를 진행할 때는 박스에서 경품을 꺼내는 방식으로 경품의 내용을 결정합니다.
즉, 100개의 경품 중 1개의 “당첨"을 넣은 박스를 준비한 경우, 100회 이내에 반드시 “당첨” 경품이 추첨됩니다.
박스에 경품 투입
박스 내부의 경품 설정에도 경품 테이블을 사용합니다. 일반 추첨 모드에서는 배출 가중치를 설정했지만, 박스 모드에서는 가중치 파라미터에 박스에 경품을 투입하는 수량을 설정합니다.
| 가중치 | |
|---|---|
| 경품A | 1 |
| 경품B | 2 |
| 경품C | 4 |
이 경우, 박스에는 처음에 7개의 경품이 들어 있으며 “경품A 1개”, “경품B 2개”, “경품C 4개"가 들어 있는 상태가 됩니다.
박스 모드의 추첨 처리는 최초 추첨 시 박스 내용물을 셔플한 배열을 준비하고, 추첨 처리는 그 배열에서 순서대로 경품을 배출합니다. 따라서 한 번이라도 추첨 처리가 이루어진 박스의 경품 테이블 내용을 변경하면 의도하지 않은 동작을 일으킵니다.
박스 리셋
박스는 “박스 리셋” 액션을 통해 내용물을 초기 상태로 되돌릴 수 있습니다. 플레이어마다 독립된 박스를 가지므로, 컴플리트 보상 지급 후 박스를 다시 뽑고 싶거나, 매달 박스를 갱신하고 싶은 용도로 활용할 수 있습니다.
추첨 모델
추첨 모델에서는 경품 테이블 중 추첨 API에서 사용할 수 있는 경품 테이블을 지정합니다. 마스터 데이터로 정의하게 됩니다.
위 예시로 말하면, 추첨 API를 호출했을 때 갑자기 SSR 캐릭터 추첨용 경품 테이블을 사용해 추첨 처리가 이루어지면 불안합니다.
추첨 모델에 다음과 같은 마스터 데이터를 정의해 두고
| 추첨 모델 이름 | 경품 테이블 이름 |
|---|---|
| 가챠 | 레어도 추첨용 경품 테이블 |
추첨 처리를 호출할 때는 추첨 모델 이름에 “가챠"를 지정해 추첨하게 됩니다.
추첨 모드(mode)와 추첨 방법(method)
추첨 모델에서는 mode로 추첨 방식을, method로 경품 테이블 참조 방법을 지정합니다.
mode | 설명 |
|---|---|
normal | 일반 모드(지정된 확률로 매번 독립적으로 추첨을 진행) |
box | 박스 모드(플레이어 고유의 박스에서 경품을 꺼냄) |
method | 설명 |
|---|---|
prize_table | 정적으로 지정된 경품 테이블을 사용 |
script | GS2-Script를 사용해 동적으로 경품 테이블을 선택 |
스크립트를 통한 경품 테이블 선택
method를 script로 설정하고 choicePrizeTableScriptId를 지정하면, 추첨 실행 시 GS2-Script를 호출해 동적으로 사용할 경품 테이블을 선택할 수 있습니다.
플레이어별 확률 변경(천장 시스템)이나, 진행 중인 이벤트에 따른 경품 테이블 전환을 구현할 수 있습니다.
스크립트 트리거
네임스페이스에 lotteryTriggerScriptId를 설정하면, 추첨 처리 전에 GS2-Script를 동기 실행할 수 있습니다. 스크립트에서는 추첨 허용/거부를 판정하거나 추첨 결과를 덮어쓸 수 있습니다.
설정할 수 있는 주요 이벤트 트리거와 스크립트 설정 이름은 다음과 같습니다.
lotteryTriggerScriptId: 추첨 처리 전에 동기 실행
추첨 결과 검증이나, 특정 조건에서의 결과 교체 같은 커스텀 처리를 구현할 수 있습니다.
트랜잭션 액션
GS2-Lottery에서는 다음과 같은 트랜잭션 액션을 제공합니다.
- 입수 액션: 추첨 실행, 박스 리셋
“추첨 실행"을 입수 액션으로 활용하면, 상점에서 상품을 구매하거나 퀘스트를 클리어했을 때의 보상으로 가챠(추첨)를 직접 실행하는 처리가 가능해집니다. 또한 “박스 리셋"을 입수 액션에 포함하면, 특정 아이템을 입수한 시점이나 이벤트의 전환점에서 박스 가챠의 내용물을 자동으로 리셋해 처음부터 다시 뽑을 수 있는 상태로 만드는 제어를 트랜잭션 내에서 안전하게 수행할 수 있습니다.
마스터 데이터 운용
마스터 데이터를 등록함으로써 마이크로서비스에서 사용 가능한 데이터와 동작을 설정할 수 있습니다.
마스터 데이터의 종류에는 다음이 있습니다.
LotteryModel: 추첨 모델. 추첨 방식과 사용할 경품 테이블을 정의합니다.PrizeTable: 경품 테이블. 경품의 가중치와 입수 액션, 또는 중첩된 경품 테이블을 정의합니다.Prize: 경품의 단위.acquireActions로 얻을 수 있는 보상을 지정하며,drawnLimit과limitFailOverPrizeId로 배출 제한과 실패 시 대체 대상을 설정할 수 있습니다.
마스터 데이터의 등록은 관리 콘솔에서 등록하는 방법 외에도, GitHub에서 데이터를 반영하거나 GS2-Deploy를 사용해 CI에서 등록하는 워크플로우를 구성할 수 있습니다.
이하는 마스터 데이터의 JSON 예시입니다.
{
"version": "2019-02-21",
"lotteryModels": [
{
"name": "lottery-0001",
"metadata": "일반 가챠",
"mode": "normal",
"method": "prize_table",
"prizeTableName": "rarity"
}
],
"prizeTables": [
{
"name": "rarity",
"metadata": "레어도 추첨",
"prizes": [
{
"prizeId": "ssr",
"type": "prize_table",
"prizeTableName": "ssr-prizes",
"weight": 3
},
{
"prizeId": "sr",
"type": "prize_table",
"prizeTableName": "sr-prizes",
"weight": 7
},
{
"prizeId": "r",
"type": "prize_table",
"prizeTableName": "r-prizes",
"weight": 90
}
]
}
]
}구현 예제
추첨 실행
추첨 실행은 게임 엔진용 SDK에서는 처리할 수 없습니다.
GS2-Showcase의 상품 구매 보상으로 추첨을 실행하는 방식으로 구현해 주세요. GS2-Showcase의 acquireActions에 GS2-Lottery의 DrawByUserId를 포함시키면, 구매 처리의 트랜잭션으로 추첨을 실행할 수 있습니다.
배출 확률 조회
경품 표시용으로, 현재 플레이어에 대한 배출 확률을 가져올 수 있습니다. 배출 수량 제한(후술)이 걸려 있는 경우, 그 영향이 반영된 확률이 반환됩니다.
var items = await gs2.Lottery.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).ProbabilitiesAsync(
lotteryName: "lottery-0001"
).ToListAsync(); const auto It = Gs2->Lottery->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Probabilities(
"lottery-0001" // lotteryName
);
TArray<Gs2::UE5::Lottery::Model::FEzProbabilityPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}var iterator = ez.lottery.namespace_(
"namespace-0001"
).me(
game_session
).probabilities(
"lottery-0001"
)
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.Lottery.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).BoxItems(
prizeTableName: "prizeTable-0001"
).ModelAsync(); const auto Domain = Gs2->Lottery->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->BoxItems(
"prizeTable-0001" // prizeTableName
);
const auto Future = Domain->Model();
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
const auto Result = Future->GetTask().Result();var domain = ez.lottery.namespace_(
"namespace-0001"
).me(game_session).box_items(
"prizeTable-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 result = await gs2.Lottery.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).BoxItems(
prizeTableName: "prizeTable-0001"
).ResetBoxAsync(
); const auto Future = Gs2->Lottery->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->BoxItems(
"prizeTable-0001" // prizeTableName
)->ResetBox(
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;var domain = ez.lottery.namespace_(
"namespace-0001"
).me(game_session).box_items(
"prizeTable-0001"
)
var async_result = await domain.reset_box(
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result추첨 모델 목록 조회
UI에서 가챠 목록을 표시하는 용도로, 네임스페이스 내에 정의된 추첨 모델을 조회할 수 있습니다.
var items = await gs2.Lottery.Namespace(
namespaceName: "namespace-0001"
).LotteryModelsAsync(
).ToListAsync(); const auto It = Gs2->Lottery->Namespace(
"namespace-0001" // namespaceName
)->LotteryModels();
TArray<Gs2::UE5::Lottery::Model::FEzLotteryModelPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}var iterator = ez.lottery.namespace_(
"namespace-0001"
).lottery_models(
)
var async_result = await iterator.load()
if async_result.error != null:
# 에러를 처리
push_error(str(async_result.error))
return
var items = async_result.result기타 기능
경품 배출 수량 제한
일반 모드로 동작시킬 때, 경품별로 배출 수량의 상한을 설정할 수 있습니다. 이 기능을 사용하면 Probabilities 함수가 응답하는 확률과 실제 확률 사이에 차이가 발생합니다. Probabilities를 사용해 배출 확률을 표시할 때는 이 기능을 사용해서는 안 됩니다.
용도로 상정하고 있는 것은 실물 경품을 배포하기 위한 추첨 처리입니다. 확률 1%로 배출되는 상품권이 있다고 할 때, 상품권이 100장밖에 준비되어 있지 않은 경우 이 기능이 도움이 됩니다. 상품권의 배출 수량 상한을 100으로 설정하고, 실패 시 대체 경품에는 꽝이나 다른 당첨 경품을 설정합니다.
이렇게 설정하면, 이미 상품권을 100장 배출한 상태에서 상품권이 당첨된 경우 상품권 대신 실패 시 대체 경품으로 설정한 경품이 배출됩니다. 실패 시 대체 경품에 수량 제한이 설정되어 있는 경우는 동일한 제한이 적용됩니다.