GS2-MegaField
GS2-MegaField는 거대한 3D 공간에 존재하는 대량의 플레이어 위치 정보를, 효율적으로 공유·동기화하기 위한 기능을 제공합니다.
MMORPG나 오픈 월드, 메타버스형 서비스 등, 같은 월드에 대량의 플레이어가 동시에 로그인하는 타이틀에서는, 모든 플레이어의 위치 정보를 전체 플레이어에게 배포하면 네트워크 대역도 CPU도 감당할 수 없게 됩니다. GS2-MegaField는 “자신의 근방에 있는 플레이어만"을 효율적으로 가져올 수 있도록 함으로써 이 문제를 해결합니다.
에어리어와 레이어
GS2-MegaField의 공간은 “에어리어(Area)“와 “레이어(Layer)“라는 두 가지 개념으로 구성됩니다.
- AreaModel(에어리어 모델): 하나의 커다란 3D 공간을 나타내는 마스터 데이터. 예를 들어 “마을 안”, “던전”, “필드” 등, 월드 내의 논리적인 구획별로 작성합니다.
- LayerModel(레이어 모델): 하나의 에어리어 안에 존재하는, 용도별 레이어. 예를 들어 “플레이어용”, “NPC용”, “아이템용"과 같이, 같은 장소를 다른 목적으로 겹쳐서 이용할 수 있습니다.
에어리어 모델은 여러 레이어 모델을 포함하는 형태로 마스터 데이터로서 정의합니다. 플레이어의 위치는 “어느 에어리어의, 어느 레이어의, 어느 좌표에 있는가"라는 형태로 표현됩니다.
graph TD AreaModel["AreaModel: town"] --> LayerPlayer["LayerModel: player"] AreaModel --> LayerNpc["LayerModel: npc"] AreaModel --> LayerItem["LayerModel: item"] Player1["Player A"] -- Spot --> LayerPlayer Player2["Player B"] -- Spot --> LayerPlayer
공간 인덱스
GS2-MegaField는 레이어 내 플레이어의 위치를 공간 인덱스(R-Tree 기반 구조)로 관리합니다. 이를 통해 “자신의 주변 N미터 이내에 있는 플레이어를 모두 가져온다"와 같은 근방 탐색을, 플레이어 수에 의존하지 않고 효율적으로 처리할 수 있습니다.
레이어 단위로 공간 인덱스가 독립되어 있어, “플레이어끼리의 근방"과 “NPC와의 근방” 같은 검색을 별도로 취급할 수 있습니다.
플레이어 위치 정보
각 플레이어는 “자신의 현재 위치"를 MyPosition으로 GS2-MegaField에 송신합니다.
MyPosition에는 다음 정보가 포함됩니다.
- position: 3D 좌표 (x, y, z)
- vector: 방향 벡터 (x, y, z)
- r: 시야·관심 반경
플레이어는 위치를 송신할 때, 동시에 “자신이 관심 있는 범위"를 Scope로 지정할 수 있으며, 그 범위에 있는 다른 플레이어의 정보를 응답으로 받을 수 있습니다.
Scope에는 다음 정보가 포함됩니다.
- layerName: 검색 대상 레이어 이름
- r: 검색 반경
- limit: 반환할 최대 건수
여러 Scope를 지정함으로써 동시에 여러 레이어의 근방 플레이어를 가져오는 것도 가능합니다.
게임 클라이언트 간 위치 동기화
GS2-MegaField는 플레이어의 위치 정보를 “어느 정도 솎아낸 근방 플레이어 목록"으로 제공합니다. 이 목록에는 각 플레이어의 위치·방향 벡터·최종 동기화 시각 등이 포함되며, 게임 클라이언트는 이 정보를 보간 처리하면서 렌더링함으로써 매끄러운 위치 동기화를 실현할 수 있습니다.
다만, GS2-MegaField 자체는 리얼타임 성능을 보장하는 상시 접속 프로토콜이 아닙니다. 고빈도 액션 동기화나, 입력 기반의 엄밀한 동기화가 필요한 경우에는 GS2-Realtime과 조합하는 것이 효과적입니다.
트랜잭션 액션
GS2-MegaField에서는 트랜잭션 액션을 제공하지 않습니다.
마스터 데이터 관리
마스터 데이터를 등록함으로써 마이크로서비스에서 이용 가능한 데이터나 동작을 설정할 수 있습니다.
마스터 데이터의 종류에는 다음이 있습니다.
AreaModel: 월드 내 논리적 구획(에어리어)의 정의LayerModel: 에어리어 안의 용도별 레이어 정의 (AreaModel의 자식으로 정의)
다음은 AreaModel / LayerModel을 설정하는 마스터 데이터의 JSON 예입니다.
{
"version": "2019-09-09",
"areaModels": [
{
"name": "town",
"metadata": "starter town",
"layerModels": [
{ "name": "player", "metadata": "players" },
{ "name": "npc", "metadata": "npcs" }
]
},
{
"name": "dungeon",
"layerModels": [
{ "name": "player" },
{ "name": "enemy" }
]
}
]
}마스터 데이터의 등록은 관리 콘솔에서 등록하는 것 외에, GitHub에서 데이터를 반영하거나, GS2-Deploy를 사용해 CI에서 등록하는 워크플로우를 구성할 수 있습니다.
구현 예제
에어리어 모델 목록 조회
월드에 어떤 에어리어가 존재하는지 가져옵니다.
var items = await gs2.MegaField.Namespace(
namespaceName: "namespace-0001"
).AreaModelsAsync(
).ToListAsync(); const auto It = Gs2->MegaField->Namespace(
"namespace-0001" // namespaceName
)->AreaModels(
);
TArray<Gs2::UE5::MegaField::Model::FEzAreaModelPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}var iterator = ez.mega_field.namespace_(
"namespace-0001"
).area_models(
)
var async_result = await iterator.load()
if async_result.error != null:
# 에러를 처리
push_error(str(async_result.error))
return
var items = async_result.result자신의 위치를 갱신하고, 근방 플레이어 조회
자신의 현재 위치를 GS2-MegaField에 등록함과 동시에, 주변에 있는 다른 플레이어의 목록을 가져옵니다. 위치 갱신은 일정 간격으로 반복 호출하는 것이 기본적인 사용 방법입니다.
var spatials = await gs2.MegaField.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Spatial(
areaModelName: "town",
layerModelName: "player"
).UpdateAsync(
position: new Gs2.Unity.Gs2MegaField.Model.EzMyPosition
{
Position = new Gs2.Unity.Gs2MegaField.Model.EzPosition
{
X = 10.0f,
Y = 0.0f,
Z = 20.0f,
},
Vector = new Gs2.Unity.Gs2MegaField.Model.EzVector
{
X = 1.0f,
Y = 0.0f,
Z = 0.0f,
},
R = 1.0f,
},
scopes: new [] {
new Gs2.Unity.Gs2MegaField.Model.EzScope
{
LayerName = "player",
R = 50.0f,
Limit = 100,
},
}
); const auto Future = Gs2->MegaField->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Spatial(
"town", // areaModelName
"player" // layerModelName
)->Update(
MakeShared<Gs2::UE5::MegaField::Model::FEzMyPosition>(
MakeShared<Gs2::UE5::MegaField::Model::FEzPosition>(10.0f, 0.0f, 20.0f),
MakeShared<Gs2::UE5::MegaField::Model::FEzVector>(1.0f, 0.0f, 0.0f),
1.0f
),
[]
{
const auto v = MakeShared<TArray<TSharedPtr<Gs2::UE5::MegaField::Model::FEzScope>>>();
v->Add(MakeShared<Gs2::UE5::MegaField::Model::FEzScope>(TEXT("player"), 50.0f, 100));
return v;
}()
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
const auto Spatials = Future->GetTask().Result();var domain = ez.mega_field.namespace_(
"namespace-0001"
).me(game_session).spatial(
"area-0001",
"layer-0001"
)
var async_result = await domain.update(
null, # position
null # scopes
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result특정 플레이어의 위치 정보 참조
사용자ID를 알고 있는 플레이어의 위치 정보를 직접 가져올 수도 있습니다.
var spatial = await gs2.MegaField.Namespace(
namespaceName: "namespace-0001"
).User(
userId: "user-0001"
).Spatial(
areaModelName: "town",
layerModelName: "player"
).ModelAsync(); const auto Future = Gs2->MegaField->Namespace(
"namespace-0001" // namespaceName
)->User(
"user-0001" // userId
)->Spatial(
"town", // areaModelName
"player" // layerModelName
)->Model();
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
const auto Spatial = Future->GetTask().Result();var domain = ez.mega_field.namespace_(
"namespace-0001"
).user("user-0001").spatial(
"area-0001", "layer-0001"
)
var async_result = await domain.model()
if async_result.error != null:
push_error(str(async_result.error))
return
var item = async_result.result보다 실전적인 정보
GS2-Realtime과의 조합
GS2-MegaField는 “넓은 월드 내에서의 전체적인 플레이어 배치"를 다루는 데 적합합니다. 반면, 근접 전투나 친구와의 협력 플레이처럼 밀리초 단위의 동기화가 필요한 장면에는 적합하지 않습니다.
이러한 경우에는, GS2-MegaField로 가져온 근방 플레이어를 트리거로 하여 GS2-Realtime의 룸에 집약하고, 근접한 플레이어끼리만 리얼타임 통신을 수행하는 하이브리드 구성이 효과적입니다.
graph LR Player["플레이어"] -- 전체적인 위치 동기화 --> MegaField["GS2-MegaField"] MegaField -- 근방 플레이어 --> Player Player -- 근접 전투 등의 동기화 --> Realtime["GS2-Realtime"] Realtime -- 같은 방 플레이어 --> Player
갱신 빈도 설계
위치 정보 갱신은 호출 빈도가 그대로 서버 부하와 네트워크 대역에 직결됩니다. 일반적으로는 화면 표시의 프레임 레이트보다 낮은 빈도(예를 들어 5~10Hz 정도)로 갱신을 수행하고, 클라이언트 측에서 보간 처리를 하는 설계가 균형을 잡기 쉽습니다.
Scope 지정의 최적화
Scope로 지정하는 반경과 건수 상한은 게임의 시야 범위나 연출에 맞게 조정합니다.
모든 플레이어가 넓은 반경으로 대량으로 검색을 수행하면 부하가 높아지므로, 화면 밖 판정에서는 좁은 반경과 적은 건수로 조회하고, 포커스 시에만 넓게 가져오는 등의 방법이 효과적입니다.