GS2-Money
게임 내 리소스 중, 현금에 상당하는 가치를 가진 리소스를 다루는 기능입니다. 일본의 자금결제법상 선불식 지급수단(자사형)에 해당하는 자산을 취급하는 경우에는 반드시 이 기능을 이용해야 합니다.
잔액
GS2-Money는 플레이어가 보유한 과금 통화의 잔액을 단순한 수량으로 관리하지 않고, 구매 시점의 가치별로 수량을 관리합니다.
예를 들어, 100엔으로 100개의 과금 통화를 구매한 경우 과금 통화 1개의 가치는 1엔 상당이 됩니다. 동시에 1000엔으로 1200개의 과금 통화를 구매할 수 있다고 합시다. 어디까지나 과금 통화의 단가를 1엔으로 하고 200개는 덤으로 무료 처리하는 것도 하나의 방법이고, 1000엔으로 구매한 경우는 단가를 0.8334엔으로 하여 다른 단가로 취급하는 것도 하나의 방법입니다.
후자와 같은 방식을 채택한 경우, GS2-Money는 “단가 1엔인 과금 통화의 잔액”, “단가 0.8334엔인 과금 통화의 잔액"을 각각 구분하여 관리하는 기능을 가지고 있습니다.
graph TD Wallet["지갑<br/>(슬롯 번호로 구분)"] Wallet --> Paid["유상 통화<br/>(paid)"] Wallet --> Free["무상 통화<br/>(free)"] Paid --> Detail1["단가 1엔의 잔액"] Paid --> Detail2["단가 0.8334엔의 잔액"] Paid --> Detail3["...구매 가치별"]
후자를 선택하는 장점은?
분명 후자는 회계 처리가 복잡해져 이점이 없는 것처럼 느껴질 수도 있습니다. 서비스 제공 측 입장에서는 정확히 그렇습니다. 하지만 입장을 바꿔 게임 플레이어의 입장에서 생각해 봅시다.
게임 안에는 운영 측에서 배포한 현금 상당 가치가 0엔인 과금 통화(통칭 무상 통화)가 있습니다. 플레이어가 과금 통화를 구매하도록 유도하기 위해, 유상으로 구매한 과금 통화(통칭 유상 통화)로만 구매할 수 있는 매력적인 상품을 과금 통화 300개로 판매한다고 합시다.
1000엔으로 1200개의 과금 통화를 구매했을 때, 1000개의 유상 통화와 200개의 무상 통화를 부여하도록 처리한 경우 플레이어는 유상 통화 300개로 구매할 수 있는 상품을 3번밖에 구매할 수 없습니다. 반면, 단가를 0.8334엔으로 하여 1200개 전부를 유상 통화로 취급하는 방법이라면 플레이어는 4번 구매할 수 있습니다. 이 차이는 플레이어 심리에 다소나마 영향을 미칩니다.
회계상의 편의를 우선할지, 플레이어의 이익을 우선할지 신중하게 검토해야 할 사양입니다.
슬롯
GS2-Money에서는 여러 개의 지갑을 가질 수 있습니다. 그 여러 지갑을 구별하기 위한 키가 슬롯입니다.
이 기능은 다른 플랫폼에서 구매한 과금 통화를 반입하지 못하도록 하는 플랫포머가 존재하기 때문에, 그 가이드라인을 준수하기 위해 존재하는 기능입니다.
하지만 이러한 가이드라인은 유상 통화에만 적용되므로, 무상 통화에 대해서는 모든 슬롯에서 공유할 수 있는 기능이 있습니다.
네임스페이스의 shareFree를 true로 설정하면, 무상 통화는 전체 슬롯 공통 잔액으로 취급됩니다.
소비 우선순위
플레이어가 과금 통화를 소비할 때, 무상 통화를 우선 소비할지, 유상 통화를 우선 소비할지를 선택할 수 있습니다. 일반적으로 무상 통화를 우선 소비하는 사양이 채택되지만, 회계상의 사정이 있는 경우에는 유상 통화를 우선할 수 있습니다. 유상 통화를 소비하는 경우에는 단가가 더 높은 통화부터 우선적으로 소비됩니다.
네임스페이스의 priority로 다음 중 하나를 지정합니다.
| priority | 설명 |
|---|---|
free | 무상 통화를 우선 소비 |
paid | 유상 통화(단가가 높은 것부터)를 우선 소비 |
무상·유상을 불문하고, 혹은 유상 통화 안에서 입수한 순서대로 소비하고 싶다는 요구가 있다는 점은 저희도 알고 있습니다. 다만 현재는 그 기능이 구현되어 있지 않습니다. 이 요건이 중요한 프로젝트를 검토 중이시라면 개발팀으로 문의해 주세요.
영수증 검증
게임 배포 플랫폼에서 추가 콘텐츠 구매 시 발급되는 영수증의 검증 기능을 갖추고 있습니다. 영수증을 검증하여 플랫포머가 올바르게 발행한 내용인지 확인함과 동시에, 과거에 게임 내에서 사용한 적이 없는지도 확인합니다.
이 기능을 이용함으로써, 부정한 영수증을 이용해 과금 통화를 입수하려는 공격을 회피할 수 있습니다.
영수증 검증을 활성화하려면 네임스페이스에 다음 인증 정보를 등록합니다.
appleKey: Apple App Store의 번들 IDgoogleKey: Google Play의 공개키enableFakeReceipt: 개발 시 더미 영수증을 허용할지 여부
트랜잭션 로그
영수증 검증 이력은 물론이고, 과금 통화의 가산·감산 이력도 전부 기록됩니다.
그리고 하루에 여러 번, 현재 게임 내에 미사용 과금 통화가 현금 상당액으로 얼마나 쌓여 있는지를 집계합니다.
집계 결과는 네임스페이스의 balance 필드에 반영됩니다.
상황에 따라 미사용 잔액의 일부를 제3자 기관에 공탁해야 하는 대응이 필요해질 수 있는데, 그럴 때 이 계산 결과를 이용할 수 있습니다.
법적 절차
GS2-Money는 각종 법적 절차를 위해 필요한 데이터를 수집하여 API를 통해 접근 가능한 상태로 보관하지만, 법적 절차 자체는 GS2 이용자인 귀하 또는 귀하가 소속된 조직이 실행해야 합니다.
어떤 절차가 필요한지에 대해서는 GS2가 책임을 질 수 없으므로 조언도 드릴 수 없습니다. 고문 변호사와 상담해 주시기 바랍니다.
스크립트 트리거
네임스페이스에 각종 스크립트 설정을 등록하면, 입출금 처리나 지갑 생성 시점에 커스텀 스크립트를 실행할 수 있습니다.
설정할 수 있는 주요 이벤트 트리거와 스크립트 설정 이름은 다음과 같습니다.
createWalletScript: 지갑 생성 시depositScript: 입금 처리 시withdrawScript: 출금 처리 시
트랜잭션 액션
GS2-Money에서는 다음 트랜잭션 액션을 제공하고 있습니다.
- 소비 액션: 잔액 소비, 영수증 기록
- 입수 액션: 잔액 가산, 영수증 기록 삭제
“잔액 가산"을 입수 액션으로 이용함으로써, 특정 이벤트 클리어 시나 가챠의 “보너스"로 직접 과금 통화(유상·무상)를 부여하는 처리를, 트랜잭션 내에서 안전하게 실행할 수 있습니다. 이를 통해 과금과 게임 내 보상을 조합한 유연한 시책이 가능해집니다.
구현 예제
잔액 취득
지갑 슬롯을 지정하여, 현재의 유상 통화 잔액(paid), 무상 통화 잔액(free), 구매 단가별 내역(detail)을 취득합니다.
var item = await gs2.Money.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Wallet(
slot: 0
).ModelAsync(); const auto Domain = Gs2->Money->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Wallet(
0 // slot
);
const auto Item = Domain->Model();var domain = ez.money.namespace_(
"namespace-0001"
).me(game_session).wallet(
0
)
var async_result = await domain.model()
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result잔액 가산
잔액을 가산하는 처리는 게임 엔진용 SDK에서는 처리할 수 없습니다.
GS2-Showcase의 구매 시 보상으로 잔액을 가산하거나, GS2-Money의 영수증 검증 API를 서버 간 통신으로 호출하여 부여하는 방법으로 구현해 주세요.
잔액 소비
이 API로 직접 잔액 소비 처리를 하는 것은 권장하지 않습니다. GS2-Showcase와 같은 서비스를 통해 과금 통화를 소비함으로써, 소비와 맞바꿔 상품을 부여하는 처리를 하나의 트랜잭션으로 안전하게 다룰 수 있습니다.
paidOnly를 true로 지정하면, 유상 통화만을 소비 대상으로 할 수 있습니다.
플랫포머의 가이드라인을 준수해야 하는 상황에서 이용해 주세요.
var result = await gs2.Money.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Wallet(
slot: 0
).WithdrawAsync(
count: 50,
paidOnly: false
);
var item = await result.ModelAsync();
var price = result.Price; const auto Future = Gs2->Money->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Wallet(
0 // slot
)->Withdraw(
50,
nullptr // paidOnly
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;var domain = ez.money.namespace_(
"namespace-0001"
).me(game_session).wallet(
0
)
var async_result = await domain.withdraw(
50, # count
null # paid_only
)
if async_result.error != null:
if async_result.error.type == "ConflictException":
# 지갑 조작 처리가 충돌했습니다. 재시도가 필요합니다
pass
if async_result.error.type == "InsufficientException":
# 지갑 잔액이 부족합니다
pass
push_error(str(async_result.error))
return
var result = async_result.result