2014년 10월 17일

언리얼 엔진의 게임 스레드 CPU 퍼포먼스 향상법

저자: * Bob Tellez

게임에서 프레임 속도가 안나오나요? 왜 그런지 아세요? 한 번에 적을 너무 많이 스폰시켜서 그럴까요, 어떤 적 하나가 너무 비싸서 그럴까요? 비주얼 이펙트를 한 트럭 만들어서 그럴까요, 아까 만든 그 멋진 스킬 시스템 때문일까요?

호흡을 가다듬고 성급한 결론을 내리지 마세요!

퍼포먼스 문제 해결을 위한 첫 단계는, 어떻게 풀어갈지 정보를 기반으로 한 의사를 결정할 수 있도록 정보를 수집하는 것입니다. "레벨에 액터가 100,000 개나 있으니까 당연히 느리지!" 같은 소리는 누구나 할 수 있습니다. 데이터를 적절히 수집했다면 완전히 무관했을 문제도 훨씬 쉽게 고칠 수 있었던 것을, 레벨에 있는 액터 갯수 줄이느라 허송 세월 보내고 말 수가 있습니다.

알았어요! 어디서부터 시작하죠?

먼저 수집해야할 정보라면, 병목현상이 게임 스레드, 렌더링 (Draw) 스레드, GPU 중 어디서 발생하는지 입니다. 그것을 알아보려면 게임을 debug 빌드가 아닌 것으로 실행한 다음 "stat unit" 콘솔 명령을 쳐서 각각의 소요 시간을 표시합니다.

CPU Performance

Frame 시간은 게임의 한 프레임을 생성하는 데 걸린 총 시간입니다. Game, Draw 스레드는 프레임 마무리 전 동기화가 맞춰지므로, 보통 Frame 시간은 위 스레드 시간 중 하나와 비슷하게 마련입니다. GPU 시간은 비디오 카드에서 씬을 렌더링하는 데 걸린 시간입니다. GPU 시간은 프레임에 동기화가 맞춰지므로, 이 역시도 Frame 시간과 비슷해 지게 마련입니다.

Frame 시간이 Game 시간에 매우 근접했다면, 게임 스레드에 병목현상이, Frame 시간이 Draw 시간에 근접했다면, 렌더링 스레드에 병목현상이, 둘 다 괜찮은데 GPU 시간이 근접했다면, 비디오 카드에 병목현상이 있는 것입니다.

이 글에서는 게임 스레드 문제 처리법에 대해서만 이야기해 보겠습니다.

아하! 게임 스레드에 병목현상이 있는 것을 알았습니다. 다음엔 어쩌죠?

게임 스레드 퍼포먼스를 살펴보기 위한 최적의 툴은 stat 프로파일을 찍어보는 것입니다. 프로파일을 시작하는 방법은 물결표(~) 키를 누르면 열리는 콘솔에 "stat startfile" 이라 입력하여 프로파일을 시작하면 됩니다. 최소 10 초 정도 실행되도록 놔두면 여러 프레임에 걸친 안정된 평균치를 구할 수 있습니다. 프로파일 기간은 길어도 좋으며, 장기간에 걸친 간헐적 문제를 감지해 내는 데도 사용할 수 있습니다만, 30 분 이상의 프로파일링은 파일 크기가 너무 커지므로 좋지 않습니다. 적정 시간만큼 샘플을 채취했으면 "stat stopfile" 명령을 입력하여 프로파일링을 끝냅니다. ue4stats 파일은 Saved/Profiling/UnrealStats 아래 프로젝트 폴더에 저장됩니다.

좋아요, 프로파일을 작성했습니다. 어떻게 열어보나요?

캡처된 프로파일은 UE4Editor 와 같은 폴더에서 찾을 수 있는 UnrealFrontend 로 열수 있습니다. 아 툴은 창 메뉴에서 찾을 수 있는 세션 프론트엔드 탭을 열어도 됩니다. 세션 프론트엔드 탭이 열리면, 작은 Profiler 탭을 열어줘야 합니다. 거기서 최근 캡처한 ue4stats 프로파일을 선택해 로드하면 됩니다.

Device Manager

프로파일은 열었는데, 무엇을 찾아봐야 할까요?

중요한 정보는 맨 밑의 함수 트리에 있습니다. GameThread 항목을 펼쳐서 "Inc Time" (포함 시간)이 2ms 이상이면서 자손이 많지 않거나 없는 항목이 나올 때까지 파고내려가 봅니다. 매 프레임 해당 stat 가 호출된 평균 횟수가 표시되는 "Calls" 열도 눈여겨 봅니다. "CPU Stall" 항목에는 현혹되지 마세요. 다른 것을 기다리느라 소요된 시간이 표시되는 것일 뿐이라 중요하지 않으며, 프레임 속도가 제한되었는지 병목현상이 게임 스레드에 있지는 않은지만 나타냅니다. 아래 그림의 프로파일에서는, 폰트 캐시 작업에 걸리는 시간이 조금 의심스럽습니다.

CPU Stall

이 문제는 바로 이번 주에 포트나이트 작업을 하면서 발생된 문제로, 카메라와 게임내 중요 오브젝트 사이의 거리에 따라 크기가 변하는 텍스트를 많이 표시하여 발생하는 문제였습니다. 매 프레임마다 텍스트 크기를 조절하고 있었기 때문에, 언리얼 엔진의 UI 시스템인 슬레이트의 폰트 캐시가 크기만 다른 동일한 문자열 수백종으로 채워지고 있었습니다. 이 문제는 일정한 거리 한계치에서 텍스트 크기를 별도로 조정해 줬어도 되는 일이나, 거리에 따라 동적으로 텍스트 스케일을 조절하던 것을 멈추는 것으로 해결했습니다.

포트나이트 일은 잘 됐네요. 그런데 제 문제는 '폰트 캐시' 문제가 아닙니다.

주로 용의선상에 올라오는 녀석들이 몇몇 있습니다.

주 용의자는 FTickFunctionTask 입니다. 이 항목 아래에는 틱이 되는 모든 액터와 컴포넌트가 있습니다. 종종 매 프레임 틱되는 액터와 컴포넌트 수를 줄이면 게임 속도가 크게 빨라집니다.

FTickFunction Task

C++ 코드 게임에서 절대 틱되지 않는 액터가 있는 경우, 액터의 생성자에 다음과 같은 코드를 넣으면 완전히 틱되지 않도록 막을 수 있습니다:

PrimaryActorTick.bCanEverTick = false;

일정 기간동안만 틱되는 액터인 경우, 생성자에 다음 부분을 대신 넣어도 됩니다:

PrimaryActorTick.bCanEverTick = true;

PrimaryActorTick.bStartWithTickEnabled = false;

그런 다음 SetActorTickEnabled 함수로 틱을 껐다 켰다 하면 됩니다.

계속 주시해야하는 것 다른 하나는 BlueprintTime 입니다. 이것을 찾아내는 가장 좋은 방법은, 포함 (합침) 뷰로 전환하여 목록에서 위치를 확인하는 것입니다. 그러면 모든 BlueprintTime 항목을 한 줄로 합쳐 넣습니다. BlueprintTime 을 선택한 다음 계층구조 뷰로 다시 전환하면, 블루프린트 코드가 실행되는 모든 곳이 선택되어, 어느 블루프린트에서 무엇을 하느라 시간이 소요되는지 매우 잘 알아볼 수 있습니다.

Average Speed

자주 떠오르는 용의자 또 하나는 TickWidgets 입니다. 이 수치가 높다면, 한 번에 표시되는 위젯 수가 너무 많다거나, 이 위젯의 특성에 대한 델리게이트가 너무 복잡하다는 뜻입니다. 표시여부(visibility)와 같은 일부 슬레이트 특성은 프레임당 몇 번씩 호출될 수가 있으므로, 크기가 작고 제때 반환을 해야 합니다.

게임에 스켈레탈 메시가 많다면? SkinnedMeshComp Tick 시간이 가끔 비싸질 수 있습니다. 프로파일에 표시된 스켈레톤의 본 수를 줄여보거나, 애님 블루프린트의 복잡도를 줄여보세요. 스켈레탈 메시가 보이지 않아 애니메이션 업데이트를 할 필요가 없는 경우, 스켈레탈 메시의 MeshComponentUpdateFlag 프로퍼티를 OnlyTickPoseWhenRendered 로 설정해 보세요. 다만 이렇게 설정된 메시는 렌더링되지 않으면 애님 노티파이가 더이상 발동되지 않으니 주의를 요합니다.

사실 가끔씩 버벅이는 원인이 무엇인지 찾고 있습니다.

가장 좋은 방법은 타임라인에서 솟구친 부분을 찾아 그 주변 프레임들을 선택한 다음 "Average" (평균)이 아닌 "Maximum" (최대)로 설정합니다. 그러면 선택된 프레임 범위의 수치가 평균치가 아닌 최대치로 변경됩니다.

Graph View

고맙습니다!

프로파일러를 사용하는 것은 전반적인 게임 퍼포먼스에 매우 중요하며, 진짜 문제를 이런 저런 가정으로 짚어내는 노고를 덜 수 있습니다. 프로파일러의 모든 기능에 대한 상세 정보는, 관련 문서를 참고해 주시기 바랍니다.