네트워크 멀티플레이 게임에서 플레이어의 움직임을 서버와 클라이언트 간에 정확하게 동기화하는 것은 필수적입니다. Unreal Engine에서는 대표적으로 FVector를 통해 위치, 속도, 가속도 등의 3D 데이터를 표현하며, 이 데이터를 Replicate를 통해 네트워크로 전송합니다. 그러나 FVector는 기본적으로 세 축(X, Y, Z)에 대해 각각 float 자료형(총 12Byte)을 사용하므로, 빈번하게 전송될 경우 대역폭에 큰 부담이 됩니다.
Lyra 샘플 프로젝트에서는 이러한 문제를 해결하기 위해 가속도 벡터 데이터를 정밀도 손실을 최소화하면서도 압축하는 기술을 도입하였습니다. 이 기법을 통해 기존 방식에 비해 약 75%에 달하는 네트워크 전송량 절감을 이끌어냈으며, 결과적으로 더욱 최적화된 멀티플레이 환경을 제공합니다.
이처럼 압축을 적용하는 이유는 명확합니다. 멀티플레이 환경에서는 다수의 캐릭터가 지속적으로 상태 정보를 송수신하게 되며, 이 과정에서 불필요하게 큰 데이터를 주고받는다면 지연(latency)이나 패킷 손실로 이어질 수 있습니다. 또한 모바일이나 저사양 환경에서도 부드러운 게임 경험을 보장하기 위해서는 전송 데이터의 경량화가 필수적입니다.
이번 글에서는 Lyra에서 실제로 사용되고 있는 이 벡터 압축 기법이 어떻게 작동하며, 어떤 방식으로 데이터를 줄이면서도 실용적인 정확도를 유지하는지를 살펴보겠습니다.
어떻게 보면 12Byte는 굉장히 적은 숫자입니다. PC RAM이 기본적으로 16GB가 탑재되고 있으며, 네트워크 대역폭은 대한민국 기준으로 평균 100mbps가 훌쩍 넘어섭니다. 그에 비하면 12Byte는 너무나도 작고 초라해보입니다. 겨우 9Byte가 대역폭에서 깍인다 하더라도 티도 안날것 같은 숫자죠. 하지만 복제해야 할 클라이언트의 수가 많아지고, 더 많은 시간동안 서버를 유지한다면 생각보다 더 많은 자원을 소모하게 될 것입니다. 그리고 그 자원은 가장 중요한 '돈'과 연결되어 있습니다.
예를 들어 보겠습니다. 인기 FPS 게임인 'Valorant'의 경우에는 좀 더 공정한 경쟁을 위해서 아주 높은 품질의 서버를 요구하고 있으며, 실제로 128Tick 서버를 운영하고 있습니다. 이 서버에 10만명이 큐를 돌려 게임을 하고 있다고 가정해 보겠습니다.
예시 시나리오:
- 플레이어 수: 100,000명
- 업데이트 빈도: 128Hz
- 시뮬레이션 시간: 1시간
압축 전 대역폭:
100,000 players × 12 bytes × 128 Hz × 3600 sec
= 55,296,000,000 bytes
= 55.3 GB/hour압축 후 대역폭:
100,000 players × 3 bytes × 128 Hz × 3600 sec
= 13,824,000,000 bytes
= 13.8 GB/hour절약량: 41.5 GB/hour(75% 절약)
$0.09/GB짜리 플랜을 사용한다면, 한달에 약 87달러를 절약할 수 있겠군요. 겨우 코드 몇 줄로 말입니다. 이런 최적화가 하나하나 쌓여서 서버의 유지비를 절감하고 게임에 더 많은 자원을 투자할 수 있는 기회가 생기게 되는 것입니다. 또한, 자원이 남는다면 제 연봉이 올라갈 기회도 조금은 늘어날지도 모릅니다.
FVector는 앞서 설명드렸다싶히, X, Y, Z축을 표현하기 위해 3개의 float 변수를 선언하여 사용하고 있습니다. 즉, 직교좌표를 이용하여 캐릭터의 가속도를 표현하고 있습니다.
압축 전: 일반적인 3D 벡터
struct FVector { float X; // 4 bytes - IEEE 754 32비트 부동소수점 float Y; // 4 bytes - IEEE 754 32비트 부동소수점 float Z; // 4 bytes - IEEE 754 32비트 부동소수점 };총 크기: 12 bytes
정밀도: 약 7자리 십진수 (32비트 float)
범위: ±3.4 × 10^38
최적화된 struct는 다음과 같이 정의하여 12Byte였던 이전과 다르게 3Byte로 압축할 수 있었습니다.
struct FLyraReplicatedAcceleration {
uint8 AccelXYRadians; // 1 byte - XY 평면 방향
uint8 AccelXYMagnitude; // 1 byte - XY 평면 크기
int8 AccelZ; // 1 byte - Z축 가속도
}
// 총 크기: 3 bytes
극 좌표로의 변환과 양자화를 통해 무려 75%를 압축할 수 있었습니다.
// 1단계: 직교좌표를 극좌표로 변환
FMath::CartesianToPolar(CurrentAccel.X, CurrentAccel.Y, AccelXYMagnitude, AccelXYRadians);
// 2단계: 정규화 및 양자화
AccelXYRadians = [0, 2π] → [0, 255] // 8비트 방향 정보
AccelXYMagnitude = [0, MaxAccel] → [0, 255] // 8비트 크기 정보
uint8 = floor((radians / 2π) × 255)uint8 = floor((magnitude / MaxAccel) × 255)// Z축은 양수/음수 모두 가능하므로 int8 사용
AccelZ = [-MaxAccel, MaxAccel] → [-127, 127]
// 변환 공식
int8 = floor((AccelZ / MaxAccel) × 127)
결론부터 말씀드리자면 맞습니다. 360도를 255로 나누어서 사용하기 때문에 약 1.4도 씩만 회전할 수 있으며, 크기 또한, 8Unit 단위로 움직이기 때문에 어느정도 손실이 발생합니다. 하지만 실용적으로 봤을 때 손실되는 데이터의 크기는 유저가 알아채기 어려운 수준이며, 엔진에서 보간이 이루어지기 때문에 득보다 실이 더 크다는게 AI의 설명입니다. (Claude, ChatGPT 교차 검증)
예시: MaxAcceleration = 2400 units/s²
- XY 방향 오차: 최대 ±1.41도, ±9.4 units/s²
- Z축 오차: 최대 ±18.9 units/s²
손실 수준:
- 방향: ±1.41도 오차
- 크기: ±0.39% 상대 오차 (255 레벨 양자화)
- Z축: ±0.79% 상대 오차 (127 레벨 양자화)
영향 평가:
✅ 시각적으로 거의 구분 불가능
✅ 게임플레이에 미치는 영향 미미
⚠️ 정밀한 물리 시뮬레이션에는 부적합
// 1단계: 정규화 해제
double AccelXYMagnitude = (ReplicatedAcceleration.AccelXYMagnitude × MaxAccel) / 255.0;
double AccelXYRadians = (ReplicatedAcceleration.AccelXYRadians × 2π) / 255.0;
// 2단계: 극좌표를 직교좌표로 변환
FMath::PolarToCartesian(AccelXYMagnitude, AccelXYRadians, X, Y);
// 3단계: Z축 복원
double Z = (ReplicatedAcceleration.AccelZ × MaxAccel) / 127.0;
- 압축 비용 (서버/오너 클라이언트)
- CartesianToPolar 변환: ~10-20 CPU cycles
- 3번의 부동소수점→정수 변환: ~15 CPU cycles
=> 총 압축 비용: 25~35 CPU cycles
- 압축 해제 비용 (시뮬레이티드 프록시)
- 3번의 정수→부동소수점 변환: ~15 CPU cycles
- PolarToCartesian 변환: ~10-20 CPU cycles
=>총 해제 비용: 25~35 CPU cycles
서버와 클라이언트가 압축과 해제를 하는데 각각 최대 35번의 사이클을 필요로 합니다. 60FPS 기준, 1 프레임에 16.6ms가 필요한데 비해, 최신 CPU에서 이정도의 사이클은 8~12ns 정도 밖에 소요되지 않습니다. 네트워크에서 얻는 이점에 비하면 아주아주 사소한 퍼포먼스 감소입니다.
- 75% 메모리 사용량 감소
- 캐시 히트율 향상: 더 많은 데이터가 CPU 캐시에 적재 가능
- 메모리 대역폭 절약: 시스템 전반적인 성능 향상
Lyra 샘플에서 적용된 이 벡터 압축 기법은 단순한 최적화를 넘어, 현대 멀티플레이어 게임 개발의 핵심 철학을 반영하는 사례입니다. 간단하면서 효과적인 압축을 통해 메모리와 대역폭 사용량을 획기적으로 절감하고, 게임플레이 품질을 유지하면서도 불필요한 자원 낭비를 방지합니다. 또한, 확장성과 범용성 덕분에, 수천에서 수만 명이 동시에 플레이하는 대규모 온라인 환경에서도 효과적으로 적용할 수 있습니다.
이 기법은 다음과 같은 개발 원칙을 잘 보여줍니다:
- 성능 우선주의: 정밀도보다는 실용성과 효율을 택한 결정
- 사용자 경험 중심 설계: 안정적인 네트워크 품질 유지로 만족도 향상
- 확장 가능한 아키텍처: 대규모 환경까지 고려한 설계
- 도메인 특화 최적화: 물리 및 입력 데이터를 게임에 맞게 맞춤화한 구조
이러한 접근 방식은 단순히 기술적인 절약을 넘어, "완벽함보다는 실용성"이라는 게임 개발의 실전 감각을 잘 보여줍니다.특히 "Epic Games"와 같은 기업이 축적해 온 멀티플레이어 경험이 녹아든 설계로, 다른 게임 프로젝트에서도 벤치마크 가치가 높은 사례라고 생각합니다.