[내일배움캠프 TIL] 39. 게임 최적화

2023. 11. 10. 23:57내일배움캠프 Unity

[최종프로젝트 11]

오늘은 게임 최적화에 관련된 내용을 공부하고 현재 프로젝트에 적용해 볼것이다.

기본적이고 많이 쓰이는 최적화 방식부터 현재 프로젝트에 쓸 수 있는 최적화 방식 위주로 알아보자!

 

0. 프로파일링

1. 불필요한 옵션 비활성화

2. 스크립트

3. 에셋 임포트

4. 그래픽스 및 GPU

5. UI

 

0. 프로파일링

  • 다양한 최적화 방식을 공부하기 전에 알아둬야할 '프로파일링'
  • 게임이 실행되고 어느 시점에 어떤 이유로 과부하가 일어나는지 확인할 수 있다.
  • 빌드 전에 필수로 체크해 보자.

 

1. 불필요한 옵션 비활성화

  • Accelerometer Frequiency 비활성화
    • '가속도 센서'를 사용하지 않는다면 빈도를 줄이거나 비활성화
  • 플레이어 설정 또는 품질 설정 비활성화
    • 지원되지 않는 플랫폼의 Auto Graphics API 비활성화
    • 오래된 CPU를 지원하지 않는다면 해당 CPU의 Target Architectures 비활성화
  • 물리 비활성화
    • 게임에서 물리를 사용하지 않는다면 Auto Simulation과 Auto Sync Transforms 해제
  • 올바른 프레임 속도 선택
    • 모바일의 경우 기본 30fps로 한계인 60fps까지 밀어붙이기 보다 타협하는것이 좋음
    • Aplication.targetFrameRate를 활용해 런타임 중에 프레임 속도를 동적으로 조정할 수도 있음
  • 대규모 계층 구조 사용 지양
    • Split your hierarchies. 게임 오브젝트가 계층 구조 내에 중첩될 필요가 없다면 부모 자식 관계를 간소화하는것이 좋다.(그룹화 하지 말라는 의미)
    • 계층구조가 복잡하면 불필요한 트랜스폼 연산과 높은 가비지 컬렉션 비용이 발생한다
  • 트랜스폼 한 번에 이동
    • 위치와 회전을 업데이트 할때 하나씩 하기보다는 Transform.SetPositionAndRotiation을 사용해 위치와 회전을 한 번에 업데이트 하면 트랜스폼을 두 번 수정하면서 발생하는 오버헤드를 방지할 수 있다.
    • Instantiate(prefab, parent); => Instantiate(prefab, parent, position, rotation);

 

2. 스크립트

  • 오브젝트 풀링☆
    • 다수의 동일한 객체를 자주 생성하거나 삭제할 시 GC의 동작이 많아져 CPU 사용량에 부정적인 영향을 준다.
    • 미리 사용할 객체를 충분한 개수만큼 생성해두었다가 사용시 꺼내서 사용하고 사용이 끝나면 반납하는 시스템
  • 스크립터블 오브젝트
    • 게임에서 변하지 않는 데이터를 위한 클래스 작성시 모노비헤이비어 보다는 스크립터블 오브젝트 사용
    • 메모리 사용량도 적고 불필요한 Start(), Update()와 같은 프로세스 콜을 줄일수있다.
  • 변수와 프로퍼티
    • 단순한 get; set; 형태보다는 변수를 사용하는 것을 추천
    • 프로퍼티도 함수콜을 사용하기 때문에 스택 프레임에서 오버헤드 발생
    • Start()같은 1회 혹은 적은 수의 호출이라면 큰 문제가 되지 않지만 Update()와 같은 루프문에서 잦은 호출이 일어나는 경우 불필요한 오버헤드가 발생하게 된다.
    • 프로퍼티가 항상 정답은 아니라는 의미?
    • (질문) : 변수를 private으로 선언하고 다른 클래스에서 접근할때 public으로 선언한 프로퍼티로 접근하곤 하는데 프로퍼티를 사용하지 않고 접근하려면 어떻게 해야할까?
    • => (답변) : 선언하는 곳에서 다르게 할것이 아니라 사용하는 곳에서가 중요. get, set을 사용할때마다 부르는것이 영향이 가는것이기 때문에 '캐싱'해서 사용할 것 
  • Resources 폴더
    • 폴더 내 모든 리소스가 사용여부와 관계없이 패키징 된다.
    • 폴더에 있는 리소스의 개수는 앱의 구동시간에 영향을 준다.
    • 앱 구동시 Resources 폴더 내의 모든 리소스를 탐색하여 lookup 테이블을 작성하는데 이로인해 리소스의 개수가 많다면 최초 구동이 느려지게 되고 또 lookup 테이블이 차지하는 메모리 용량도 증가하게 된다.
    • Resources 폴더 사용을 줄이고 사용하더라도 불필요한 리소스는 제거할 것
  • 빈 Unity 이벤트 함수는 삭제
    • Start, Update가 대표적
    • Unity이벤트 함수는 비어있어도 선언되어 있다면 내부의 각 이벤트 컨테이너에 추가되고 이벤트 발생시에 매번 호출되기 때문에 오버헤드 발생
    • 스크립트의 개수보다 생성된 컴포넌트의 개수가 더 영향을 주기 때문에 게임 내에 생성된 컴포넌트 내에 사용하지 않는 이벤트 함수가 선언되어 있다면 상당히 많은 이벤트 콜에 의한 오버헤드가 발생하기 때문에 비용이 발생할 수 있음
    • Release에서는 별다른 구현이 없지만 디버깅 등의 용도로 사용하는 경우 define 분기를 통해 최종 빌드에선 제외될 수 있도록 관리하는 것을 권장
  • Awake, Start 함수는 최대한 가볍게
    • 무거운 Awake와 Start는 객체의 초기화를 지연시키게 되고, 결국 Scene의 초기 동작을 지연시키게 된다.
    • 유니티의 처음 렌더링 결과물이 스크린에 표시되는 시점이 씬의 모든 오브젝트의 스타트호출이 완료된 이후이기 때문에 초기화의 지연은 결국 씬을 처음 띄우는 시간의 지연으로 이어진다.
    • 무거운 초기화 로직은 가능하면 Update 내의 별도의 FSM 등을 통해 별도의 초기화 구간을 구성하여 진행하는 것을 권장
  • Hierarchy 복잡성 줄이기
    • hierarchy의 계층 구조에서 root gameobject는 모든 하위 오브젝트의 트랜스폼 데이터를 배열로 들고있다.
    • 조그마한 트랜스폼 변화에도 하위의 모든 계층에 대한 트랜스폼 계산을 다시하기 때문에 가급적 트랜스폼 변화가 있는 오브젝트를 별도의 트리로 관리하는것이 좋다.
    • hierarchy 구조가 변경되는 내부적으로 root 오브젝트의 트랜스폼 배열의 메모리가 재 할당이 일어나며 모든 배열 값을 다시 설정한다. 이로 인해 GC가 발생하기 때문에 가급적 hierarchy 구조는 변경하지 않는것이 좋다.
    • 계층적 구조가 필요한 경우가 아니라면 가능한 root에 게임오브젝트를 배치하는 것이 성능면에서 좋다.(관리 하기 편하다고 그룹화하지 말 것)
  • 참조 변수 캐싱
    • Find 사용 금지!! 
    • 생성되어 있는 모든 오브젝트를 순회하며 조건 검사를 진행하기 때문에 상당한 오버헤드가 발생한다. 가능하면 직접 참조하거나 초기화 전에 검색하여 검색결과를 캐싱하여 사용하는 것이 좋다.
    • 대부분 비교 검사는 스트링으로 되기 때문에 단순한 인트형 비교보다 비용이 비싸다.
    • 가능하다면 검색된 결과는 별도로 캐싱하여 최대한 재활용한다.
    • Getcomponent() 등을 사용하는 곳에서 매번 부르는것이 아닌 Start에서 변수에 저장하는 것으로 이해하면 쉬움

 

3. 에셋 임포트

  • 올바른 텍스처 임포트
    • POT(Power of Two) 사용 : 유니티에서 모바일 텍스처 압축 형식을 사용하려면 POT 텍스처 필요. 미리 맞춰두지 않는다면 비용이 크게 발생하므로 크기를 2의 배수로 설정할 것
    • 텍스처 아틀라스 사용 : 하나의 텍스처에 여러 텍스처를 배치하면 드로우 콜을 줄이고 렌더링 속도를 높일 수 있다. 위의 POT와도 연관이 있는것으로 아틀라스의 크기를 POT에 맞추어 생성하는 것이 좋다.
    • 불필요한 밉맵 비활성화 : 2D 스프라이트와 UI 그래픽처럼 화면상에 일정한 크기로 유지되는 텍스처에는 밉맵이 불필요. (카메라의 거리에 따라 다른 3D 모델에 대해서는 밉맵을 활성화 된 상태로 유지)
  • 텍스처 압축
    • 압축되지 않은 텍스처와 압축을 한 텍스처는 화질에서 큰 차이를 보이지 않지만 메모리 사용 면에선 큰 차이를 보인다. 모바일 개발시엔 지원되는 ASTC 압축을 사용하는 것이 좋다.
  • 메시 임포트 설정
    • 메시 압축 : 과감한 압축으로 디스크 공간을 줄일 수 있다.(런타임 시 메모리에는 영향을 주지 않음)
      • 실제 압축이라기 보다는 메시 데이터 안에서 불필요해보이는 버텍스를 제거하는 것. 그러다 보니 mesh shape에 영향을 줄 수 있으니 옵션 적용 후 결과물을 확인하고 최대한 높은 압축을 선택하는 것이 권장됨
    • Rig와 BlendShapes 비활성화 : 메시에 골격 또는 블렌드 셰이프 애니메이션이 필요하지 않다면 옵션 비활성화

 

4. 그래픽스 및 GPU 최적화(여기부턴 이해가 잘 되지 않은 상태)

  • 유니티는 프레임마다 렌더링해야하는 오브젝트를 지정한 다음 드로우 콜을 만든다. '드로우 콜'은 오브젝트를 그리기 위한 그래픽스 API 호출이며 '배치(Batch)'는 함께 실행되는 드로우 콜의 그룹이다.
    • 동적 배칭 : 작은 메시의 경우 유니티는 CPU에서 버텍스를 그룹화하고 변환한 다음 모두를 한 번에 그린다. 
    • 정적 배칭 : 움직이지 않는 지오메트리의 경우 유니티는 동일한 머테리얼을 공유하는 메시에 대한 드로우 콜을 줄일 수 있다. 동적 배칭에 비해 더 효율적이지만 메모리 사용량은 늘어난다.
    • GPU 인스턴싱 : 동일한 오브젝트의 수가 많을 때 이 기법을 사용하면 그래픽 하드웨어의 사용을 통해 더 효율적인 배칭이 가능하다.
  • 동적 광원 수 줄이기
    • 모바일 게임에는 동적 광원을 지나치게 많이 추가하지 않는 것이 매우 중요. 동적 메시에는 커스텀 셰이더 이펙트나 라이트 프로브, 정적 메시에는 베이크된 광원과 같은 대안 고려
  • 그림자 비활성화
    • MeshRenderer 및 광원별로 그림자 드리우기를 비활성화 할 수 있다. 가능하면 그림자를 비활성화 해 드로우 콜을 줄이는 것이 좋다. 그림자옵션을 비활성화 하는 대신 캐릭터 아래의 간단한 메시나 쿼드에 블러된 텍스처를 적용하여 가짜 그림자를 만드는 것을 추천
  • 라이트맵에 조명 베이크
    • GI(Global Illumination, 전역 조명)로 정적 지오메트리에 극적인 조명을 추가하자. 오브젝트를 Contribute GI로 표시하면 라이트맵의 형태로 고품질 조명을 저장할 수 있다.
  • 광원 레이어 사용
    • 광원이 여러개인 복잡한 씬의 경우 레이어를 사용해 오브젝트를 분리한 다음 각 광원의 영향을 특정 컬링 마스크로 한정한다.
  • 디테일 수준(LOD) 사용
    • 오브젝트가 멀리 이동하면 LOD를 사용해 단순한 메시와 머테리얼, 셰이더를 사용하도록 조정하거나 전환하여 GPU 성능을 보조할 수 있다.
  • 오클루전 컬링을 사용하여 숨겨진 오브젝트 제거☆
    • 다른 오브젝트 뒤에 숨겨진 오브젝트는 계속 렌더링 되며 리소스 비용을 발생시킨다. 오클루전 컬링을 사용하면 이런 보이지 않는 불필요한 오브젝트를 폐기할 수 있다.
    • 카메라 뷰 바깥의 절두체 컬링은 자동인 반면 오클루전 컬링은 베이크 된 과정이다.
    • 오브젝트를 Static Occluders 또는 Occludees로 표시한 후 Window > Rendering > Occlusion Culling을 통해 베이크
  • 모바일 기기의 네이티브 해상도 사용 지양
    • 스마트폰과 태블릿이 발전함에 따라 새로 출시되는 기기일수록 높은 해상도를 자랑하곤 함
    • Screen.SetResolution(sidth, height, false)를 사용하여 출력 해상도를 낮추면 어느정도의 성능을 다시 확보할 수 있다. 여러 해상도를 프로파일링하여 화질과 속도 사이에서 최적의 균형을 찾는것이 좋다.
  • 카메라 사용 제한
    • 모든 카메라는 작업 수행에 어느 정도의 오버헤드를 유발한다. 렌더링에 필요한 카메라 컴포넌트만 사용
    • 필요시 버츄얼 카메라 활용
  • 셰이더를 단순하게 유지
    • URP는 이미 모바일 플랫폼에 최적화된 경량 릿 셰이더와 언릿 셰이더를 다양하게 포함한다.
    • 이는 런타임 메모리 사용량에 상당한 효과를 미칠 수 있으므로 셰이더 베리에이션을 최대한 낮게 유지하는 것이 좋다.
  • 오버 드로우(겹쳐 그려진 것)와 알파 블렌딩(투명도)의 최소화
    • 모바일 플랫폼은 오버드로우와 알파 블렌딩의 영향을 크게 받는다. 불필요한 투명 및 반투명 이미지 혹은 이펙트를 겹치지 않는것이 좋다.
  • 포스트 프로세싱 이펙트 제한
    • 글로우와 같은 전체화면 포스트 프로세싱 이펙트는 성능을 크게 떨어뜨릴 수 있다. 프로젝트의 아트 방향성에 따라 주의하며 사용해야함.

 

5. UI

  • 보이지 않는 UI는 Hide 시키기
    • 카메라 밖으로 벗어나거나 다른 UI에 가려져 실제로 보이지 않는 UI도 실제로는 매 프레임마다 렌더링되고있다. 명시적으로 Activate를 끄거나 Hide 옵션을 설정하면 불필요한 렌더링 비용을 줄일 수 있다.
  • UI를 움직인다면 Canvas를 분리하자
    • 정적인 UI와 동적인 UI를 분리시키면 UI가 변경되면서 발생하는 UI Mesh 재생성을 최소화 할 수 있다.
  • 하나의 Canvas에 있는 UI의 Z값을 동일하게 설정하자
    • 같은 Canvas라도 UI의 Z값이 다르면 Batching이 깨진다.
  • 같은 텍스쳐와 머테리얼을 사용하자
    • Atlas를 적극 활용해 같은 머테리얼과 텍스처를 사용해 드로우 콜을 줄이자
  • Graphic Raycaster를 비활성화 하자
    • 마우스나 터치 등의 입력 이벤트 처리가 필요한 Canvas만 Graphic Raycaster를 활성화
    • 입력 이벤트가 발생하면 Graphic Raycaster 옵션이 활성화된 Canvas 밑에 Raycast가 선택된 모든 UI 요소들을 대상으로 입력에 대한 처리를 시도하기 때문
  • 풀스크린 UI(질문사항)
    • Pause나 메인Menu를 띄울때 전체화면 UI 사용을 권장
    • 전체화면 UI를 사용하여 UI 뒤편의 게임화면이 가려지더라도 여전히 게임화면의 렌더링 비용이 발생한다. 전체화면 UI가 활성화 되면 게임 화면의 Camera와 다른 UI등 보이지 않는 오브젝트를 모두 비활성화 해 렌더링 비용을 줄일수 있다.
    • 가려지는 UI는 HIde 시키기
    • Frame Rate를 낮추자
    • 메인 게임 렌더링을 비활성화 하고 Frame Rate를 낮추면 CPU, GPU 부하를 대폭 줄일수 있고, 메뉴를 열고 있는 시간만큼 CPU와 GPU가 쉴 수 있게 되어 과부하에 의한 Throttling 현상을 예방하는데 도움이 된다. 이는 게임 전체의 Frame Rate를 안정적으로 유지하는데 아주 중요하다.
    • Throttling 현상이란 : 기기의 CPU, GPU등이 지나치게 과열될때 기기의 손상을 막고자 클럭과 전압을 강제적응로 낮추거나 전원을 꺼 발열을 줄이는 기능