GS2-Ranking
게임의 점수나 클리어 타임을 겨루는 랭킹 기능을 실현합니다.
랭킹에는 2종류가 존재하며 “참가자 전원이 동일한 보드에서 경쟁하는 것"과 “구독한 플레이어의 점수와 경쟁하는 것"이 있습니다. 전자를 글로벌 랭킹, 후자를 스코프 랭킹이라고 부릅니다.
글로벌 랭킹
글로벌 랭킹은 모든 플레이어와 경쟁하기 위한 랭킹 기능을 제공합니다.
GS2-Ranking 에서는 수억 명 이상의 플레이어가 참가하는 대규모 랭킹을 실현할 수 있습니다.
대신, 랭킹의 집계는 리얼타임이 아니라 사전에 설정한 주기로 집계 처리가 이루어지며 그 집계 결과를 바탕으로 순위 계산 등을 수행합니다.
카테고리
랭킹의 종류를 설정합니다. 순위를 매길 때 점수가 큰 쪽이 우수한지, 작은 쪽이 우수한지를 설정해야 합니다.
집계 간격
랭킹의 집계를 수행하는 간격을 설정합니다. 최소 15분, 최대 24시간 범위에서 지정할 수 있습니다.
집계 간격 설정에서 주의해야 할 점은, 여기서 설정하는 간격이 이전 집계 시작 시각으로부터의 간격이 아니라 이전 집계 종료 시각으로부터의 간격이라는 것입니다. 집계 처리에 5분이 걸리는 상황에서 00:00 에 첫 번째 집계가 실행된 경우를 생각해 봅시다.
| 집계 시작 시각 | 집계 종료 시각 |
|---|---|
| 00:00 | 00:05 |
| 00:20 | 00:25 |
| 00:40 | 00:45 |
고정 집계 시각
집계 간격을 24시간으로 설정했을 때, 집계되는 시각을 고정하고 싶다는 요구가 있습니다. 이러한 요구에 대응하기 위해, 지정한 시각이 되면 집계 주기가 되지 않았더라도 집계를 실행하는 기능을 제공하고 있습니다.
집계 간격을 24시간, 고정 집계 시각을 오전 5시로 설정했다고 가정합니다.
| 집계 시작 시각 | 집계 종료 시각 | |
|---|---|---|
| 2020-01-01 05:00 | 2020-01-01 05:05 | |
| 2020-01-02 05:00 | 2020-01-02 05:05 | ← 23시간 55분밖에 경과하지 않았지만, 고정 집계 시각이 되었기 때문에 집계함 |
| 2020-01-03 05:00 | 2020-01-03 05:05 | ← 23시간 55분밖에 경과하지 않았지만, 고정 집계 시각이 되었기 때문에 집계함 |
점수의 유효 범위
점수로 등록을 허용할 값의 범위를 설정할 수 있습니다. 이를 통해 명백히 부적절한 점수 등록이 있을 때 등록 처리를 하지 않고 폐기할 수 있습니다.
이 경우, 부정한 점수의 경계를 조사하기 어렵게 하기 위해 클라이언트에는 오류가 반환되지 않습니다.
점수 등록 가능 기간
점수 등록을 허용하는 기간 설정으로 GS2-Schedule 의 이벤트를 연결할 수 있습니다. 점수 접수 기간 외에 점수를 전송해도 점수는 폐기됩니다.
점수 등록 가능 기간 외에는 집계 처리가 이루어지지 않으므로 집계와 관련된 비용은 발생하지 않지만, 스케줄 판정을 위한 GS2-Schedule 의 API 호출은 발생합니다. 따라서 더 이상 참조하지 않을 것이 명백한 랭킹은 마스터 데이터에서 삭제하는 것을 권장합니다.
랭킹 데이터 접근 가능 기간
랭킹 데이터에 접근 가능한 기간으로 GS2-Schedule 의 이벤트를 연결할 수 있습니다. 이벤트 종료 후 점수 참조도 할 수 없게 하는 경우 등에 활용할 수 있습니다.
랭킹의 세대
카테고리별로 세대를 설정할 수 있습니다. 세대를 변경함으로써 카테고리 이름을 변경하지 않고도 랭킹 등록 내용을 초기화할 수 있습니다.
점수 갱신
점수를 전송할 때 서버에서는 마지막으로 등록된 점수를 유효한 점수로 처리합니다. 따라서 가장 우수한 점수를 랭킹으로 기록하고 싶은 경우, 클라이언트에서 점수의 우열을 판정하여 점수를 전송할지 여부를 판단해야 합니다.
순위 취득
지정한 사용자ID의 플레이어 순위를 취득할 수 있습니다. 이 처리에서는 가능한 한 최신 상황에 가까운 순위를 응답하려고 시도합니다.
사전에 집계된 집계 결과 내용 중에서 최신 점수라면 몇 위가 될지를 계산하여 그 순위를 응답합니다. 따라서 점수 갱신 직후에 순위를 취득한 경우에도, 집계 시각이 되지 않았더라도 최신 점수를 사용한 순위에 해당하는 값을 얻을 수 있습니다.
지정한 점수 주변의 랭킹 취득
점수를 지정하여 그 주변의 랭킹을 취득할 수 있습니다. 동일한 점수가 대량으로 존재하는 경우, 목록의 중심에 지정한 점수가 설정되지 않을 수 있습니다.
스코프 랭킹
스코프 랭킹은 친구 내 랭킹과 같이 극히 일부 플레이어 내에서의 랭킹을 실현합니다.
이 기능을 실현하기 위해, 각 플레이어에게 점수 버킷을 마련하고 플레이어가 점수를 갱신했을 때 자신의 점수를 구독하고 있는 플레이어의 버킷 데이터도 갱신함으로써 각 플레이어는 자신의 버킷 내 점수를 사용하여 랭킹을 계산하는 방식으로 실현하고 있습니다.
마스터 데이터 관리
랭킹의 카테고리는 마스터 데이터로 정의합니다.
CategoryModel: 랭킹의 카테고리 설정(정렬 순서, 집계 간격, 고정 집계 시각, 점수의 유효 범위, 등록 가능 기간, 참조 가능 기간, 세대, 점수 보유 방식 등)
마스터 데이터 등록은 관리 콘솔에서 등록하는 것 외에도, GitHub에서 데이터를 반영하거나 GS2-Deploy를 사용하여 CI에서 등록하는 워크플로우를 구성하는 것도 가능합니다.
스크립트 트리거
GS2-Ranking 에서는 스크립트 트리거를 제공하지 않습니다.
트랜잭션 액션
GS2-Ranking 에서는 트랜잭션 액션을 제공하지 않습니다.
구현 예제
점수 등록
이 API는 편의성을 위해 ApplicationAccess로 호출할 수 있게 되어 있습니다. 하지만 임의의 점수로 전송할 수 있다는 것은 취약점이 됩니다.
따라서 가능하다면 이 API를 클라이언트에서 호출할 수 없도록 설정하고, 신뢰할 수 있는 전송원에서만 점수 등록을 허용하도록 해야 합니다.
예를 들어 아이템 소지 수량 랭킹을 실현하고 싶다면, GS2-Inventory 에서 아이템을 획득할 때 트리거되는 스크립트에서 점수로 아이템 소지 수량을 등록하는 편이 더 안전하게 처리할 수 있습니다.
var result = await gs2.Ranking.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).RankingCategory(
categoryName: "category-0001"
).PutScoreAsync(
score: 1000L,
metadata: null
);
var item = await result.ModelAsync(); const auto Domain = Gs2->Ranking->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->RankingCategory(
"category-0001" // categoryName
);
const auto Future = Domain->PutScore(
1000L,
nullptr // metadata
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;var domain = ez.ranking.namespace_(
"namespace-0001"
).me(game_session).ranking_category(
"category-0001",
null
)
var async_result = await domain.put_score(
1000, # score
null # metadata
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result순위 취득(글로벌)
var item = await gs2.Ranking.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).RankingCategory(
categoryName: "category-0001"
).ModelAsync(
scorerUserId : "user-0001"
); const auto Domain = Gs2->Ranking->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->RankingCategory(
"category-0001" // categoryName
)->Ranking(
"user-0001" // scorerUserId
);
const auto Future = Domain->Model();
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
const auto Item = Future->GetTask().Result();var domain = ez.ranking.namespace_(
"namespace-0001"
).me(game_session).ranking_category(
"category-0001",
null
).ranking(
"user-0001",
null
)
var async_result = await domain.model()
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result랭킹 취득
var items = await gs2.Ranking.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).RankingCategory(
categoryName: "category-0001"
).RankingsAsync(
).ToListAsync(); const auto Domain = Gs2->Ranking->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->RankingCategory(
"category-0001" // categoryName
);
const auto It = Domain->Rankings(
);
TArray<Gs2::UE5::Ranking::Model::FEzRankingPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}var iterator = ez.ranking.namespace_(
"namespace-0001"
).me(
game_session
).ranking_category(
"category-0001"
).rankings(
)
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.Ranking.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).SubscribeAsync(
categoryName: "category-0001",
targetUserId: "user-0002"
);
var item = await result.ModelAsync(); const auto Domain = Gs2->Ranking->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->RankingCategory(
"category-0001" // categoryName
);
const auto Future = Domain->Subscribe(
"user-0002" // targetUserId
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
// obtain changed values / result values
const auto Future2 = Future->GetTask().Result()->Model();
Future2->StartSynchronousTask();
if (Future2->GetTask().IsError()) return false;
const auto Result = Future2->GetTask().Result();var domain = ez.ranking.namespace_(
"namespace-0001"
).me(game_session).ranking_category(
"category-0001",
null
)
var async_result = await domain.subscribe(
"user-0002" # target_user_id
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result다른 플레이어의 구독 해제(스코프)
var result = await gs2.Ranking.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).SubscribeUser(
categoryName: "category-0001",
targetUserId: "user-0002"
).UnsubscribeAsync(
); const auto Domain = Gs2->Ranking->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->RankingCategory(
"category-0001" // categoryName
)->SubscribeUser(
"user-0002" // targetUserId
);
const auto Future = Domain->Unsubscribe(
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;var domain = ez.ranking.namespace_(
"namespace-0001"
).me(game_session).ranking_category(
"category-0001",
null
).subscribe_user(
"user-0002"
)
var async_result = await domain.unsubscribe(
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result구독 중인 플레이어 목록 취득
var items = await gs2.Ranking.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).RankingCategory(
categoryName: "category-0001"
).SubscribeUsersAsync(
).ToListAsync(); const auto Domain = Gs2->Ranking->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->RankingCategory(
"category-0001" // categoryName
);
const auto It = Domain->SubscribeUsers(
);
TArray<Gs2::UE5::Ranking::Model::FEzSubscribeUserPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}var iterator = ez.ranking.namespace_(
"namespace-0001"
).me(
game_session
).ranking_category(
"category-0001"
).subscribe_users(
)
var async_result = await iterator.load()
if async_result.error != null:
# 에러를 처리
push_error(str(async_result.error))
return
var items = async_result.result점수 보유 설정
글로벌 랭킹·스코프 랭킹 모두, 사용자ID별로 보유할 수 있는 점수를 1개로 할지, 여러 개 보유할 수 있게 할지를 설정할 수 있습니다. 등록한 점수 중 마지막 것만 랭킹 대상으로 할지, 등록한 모든 점수를 랭킹 대상으로 할지 선택할 수 있습니다.
모든 점수를 랭킹 대상으로 하는 경우, 데이터양이 늘어나기 쉬우며 GS2 이용 요금에도 영향을 미칩니다. 게임 시스템상 중요한 이유가 없다면, 마지막으로 등록된 점수 1개만을 랭킹 대상으로 하도록 설정하는 것을 권장합니다.