GS2-SeasonRating SDK for Game Engine API 레퍼런스
모델
EzBallot
투표용지
특정 매치 세션에 대해 각 플레이어에게 발급되는 투표권입니다.
대전 컨텍스트(시즌, 세션, 참가 인원 수)와 플레이어의 사용자 ID를 포함합니다.
투표용지는 서버에 의해 서명되어 플레이어에게 반환되며, 플레이어는 게임 결과를 기입하여 WrittenBallot으로 다시 제출합니다.
투표 집계 시, 모든 참가자의 투표용지를 다수결로 비교하여 결과 조작을 방지합니다.
| 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 | |
|---|---|---|---|---|---|---|
| userId | string | ✓ | ~ 128자 | 사용자ID | ||
| seasonName | string | ✓ | ~ 128자 | 시즌 이름 이 대전의 레이팅 계산에 사용되는 시즌 모델의 이름. 이 대전에 적용되는 티어 구조와 포인트 변동 규칙을 정의하는 SeasonModel을 참조합니다. | ||
| sessionName | string | ✓ | ~ 128자 | 세션 이름 이 투표용지가 속한 매치 세션의 이름. 동일한 세션 내의 모든 투표용지는 투표 프로세스에서 함께 집계됩니다. | ||
| numberOfPlayer | int | ✓ | 2 ~ 10 | 참가 인원 수 이 대전의 총 참가자 수. 투표 완료 여부 판정에 사용됩니다. 결과 확정에는 전체 투표용지의 수집(또는 과반수 성립)이 필요합니다. 유효 범위: 2~10. |
EzSeasonModel
시즌 모델
시즌 기간 동안 적용되는 티어 구조와 포인트 변동 규칙을 정의하는 마스터 데이터입니다.
각 티어별 포인트 변동 범위·참가료·랭크업 보너스를 설정하고, 포인트 관리에 사용할 Experience 모델을 지정합니다.
실제 포인트 및 티어의 사용자 데이터는 GS2-Experience에 의해 관리됩니다.
| 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 | |
|---|---|---|---|---|---|---|
| seasonModelId | string | ※ | ~ 1024자 | 시즌 모델
GRN
※ 서버가 자동으로 설정 | ||
| name | string | ✓ | ~ 128자 | 시즌 모델 이름 시즌 모델 고유의 이름. 영숫자와 -(하이픈) _(밑줄) .(마침표)로 지정합니다. | ||
| metadata | string | ~ 128자 | 메타데이터 메타데이터에는 임의의 값을 설정할 수 있습니다. 이 값들은 GS2의 동작에는 영향을 주지 않으므로, 게임 내에서 사용하는 정보를 저장하는 용도로 사용할 수 있습니다. | |||
| tiers | List<EzTierModel> | ✓ | 1 ~ 100 items | 티어 모델 목록 시즌의 랭킹 사다리를 구성하는 티어 정의의 순서가 있는 목록. 각 티어는 고유한 포인트 변동 규칙(참가료, 최소/최대 변동량, 승급 보너스)을 정의합니다. 플레이어는 GS2-Experience에서 관리되는 누적 포인트에 따라 티어를 진행합니다. 최소 1개 티어, 최대 100개 티어. | ||
| experienceModelId | string | ✓ | ~ 1024자 | 경험치 모델 ID 시즌 포인트와 티어 진행 관리에 사용되는 GS2-Experience 경험치 모델의 GRN. 경험치 모델의 랭크 임계값이 티어 경계를 결정하며, 경험치 값은 플레이어의 현재 시즌 포인트를 나타냅니다. 대전 결과에 따른 포인트 변동은 이 경험치 모델에 적용됩니다. |
EzTierModel
티어 모델
티어 모델은 시즌 내 각 티어의 포인트 변동 규칙을 정의하는 모델입니다.
순위에 따른 포인트 변동 범위, 참가료, 승급 시 보너스를 설정합니다.
포인트의 실제 데이터는 GS2-Experience에 의해 관리되지만, 그 증감 로직은 TierModel의 설정에 따라 결정됩니다.
| 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 | |
|---|---|---|---|---|---|---|
| metadata | string | ~ 128자 | 메타데이터 메타데이터에는 임의의 값을 설정할 수 있습니다. 이 값들은 GS2의 동작에는 영향을 주지 않으므로, 게임 내에서 사용하는 정보를 저장하는 용도로 사용할 수 있습니다. | |||
| raiseRankBonus | int | ✓ | 0 ~ 10000 | 랭크 승급 보너스 플레이어가 이 티어로 승급했을 때 가산되는 보너스 포인트로, 즉각적인 강등을 방지하는 완충 역할을 합니다. 예를 들어 100으로 설정하면, 승급 기준점보다 100포인트 많은 상태로 새 티어를 시작합니다. 유효 범위: 0~10000. | ||
| minimumChangePoint | int | ✓ | -99999999 ~ -1 | 최소 변동 포인트 한 번의 대전 결과로 발생할 수 있는 최소(가장 음수인) 포인트 변동량으로, 일반적으로 최악의 패배 시 값을 나타냅니다. 음수 값이어야 합니다. 패배한 플레이어의 실제 포인트 변동은 이 값과 0 사이가 됩니다. 유효 범위: -99999999~-1. | ||
| maximumChangePoint | int | ✓ | 1 ~ 99999999 | 최대 변동 포인트 한 번의 대전 결과로 발생할 수 있는 최대(가장 양수인) 포인트 변동량으로, 일반적으로 최고의 승리 시 값을 나타냅니다. 양수 값이어야 합니다. 승리한 플레이어의 실제 포인트 변동은 0과 이 값 사이가 됩니다. 유효 범위: 1~99999999. |
EzGameResult
대전 결과
대전에서 한 플레이어의 결과를 나타내는 모델입니다.
플레이어의 사용자 ID와 순위를 보유합니다. WrittenBallot 내에서 모든 참가자의 결과를 보고하는 데 사용됩니다.
투표 집계 시, 제출된 모든 투표용지의 대전 결과를 다수결로 비교하여 공식 결과를 결정합니다.
| 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 | |
|---|---|---|---|---|---|---|
| rank | int | ✓ | 0 ~ 2147483646 | 순위 대전에서 이 플레이어의 최종 순위. 1은 1위(승자)를 나타냅니다. 순위 값은 TierModel의 설정에 따라 포인트 변동량을 결정하는 데 사용됩니다. | ||
| userId | string | ✓ | ~ 128자 | 사용자ID |
EzSignedBallot
서명된 투표용지
GS2-Key를 사용하여 서버가 암호 서명한 투표용지입니다.
서명을 통해 투표용지의 내용(시즌, 세션, 플레이어, 참가 인원 수)이 변조되지 않았음을 보장합니다.
투표 제출 시, 서버 측에서 서명된 투표용지를 검증한 후 게임 결과가 수락됩니다.
| 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 | |
|---|---|---|---|---|---|---|
| body | string | ✓ | ~ 1024자 | 서명 대상 데이터 서명 대상이 되는 투표용지 데이터의 직렬화된 JSON 표현. 투표용지의 내용(사용자 ID, 시즌 이름, 세션 이름, 참가 인원 수)을 서명과 대조할 수 있는 형식으로 포함합니다. 최대 1024자. | ||
| signature | string | ✓ | ~ 256자 | 서명 투표용지 본문에 대해 GS2-Key가 생성한 암호 서명. 투표용지가 서버에 의해 발급되었으며 클라이언트에 의해 변경되지 않았음을 검증하는 데 사용됩니다. Base64 인코딩, 최대 256자. |
EzVerifyActionResult
검증 액션 실행 결과
EzConsumeActionResult
소비 액션 실행 결과
EzAcquireActionResult
획득 액션 실행 결과
EzTransactionResult
트랜잭션 실행 결과
서버 사이드에서 트랜잭션 자동 실행 기능을 이용하여 실행된 트랜잭션의 실행 결과
| 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 | |
|---|---|---|---|---|---|---|
| transactionId | string | ✓ | 36 ~ 36자 | 트랜잭션 ID | ||
| verifyResults | List<EzVerifyActionResult> | 0 ~ 10 items | 검증 액션의 실행 결과 목록 | |||
| consumeResults | List<EzConsumeActionResult> | [] | 0 ~ 10 items | 소비 액션의 실행 결과 목록 | ||
| acquireResults | List<EzAcquireActionResult> | [] | 0 ~ 100 items | 획득 액션 실행 결과 리스트 |
메서드
getSeasonModel
특정 시즌 레이팅 정의의 상세 정보를 가져오기
시즌 이름을 지정하여, 티어 설정 및 규칙을 포함한 상세 정보를 가져옵니다.
응답에는 다음이 포함됩니다:
- 티어 정의: 레이팅 티어 목록(예: 브론즈 0포인트, 실버 1000포인트, 골드 2500포인트)과 랭크업 시 부여되는 보너스 포인트
- 경험치 모델 참조: 레이팅 값의 저장 및 관리에 사용되는 GS2-Experience 모델
- 도전 기간 이벤트: GS2-Schedule 이벤트와 연계되어 있는 경우, 경쟁 시즌이 유효하여 레이팅 대전에 참가할 수 있는 기간
랭크전 화면에서 시즌의 상세 정보를 표시할 때 사용합니다. 예를 들어 플레이어의 현재 티어, 다음 티어까지 필요한 포인트, 시즌이 현재 활성 상태인지 여부를 표시할 수 있습니다.
Request
| 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 | |
|---|---|---|---|---|---|---|
| namespaceName | string | ✓ | ~ 128자 | 네임스페이스 이름 네임스페이스 고유의 이름입니다. 영숫자 및 -(하이픈) _(언더스코어) .(마침표)로 지정합니다. | ||
| seasonName | string | ✓ | ~ 128자 | 시즌 모델 이름 시즌 모델 고유의 이름. 영숫자와 -(하이픈) _(밑줄) .(마침표)로 지정합니다. |
Result
| 타입 | 설명 | |
|---|---|---|
| item | EzSeasonModel | 시즌 모델 |
구현 예제
var domain = gs2.SeasonRating.Namespace(
namespaceName: "namespace-0001"
).SeasonModel(
seasonName: "mode1"
);
var item = await domain.ModelAsync(); var domain = gs2.SeasonRating.Namespace(
namespaceName: "namespace-0001"
).SeasonModel(
seasonName: "mode1"
);
var future = domain.ModelFuture();
yield return future;
var item = future.Result; const auto Domain = Gs2->SeasonRating->Namespace(
"namespace-0001" // namespaceName
)->SeasonModel(
"mode1" // seasonName
);
const auto Future = Domain->Model();
Future->StartSynchronousTask();
if (Future->GetTask().IsError())
{
return false;
}var domain = ez.season_rating.namespace_(
"namespace-0001"
).season_model(
"mode1"
)
var async_result = await domain.model()
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result값 변경 이벤트 핸들링
var domain = gs2.SeasonRating.Namespace(
namespaceName: "namespace-0001"
).SeasonModel(
seasonName: "mode1"
);
// 이벤트 핸들링 시작
var callbackId = domain.Subscribe(
value => {
// 값이 변화했을 때 호출됨
// value에는 변경 후의 값이 전달됨
}
);
// 이벤트 핸들링 정지
domain.Unsubscribe(callbackId); var domain = gs2.SeasonRating.Namespace(
namespaceName: "namespace-0001"
).SeasonModel(
seasonName: "mode1"
);
// 이벤트 핸들링 시작
var callbackId = domain.Subscribe(
value => {
// 값이 변화했을 때 호출됨
// value에는 변경 후의 값이 전달됨
}
);
// 이벤트 핸들링 정지
domain.Unsubscribe(callbackId); const auto Domain = Gs2->SeasonRating->Namespace(
"namespace-0001" // namespaceName
)->SeasonModel(
"mode1" // seasonName
);
// 이벤트 핸들링 시작
const auto CallbackId = Domain->Subscribe(
[](TSharedPtr<Gs2::SeasonRating::Model::FSeasonModel> value) {
// 값이 변화했을 때 호출됨
// value에는 변경 후의 값이 전달됨
}
);
// 이벤트 핸들링 정지
Domain->Unsubscribe(CallbackId);var domain = ez.season_rating.namespace_(
"namespace-0001"
).season_model(
"mode1"
)
# 이벤트 핸들링 시작
var callback_id = domain.subscribe_model(func(value):
# 값이 변화했을 때 호출됨
# value에는 변경 후의 값이 전달됩니다
pass
)
# 이벤트 핸들링 정지
domain.unsubscribe_model(callback_id)이 이벤트는 SDK가 가진 로컬 캐시의 값이 변경되었을 때 호출됩니다.
로컬 캐시는 SDK가 가진 API의 실행, 또는 GS2-Gateway의 알림을 활성화한 GS2-Distributor를 통한 스탬프 시트의 실행, 또는 GS2-Gateway의 알림을 활성화한 GS2-JobQueue의 실행에 의해 변화한 것만이 대상이 됩니다.
따라서 이 방법 이외로 값이 변경된 경우 콜백은 호출되지 않습니다.
listSeasonModels
시즌 레이팅 정의의 목록을 가져오기
네임스페이스에 정의된 모든 시즌 모델을 가져옵니다.
각 시즌 모델은 독립된 경쟁 레이팅 시스템을 나타냅니다. 예를 들어 “랭크 배틀 시즌 1”, “아레나 레이팅”, “토너먼트 레이팅” 등입니다.
시즌 레이팅은 플레이어가 대전을 진행하고, 승패에 따라 레이팅이 오르내리는 시스템입니다.
레이팅은 GS2-Experience 를 사용해 저장되므로, 레이팅 값은 경험치처럼 취급됩니다. 증감하며, 랭크업(예: 브론즈 → 실버 → 골드)을 트리거할 수 있습니다.
각 시즌 모델에는 다음이 정의되어 있습니다:
- 레이팅 티어(예: 브론즈, 실버, 골드, 플래티넘)와 승급 임계값
- 레이팅 값을 저장하는 GS2-Experience 모델
- 선택적으로, 경쟁 시즌이 유효한 기간을 제어하는 GS2-Schedule 이벤트(도전 기간)
게임의 랭크전 화면에서 이용 가능한 경쟁 모드 목록을 표시할 때 사용합니다.
Request
| 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 | |
|---|---|---|---|---|---|---|
| namespaceName | string | ✓ | ~ 128자 | 네임스페이스 이름 네임스페이스 고유의 이름입니다. 영숫자 및 -(하이픈) _(언더스코어) .(마침표)로 지정합니다. |
Result
| 타입 | 설명 | |
|---|---|---|
| items | List<EzSeasonModel> | 시즌 모델 목록 |
구현 예제
var domain = gs2.SeasonRating.Namespace(
namespaceName: "namespace-0001"
);
var items = await domain.SeasonModelsAsync(
).ToListAsync(); var domain = gs2.SeasonRating.Namespace(
namespaceName: "namespace-0001"
);
var it = domain.SeasonModels(
);
List<EzSeasonModel> items = new List<EzSeasonModel>();
while (it.HasNext())
{
yield return it.Next();
if (it.Error != null)
{
onError.Invoke(it.Error, null);
break;
}
if (it.Current != null)
{
items.Add(it.Current);
}
else
{
break;
}
} const auto Domain = Gs2->SeasonRating->Namespace(
"namespace-0001" // namespaceName
);
const auto It = Domain->SeasonModels(
);
TArray<Gs2::UE5::SeasonRating::Model::FEzSeasonModelPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}값 변경 이벤트 핸들링
var domain = gs2.SeasonRating.Namespace(
namespaceName: "namespace-0001"
);
// 이벤트 핸들링 시작
var callbackId = domain.SubscribeSeasonModels(
() => {
// 리스트의 요소가 변화했을 때 호출됨
}
);
// 이벤트 핸들링 정지
domain.UnsubscribeSeasonModels(callbackId); var domain = gs2.SeasonRating.Namespace(
namespaceName: "namespace-0001"
);
// 이벤트 핸들링 시작
var callbackId = domain.SubscribeSeasonModels(
() => {
// 리스트의 요소가 변화했을 때 호출됨
}
);
// 이벤트 핸들링 정지
domain.UnsubscribeSeasonModels(callbackId); const auto Domain = Gs2->SeasonRating->Namespace(
"namespace-0001" // namespaceName
);
// 이벤트 핸들링 시작
const auto CallbackId = Domain->SubscribeSeasonModels(
[]() {
// 리스트의 요소가 변화했을 때 호출됨
}
);
// 이벤트 핸들링 정지
Domain->UnsubscribeSeasonModels(CallbackId);이 이벤트는 SDK가 가진 로컬 캐시의 값이 변경되었을 때 호출됩니다.
로컬 캐시는 SDK가 가진 API의 실행, 또는 GS2-Gateway의 알림을 활성화한 GS2-Distributor를 통한 스탬프 시트의 실행, 또는 GS2-Gateway의 알림을 활성화한 GS2-JobQueue의 실행에 의해 변화한 것만이 대상이 됩니다.
따라서 이 방법 이외로 값이 변경된 경우 콜백은 호출되지 않습니다.
createVote
대전 결과 보고용 서명된 투표 용지를 작성한다
플레이어가 대전 결과를 보고하는 데 사용할 서명된 투표 용지를 생성합니다.
이는 대전 결과 보고 플로우의 첫 번째 단계입니다. 대전에 참가한 각 플레이어는 자신의 투표 용지를 작성해야 합니다.
대전 결과를 보고하는 전체 흐름:
- 대전이 종료되고 각 플레이어의 게임 클라이언트가 CreateVote 를 호출하여 서명된 투표 용지를 가져옵니다
- 각 플레이어가 투표 용지와 대전 결과를 Vote 로 전송합니다(개별 투표)
— 또는 승리한 쪽이 모든 투표 용지를 모아 VoteMultiple 로 한 번에 전송합니다(즉시 결과 반영) - 시스템이 투표 용지를 검증하고, 다수결로 결과를 결정하며, 각 플레이어의 레이팅을 갱신합니다
파라미터:
- seasonName: 이 대전이 어느 시즌 레이팅 시스템에 속하는지(예: “ranked-battle-season-1”)
- sessionName: 이 특정 대전을 식별하는 고유 ID(같은 대전의 모든 플레이어가 공유)
- numberOfPlayer: 대전에 참가한 플레이어 수(2~10명)
- keyId: 투표 용지 서명에 사용하는 암호화 키(변조 방지)
반환되는 투표 용지에는 바디(데이터)와 시그니처(변조되지 않았다는 증명)가 포함됩니다. 투표 시에는 둘 다 필요합니다.
Request
| 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 | |
|---|---|---|---|---|---|---|
| namespaceName | string | ✓ | ~ 128자 | 네임스페이스 이름 네임스페이스 고유의 이름입니다. 영숫자 및 -(하이픈) _(언더스코어) .(마침표)로 지정합니다. | ||
| seasonName | string | ✓ | ~ 128자 | 시즌 모델 이름 시즌 모델 고유의 이름. 영숫자와 -(하이픈) _(밑줄) .(마침표)로 지정합니다. | ||
| sessionName | string | ✓ | UUID | ~ 128자 | 세션 이름 세션 고유의 이름. 영숫자와 -(하이픈) _(밑줄) .(마침표)로 지정합니다. | |
| gameSession | GameSession | ✓ | GameSession | |||
| numberOfPlayer | int | ✓ | 2 ~ 10 | 참가 인원 | ||
| keyId | string | “grn:gs2:{region}:{ownerId}:key:default:key:default” | ~ 1024자 | 암호화 키 GRN |
Result
| 타입 | 설명 | |
|---|---|---|
| item | EzBallot | 투표용지 |
| body | string | 서명 대상 데이터 |
| signature | string | 서명 데이터 |
구현 예제
var domain = gs2.SeasonRating.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Ballot(
seasonName: "season-0001",
sessionName: "gathering-0001",
numberOfPlayer: 4,
keyId: "key-0001"
);
var item = await domain.ModelAsync(); var domain = gs2.SeasonRating.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Ballot(
seasonName: "season-0001",
sessionName: "gathering-0001",
numberOfPlayer: 4,
keyId: "key-0001"
);
var future = domain.ModelFuture();
yield return future;
var item = future.Result; const auto Domain = Gs2->SeasonRating->Namespace(
"namespace-0001" // namespaceName
)->Me(
GameSession
)->Ballot(
"season-0001", // seasonName
"gathering-0001", // sessionName
4, // numberOfPlayer
"key-0001" // keyId
);
const auto Future = Domain->Model();
Future->StartSynchronousTask();
if (Future->GetTask().IsError())
{
return false;
}var domain = ez.season_rating.namespace_(
"namespace-0001"
).me(game_session).ballot(
"season-0001",
"gathering-0001",
4,
"key-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 domain = gs2.SeasonRating.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Ballot(
seasonName: "season-0001",
sessionName: "gathering-0001",
numberOfPlayer: 4,
keyId: "key-0001"
);
// 이벤트 핸들링 시작
var callbackId = domain.Subscribe(
value => {
// 값이 변화했을 때 호출됨
// value에는 변경 후의 값이 전달됨
}
);
// 이벤트 핸들링 정지
domain.Unsubscribe(callbackId); var domain = gs2.SeasonRating.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Ballot(
seasonName: "season-0001",
sessionName: "gathering-0001",
numberOfPlayer: 4,
keyId: "key-0001"
);
// 이벤트 핸들링 시작
var callbackId = domain.Subscribe(
value => {
// 값이 변화했을 때 호출됨
// value에는 변경 후의 값이 전달됨
}
);
// 이벤트 핸들링 정지
domain.Unsubscribe(callbackId); const auto Domain = Gs2->SeasonRating->Namespace(
"namespace-0001" // namespaceName
)->Me(
GameSession
)->Ballot(
"season-0001", // seasonName
"gathering-0001", // sessionName
4, // numberOfPlayer
"key-0001" // keyId
);
// 이벤트 핸들링 시작
const auto CallbackId = Domain->Subscribe(
[](TSharedPtr<Gs2::SeasonRating::Model::FBallot> value) {
// 값이 변화했을 때 호출됨
// value에는 변경 후의 값이 전달됨
}
);
// 이벤트 핸들링 정지
Domain->Unsubscribe(CallbackId);var domain = ez.season_rating.namespace_(
"namespace-0001"
).me(game_session).ballot(
"season-0001",
"gathering-0001",
4,
"key-0001"
)
# 이벤트 핸들링 시작
var callback_id = domain.subscribe_model(func(value):
# 값이 변화했을 때 호출됨
# value에는 변경 후의 값이 전달됩니다
pass
)
# 이벤트 핸들링 정지
domain.unsubscribe_model(callback_id)이 이벤트는 SDK가 가진 로컬 캐시의 값이 변경되었을 때 호출됩니다.
로컬 캐시는 SDK가 가진 API의 실행, 또는 GS2-Gateway의 알림을 활성화한 GS2-Distributor를 통한 스탬프 시트의 실행, 또는 GS2-Gateway의 알림을 활성화한 GS2-JobQueue의 실행에 의해 변화한 것만이 대상이 됩니다.
따라서 이 방법 이외로 값이 변경된 경우 콜백은 호출되지 않습니다.
vote
자신의 투표용지를 제출하여 대전 결과에 투표합니다 (개별 투표)
각 플레이어가 자신의 서명된 투표용지와 대전 결과(누가 이기고 누가 졌는지)를 함께 제출합니다.
대전 결과를 보고하는 두 가지 방법 중 하나로, 각 플레이어가 독립적으로 투표하는 단순한 방식입니다.
개별 투표의 작동 방식:
- 대전이 종료된 후, 각 플레이어가 CreateVote를 호출하여 서명된 투표용지를 받은 다음 Vote를 호출하여 제출합니다
- 시스템은 첫 투표로부터 최대 5분간 모든 플레이어의 투표를 기다립니다
- 모든 플레이어가 투표를 완료하거나 5분이 경과하면 시스템은 다수결로 결과를 결정합니다
- 투표가 동수인 경우(예: 2명이 “플레이어 A 승리”, 2명이 “플레이어 B 승리"라고 응답), 결과는 기본적으로 폐기됩니다(스크립트로 변경 가능)
- 결정된 결과에 따라 각 플레이어의 레이팅 포인트가 조정됩니다
이 방식은 구현이 간단하지만 지연이 발생합니다 — 모든 투표가 완료되거나 5분의 대기 시간이 지날 때까지 결과가 반영되지 않습니다.
즉시 결과를 반영하고 싶다면, 승리한 측이 모든 투표용지를 모아 한 번에 제출하는 VoteMultiple을 대신 사용하세요.
파라미터:
- ballotBody / ballotSignature: CreateVote로 획득한 서명된 투표용지
- gameResults: 대전 결과 — 참가 플레이어와 순위(1위 = 승자, 2위 = 패자 등) 목록
- keyId: 투표용지 서명을 검증하는 데 사용되는 암호화 키
Request
| 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 | |
|---|---|---|---|---|---|---|
| namespaceName | string | ✓ | ~ 128자 | 네임스페이스 이름 네임스페이스 고유의 이름입니다. 영숫자 및 -(하이픈) _(언더스코어) .(마침표)로 지정합니다. | ||
| ballotBody | string | ✓ | ~ 1024자 | 투표용지의 서명 대상 데이터 | ||
| ballotSignature | string | ✓ | ~ 256자 | 투표용지의 서명 | ||
| gameResults | List<EzGameResult> | 0 ~ 10 items | 대전 결과 대전을 진행한 플레이어 그룹에 속한 사용자 ID 목록 | |||
| keyId | string | “grn:gs2:{region}:{ownerId}:key:default:key:default” | ~ 1024자 | 암호화 키 GRN |
Result
| 타입 | 설명 | |
|---|---|---|
| item | EzBallot | 투표용지 |
구현 예제
var domain = gs2.SeasonRating.Namespace(
namespaceName: "namespace-0001"
);
var result = await domain.VoteAsync(
ballotBody: "ballotBody...",
ballotSignature: "ballotSignature...",
gameResults: new List<Gs2.Unity.Gs2SeasonRating.Model.EzGameResult> {
new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
Rank = 1,
UserId = "user-0001",
},
new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
Rank = 2,
UserId = "user-0002",
},
new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
Rank = 2,
UserId = "user-0003",
},
new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
Rank = 3,
UserId = "user-0004",
},
},
keyId: "key-0001"
);
var item = await result.ModelAsync(); var domain = gs2.SeasonRating.Namespace(
namespaceName: "namespace-0001"
);
var future = domain.VoteFuture(
ballotBody: "ballotBody...",
ballotSignature: "ballotSignature...",
gameResults: new List<Gs2.Unity.Gs2SeasonRating.Model.EzGameResult> {
new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
Rank = 1,
UserId = "user-0001",
},
new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
Rank = 2,
UserId = "user-0002",
},
new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
Rank = 2,
UserId = "user-0003",
},
new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
Rank = 3,
UserId = "user-0004",
},
},
keyId: "key-0001"
);
yield return future;
if (future.Error != null)
{
onError.Invoke(future.Error, null);
yield break;
}
var future2 = future.Result.ModelFuture();
yield return future2;
if (future2.Error != null)
{
onError.Invoke(future2.Error, null);
yield break;
}
var result = future2.Result; const auto Domain = Gs2->SeasonRating->Namespace(
"namespace-0001" // namespaceName
);
const auto Future = Domain->Vote(
"ballotBody...", // ballotBody
"ballotSignature...", // ballotSignature
[]
{
auto v = MakeShared<TArray<TSharedPtr<Gs2::UE5::SeasonRating::Model::FEzGameResult>>>();
v->Add(
MakeShared<Gs2::UE5::SeasonRating::Model::FEzGameResult>()
->WithRank(TOptional<int32>(1))
->WithUserId(TOptional<FString>("user-0001"))
);
v->Add(
MakeShared<Gs2::UE5::SeasonRating::Model::FEzGameResult>()
->WithRank(TOptional<int32>(2))
->WithUserId(TOptional<FString>("user-0002"))
);
v->Add(
MakeShared<Gs2::UE5::SeasonRating::Model::FEzGameResult>()
->WithRank(TOptional<int32>(2))
->WithUserId(TOptional<FString>("user-0003"))
);
v->Add(
MakeShared<Gs2::UE5::SeasonRating::Model::FEzGameResult>()
->WithRank(TOptional<int32>(3))
->WithUserId(TOptional<FString>("user-0004"))
);
return v;
}(), // gameResults
"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 Future2->GetTask().Error();
}
const auto Result = Future2->GetTask().Result();var domain = ez.season_rating.namespace_(
"namespace-0001"
)
var async_result = await domain.vote(
"ballotBody...", # ballot_body
"ballotSignature...", # ballot_signature
[
Gs2SeasonRatingEzGameResult.new()
.with_rank(1)
.with_user_id("user-0001"),
Gs2SeasonRatingEzGameResult.new()
.with_rank(2)
.with_user_id("user-0002"),
Gs2SeasonRatingEzGameResult.new()
.with_rank(2)
.with_user_id("user-0003"),
Gs2SeasonRatingEzGameResult.new()
.with_rank(3)
.with_user_id("user-0004"),
], # game_results
"key-0001" # key_id
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.resultvoteMultiple
모든 투표용지를 모아 대전 결과를 한 번에 제출합니다 (즉시 결과 반영)
승리한 측의 대표 플레이어가 모든 참가자의 서명된 투표용지를 모아 대전 결과와 함께 한 번에 제출합니다.
이 방식은 결과가 즉시 반영됩니다. 5분간의 투표 대기 시간이 없습니다.
승리한 측이 투표용지를 모아야 하는 이유:
- 패배한 측은 “우리가 이겼다"고 거짓말할 유인이 있지만, 승리한 측은 승리에 대해 거짓말할 이유가 없습니다
- 일부 패배 플레이어가 투표용지를 제출하지 않더라도, 과반수의 투표용지만 모이면 결과를 확정할 수 있습니다
- 예를 들어 2대2 대전에서 4명 중 3명이 “팀 A 승리"라는 투표용지를 제출하면 과반수로 충분합니다
일반적인 구현 방법:
- 대전이 종료되면 각 플레이어가 CreateVote를 호출하여 서명된 투표용지를 받습니다
- 각 플레이어는 게임의 통신 경로(예: 실시간 서버, P2P)를 통해 승리한 측의 대표 플레이어에게 투표용지(본문 + 서명)를 전송합니다
- 대표 플레이어가 수집한 모든 투표용지와 대전 결과를 VoteMultiple로 제출합니다
- 시스템이 즉시 투표용지를 검증하고 결과를 결정하여 모든 플레이어의 레이팅을 업데이트합니다
파라미터:
- signedBallots: 수집한 모든 서명된 투표용지 목록(각 플레이어의 본문 + 서명 쌍)
- gameResults: 대전 결과 — 참가 플레이어와 순위 목록
- keyId: 투표용지 서명을 검증하는 데 사용되는 암호화 키
Request
| 타입 | 활성화 조건 | 필수 | 기본값 | 값 제한 | 설명 | |
|---|---|---|---|---|---|---|
| namespaceName | string | ✓ | ~ 128자 | 네임스페이스 이름 네임스페이스 고유의 이름입니다. 영숫자 및 -(하이픈) _(언더스코어) .(마침표)로 지정합니다. | ||
| signedBallots | List<EzSignedBallot> | 0 ~ 10 items | 서명이 첨부된 투표용지 리스트 | |||
| gameResults | List<EzGameResult> | 0 ~ 10 items | 투표 내용. 대전을 진행한 플레이어 그룹 1에 속한 사용자 ID 리스트 | |||
| keyId | string | “grn:gs2:{region}:{ownerId}:key:default:key:default” | ~ 1024자 | 암호화 키 GRN |
Result
| 타입 | 설명 | |
|---|---|---|
| item | EzBallot | 투표용지 |
구현 예제
var domain = gs2.SeasonRating.Namespace(
namespaceName: "namespace-0001"
);
var result = await domain.VoteMultipleAsync(
signedBallots: new List<Gs2.Unity.Gs2SeasonRating.Model.EzSignedBallot> {
new Gs2.Unity.Gs2SeasonRating.Model.EzSignedBallot() {
Body = "aaa",
Signature = "bbb",
},
new Gs2.Unity.Gs2SeasonRating.Model.EzSignedBallot() {
Body = "aaa",
Signature = "bbb",
},
new Gs2.Unity.Gs2SeasonRating.Model.EzSignedBallot() {
Body = "aaa",
Signature = "bbb",
},
new Gs2.Unity.Gs2SeasonRating.Model.EzSignedBallot() {
Body = "aaa",
Signature = "bbb",
},
},
gameResults: new List<Gs2.Unity.Gs2SeasonRating.Model.EzGameResult> {
new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
Rank = 1,
UserId = "user-0001",
},
new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
Rank = 2,
UserId = "user-0002",
},
new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
Rank = 2,
UserId = "user-0003",
},
new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
Rank = 3,
UserId = "user-0004",
},
},
keyId: "key-0001"
);
var item = await result.ModelAsync(); var domain = gs2.SeasonRating.Namespace(
namespaceName: "namespace-0001"
);
var future = domain.VoteMultipleFuture(
signedBallots: new List<Gs2.Unity.Gs2SeasonRating.Model.EzSignedBallot> {
new Gs2.Unity.Gs2SeasonRating.Model.EzSignedBallot() {
Body = "aaa",
Signature = "bbb",
},
new Gs2.Unity.Gs2SeasonRating.Model.EzSignedBallot() {
Body = "aaa",
Signature = "bbb",
},
new Gs2.Unity.Gs2SeasonRating.Model.EzSignedBallot() {
Body = "aaa",
Signature = "bbb",
},
new Gs2.Unity.Gs2SeasonRating.Model.EzSignedBallot() {
Body = "aaa",
Signature = "bbb",
},
},
gameResults: new List<Gs2.Unity.Gs2SeasonRating.Model.EzGameResult> {
new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
Rank = 1,
UserId = "user-0001",
},
new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
Rank = 2,
UserId = "user-0002",
},
new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
Rank = 2,
UserId = "user-0003",
},
new Gs2.Unity.Gs2SeasonRating.Model.EzGameResult() {
Rank = 3,
UserId = "user-0004",
},
},
keyId: "key-0001"
);
yield return future;
if (future.Error != null)
{
onError.Invoke(future.Error, null);
yield break;
}
var future2 = future.Result.ModelFuture();
yield return future2;
if (future2.Error != null)
{
onError.Invoke(future2.Error, null);
yield break;
}
var result = future2.Result; const auto Domain = Gs2->SeasonRating->Namespace(
"namespace-0001" // namespaceName
);
const auto Future = Domain->VoteMultiple(
[]
{
auto v = MakeShared<TArray<TSharedPtr<Gs2::UE5::SeasonRating::Model::FEzSignedBallot>>>();
v->Add(
MakeShared<Gs2::UE5::SeasonRating::Model::FEzSignedBallot>()
->WithBody(TOptional<FString>("aaa"))
->WithSignature(TOptional<FString>("bbb"))
);
v->Add(
MakeShared<Gs2::UE5::SeasonRating::Model::FEzSignedBallot>()
->WithBody(TOptional<FString>("aaa"))
->WithSignature(TOptional<FString>("bbb"))
);
v->Add(
MakeShared<Gs2::UE5::SeasonRating::Model::FEzSignedBallot>()
->WithBody(TOptional<FString>("aaa"))
->WithSignature(TOptional<FString>("bbb"))
);
v->Add(
MakeShared<Gs2::UE5::SeasonRating::Model::FEzSignedBallot>()
->WithBody(TOptional<FString>("aaa"))
->WithSignature(TOptional<FString>("bbb"))
);
return v;
}(), // signedBallots
[]
{
auto v = MakeShared<TArray<TSharedPtr<Gs2::UE5::SeasonRating::Model::FEzGameResult>>>();
v->Add(
MakeShared<Gs2::UE5::SeasonRating::Model::FEzGameResult>()
->WithRank(TOptional<int32>(1))
->WithUserId(TOptional<FString>("user-0001"))
);
v->Add(
MakeShared<Gs2::UE5::SeasonRating::Model::FEzGameResult>()
->WithRank(TOptional<int32>(2))
->WithUserId(TOptional<FString>("user-0002"))
);
v->Add(
MakeShared<Gs2::UE5::SeasonRating::Model::FEzGameResult>()
->WithRank(TOptional<int32>(2))
->WithUserId(TOptional<FString>("user-0003"))
);
v->Add(
MakeShared<Gs2::UE5::SeasonRating::Model::FEzGameResult>()
->WithRank(TOptional<int32>(3))
->WithUserId(TOptional<FString>("user-0004"))
);
return v;
}(), // gameResults
"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 Future2->GetTask().Error();
}
const auto Result = Future2->GetTask().Result();var domain = ez.season_rating.namespace_(
"namespace-0001"
)
var async_result = await domain.vote_multiple(
[
Gs2SeasonRatingEzSignedBallot.new()
.with_body("aaa")
.with_signature("bbb"),
Gs2SeasonRatingEzSignedBallot.new()
.with_body("aaa")
.with_signature("bbb"),
Gs2SeasonRatingEzSignedBallot.new()
.with_body("aaa")
.with_signature("bbb"),
Gs2SeasonRatingEzSignedBallot.new()
.with_body("aaa")
.with_signature("bbb"),
], # signed_ballots
[
Gs2SeasonRatingEzGameResult.new()
.with_rank(1)
.with_user_id("user-0001"),
Gs2SeasonRatingEzGameResult.new()
.with_rank(2)
.with_user_id("user-0002"),
Gs2SeasonRatingEzGameResult.new()
.with_rank(2)
.with_user_id("user-0003"),
Gs2SeasonRatingEzGameResult.new()
.with_rank(3)
.with_user_id("user-0004"),
], # game_results
"key-0001" # key_id
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result