GS2-Dictionary
GS2-Dictionary 에서는 게임 내에서 입수한 아이템이나 캐릭터의 도감 기능을 구현합니다.
기본적으로 GS2-Inventory 의 단순화된 구현 버전이라고 생각하시면 되며, 입수 완료/미입수라는 2값의 소지 상태를 관리할 수 있습니다.
도감 이외의 용도로도 사용할 수 있습니다. 예를 들어 아바타 파츠의 소지 상태가 좋은 예입니다. 아바타 파츠를 가지고 있는지 여부는 파츠별로 2값으로 상태를 관리할 수 있으면 충분하므로 GS2-Dictionary 에 적합합니다. 그 외에도 완료된 튜토리얼 단계 관리, 해금된 컷신 관리, 해금된 BGM 목록 등 2값 상태로 표현할 수 있는 요소 전반에 활용할 수 있습니다.
graph TD Boss["보스를 토벌"] -- 보상으로 엔트리 추가 --> Dictionary["GS2-Dictionary 에 기록"] Shop["상점에서 구매"] -- 보상으로 엔트리 추가 --> Dictionary Dictionary --> Browse["플레이어가 도감을 열람"] Dictionary -- 소지 증명 서명 --> Formation["GS2-Formation 등에서 이용"]
GS2-Inventory 와의 차이점
스택 개념의 유무
GS2-Inventory 는 동일한 아이템을 여러 개 소지할 때 스택이라는 개념이 있습니다. 이는 포션을 최대 99개까지 스택할 수 있고, 99개를 초과하면 두 번째 스택을 생성하는 것과 같은 사양입니다.
이 사양이 있기 때문에 GS2-Inventory 에서는 “포션의 소지 수량을 조회한다"는 API 의 반환값이 리스트 형태입니다. 이는 포션이 여러 스택으로 존재할 가능성이 있기 때문입니다. 이 사양은 포션과 같은 데이터를 관리하는 데는 편리하지만, 도감과 같은 단순한 소지 상태를 관리하기에는 과도한 사양이며, 리스트를 다루면 코드량도 늘어납니다.
입수 처리로 여러 엔트리를 등록 가능
GS2-Inventory 는 포션 입수와 엘릭서 입수를 각각 별도의 API 요청으로 처리해야 합니다. GS2-Dictionary 는 한 번의 API 요청으로 최대 100개의 엔트리를 등록할 수 있습니다.
이를 통해 퀘스트 클리어 보상으로 “쓰러뜨린 몬스터 전종을 한꺼번에 도감에 등록"과 같은 처리를 한 번의 요청으로 효율적으로 수행할 수 있습니다.
엔트리의 검증과 삭제
GS2-Dictionary 에서는 한 번 기록한 엔트리를 삭제하거나, 특정 엔트리를 입수했는지(또는 미입수인지) 검증할 수 있습니다. 기간 한정 이벤트 아이템의 소지 상태를 초기화하거나, 특정 아이템을 입수한 경우에만 퀘스트를 시작할 수 있도록 제한하는 등의 운영이 가능합니다.
기능 비교
| 항목 | GS2-Dictionary | GS2-Inventory |
|---|---|---|
| 상태 관리 | 소지/미소지의 2값 | 수량·스택 관리 |
| 1회 요청당 등록 수 | 최대 100건 | 1건씩 |
| 즐겨찾기 기능 | 있음 | 없음 |
| 소지 증명 서명 | 있음 | 있음(ItemSet 단위) |
| 주요 용도 | 도감, 아바타 파츠, 해금 상태 | 일반적인 아이템 소지 |
마스터 데이터(EntryModel)
도감에 등록 가능한 엔트리를 마스터 데이터로 정의합니다.
EntryModel 의 주요 설정 항목은 다음과 같습니다.
| 항목 | 설명 |
|---|---|
name | 엔트리 이름(플레이어별 기록 키) |
metadata | 클라이언트에서 이용하는 임의의 메타데이터(표시 이름, 이미지 참조, 희귀도 등) |
마스터 데이터의 JSON 예:
{
"version": "2020-04-30",
"entryModels": [
{
"name": "monster-0001",
"metadata": "{\"displayName\":\"슬라임\",\"rarity\":1}"
},
{
"name": "monster-0002",
"metadata": "{\"displayName\":\"고블린\",\"rarity\":2}"
}
]
}마스터 데이터 등록은 관리 콘솔에서 등록하는 방법 외에도, GitHub 에서 데이터를 반영하거나 GS2-Deploy 를 사용하여 CI 에서 등록하는 워크플로우를 구성할 수 있습니다.
즐겨찾기 기능
GS2-Dictionary 에서는 기록된 엔트리에 “즐겨찾기"를 지정할 수 있습니다. AddLikes 나 DeleteLikes 로 즐겨찾기 목록을 관리하고, Likes API 로 목록 조회나 변경 알림 구독이 가능합니다. 즐겨찾기 정보를 활용하면 도감 화면의 필터링이나 UI 에서의 우선 표시를 구현할 수 있습니다.
즐겨찾기 엔트리는 내부적으로 LikeToc 라는 단위로 정리되어 저장되므로, 한 사용자가 대량의 즐겨찾기를 등록하더라도 효율적으로 조회할 수 있습니다.
소지 증명 서명
GS2-Dictionary 는 엔트리의 소지 상태에 대해 서명된 데이터를 발급할 수 있습니다. 다른 GS2 마이크로서비스와 연동할 때, 서버 간 통신을 거치지 않고도 “실제로 해당 엔트리를 소지하고 있음"을 보증할 수 있습니다.
sequenceDiagram participant Player participant Dictionary as GS2-Dictionary participant Formation as GS2-Formation Player ->> Dictionary: GetEntryWithSignature(entry, keyId) Dictionary -->> Player: Body + Signature Player ->> Formation: SetForm(itemId, body, signature) Formation ->> Formation: 서명 검증 Formation -->> Player: 성공
검증하는 측의 서비스는 body 안에 포함된 정보(엔트리 이름이나 사용자ID)를 signature 를 사용하여 검증할 수 있습니다.
스크립트 트리거
네임스페이스에 entryScript 나 duplicateEntryScript 를 설정하면 엔트리 등록 전후나 중복 등록 시에 커스텀 스크립트를 호출할 수 있습니다. 스크립트는 동기·비동기 실행 방식을 선택할 수 있으며, 비동기에서는 GS2-Script 나 Amazon EventBridge 를 이용한 외부 연동도 가능합니다.
설정 가능한 주요 이벤트 트리거와 스크립트 설정명은 다음과 같습니다.
entryScript(완료 알림:entryDone): 엔트리 등록 전후duplicateEntryScript: 이미 등록된 엔트리를 재등록했을 때
duplicateEntryScript 를 이용하면, 예를 들어 “이미 도감에 등록된 희귀 몬스터를 다시 드롭한 경우에는 대신 다른 통화를 지급한다"와 같은 전환 처리를 구현할 수 있습니다.
트랜잭션 액션
GS2-Dictionary 에서는 다음과 같은 트랜잭션 액션을 제공합니다.
- 검증 액션: 엔트리 소지 상황 검증
- 소비 액션: 엔트리 삭제
- 입수 액션: 엔트리 추가
“엔트리 소지 상황 검증"을 검증 액션으로 이용하면, 특정 몬스터를 토벌 완료한(도감에 등록된) 플레이어만 도전할 수 있는 퀘스트나, 특정 아이템을 한 번이라도 입수한 적이 있는 플레이어만 구매할 수 있는 상품과 같은 제한을 설정할 수 있습니다.
도감에 기록하는 방법
도감에 대한 기록(Entry 추가)은 게임 엔진용 SDK 에서 직접 호출할 수 없습니다.
GS2 의 보안 모델상, 클라이언트에서 일방적으로 변경하는 것을 금지하고 서버가 신뢰하는 경로에서만 추가되도록 설계되어 있기 때문입니다.
구체적으로는 다음과 같은 형태로 트랜잭션 액션에 포함시켜 사용합니다.
- GS2-Quest 의 퀘스트 클리어 보상으로 “쓰러뜨린 몬스터를 도감에 등록"하는 입수 액션을 설정
- GS2-Showcase 에서 구매한 아이템의 보상으로 “해당하는 엔트리를 도감에 등록"하는 입수 액션을 설정
- GS2-Mission 의 미션 달성 보상으로 “특정 엔트리를 등록"하는 입수 액션을 설정
구현 예제
도감에 기록 가능한 마스터 데이터 목록 조회
var items = await gs2.Dictionary.Namespace(
namespaceName: "namespace-0001"
).EntryModelsAsync(
).ToListAsync(); const auto Domain = Gs2->Dictionary->Namespace(
"namespace-0001" // namespaceName
);
const auto It = Domain->EntryModels(
);
TArray<Gs2::UE5::Dictionary::Model::FEzEntryModelPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}var iterator = ez.dictionary.namespace_(
"namespace-0001"
).entry_models(
)
var async_result = await iterator.load()
if async_result.error != null:
# 에러 처리
push_error(str(async_result.error))
return
var items = async_result.result도감에 기록된 엔트리 목록 조회
var items = await gs2.Dictionary.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).EntriesAsync(
).ToListAsync(); const auto Domain = Gs2->Dictionary->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
);
const auto It = Domain->Entries(
);
TArray<Gs2::UE5::Dictionary::Model::FEzEntryPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}var iterator = ez.dictionary.namespace_(
"namespace-0001"
).me(
game_session
).entries(
)
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.Dictionary.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Entry(
entryModelName: "entry-0001"
).ModelAsync();
var acquiredAt = item.AcquiredAt; const auto Domain = Gs2->Dictionary->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Entry(
"entry-0001" // entryModelName
);
const auto Future = Domain->Model();
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
const auto Result = Future->GetTask().Result();
const auto AcquiredAt = Result->GetAcquiredAt();var domain = ez.dictionary.namespace_(
"namespace-0001"
).me(game_session).entry(
"entry-0001"
)
var async_result = await domain.model()
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result소지 증명 서명 조회
GS2 내의 다른 마이크로서비스와 연동할 때, 실제로 GS2-Dictionary 에 엔트리가 기록되어 있는지 보증된 데이터를 요구받는 경우가 있습니다.
예를 들어, GS2-Dictionary 로 아바타 파츠의 소지 상태를 관리하고, GS2-Formation 으로 아바타 파츠의 편성 상태를 관리한다고 가정해 봅시다. GS2-Formation 에 헤어스타일을 설정할 때 “hair-0001"이라는 파츠를 설정하도록 API 요청을 보내게 되는데, GS2-Formation 은 소지 증명 서명과 함께 “hair-0001"을 지정하도록 요구합니다.
이를 통해 GS2-Formation 은 뒤에서 GS2-Dictionary 와 통신하여 실제로 소지하고 있는 파츠인지 판단할 필요가 없어집니다.
var result = await gs2.Dictionary.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Entry(
entryModelName: "entry-0001"
).GetEntryWithSignatureAsync(
keyId: "grn:gs2:{region}:{yourOwnerId}:key:namespace-0001:key:key-0001"
);
var body = result.Body;
var signature = result.Signature; const auto Domain = Gs2->Dictionary->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Entry(
"entry-0001" // entryModelName
);
const auto Future = Domain->GetEntryWithSignature(
"grn:gs2:{region}:{yourOwnerId}:key:namespace-0001:key:key-0001" // keyId
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
const auto Future2 = Future->GetTask().Result()->Model();
Future2->StartSynchronousTask();
if (Future2->GetTask().IsError()) return false;
const auto Result = Future2->GetTask().Result();
const auto Body = Result->Body;
const auto Signature = Result->Signature;var domain = ez.dictionary.namespace_(
"namespace-0001"
).me(game_session).entry(
"entry-0001"
)
var async_result = await domain.get_entry_with_signature(
"key-0001" # key_id
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result즐겨찾기 엔트리 등록
var domain = gs2.Dictionary.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
);
var result = await domain.AddLikesAsync(
entryModelNames: new List<string> {
"entry-0001",
}
); const auto Domain = Gs2->Dictionary->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
);
const auto Future = Domain->AddLikes(
{
"entry-0001",
}
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;var domain = ez.dictionary.namespace_(
"namespace-0001"
).me(game_session)
var async_result = await domain.add_likes(
[
"entry-0001",
"entry-0002",
"entry-0003",
] # entry_model_names
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result즐겨찾기 엔트리 목록 조회
var domain = gs2.Dictionary.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
);
var items = await domain.LikesAsync(
).ToListAsync(); const auto Domain = Gs2->Dictionary->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
);
const auto It = Domain->Likes(
);
TArray<Gs2::UE5::Dictionary::Model::FEzLikePtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}var iterator = ez.dictionary.namespace_(
"namespace-0001"
).me(
game_session
).likes(
)
var async_result = await iterator.load()
if async_result.error != null:
# 에러 처리
push_error(str(async_result.error))
return
var items = async_result.result즐겨찾기 엔트리 삭제
var domain = gs2.Dictionary.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
);
await domain.DeleteLikesAsync(
entryModelNames: new List<string> {
"entry-0001",
}
); const auto Domain = Gs2->Dictionary->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
);
const auto Future = Domain->DeleteLikes(
{
"entry-0001",
}
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;var domain = ez.dictionary.namespace_(
"namespace-0001"
).me(game_session)
var async_result = await domain.delete_likes(
[
"entry-0001",
"entry-0002",
"entry-0003",
] # entry_model_names
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result