저번 물리 연산에 이어서 Dedicated 서버를 통해 Server Authoritative 한 CSP 모델을 만들어 보겠습니다.
CSP를 구현하는 이유는 다음과 같습니다.
따라서 클라이언트에서 먼저 예측해서 움직이기 + 서버 정보를 받으면 입력 정보들을 기반으로 재시뮬레이션하면 입력지연도 없앨 수 있고, 서버 정보를 토대로 다시 계산하기 때문에 치트도 방지할 수 있습니다.
우선, 클라이언트 측에서 서버로 입력 정보를 보냅니다.
// PlayerController.Client.cs
private void Tick()
{
///
/// Get Input, Simulate State, Predict ...
///
// Send Input info to Server
ServerManagers.Dedi.Send(null, Serializer.Serialize<PlayerInput>(PacketType.C2S_Input, input));
}
서버는 입력 정보를 토대로 게임의 정답 상태를 시뮬레이션 합니다.
해당 상태가 무조건 정답이 되며, 이를 통해 계산한 정보들만 신뢰할 수 있게 됩니다. (치트 방지)
// PlayerController.Server.cs
private void OnGetInput(IPEndPoint clientEP, PlayerInput input)
{
// Simulate
curState = Simulate(curState, input, Time.fixedDeltaTime);
curState.tick = input.tick;
inputBuffer[input.tick] = input;
stateBuffer[input.tick] = curState;
ApplyState(curState);
// Send "Correct" snapshot to Client
ServerManagers.Dedi.Send(clientEP, Serializer.Serialize<PlayerState>(PacketType.S2C_Snapshot, curState));
}
다시 정답 상태를 클라이언트로 보내서 치팅을 방지하고 항상 옳은 상태를 유지할 수 있도록 합니다.
클라이언트 측에서 받는 정답 상태는 과거의 상태이므로, InputBuffer에 있는 정보들로 다시 재시뮬레이션을 해야합니다.
// PlayerController.Client.cs
private void OnGetSnapshot(PlayerState state)
{
PlayerState simulateState = state;
int tick = (state.tick + 1) % BUFFER_SIZE;
while (tick != (currentTick + 1) % BUFFER_SIZE)
{
simulateState = Simulate(simulateState, inputBuffer[tick], TICK_DT);
tick = (tick + 1) % BUFFER_SIZE;
}
Reconcile(simulateState);
ApplyState(curState);
stateBuffer[currentTick] = curState;
}
이렇게 하면 클라이언트 측에서 아무리 수치를 조작한다고해도, 항상 서버로부터 오는 정답 상태의 영향을 받게 됩니다.
아래는 서버가 없을 때와 있을 때의 테스트 영상입니다.


두 영상 모두 클라측에서 속도를 빠르게 조정했습니다.
위쪽에서는 서버를 무시하기 때문에 클라이언트가 임의로 조작한 대로 움직이는 반면에,
아래쪽에서는 서버의 정답을 받을 때마다 끊기면서 속도가 서버가 설정한대로 움직이게 되는 모습을 확인할 수 있었습니다.
이제 FPS 게임에 걸맞게 IK를 사용하여 사격 애니메이션을 만들고, 이후에 Lag Compensation까지 구현하여 Server Authoritative 하게 플레이어간 상호작용도 해결해보겠습니다.