반응형

스타크래프트나 워크래프트 같은 RTS 게임에서 수천 마리의 유닛이 끊김 없이 동기화되는 원리는 무엇일까?
그 핵심에는 락스텝(Lockstep) 프로토콜이 있다.
이번에는 락스텝의 핵심 개념과 작동 원리, 그리고 이를 구현하기 위한 절대 규칙인 결정론에 대해 정리하였다.
1. 락스텝(Lockstep)이란?
락스텝의 핵심은 결과를 보내지 않고, 입력을 보낸다는 것이다. 이를 이해하기 위해 일반적인 방식과 비교해 보자.
- 상태 동기화 (State Sync)
- MMORPG나 FPS 장르에서 주로 사용되는 방식이다.
- 작동: A가 B를 공격하여 체력이 90이 되었다면, 서버가 "B의 체력은 90이다"라고 결과를 통보한다.
- 한계: 유닛이 많아질수록 서버가 전송해야 할 데이터(위치, HP, 상태 등)가 기하급수적으로 늘어난다.
// [State Sync 방식]
// 서버가 모든 것을 계산하고, 클라이언트는 결과를 받아 덮어쓴다.
public void OnServerPacketReceived(StatePacket packet)
{
// 서버가 "좌표는 (10, 20)이다"라고 알려줌
unit.Position = packet.NewPosition;
// 서버가 "체력은 90이다"라고 알려줌
unit.HP = packet.NewHP;
}
- 락스텝 (Lockstep)
- RTS나 대전 격투 게임에서 주로 사용되는 방식이다.
- 작동: A가 공격 키를 누르면, 서버는 "A가 공격 명령을 내렸다"라는 입력(Command) 정보만 중계한다.
- 결과: 각 클라이언트는 전달받은 입력을 바탕으로 시뮬레이션을 수행하여, "따라서 B의 체력은 90이 된다"라는 결과를 직접 도출한다.
// [Lockstep 방식]
// 서버는 "누가 무엇을 눌렀는지"만 알려주고, 계산은 클라이언트가 직접 한다.
public void OnServerPacketReceived(InputPacket packet)
{
// 서버: "플레이어 A가 공격(Attack) 키를 입력했다."
if (packet.Command == CommandType.Attack)
{
// 클라이언트 로직:
// "그럼 데미지 공식에 따라 체력을 10 깎아야겠군." (직접 연산)
unit.HP -= 10;
// "공격 모션도 재생하고 이펙트도 터뜨리자."
unit.PlayAnimation("Attack");
}
}
이러한 방식의 차이는 유닛의 규모가 커질수록 극명하게 드러난다.
만약 1,000마리의 유닛을 한 번에 이동시킨다고 가정해 보자.
- State Sync: 1,000마리 각각의 변화된 좌표값(Result)을 매 프레임 서버가 전송해야 한다. 대역폭이 버티지 못하고 렉이 발생할 것이다.
- Lockstep: 유닛이 몇 마리든 상관없다. 서버는 "플레이어가 1,000마리를 드래그해서 (100, 100) 좌표로 이동 명령을 내렸다" 라는 단 하나의 패킷(Input)만 전송하면 된다.
따라서 락스텝을 사용하면 네트워크 트래픽을 획기적으로 줄일 수 있다.
2. 작동 원리 (The Mechanism)
이름 그대로 잠그고(Lock), 나아가는(Step) 방식이다.
- Step 1 (수집): 모든 플레이어의 입력을 수집한다. (입력이 없는 경우에도 '대기' 입력을 전송한다.)
- Step 2 (Lock): 모든 플레이어의 입력 패킷이 도착할 때까지 게임 진행을 멈추고 기다린다.
- Step 3 (Step): 모든 입력이 도착하면, 동시에 다음 턴(Turn)의 시뮬레이션을 진행한다.
이는 네트워크 지연(Latency)이 존재하는 환경에서 모든 참여자가 동일한 시점을 공유하기 위한 강제적 동기화 모델이다.
public class LockstepEngine
{
private int currentTurn = 0;
// 게임 루프 (Unity의 Update와 별개로 동작하는 논리 루프)
public void GameLoop()
{
while (IsGameRunning)
{
// 1. 모든 플레이어의 입력이 도착했는가? (Check)
if (NetworkManager.HasAllInputsForTurn(currentTurn))
{
// 2. 입력이 다 왔다면 시뮬레이션 진행 (Step)
Input[] inputs = NetworkManager.GetInputs(currentTurn);
ProcessInputs(inputs); // 입력 처리
Physics.Simulate(); // 물리 시뮬레이션
currentTurn++; // 다음 턴으로 증가
}
else
{
// 3. 아직 안 온 입력이 있다면? 멈춰서 기다림 (Lock)
// 렌더링은 계속되지만, 게임 로직(유닛 이동 등)은 멈춤
Wait();
}
}
}
}
3. 절대 규칙: 결정론 (Determinism)
락스텝을 구현하기 위한 대전제는 결정론(Determinism)이다.
- 정의: 동일한 초기값(Seed)에 동일한 입력(Input)을 대입하면, 지구상 어떤 컴퓨터에서 실행하더라도 100% 똑같은 결과가 도출되어야 한다.
- 이유: 결과를 서버로부터 수신하는 것이 아니라 각 클라이언트가 개별적으로 계산(Simulation)하기 때문이다.
- 위험성: 단 0.0001의 오차라도 발생한다면, 시간이 지날수록 나비효과처럼 차이가 벌어져 서로 다른 게임 진행 상황을 보게 된다(Desync 현상).
가장 큰 장애물: 부동소수점 (Float)
컴퓨터의 float 연산은 CPU 아키텍처나 컴파일러의 최적화 방식에 따라 미세한 오차가 발생할 수 있다.
- 따라서 락스텝 프로토콜에서는 float나 double의 사용이 엄격히 금지된다.
- 이러한 오차를 원천 차단하기 위해, 정수(int)를 기반으로 소수를 표현하는 고정 소수점(Fixed Point) 기술의 구현이 필수적으로 동반되어야 한다.
마무리
- 락스텝은 결과를 받지 않고 입력(Input)을 공유하여 각자 시뮬레이션을 수행하는 방식이다.
- 모든 플레이어는 완벽하게 동일한 연산 결과(결정론)를 가져야 한다.
- 이를 위해서는 float와 같은 불확실한 연산을 배제하고 고정 소수점 연산을 사용해야 한다.
Reference
Lockstep (computing) - Wikipedia
Floating Point Determinism - Gaffer On Games
Lockstep Protocol for Multiplayer - Reducing Latency - gamedev.net