호빵의 IT 개발소

[유니티/C#] 최적화 시작 본문

C#, 유니티/유니티엔진 기초

[유니티/C#] 최적화 시작

호빵Stack 2022. 6. 28. 00:14

병목 파악

  • 병목 현상은 전체 시스템의 성능이나 용량이 하나의 구성 요소로 인해 제한을 받는 현상
  • CPU - 너무 많은 DP CALL(CPU가 GPU에 그리라는 명령을 직접적으로 호출), 복잡한 스크립트나 물리 연산
  • Vertex Processing - 너무 많은 버텍스들(Vertex : '정점' 으로서 3D오브젝트를 표현 하기위한 3차원의 점,선,면 개념중 점에 해당), 버텍스당 너무 많은 연산
  • *Fragment Processing - 너무 많은 픽셀, 오버 드로우(오버드로는 렌더링의 단일 프레임에서 시스템이 화면에 한 픽셀을 여러 번 그리는 것을 의미), 프래그먼트당 너무 많은 연산(Fragment Shader / Pixel Shader)
  • Band Width - 크고, 압축되지 않은 텍스쳐( 3차원 물체의 표면에 2차원 이미지를 입혀서 적은 삼각형으로도 높은 디테일을 표현할 수 있게 해주는 랜더링 관련 요소), 고해상도 프레임 버퍼(화면에 그려질 화면 전체에 대한 정보를 담는 공간, 메모리)

*Fragment Processing : 프래그먼트 프로그램(셰이더)에 의해 수행되며, 다양한 처리를 통해 프래그먼트의 색상을 결정한다. 

 

 

스크립트

  • 유니티의 핵심 기능은 C++로 제작 되어있음(예 : 스크립트에서 사용하는 Transform.position 에서 Transform은 C#속성, position은 C++영역)
  • 유니티 객체들을 멤버 변수에 저장해서 캐싱하여 사용
private Damage;

void Awake()
{
	_damage = GetComponent<Damage>();
}
  • FindObject 계열 함수는 느리다. (미리 찾아서 캐싱) - Find는 프로젝트 안의 모든 오브젝트를 순환하여 알 맞는 클래스의 스트링을 비교하여 적용하기 때문에.
  • Instantiate와 Destroy 함수를 이용한 Prefab의 생성/해제는 비용이 크다 - 활성화/비활성화를 활용한 오브젝트 풀을 사용하는 것이 좋습니다.
  • Updat 함수보다는 Coroutine을 활용한다.
  • 박싱과 언박싱은 부하가 큰 작업이다. (https://hobbangim.tistory.com/145)
  • 나눗셈보다는 곱셉이 빠르다. - 예) 100/10 보다는 100*0.1이 빠름
  • magnitude 보다는 sqrMagnitude를 사용해서 비교한다. (제곱근 계산 X)
  • 삼각함수의 값은 상수로 저장하고 사용하는 것이 좋다.
  • 문자열은 readonly 혹은 const키워드를 사용해 가비지 컬렉션으로부터 벗어나도록 한다.

 

 

가비지 컬렉션(https://hobbangim.tistory.com/148)

  • Mono의 동적 메모리 관리 때문에 메모리 해제를 위해 GC가 자동 호출된다.
  • GC는 언제 일어날지 모른다.
  • GC가 일어나면 게임이 멈추는 현상이 발생하게 된다.
  • 동적 메모리 해제가 가능한 일어나지 않도록 하는 것이 GC 관리의 핵심
  • 우리가 쓰고 있는 MonoBehavior는 메모리 관리에 GC가 자동 호출되도록 설계되어 있습니다. 이 GC가 프로그래머의 입장에서 좋을 수도 있고 그렇지 않을 때도 있습니다. GC가 실행되는 동안 많은 양을 처리하게 되면 렉 현상을 겪게 되며 그렇지 않기 위해서는 게임을 만들 때 GC를 고려하여 만들어야 합니다. 가비지 컬렉션의 할 일을 줄여주기 위한 방법에 대해 알아보겠습니다.
1) 무엇이든 동적 생성 및 해제는 굉장히 비용이 큰 작업이다. 
- 위에 언급된 바가 있는 오브젝트 풀링 기법을 사용해 메모리를 관리하는 것이 좋습니다.

2) 오브젝트가 해제되면 다음 과정으로는 GC가 동작되어 렉이 걸릴 수 밖에 없다.
- 오브젝트를 만들어둔 후 활성화 또는 비활성화를 이용해 사용하도록 하는 것이 좋습니다.

3) 문자열 병합은 StringBuilder의 Append를 사용하면 좋다.
- string + string은 임시 문자열을 뱉기 때문에 가비지 컬레션이 일어나는 환경을 제공하기 때문입니다.

4) foreach 대신에 for를 이용하도록 한다.
- foreach는 한번 돌리면 24byte의 가비지 메모리를 생성시키게 되며 수없이 많이 돌면 더 많은 메모리를 생성시키게 되므로 for문을 이용하도록 하는 편이 좋습니다.

5) 태그 비교에서는 CompareTag()를 사용 한다.
- 객체의 tag 프로퍼티를 호출하는 것은 추가 메모리를 할당하며 복사를 하게 됩니다.

6) 모든 비교문에서 .equals()를 사용하도록 한다.
- "=="구문으로 사용하게되면 임시적인 메모리가 남게 되며 가비지 컬렉션이 할 일이 늘게 됩니다.

7) 데이터 타입은 Class 대신 Struct를 사용하여 만들어 주면 메모리 관리가 된다.
- 구조체는 메모리 관리를 Stack에서 하므로 GC에 들어가지 않게 됩니다.

8) 즉시 해제시에는 Dispose를 수동으로 호출하게 되면 즉신 클린업 된다.

9) 임시 객체를 만들어내는 API를 조심해야 한다.
- GetComponents<T>, Mesh, Vertices, Camera,allCameras 등등

10) 객체의 변경 사항에 대해 캐싱한다.
- 객체의 이동과 변형에 대한 처리를 캐싱해서 매 프레임당 한번만 처리합니다.

11) 컴포넌트 참조를 캐싱한다.
- GetComponent()는 한번만 호출하며 객체를 캐싱해서 사용합니다.

12) 콜백 함수중 쓰지 안흔 함수는 제거 한다.
- Start(), Update(), OnDestroy() 등.. 비이ㅓ있어도 성능에 영향을 끼치므로 사용하지 않으면 지워주도록 합니다.

 

 

리소스 최적화

1) 권장 압축 텍스쳐 사용하기

- 아이폰(PowerVR) : PVRCT

- 안드로이드(Tegra) : DXT

- 안드로이드(Adreno) : ATC

- 안드로이드(공통) : ETC1

 

2) 텍스쳐

# 텍스쳐 사이즈는 무조건 2의 제곱이어야 한다.

- POT(Power of Two)

- POT가 아닌 경우 POT 텍스쳐로 변환되어 로딩된다.

- 900 x 900 -> tlfwpfhsms 1024 x 1024로 변환

- 화면 해상도에 맞추어 1280으로 만드는 경우엔 실제 메모리에 2048로 생성됩니다.

 

# 텍스쳐 아틀라스를 활용한다.

- 텍스쳐 아틀라스로 최대한 묶음

- UI만이 아니라 같은 재질의 오브젝트들을 묶음

- 한 화면에 나오는 텍스쳐끼리(UI)

- 알파가 있는 텍스쳐끼리, 알파가 없는 텍스쳐끼리 묶어야 합니다.

- 압축된 텍스쳐와 밉맵을 사용한다.(대역폭 최적화)

- 32 bit가 아닌 16 bit 텍스쳐 사용도 상황에 맞게 고려 합니다.

 

# 텍스쳐 메모리 사용량 프로파일링

- 메모리 사용량 로그를 서버에 남기면 프로파일링에 도움이 됩니다.

 

3) Mesh

# Import시에 언제나 "Optimize Mesh" 옵션 사용한다.

- 변환 전, 후 버텍스 캐쉬를 최적화 해줍니다.

 

# 언제나 Optimize Mesh Data 옵션을 사용한다.

- Player Setting -> Othe Settings

- 사용하지 않는 버텍스 정보들을 줄여준다.

 

4) 오디오

# 모바일에서 스트레오는 의미 없다

- 모두 92kb, 모노로 인코딩

 

# 사운드 파일을 Import하면 디폴트로 3D 사운드로 설정

- 2D 사운드로 변경

 

# 압축 사운드(mp3, ogg), 비압축 사운드(wav) 구별

- 비압축 사운드 : 순간적인 효과음, 이펙트 등..

- 압축 사운드 : 배경 음악

 

5) 폰트 리소스 최적화

# Packed Font를 사용

- R, G, B, A 채널에 저장하는 기법으로 메모리 용량을 1/4 절약하도록 합니다.

- Packed Font는 단점이 많습니다. 일반적으로 글씨에 그림자도 못 넣고 알파도 적용이 안됩니다. NGUI Atlas 적용도 되지 않습니다.

 

6) 리소스 기타

# ResourceLoadAsync()함수는 엄청 느리다.

- 게임 레벨 로드 시에 사용했을 경우, 일반 함수에 비해 수십 배나 더 느립니다.

 

그래픽스 최적화

1) Draw Call(DP Call)

- "적절한 DP Call은 얼마 정도 인가요?" : 일반적으로 100 이하를 추천합니다. 보통 70 ~ 100 정도가 일반적입니다.

- 하지만 실제로는 케이스 바이 케이스

 

2) Culling

- 가장 빠르게 그리는 방법은 아무것도 그리지 않는 것입니다.

 

3) 프러스텀(Frustum) 컬링

- 각 Layer 별로 컬링 거리를 설정하는 것이 가능합니다.

- 멀리 보이는 중요한 오브젝트(ex. 성, 산맥..)는 거리를 멀게 설정하고 중요도가 낮은 풀이나 나무 등은 컬링 거리를 짧게 설정합니다.

 

4) 오클루젼(Occlusion) 컬링

- Window -> Occlusion Culling 메뉴에서 설정 가능합니다.

- 카메라에 보이는 각도의 오브젝트들만 렌더링하는 기법

 

5) 오브젝트 통합 (Combine)

- 드로우 콜은 오브젝트에 설정된 재질의 셰이더 패스당 하나씩 일어납니다.

- 렌더러에 사용된 재질의 수만큼 드로우 콜이 발생합니다.

- Combine(통합)

   [1] 성질이 동일한 오브젝트들은 하나의 메쉬와 재질을 사용하도록 통합

   [2] Script 패키지 - CombineChildren 컴포넌트 제공(하위 오브젝트를 모두 하나로 통합)

   [3] 통합하는 경우 텍스쳐는 하나로 합쳐서, Texture Atlas를 사용해야 된다.

 

6) Batch

- Static Batch

   [1] Edit -> Project Setting -> Player에서 설정합니다.

   [2] 움직이지 않는 오브젝트들은 static으로 설정해서 배칭이 되게 합니다.

   [3] Static으로 설정된 게임 오브젝트에서 동일한 재질을 사용할 경우 자동으로 통합합니다.

   [4] 통합되는 오브젝트를 모두 하나의 커다란 메쉬로 만들어서 따로 저장합니다.(메모리 사용량 증가)

- Dynamic Batch

   [1] 움직이는 물체를 대상으로 동일한 재질을 사용하는 경우 자동으로 통합 합니다.

   [2] 동적 배칭은 계산량이 많으므로 정점이 900개 미만인 오브젝트만 대상이 됩니다.

 

7) Lighting(라이팅)

- 라이트 맵을 사용한다.

   [1] 고정된 라이트와 오브젝트의 경우(배경) 라이트 맵을 최대한 활용합니다.

   [2] 아주 빠르게 실행 됩니다. (Per-Pixel Light 보다 2~3배)

   [3] 더 좋은 결과를 얻을 수 있는 GI와 Light Mapper를 사용할 수 있습니다.

-라이트 렌더 모드

   [1] 라이팅 별로 Render Mode : Important / Not Important 설정이 가능

   [2] 게임에서 중요한 동적 라이팅만 Important로 설정 (Per-Pixel Light)

   [3] 그렇지 않은 라이트들은 Not Important로 설정

 

8) Overdraw

- 화면의 한 픽셀에 두 번 이상 그리게 되는 경우 (Fill rate)

   [1] DP Call의 문제만큼이나 Overdraw로 인한 프레임 저하도 중요한 문제

   [2] 특히 2D 게임에서는 DP Call 보다 더욱 큰 문제가 됩니다.

- 기본적으로 앞에서 뒤로 그린다.

   [1] Depth testing으로 인해 오버드로우를 방지합니다.

   [2] 하지만 알파 브렌딩이 있는 오브젝트의 경우에는 알파 소팅 문제가 발생합니다.

- 반투명 오브젝트의 개수의 제한을 건다.

   [1] 반투명 오브젝트는 뒤에서부터 앞으로 그려야 합니다. -> Overdraw 증가

   [2] 반투명 오브젝트의 지나친 사용에는 주의해야 합니다.

- 유니티 Render Mode를 통해 overdraw 확인이 가능

 

9) 유니티 셰이더

- 기본 셰이더는 모바일용 셰이더 사용

   [1] 기본 셰이더를 사용할 경우에 모바일용 셰이더를 사용합니다. Mobile -> VertexLit은 가장 빠른 셰이더

- 복잡한 수학 연산

   [1] pow, exp, log, cos, sin, tan 같은 수학 함수들은 고비용입니다.

   [2] 픽셀별 그런 연산을 하나 이상 사용하지 않는 것이 좋습니다.

   [3] 텍스쳐 룩업 테이블을 만들어 사용하는 방법도 좋습니다.

   [4] 알파 테스트 연산(discaed)은 느립니다.

   [5] 기본적인 연산보다는 최적화 시키고 간략화 시킨 공식들을 찾아서 사용할 수 있습니다.

- 실수 연산

   [1] float : 32 bit - 버텍스 변환에 사용, 아주 느린 성능 ( 픽셀 셰이더에서 사용은 피함)

   [2] Half : 16 bit - 텍스쳐 uv에 적합하며 대략 2배 빠릅니다.

   [3] fixed : 10bit - 컬러, 라이트 계싼과 같은 고성능 연산에 적합. 대략 4배 빠름

- 라이트 맵을 사용

   [1] 위의 라이팅에서 설명한 라이트 맵을 사용합니다.

 

 

물리엔진 최적화

1) Fixed Update 주기 조절

- FixedUpdate()는 Update와 별도로 주기적으로 불리며, 주로 물리 엔진 처리

- 디폴트는 0.02초, 즉 1초에 50번 호출합니다.

- TimeManager에서 수정이 가능합니다.

- 게임에 따라 0.2초 정도(혹은 이상)로 수정해도 문제 없습니다.

 

2) 물리 엔진 설정

- Static Object

   [1] 움직이지 않는 배경 물체는 Static으로 설정합니다.

- 충돌체의 이동

   [1] 리지드 바디가 없는 고정 충돌체를 움직이면 CPU 부하가 발생합니다. - 물리 월드 재구성

   [2] 이럴 경우에 리지드 바디를 추가하고 IsKinematic 옵션을 사용

- Maximum Allowed timestep 조정

   [1] 시스템에 부하가 걸려 지정된 시간보다 오래 걸릴 경우, 물리 계싼을 건너 뛰는 설정

- Solver Iteration Count 조정

   [1] 물리 관련 계산을 얼마나 정교하게 할지를 지정합니다. (높을수록 정교함)

   [2] Edit -> Project Setting -> Physics

- Sleep 조절

   [1] 리지드바디의 속력이 설정된 값보다 작을 경우 휴면 상태에 들어갑니다.

   [2] Physics.Sleep() 함수를 이용하면 강제 휴면 상태를 만들 수 있습니다.

 

3) 2D 물리 vs 3D 물리

- 2D 게임에는 2D 물리를 (RigidBody2D), 3D 게임에는 3D 물리를(RigidBody)

 

4) 물리 엔진 스크립트

- 래그돌 사용을 최소화 한다.

   [1] 래그돌은 물리 시뮬레이션 루프의 영역이 아니기 때문에 꼭 필요할 때만 활성화 합니다.

- 태그 대신 레이어 사용

   [1] 물리 처리에서 레이어가 훨씬 유리합니다. 성능과 메모리에서 장점을 가집니다.

- 메쉬 콜라이더는 절대 사용하지 않습니다.

- 레이캐스트와 Sphere Check 같은 충돌 감지 요소를 최소화 합니다.

 

5) Tilemap Collision Mesh

- 2D 게임에서 타일 맵의 Collision Mesh를 최적화 한다.

   [1] Tilemap을 디폴트로 사용해서 각 타일별로 충돌 메쉬가 있는 경우 물리 부하가 커집니다.

   [2] 연결된 Tilemap을 하나의 Collision Mesh로 물리 연산을 최적화 합니다.

 

 

 

 


출처 및 참조 :

https://loadofprogrammer.tistory.com/148

'C#, 유니티 > 유니티엔진 기초' 카테고리의 다른 글

[유니티] Euler, Quaternion  (0) 2022.07.04
[유니티] NGUI, UGUI, DOTween  (0) 2022.06.29
[유니티] 유니티 엔진의 특징  (0) 2022.06.27
[유니티/C#] Invoke  (0) 2022.06.21
[유니티/C#] 코루틴(Coroutine)  (0) 2022.06.10
Comments