본문 바로가기

블로그 / 기술

PallMark 적 무리 AI와 144fps: 평균 fps가 숨긴 것들

수십 마리가 몰려드는 PallMark 탑에서 적 AI를 단순하게 유지한 이유와, 144fps 목표를 지키려고 평균이 아닌 하위 1%와 p99를 재며 찾은 병목들을 정리했다.

Salmonholic ·· 5분 읽기

성소 아레나에서 수십 마리의 기사와 날개 달린 적이 한 지점으로 몰려든 PallMark 전투 장면

PallMark가 목표로 하는 장면은 하나다. 적이 사방에서 몰려오고, 스킬 한 번에 그 무리가 쓸려 나간다. 이 장면에는 조건이 두 개 붙는다. 적이 많아야 하고, 그래도 화면이 끊기지 않아야 한다. 성능 목표는 144fps다. 9월 2일에 한 번 더 확인했고 낮추지 않기로 했다. 한 프레임에 쓸 수 있는 시간은 6.944ms다.

이 글은 적 무리의 AI를 어떻게 단순하게 유지했는지, 그리고 144fps를 재는 방법을 두 번 바꾼 이야기다.

적 AI는 일부러 단순하다

게임 규칙은 20Hz 결정론 시뮬레이션이 돌린다. 적 AI도 그 안에 있다. 적 한 마리가 매 틱 하는 일은 많지 않다.

규칙 값 설명
감지 거리 12유닛 이 안에 들어온 가장 가까운 상대를 고른다
추격 한계 24유닛 대상이 이 밖으로 나가면 다시 고른다
피격 반응 추격 한계까지 맞으면 잠든 적도 깨어나 가장 가까운 상대를 찾는다
추격 재시작 지연 5틱(0.25초) 멈췄던 플레이어가 다시 움직이면 이만큼 뒤에 따라온다
도발 기간제 도발한 대상을 만료 틱까지 강제로 노린다

동률이면 ID 순서로 기존 후보를 유지한다. 결정론이 깨지지 않게 하려는 규칙이다. 추격 재시작 지연은 원래 20틱, 1초였다. 9월 9일에 플레이해 보니 내가 움직이기 시작해도 적이 한참 멈춰 있었다. 5틱으로 줄이면서 코드 상수가 아니라 데이터 값으로 옮겼다.

길찾기는 몬스터마다 따로 하지 않는다. 목표 칸 하나에 대해 8방향 너비 우선 탐색으로 흐름 필드를 한 번 만들고, 그 목표를 쫓는 몬스터 전부가 같은 필드를 읽는다. 목표 칸별로 캐시해 두기 때문에 적이 60마리든 100마리든 탐색은 목표 수만큼만 한다.

몰려든 적끼리는 겹친 만큼 서로 밀어낸다. 한 틱에 최대 0.35유닛까지다. 플레이어와 겹치면 플레이어는 움직이지 않고 적만 옆으로 비킨다. 무리 한가운데서도 내 캐릭터가 밀려나지 않아야 조작한 대로 움직인다는 느낌이 남는다.

자동 플레이 봇의 포위 대응을 바꾼 커밋도 여럿 있지만, 그것은 측정용 봇의 행동이다. 봇이 둘러싸였을 때 도망가지 않고 광역기를 쓰게 바꾼 것은 사람처럼 플레이하게 해서 밸런스를 제대로 재려는 목적이었다. 제품의 적 규칙과 Sim은 건드리지 않았다.

60마리를 처음 띄운 날

8월 28일, Unity 클라이언트에 적 60마리를 처음 띄웠다. 커밋 메시지에는 “대군 60마리(197→281fps, 거리별 Animator 예산)“라고 적었다. 멀리 있는 적의 애니메이션을 덜 자주 갱신하는 방법이었다.

플레이어와 거리 애니메이터 갱신
5유닛 이하 매 프레임
5~9유닛 2프레임에 한 번, 속도 2배
9유닛 초과 4프레임에 한 번, 속도 4배

건너뛴 프레임만큼 재생 속도를 올려 동작 시간은 맞춘다. 적마다 위상을 달리 줘서 갱신이 한 프레임에 몰리지 않게 했다. 탑다운 시점에서 먼 적의 동작이 조금 끊겨도 눈에 띄지 않았다.

평균 fps는 거짓말을 했다

9월 8일 아침 보고서에 처음에는 “프레임률 143~195fps”라고 적었다. 평균은 목표 위였다. 그런데 하위 1%와 순간 최저를 재자 전혀 다른 그림이 나왔다.

진단용 코드를 걷어내기 전후의 프레임 지표 비교 같은 전투를 두고 144fps 미달 비율, 하위 1% fps, 순간 최저 fps를 잰 결과.

144fps를 못 넘은 프레임이 75.4%였다. 원인은 적 무리가 아니라 우리 코드였다. 둘 다 진단용으로 넣었다가 남은 잔재였다. 클래스 선택 미리보기가 화면에 없는데도 캐릭터 카메라를 매 프레임 그리고 있었다. 프레임당 2.54ms였다. 또 매 렌더 프레임마다 몬스터 전원의 좌표를 34열짜리 CSV로 만들어 100틱마다 통째로 로그에 썼다. 전투 한 판에 로그가 67.7MB였고, 그 프레임 하나가 55ms 걸렸다.

둘을 걷어내자 미달 프레임은 6.7%, 하위 1%는 49.5에서 96.2fps가 됐다. 되돌리면 다시 나빠지는 것까지 확인했다. 그래도 6.7%가 남아 이 항목은 닫지 않았다. 이날부터 성능 보고에는 평균 대신 p95와 p99, 예산 초과 비율을 적는다.

p99를 흔든 것은 GC였다

9월 26일, 마을을 “탑 아래 야영지”로 다시 꾸민 뒤 Windows 테스트 PC(RTX)에서 쟀다. 평균 184fps, p95 6.88ms로 예산 안이었지만 p99가 7.54~7.81ms로 넘쳤다. 느린 프레임과 빠른 프레임의 드로콜·배치·삼각형 수는 똑같았다. 렌더 부하가 아니라 CPU 쪽 튐이라는 뜻이었다.

맥에서 프로파일러로 느린 프레임만 모아 보니 GC 수집이 걸려 있었다. 시뮬레이션이 20Hz 틱마다 메모리를 할당해 약 8초마다 GC가 돌았다. 첫 번째로 패시브 카드 조회가 같은 계산을 중복하던 곳을 고쳐 틱당 할당을 441KB에서 313KB로 줄였다. 마을 이동 중 p99는 4.35ms에서 3.78ms가 됐다(맥 기준).

두 번째로 적 40마리가 싸우는 탑 1층에서 할당을 추적했다. 유니크 효과와 스킬 선택, 스킬 기믹 쪽의 중복 할당을 걷어내 틱당 401KB를 333KB로 줄였다. 이 작업에는 조건이 붙었다. 동작이 하나도 바뀌면 안 된다. 순회 순서와 난수 호출 순서를 그대로 두고, 적합성 테스트의 최종 상태 해시와 이벤트가 전부 같아야 했다. 하나라도 바뀌면 그 변경은 되돌리기로 했다.

다만 이 두 번째 수정은 맥에서 탑 p99가 6.43ms에서 7.31ms로 오히려 나쁘게 나왔다. 프레임 수 차이인지 발열인지 가려낼 수 없었다. 그래서 결론을 내지 않고 “Windows 테스트 PC에서 수정 전후를 같은 조건으로 세 번씩 재서, 나빠졌으면 되돌린다”는 판정 계약을 적어 두었다. 좋아졌다고 쓰기엔 근거가 모자랐다.

돌아보면

적이 많은 게임이라서 적 AI부터 의심하기 쉬웠다. 하지만 실제로 프레임을 잡아먹은 것은 화면 밖의 미리보기 카메라와 로그, 그리고 틱마다 새로 만드는 작은 객체들이었다. 적 AI는 단순하게 두고 흐름 필드를 공유했기 때문에 오히려 문제가 되지 않았다.

평균 fps만 봤다면 9월 8일에 “통과”라고 적고 넘어갔을 것이다. 무엇을 재느냐가 무엇을 고치느냐를 정했다.

층이 오를수록 적이 얼마나 많아지는지는 탑 100층 곡선 글에, 틱마다 같은 결과를 내는 시뮬레이션은 포팅 글에 썼다. PallMark는 개발 중이며 Steam 출시 예정이다. 자세한 소개는 PallMark 페이지에 있다.

#PallMark#성능 최적화#Unity#게임 AI#144fps

함께 읽으면 좋은 글

전체 글 →