이 페이지는 개념 설명 — 좌표를 왜, 어떻게 눈금으로 바꾸는지만 다룬다. 서버에 적용한 결과와 A/B 실측은 별도 페이지에 있다.
플레이어가 움직이면 그 섹터의 이번 틱 변경분이 묶음 패킷 하나로 나간다. 그 안에 들어가는 1인분이 아래 14바이트다 (Protocol.h의 SectorUpdateEntry).
| 필드 | 타입 | 크기 | 실제로 쓰는 범위 |
|---|---|---|---|
| playerId | int32 | 4 B | 동접 6,000명 |
| direction | uint8 | 1 B | 5가지 (없음·상·하·좌·우) |
| moveState | uint8 | 1 B | 2가지 (멈춤·이동) |
| x | float | 4 B | 0 ~ 120 |
| y | float | 4 B | 0 ~ 120 |
| 합계 | 14 B |
여기서 1바이트를 깎으면 시야 안 사람 수만큼 곱해서 빠진다. 한 사람의 이동 한 번이 시야 안 1,010명에게 복사되기 때문이다 — MMO의 팬아웃 증폭이고, 방 하나에 64명인 FPS에는 없는 성질이다.
이 엔트리가 전체 트래픽에서 차지하는 비중이 얼마인지는 별도 실측 페이지에서 다룬다.
float이 왜 낭비인가| float이 담을 수 있는 것 | 우리가 실제로 쓰는 것 | |
|---|---|---|
| 범위 | ± 3.4 × 10³⁸ | 0 ~ 120 |
| 정밀도 | 유효숫자 7자리 | 0.002면 충분 |
| 크기 | 4 바이트 | 2 바이트로 충분 |
float은 아주 큰 수와 아주 작은 수를 같은 4바이트로 다루려고 지수부와 가수부를 나눠 쓴다. 그 유연함의 대가가 4바이트인데, 우리는 0~120이라는 좁은 구간 하나만 쓰므로 대가만 치르고 혜택은 안 쓰는 셈이다.
게다가 float은 눈금 간격이 일정하지 않다. 지수부를 두는 대가로 값이 커질수록 표현할 수 있는 눈금이 듬성듬성해진다. — 1 근처에서는 간격이 0.00000012인데 100 근처에서는 0.0000076으로 64배 벌어진다. 같은 맵 안에서도 원점 쪽은 촘촘하고 끝은 듬성등성하다. 반면 고정소수점은 어디서나 간격이 같다.
맵이 120이라 지금은 양쪽 다 넉넉해 차이가 안 난다. 하지만 맵이 10만 단위로 커지면 float 간격은 0.008까지 벌어져 1/512 눈금(0.002)보다 오히려 듬성듬성하다. 대형 오픈월드가 원점에서 멀어질수록 위치가 떨리는 게 이 때문이고, 언리얼이 좌표를 double로 넓히거나(LWC) 원점 자체를 옮기는(floating origin) 이유다.
이건 설계 결정이 아니라 개발 편의였다 — 서버 내부 표현(float)을 그대로 와이어에 태웠을 뿐이다. 언리얼이 FVector_NetQuantize를 타입으로 제공하고, Quake 3가 좌표를 1/8 단위 정수로 보낸 것처럼, 네트워크 게임에서 좌표를 그대로 보내지 않는 건 표준 관행이다.
바꾸는 건 와이어뿐이다. 서버 내부 계산은 float 그대로 둔다 — 이동은 speed × deltaTime 곱셈이라 소수가 필연이고, 정수로 누적하면 나머지가 한쪽으로만 쌓인다. 이동·섹터 판정·벽 판정을 전혀 건드리지 않으므로 게임 로직에는 영향이 없다.
바닥에 일정 간격으로 눈금을 긋고 번호만 보낸다. 받는 쪽은 간격을 알고 있으니 되돌릴 수 있다. 이렇게 연속된 값을 정해진 눈금으로 끊는 것을 양자화라 하고, 그중 눈금 간격을 상수로 고정한 방식이 고정소수점이다(다른 종류는 5절).
보낼 때 qx = (uint16)(x * 512 + 0.5f) // 반올림
받을 때 x = qx * (1.0f / 512)
예) 63.7 → 32,614 → 63.6992 (오차 0.0008)
uint16의 한계 65,535 안에 들어간다 → 2바이트