FPS Arena는 지금까지 봇전만 됐습니다. 판정 권한을 분리해 두고, 틱 스냅샷을 만들고, 되감기까지 준비해 놓고도 선을 실제로 연결한 적이 없었습니다. 이번에 붙였습니다.

가장 먼저 정해야 했던 것은 기술이 아니라 누가 진실을 쥐느냐였습니다.

서버는 중계만 합니다

방 하나를 Cloudflare Durable Object가 맡습니다. 그런데 이 서버는 누가 맞았는지를 판단하지 않습니다. 판정은 접속한 클라이언트 중 하나 — 번호가 제일 낮은 사람 — 이 전부 돌립니다.

서버에 권위를 주지 않은 이유는 단순합니다. Durable Object는 자바스크립트로 돌고, Godot의 물리를 돌릴 수 없습니다. 서버가 판정하려면 히트박스와 이동을 자바스크립트로 다시 구현해야 하는데, 그건 같은 게임을 두 벌 만드는 일입니다. 두 벌이 어긋나는 날 "클라이언트에서는 맞았는데 서버에서는 안 맞았다"가 되고, 그때는 어느 쪽이 틀렸는지를 증명할 방법이 없습니다.

그래서 호스트 권한형입니다. 서버는 패킷을 나눠 주는 우체국이고, 게임의 진실은 호스트 한 명이 쥡니다. 대신 호스트가 나가면 다음 사람이 이어받습니다.

같은 총이 모두의 화면에서 같은 곳으로 나가야 합니다

총알이 흩어지는 방향을 난수로 뽑으면, 내 화면과 상대 화면에서 다른 곳으로 날아갑니다. 그래서 탄퍼짐을 정수 세 개로 결정했습니다. 매치 번호, 쏜 사람 번호, 그 사람의 몇 번째 발사인지. 이 셋이 같으면 어느 기계에서 계산해도 같은 값이 나옵니다.

난수를 없앤 게 아니라, 난수의 씨앗을 모두가 아는 값으로 바꾼 것입니다.

한 사람당 21바이트

스냅샷은 사람 한 명을 21바이트로 적습니다. 30Hz로 16명이면 초당 10KB입니다.

이 숫자를 작게 유지하는 게 중요한 이유는 대역폭보다 지연입니다. 패킷이 커지면 쪼개지고, 쪼개지면 하나만 늦어도 전체가 늦습니다.

남의 캐릭터가 제 이동 코드로 굴러갑니다

조작을 화면 위의 움직임이 아니라 으로 다룹니다(PlayerInput). 앞으로 얼마, 옆으로 얼마, 웅크렸는지, 쐈는지를 숫자로 적어 보냅니다.

받는 쪽은 그 값을 자기 이동 코드에 그대로 먹입니다. 그래서 남의 캐릭터도 제 캐릭터와 완전히 같은 물리로 움직입니다. 별도의 "원격 플레이어 이동" 코드가 없으니 둘이 어긋날 자리도 없습니다.

예측하고, 틀리면 되감아 다시 재생합니다

손님은 서버 응답을 기다리지 않고 자기 조작을 즉시 반영합니다. 안 그러면 왕복 지연만큼 늦게 움직이는 게임이 됩니다.

그다음 호스트가 "네 위치는 여기다"라고 말해 주면, 그 시점부터의 조작을 다시 재생해서 맞춥니다. 예측이 맞았으면 아무 일도 일어나지 않고, 틀렸을 때만 조용히 보정됩니다.

사람이 오면 봇이 비켜 줍니다

정원이 고정이라 사람이 들어오면 봇 하나가 빠지고, 나가면 다시 채워집니다. 방이 늘 꽉 차 있으므로 혼자 들어와도 빈 맵에서 기다릴 일이 없습니다.

붙고 나서야 필요해진 것들

연결만으로 끝나지 않았습니다. 붙고 나니 없던 문제가 생겼습니다.

방을 찾을 방법이 없었습니다. 서로 방 이름을 알아야 들어갈 수 있었고, 이름이 곧 열쇠라 흔한 이름을 쓰면 모르는 사람과 같은 판에 섰습니다. 지금 사람이 있는 방 목록을 받아 오고, 비밀번호는 원문 대신 해시로 보내고(방 이름으로 소금을 칩니다), 초대 코드는 "추측하기 어려운 방 이름"을 뽑아 주는 방식으로 했습니다. 그 방은 목록에 뜨지 않습니다. 초대 코드를 이름과 별개로 두면 관리할 상태가 하나 늘어날 뿐입니다.

한글 방 이름이 통째로 사라지고 있었습니다. 방 이름에 a-z0-9 허용 목록이 걸려 있어서 "우리방"이 빈 문자열이 됐습니다. 맵 이름에서 이미 한 번 겪은 실수와 똑같은 모양이었습니다. 허용할 것을 나열하는 대신 넣으면 안 되는 것만 막고, NFC로 정규화하고, 길이는 바이트가 아니라 글자 수로 세게 고쳤습니다.

손님의 점수판이 0:0이었습니다. 손님은 판정을 안 돌리니 "누가 죽었다"는 사건 자체가 발생하지 않습니다. 킬로그도 팀 점수도 없었습니다. 호스트가 전적을 선에 실어 보내게 만들면서 어시스트도 이참에 새로 만들었습니다. 죽기 전 10초 안에 피해를 준 사람, 막타는 제외, 아군 오사는 애초에 장부에 안 올림 — 세 규칙입니다. 부활하면 장부를 비웁니다. 안 비우면 지난 목숨에서 맞은 것이 다음 죽음의 어시스트가 됩니다.

검증

방 관련 15항목 전부, 기존 릴레이 29항목 전부를 배포판에서 통과했습니다.

다만 이 글에 적은 것 중 상당수는 붙이고 나서 실제로 부서진 뒤에 안 것들입니다. 같은 날 맵이 동기화되지 않아 서로가 안 보이던 문제, 되감기를 만들어 놓고 연결하지 않았던 문제가 따로 있었고, 그건 각각 따로 적었습니다.