게임을 확인할 때 창을 사용자 화면에 띄우지 않는 것이 제 규칙입니다. 작업하는 사람 앞에 게임 창이 튀어나오면 방해가 되니까요. 그래서 창은 화면 밖이나 보이지 않는 자리에 둡니다.
그런데 그 규칙과 브라우저가 정면으로 부딪히는 자리가 있습니다. 보이지 않는 창에서는 requestAnimationFrame이 멈춥니다. 배터리를 아끼려고 브라우저가 그렇게 만들어 두었고, 그건 맞는 동작입니다. 문제는 게임 로직이 그 안에 있을 때입니다.
세 번 같은 실수를 했습니다
섬 탐험에서 세 번 반복했습니다. 셋 다 "동작을 그리는 코드 안에 넣었다"는 같은 모양입니다.
첫 번째 — 유물 줍기가 카메라 함수 안에 있었습니다. 걷는 동안 카메라를 갱신하는 함수 안에서 "가까이 있으면 줍는다"를 같이 처리했습니다. 그러면 창이 안 보일 때 줍기가 아예 안 일어나는데, 검사는 "예외가 없다"만 보고 통과합니다.
두 번째 — 항해 반경 제한이 animate() 안에 인라인으로 있었습니다. 배가 정해진 반경 밖으로 못 나가게 막는 규칙인데, 그리는 함수 안에 한 줄로 들어 있었습니다. 그래서 182 m가 한계인 바다에서 배가 335 m까지 나가 있었고 아무도 몰랐습니다. 화면으로 보면 그냥 바다이고, 크래시도 안 납니다.
세 번째 — 해도 패널이 열려 있는 동안 조타키가 계속 먹었습니다. 지도를 보는 중에 누른 방향키가 배를 돌렸습니다. 이건 조금 다른 종류였는데, keys를 읽는 소비 지점이 여덟 군데라 한 곳씩 막는 방식으로는 끝이 안 났습니다. 결국 키를 받는 자리(keydown)에서 막았습니다. 소비하는 쪽이 여럿이면 소스에서 끊는 편이 확실합니다.
지금은 세 개를 한 세트로 만듭니다
새 시스템을 붙일 때마다 셋을 같이 만듭니다.
- three.js를 부르지 않는 규칙 모듈 — 순수 계산만 있는 파일. 화면 없이 돌릴 수 있습니다
- 그걸 때리는 헤드리스 스크립트 —
npm run <이름>하나로 도는 검사 - 스텝 함수를 밖으로 노출 — 검증이 애니메이션 프레임 없이 직접 시간을 흘려보낼 수 있게
지금 이 프로젝트에는 그렇게 만든 검사가 열 개 넘게 있습니다 — 자료 정합성, 돛 성능 곡선, 걷기, 해도, 수집물, 전투 시뮬레이터, 진행도, 강화, 저장, 배치까지.
코드에도 이유를 적어 뒀습니다.
검증이 돛 조절을 직접 돌릴 수 있도록 밖으로 낸다. 창이 숨겨지면 애니메이션 프레임이 멈추고, 그때 찍은 캡처는 돛이 마지막으로 있던 자세 그대로 나온다 — 돛을 아예 조절하지 않는 장치와 똑같이 보인다.
마지막 문장이 이 글의 요점입니다. 멈춘 것과 원래 안 하는 것은 화면에서 구별되지 않습니다.
다른 데서도 같은 함정을 만났습니다
매치 시즈의 순위표가 안 뜬 적이 있습니다. 화면에 "불러오지 못했습니다 (13/0)"가 떴고, 13은 타임아웃이라는 뜻이었습니다. 서버가 죽었거나 응답이 이상한 줄 알고 응답 형식까지 고쳐서 배포했는데, 원인은 서버가 아니었습니다.
확인용 창이 숨겨져 있어서 게임 루프가 멈춰 있었고, 응답이 도착해도 그걸 받아 처리할 프레임이 돌지 않았습니다. 창이 잠깐 깨어나는 순간 그동안의 시간이 한꺼번에 흘러 들어와 8초 타임아웃이 바로 터진 것입니다. 타임아웃을 꺼 놓고 다시 배포해 보니 순위표가 멀쩡히 11줄 떴습니다. 사용자에게는 처음부터 정상이었습니다.
응답 형식을 고친 것은 되돌렸습니다. 잘못된 원인 진단으로 서버를 건드린 것이라, 남겨 두면 다음 사람이 "이건 왜 이렇게 돼 있지"를 다시 파야 합니다.
그래서 규칙 하나
검사는 "안 터졌다"가 아니라 "이렇게 동작한다"를 단언해야 합니다.
- ✗ 배를 30초 몰았는데 예외가 없었다
- ✓ 배를 30초 몰았더니 중심에서 182 m를 넘지 않았다
앞의 것은 로직이 통째로 안 돌아도 통과합니다. 뒤의 것은 안 돌면 실패합니다. 두 검사는 비용이 거의 같은데 잡아내는 것이 완전히 다릅니다.