느린 폰에서 저절로 가벼워지게 — Salmonholic의 적응형 품질 4단계
기기마다 성능이 다른 브라우저 게임에서, 실제 프레임 시간을 보고 입자·소리·화면 흔들림만 자동으로 줄이는 적응형 품질을 넣은 기록. 전투 로직은 건드리지 않아 리플레이 검증은 그대로 통과한다.

Salmonholic은 브라우저에서 도는 10인 배틀로얄 서바이버다. 후반으로 갈수록 연어 떼가 수백 마리로 불고, 무기 이펙트와 피격 입자가 그 위에 겹친다. 데스크톱에서는 문제가 없다. 그런데 브라우저 게임은 어떤 기기에서 열릴지 미리 알 수 없다. 5년 된 폰에서도 열리고, 메모리 3GB짜리 보급형에서도 열린다. 같은 그래픽 설정을 모두에게 쓰면, 데스크톱은 남아돌고 약한 폰은 후반에 프레임이 무너진다.
초반에는 그려야 할 것이 적다. 같은 경기라도 커버의 후반 장면이 되면 화면에 뿌리는 입자가 몇 배로 는다.
고정 설정 화면을 하나 더 만들어 “저사양 모드”를 고르게 할 수도 있었다. 하지만 처음 들어온 사람에게 자기 폰 성능을 묻는 건 무리다. 그래서 0.22.0(커밋 2a20f16)에서 기기가 아니라 실제 프레임 시간을 보고 스스로 단계를 바꾸는 적응형 품질을 넣었다. 이 글은 그 설계 기록이다.
시작 단계만 기기로 추정한다
품질은 high · med · low · min 네 단계다(내부로는 0~3). 첫 단계만 기기 힌트로 정하고, 그 뒤는 측정으로 조정한다.
웹에서는 브라우저가 알려 주는 값만 쓴다. navigator.deviceMemory가 4GB 이하이거나 터치 입력이 있으면 한 단계 낮춰 med로 시작하고, 메모리가 3GB 이하이면서 터치까지 있는 기기(주로 보급형 안드로이드 크롬)는 low로 시작한다. 아이폰은 이 메모리 값을 주지 않아서 med로 둔다. 데스크톱은 high다. 추정이 빗나가도 상관없다. 첫 단계일 뿐이고, 몇 초 안에 실제 프레임이 맞는 단계로 끌어다 놓는다.
프레임 시간을 2초 중앙값으로 본다
판정은 단순하다. 매 프레임의 실제 렌더 시간을 모아 최근 2초의 중앙값을 본다. 평균이 아니라 중앙값을 쓰는 이유는 가끔 튀는 한두 프레임에 휘둘리지 않기 위해서다.
- 중앙값이 22밀리초(약 45fps)를 3초 동안 넘으면 한 단계 내린다.
- 중앙값이 14밀리초(약 71fps)를 10초 동안 밑돌면 한 단계 올린다.
- 단계를 바꾼 뒤 5초 동안은 다시 바꾸지 않는다.
내릴 때는 빠르게(3초), 올릴 때는 느긋하게(10초) 반응하게 비대칭으로 뒀다. 끊기는 건 바로 손봐야 하지만, 올렸다가 다시 끊기면 왔다 갔다 하며 더 거슬리기 때문이다. 5초 대기까지 더해 한번 정해진 단계가 쉽게 흔들리지 않게 했다.
탭을 숨기거나 일시정지·재접속 중인 프레임, 1초를 넘는 프레임은 표본에서 버린다. 백그라운드로 내려갔다 돌아올 때 생기는 거대한 delta가 “느려졌다”로 오인되면 안 되기 때문이다. 테스트에서는 12프레임마다 한 번씩 80밀리초가 튀는 입력을 넣어도 중앙값이 흔들리지 않아 high를 유지하는 것, 반대로 300밀리초짜리 저성능은 제대로 하향을 감지하는 것을 확인한다.
단계가 내려갈 때마다 입자 예산이 절반씩 줄고, 동시 목소리와 화면 흔들림도 함께 낮아진다. 수치는 ui/adaptive_quality.gd에서 가져왔다.
내리는 건 보이는 것뿐이다
여기가 이 설계의 핵심이다. 단계가 내려가도 게임 로직은 한 글자도 바뀌지 않는다. 바뀌는 건 세 가지, 전부 눈과 귀에만 닿는 값이다.
| 단계 | 입자 예산 | 동시 목소리 | 화면 흔들림 |
|---|---|---|---|
| high | 1600개 | 24개 | 100% |
| med | 800개 | 16개 | 55% |
| low | 400개 | 10개 | 25% |
| min | 160개 | 6개 | 0% |
입자는 예산을 넘으면 새로 만들지 않고, 동시에 울리는 효과음 목소리도 상한에 맞춰 오래된 것부터 뺀다. min에서는 화면 흔들림을 아예 끈다. 몬스터 수, 피해 판정, 성장, 난수 — 경기 결과를 가르는 것은 모두 그대로다.
이게 중요한 이유는 Salmonholic이 서버가 판정하는 온라인 게임이고, 같은 입력은 어느 기기에서든 같은 결과가 나와야 하기 때문이다. 리플레이가 서버 기록과 한 프레임도 어긋나지 않는지 검사하는 골든 테스트가 있는데, 적응형 품질을 넣고도 이 테스트는 그대로 통과한다. 품질 코드가 전투 상태나 난수를 읽지 않도록 아예 분리해 뒀기 때문이다.
한 가지 더 신경 쓴 건 입자를 “어느 것을 그릴지” 고르는 방법이다. 매 프레임 앞에서부터 세다가 예산에서 자르면, 입자 배열이 바뀔 때마다 그려지는 입자가 달라져 화면이 깜빡인다. 그래서 입자마다 가진 고정값(수명·크기)으로 그릴지 말지를 정해, 같은 입자는 계속 같은 판정을 받게 했다. 덜 그려도 깜빡이지 않는다.
한 번 배운 단계는 저장한다
자동 모드에서 한 단계로 15초 동안 안정되면 그 단계를 기기에 저장한다. 다음에 다시 들어오면 처음부터 그 단계로 시작하니, 매번 똑같이 끊기면서 단계를 찾아 내려가는 과정을 반복하지 않는다. 사람이 설정에서 직접 품질을 골랐거나 테스트·봇 실행인 경우에는 저장하지 않는다. 사용자의 선택을 측정값이 덮어쓰면 안 되기 때문이다.
텔레메트리로는 최종 단계, 모드, 바뀐 횟수, 마지막 프레임 중앙값만 남긴다. 개인을 식별하는 값은 없다. 이 숫자로 “어떤 조건에서 사람들이 어느 단계에 머무는지”를 보고 기본값과 문턱을 다시 조정할 생각이다.
처음부터 완벽한 그래픽 옵션을 만드는 대신, 측정하고 반응하는 쪽을 골랐다. 기기를 묻지 않고, 프레임이 답하게 한 셈이다. 이 작업은 모바일 첫 판이 멈추던 문제를 쫓던 시기와 겹친다. 그때 성능을 어떻게 추적했는지는 기본 모드 실험과 첫 판 멈춤 이야기에, 브라우저 빌드 자체의 성능 문제는 Godot 4 웹 export 기록에 적어 뒀다. 완성된 게임은 Salmonholic 게임 페이지에서 바로 할 수 있다.


