온라인 멀티를 붙이기 전에 "기반 공사는 끝났다"고 적어 뒀습니다. 판정 권한 분리, 틱 스냅샷, 그리고 되감기. 실제로 연결해 보니 되감기는 만들어만 놓고 아무 데도 이어 붙이지 않은 상태였습니다.
rewound()를 부르는 곳이 스모크 테스트 한 군데뿐이었습니다. 테스트는 통과하고 있었고, 실제 게임에서는 한 번도 불리지 않았습니다.
왜 되감아야 하는가
손님이 보는 화면은 이미 과거입니다. 호스트가 보낸 스냅샷이 도착하는 데 걸린 시간만큼 늦습니다. 그 사이 상대가 움직였다면, 손님이 정확히 조준해서 쏴도 호스트 기준으로는 빈 공간입니다.
호스트가 모든 사격을 "지금"으로 판정하면 손님만 계속 빗나갑니다. 실력 문제가 아니라 구조 문제입니다.
그 사람이 보던 시각으로 되돌립니다
조작 패킷에 "내가 보던 시각"을 같이 싣습니다. 마지막으로 받은 틱 프레임의 시각입니다.
호스트는 사격을 판정할 때 그 시각으로 상대 히트박스만 되돌린 뒤 판정하고, 곧바로 복구합니다. 지형이나 내 위치는 안 건드립니다.
상한은 250ms입니다. 상한이 없으면 코너 뒤로 숨은 사람이 계속 맞습니다. 이미 안전한 곳으로 피했는데 0.5초 전 위치로 되감겨 맞는다면, 맞은 쪽 입장에서는 벽을 뚫고 총알이 들어온 것과 구분되지 않습니다. 미래로는 되감지 않습니다.
되감고 쐈는데 총알이 통과했습니다
연결하고 나서도 안 맞았습니다.
원인은 Godot이 변환을 다음 물리 스텝에 몰아서 반영한다는 것이었습니다. 노드의 위치를 코드로 옮겨도 물리 서버는 아직 모릅니다. 그런데 되감기는 한 프레임 안에서 되돌리고 → 쏘고 → 복구합니다. 그 사이에 물리 갱신이 없으니 레이캐스트는 되돌리기 전 위치를 봅니다. 되감기 전체가 무효였습니다.
스냅샷을 적용할 때 force_update_transform()으로 물리 서버에 즉시 밀어 넣게 고쳤습니다.
이걸 놓친 이유가 더 뼈아팠습니다. 기존 검사는 global_position만 비교하고 있었습니다. 노드의 좌표는 제대로 바뀌어 있으니 검사는 초록불이었습니다. 노드가 옮겨졌는지와 물리가 그걸 아는지는 다른 질문인데, 전자만 묻고 있었습니다.
호스트가 바뀌어도 봇이 초기화되지 않게
호스트가 나가면 다음 사람이 판정을 이어받습니다. 그때 봇들이 체력 만땅에 탄창 가득 찬 상태로 되살아나면 교전 중이던 사람 입장에서는 갑자기 상대가 회복한 셈입니다.
체력(비율)과 탄창을 프레임에 실어 보냅니다. 손님 쪽 인형이 그 값을 들고 있다가, 판정을 넘겨받는 순간 그대로 깨어납니다. 한 사람당 21바이트에서 23바이트가 됐습니다.
규약 번호를 세 군데에 적어 뒀습니다
버전을 올리면서 세 군데 중 두 군데만 고쳤습니다. 클라이언트가 옛 번호로 접속했고 서버가 거절했습니다.
같은 값을 여러 곳에 적어 두면 언젠가 한 곳을 빠뜨립니다. net.gd에 따로 적던 것을 없애고 한 곳만 남겼습니다. 고칠 곳이 하나면 빠뜨릴 수가 없습니다.
남는 것
"코드가 있다"와 "그 코드가 실제로 돈다"는 다릅니다. 이번 건은 테스트가 있었기 때문에 오히려 더 오래 숨었습니다. 테스트가 유일한 호출자면, 그 테스트는 기능이 아니라 자기 자신을 검사하고 있는 것입니다.