본문 바로가기

Starters 부트캠프/B - log

유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 25주차 학습 일지

이번 주 역시 현업 관련해서 딥한 강의들이 진행되었다.

유니티 개발에서 우리가 어려워했던 내용들에 대한 강의부터 최적화 등 상당히 많은 내용을 다루셨는데, 확실히 혼자서 공부해서는 알 수 없었던 것들을 배울 수 있어서 너무 좋았다.

 

팀 프로젝트는 디테일한 부분에서 생각보다 어려움을 겪고 있다.

특히나 터렛 모드 설정, 인벤토리 툴팁, 상점 등 기능을 하나씩 더 추가하면서 생각지도 못한 버그들이 많아졌고, 원래 계획했던 것보다 오랜 시간이 걸리고 있다.

 


 

폰트 에셋, 렌더링 파이프라인

유니티에서 TMPro를 사용할 때에 만들게 되는 폰트 에셋, 아틀라스에 대한 내용과 항상 유니티 프로젝트에서 이루어지고 있지만, 정작 잘 알지 못했던 렌더링 파이프라인에 대해서도 더 자세히 얘기해주셨다.

 

Font Asset

Text vs TMPro

Text와 textMeshPro는 모두 유니티에서 글자를 출력하는 UI이다.

하지만, 두 가지는 큰 차이가 있는데, 이는 각각을 world position으로 생성해보면, TextMeshPro 글자가 많이 깨지는 것을 볼 수 있다.

 

이런 차이가 나는 이유는 Text는 텍스쳐를 그때 그때 만들어서 쓰는 다이내믹 폰트인데 반해서 TextMeshPro는 미리 폰트 아틀라스로 텍스쳐를 만들어놓고 사용하기 때문이다.

그런 고로 크기 상관없이 Text는 그때그때 텍스쳐를 그려서 깨질 일은 없지만, 반대로 말하면, 매번 크기에 따라 새로운 메모리 공간을 쓰게 되어 낭비가 될 수 있다. 그래서 특히 다국어 지원 게임에서는 폰트 별로 그리고, 사용한 단어만 따로 모아서 아틀라스로 만들어 사용하고는 한다.

 

폰트가 깨지는 경우

TMPro로 생성한 폰트 에셋을 사용하는데 자꾸 글씨가 깨져서 나오는 경우, 다음과 같은 사항들을 살펴보자.

1. Font Asset Creater 로그에서 성공한 개수 살펴보기
2. Auto Sizing 옵션을 바꿔보기
3. 정말 글자 수가 많아서 아틀라스 페이지를 넘어가는 경우

 

2번의 경우는 아틀라스의 크기를 수동으로 설정하면 된다. 

자신이 만들 글자의 크기가 얼마나 큰지에 따라서, 폰트의 글자 수에 따라서 아틀라스의 크기를 조절하면 되는데, 만약 설정한 아틀라스 크기보다 생성할 폰트의 크기가 커지게 되면, 패딩 값이 축소되면서 깨지거나 아예 누락되어 버릴 수가 있다.

이 경우 아틀라스 텍스쳐 크기를 더 키우거나 사용하는 글자만 추려서 만들어야 한다. (아니면, 사용하는 용도에 따라서 여러 장으로 만들어야 할 수도 있다.)

 

다국어 개발 시에 이런 고민이 많아지는데..

보통은 게임을 다 만들어두고, 사용한 단어마다 id를 설정해서 csv 파일 등으로 목록을 만들어두고, 그에 대응하는 여러 나라들의 단어들을 같이 저장한 뒤에 게임 시작 시에 설정된 언어에 맞는 단어를 불러오게 하는 식으로 처리를 하게 된다.

이 경우 역시 언어 별로, 같은 곳에 사용된 단어들 등으로 아틀라스를 나눠서 뽑아두는 편이 좋다.

 

 

Rendering PipeLine

그래픽 파이프라인

렌더링 파이프라인의 하위개념 중 하나인데, 그래픽 파이프라인은 "3차원 컴퓨터 그래픽스에서 3차원 이미지를 2차원 래스터 이미지로 표현을 하기 위한 단계적인 방법을 말한다."라고 위키백과에 설명이 되어있다.

즉, 우리가 만드는 가상 세계의 3차원 도형들을 2차원인 화면 상으로 그리는 작업이라고 보면 될 것 같다.

 

중요한 것은 이 그래픽(스) 파이프라인에 여러 종류가 있는데, 그 중에 OpenGL에서 개발한 것을 Rendering PipeLine이라고 하는 듯하다.

예를 들면, 애플의 Metal가 있겠다.

 

렌더링 파이프라인 과정

OpenGL Rendering PipeLine Overview의 이미지 참고

 

들어도 들어도 어려운 렌더링 파이프라인에 대해서 내가 이해한 것만 정리해보겠다. ㅠ

참고로 노란색은 무조건 하는 단계이고, 파란 색 중에서 점선으로 이루어져 있는 박스는 생략이 가능한 단계이다.

 

Vertex Specification - Vertex Shader는 정점을 그려주는 단계이다. 

다만, 이 과정에서는 실제로 화면에 그리는 것이 아니라 가상으로 정점을 구축? 만 하고, 카메라를 계산하지 않으므로 상대 좌표로 모든 정점을 생성하게 된다.

ingoing vertex, outgoing vertex로 그리는 방향이 존재하는데, ingoing은 순서대로 그리는 것이고, outgoing은 반대로 그리도록 하는 것이다.

 

Vertex Post-Processing 단계에서 실제로 쉐이더를 그리기 시작하고, Resterization 단계에서는 정점 사이의 선 그리고, 면을 그리기 시작한다. 

여기서 UV나 Color 그리고, 노말 맵을 넣어주게 된다. 

fragment shader를 만약 적용했다면, 해당 쉐이더의 color 등의 쉐이더 설정이 fragment shader 단계에서 적용된다.

 

마지막으로 Per-Sample Operations에서는 blending과 depth 연산이 들어간다.

alpha 값이 바로 여기서 적용되는데, 다른 두 개의 이미지가 앞 뒤로 있을 때에 뒤의 이미지와 앞의 이미지가 얼마나 섞여 보일지 정하는 것이 바로 알파 값이기 때문이다.

 

 

게임 최적화

유니티 뿐 아니라 여러 개발을 하는 데 있어서 필요한 최적화에 대해서 넓게 다루었다.

솔직히 거의 대부분 처음 들어보는 개념들이라서 완벽하게 이해할 수 없었지만, 그래도 이해한 만큼만 아래에 간단하게 정리하려고 한다.

 

코드 최적화

Update()에서 사용 줄여야 되는 코드들

매 프레임마다 반복적으로 실행되는 Update문에서 사용을 자제해야 하는 코드들을 정리했다.

 

1. instantiate, new Obj처럼 생성, 할당하는 코드

    : 업데이트 마다 선언, 할당하면, 계속 할당하는 데에 메모리를 새로 생성하므로 좋지 않다. 미리 전역 변수로 선언하고, 할당해놓은 다음 가져다가 사용하자.

2. Resources.Load, File.io

    : 매 프레임마다 파일을 색인하고, 가져다가 할당하는 것 역시 상당히 메모리적으로 무거운 함수이다.

3. addComponent, getComponent 

    : 내부적으로 사용되는 reflection 역시, 반복적으로 사용하기에 너무 무겁다.

4. Find, FindObjectOfType 등

    : 역시나 reflection을 사용하므로 사용을 자제하는 편이 좋다.

 

Async (비동기)

async라는 말이 붙어서 비동기로 실행할 수 있는 함수들이 있다.

비동기는 코드 순서적으로 실행하는 것이 아니라 다른 코드를 실행함과 동시에 해당 함수 역시 처리한다는 뜻인데, 이를 더 빠르다고 착각하면 안 되고, 적절한 상황에서만 사용해야 한다.

 

1. Resources.Load + Async

    : 앱 내부적으로 파일을 다운 받는 과정에서 로딩 바를 비동기적으로 사용한다. 이 경우는 비동기로 하지 않으면, 0%에서 파일 로드가 진행되고, 완전히 끝날 때까지 로딩 바를 진행시키지 않다가 100%가 된 이후에 한 번에 다 채워버리기 때문이다. 

 

특히 Resources.Load의 Async는 Enumerator의 일종의 리스트로 리턴이 되는데 (현재 파일 / 전체 파일 수) 이를 이용해서 로딩 바를 채우기도, 어떤 파일을 처리하고 있는지 문구를 바꾸기도 쉽다. 

(원래라면, 코루틴을 돌려서 파일 다운로드의 분기를 나눠서 처리하던 것을 AsyncOperation으로 대체할 수 있다.)

 

2. 오픈 월드에서의 SceneManager.Load + Async

    : 씬 로드를 Additive모드와 비동기로 불러오면, 오픈월드에서 자연스럽게 맵이 이어지는 것처럼 만들 수 있다. 유저는 로딩이 더 적어졌다고 느끼게 되는데, 이는 실제로 빨라졌다기보다는 맵을 로드하는 동안 플레이도 같이 진행되어서 유저 입장에서 맵을 로드하는 중이라는 인식이 흐려져서 그렇게 느껴지는 것이다.

 

Scriptable Object

변수가 아닌 상수들을 저장하고 가져다 쓰는데 아주 좋은 역할을 한다.

이 경우 instantiate처럼 계속 메모리가 쓰이지 않고, 상수 데이터만 가져오기 때문에 최적화에 도움이 된다.

 

Debug.Log

디버그 모드에서는 매우 중요한 역할을 하는 코드이지만, 당연하게도 릴리즈 버전에서는 모두 지워버려야 한다.

실제로 디버그 로그는 로그 텍스트 파일을 열어서 쓰는 과정 (File.IO) 때문에 프로그램을 느리게 하는 원인이 될 수 있다.

 

디버그 로그를 스크립트에서 모두 지우기 힘들거나 나중에 다시 개발 모드에서 사용해야 하는 경우는 Project Settings - Player - Script Define Symbol에서 유저 심볼로 디버그 모드임을 알리는 심볼을 추가하고, 스크립트에서 #if '심볼이름' 으로 해당 모드에서만 debug.log를 실행하게끔 할 수도 있다. 자세한 내용은 아래 블로그에서 참고하면 좋을 것 같다.

 

 

유니티 「Define Symbol」을 통한 Debug 전처리기

안녕하세요. 창작자 픽케입니다. 앞서 포스팅으로 소개했던 전처리기(Preprocessor)는 다양한 플랫폼(Pla...

blog.naver.com

 

 

그래픽 최적화

TargetFramerate

gpu는 상황이 괜찮다면, fps를 괜찮은 만큼(상한치)까지 계속 늘리려고 한다. 하지만, 게임에 따라, 상황에 따라 더 올릴 필요가 없다면, 이를 조정하면 좋다.

실제로 인간의 눈으로 정확히 분간할 수 있는 수준은 30fps이므로, 크게 fps가 좋아야 할 게임이 아니라면, 이 이상으로  높일 필요가 없고, 대사 등 정지 화면이 사용되는 순간에는 10 ~ 15 fps 정도로 낮춰도 무방하다.

 

여기서 cpu나 gpu 중 실제로 문제가 되어서 프로그램이 다운되거나 에러가 나는 경우 보통은 디버그로 알 수 있지만, 안드로이드 등으로 빌드하면, 알기가 쉽지 않다.

이 경우에는 logcat으로 알아보게 되는데, systrace를 사용하면, 실행 중에 생기는 .so 파일을 이용해서 개발자가 문제가 무엇인지 logcat으로 디버그 할 수 있다.

 

Texture/Object/Mesh - read & write enable

게임 도중에 텍스쳐를 바꿀 일 없다면, 체크를 해제하는 것이 좋다.

체크를 하면, 메모리에 계속 상주해서 상태를 체크하게 된다.

 

해상도

사실 게임 해상도를 낮추는 것이 프로그램의 속도를 높일 때에는 가장 효과적이다.

크게 해상도에 신경 쓸 필요가 없는 게임이라면, 해상도를 조절하는 것도 좋은 방법이다.

다만, SD 이하부터는 오래된 게임처럼 될 수 있으므로, 그런 효과를 바라는 것이 아니라면, 그 이상으로만 조절하는 편이 좋겠다.

(아니면, 유저마다 조절할 수 있도록 옵션을 만들어 주는 것도 좋다.)

 

UI, 2D에서의 최적화

Sprite Packer

스프라이트를 불러올 때에는 컴퓨터는 POT(2의 n승) 크기로 가져오는 데, 이 값을 맞추기 위해서 매번 빈 곳에 패딩을 넣게 된다. 

이 과정에서 메모리 낭비가 나오게 되고, 이를 방지하기 위해서 스프라이트를 POT 크기로 맞추거나 Sprite Packer를 통해서 폰트 아틀라스 처럼 하나의 큰 POT 스프라이트를 만들어서 쪼개 쓸 수가 있다.

 

다만, 압축 방식에 따라서 구분해서 합쳐야 한다.

예를 들어 RGB 방식의 스프라이트들과 RGBA 스프라이트들을 같이 패킹 해버리면, 알파 값이 사라져 버릴 수 있기 때문이다.

 

압축 포맷

스프라이트의 압축 방식도 최적화에서 중요한 요소이다.

각 플랫폼은 지원하는 압축 방식들이 있는데, 이를 잘 맞춰서 포맷을 정해야 게임에서 스프라이트가 정상적으로 나오고, 메모리 낭비도 잡을 수 있다.

 

플랫폼 별 지원하는 포맷

 

압축 포맷을 변경하기 위해서는 Overwrite를 체크해야 한다.

포맷을 바꾸기 위해서는 원하는 플랫폼을 선택하고, overWrite를 체크한 뒤에 변경하면 된다.

다만, 디폴트 값이 기본이므로 더 자주 사용할 압축 포맷을 디폴트 값으로 설정하고, 아닌 포맷 방식만 각각 overwrite 해서 넣는 편이 좋다.

 

캔버스 최적화

캔버스는 하위 내용이 수정되면, 해당 캔버스의 내용을 다 다시 그린다.(drawCall)

그렇기 때문에 하위 캔버스를 두어서 다시 그려지는 것만 넣는 것이 좋다.

 

압축 형식이나 같은 스프라이트 아틀라스에 있는 UI나 같은 채널(RGB, RGBA)을 쓰는 UI들을 최대한 묶는 편이 좋다.

 

그리고, 카메라 밖에서 ui를 계속 그릴 필요가 없다면, Camera Culling을 꺼버리는 것도 좋다.

(렌더러 컴포넌트를 꺼버리는 것보다 캔버스 자체의 엑티브를 꺼버리는 편이 더 좋다.)

마찬가지로, 터치나 클릭될 필요가 없는 UI는 모두 Graphics Raycaster를 끄는 것도 최적화에 도움이 된다.

 

Shadow

2D나 UI로만 활용하는 게임 중에서 그림자가 있을 필요가 없는 게임이라면, shadow를 오브젝트마다 혹은 아예 프로젝트에서 꺼버리는 것이 좋다. (Light 옵션에서)

 

 

오디오 최적화

압축 포맷 (Compression format)

오디오 파일은 기본적으로 스테레오를 사용하는데, 이는 두 개의 주파수를 통해서 입체적인 느낌을 들게 한다. 하지만, 이는 그야말로 용량이 두 배라서, 배경 음악처럼 굳이 멀고 가깝고 등의 더 입체적인 효과가 필요한 음악이 아닌 경우 mono로 변경해버려도 된다. 이 경우는 force to mono를 사용하면 된다.

 

Load type

이 역시 압축 포맷 옵션이다.

아래의 설명에 따라서 적절한 것을 사용하면 된다.

 

1. Streaming 

    : 긴 음악에 적합한 포맷

    - 조금씩 쪼개서 음악을 가져오는 방식으로 로딩 시간이 짧아지고, 중간에 씬을 바꾸는 등으로 끊기는 경우에 메모리 적으로 이득을 볼 수 있다. 긴 음악의 경우 압축 손실이나 오버헤드 문제가 많으므로 무조건 이 옵션을 사용하자.

 

2. Compressed In Memory 

    : 압축 상태로 로드하는 포맷 옵션. 

    - 하나의 압축 포맷을 정해서 해당 압축 상태 그대로 로드한다. 

    - 압축은 음질이 낮아지거나 깨지는 일이 종종 생길 수 있는데, 음악이 길지 않고, 압축한 상태로도 듣기에 크게 문제가 없다면, 이 옵션을 사용하면 된다.

    - 다만, 코덱 등으로 풀어주어야 하는 음원의 경우 오버헤드가 일어날 수 있다.

 

3. Decompress OnLoad 

    : 로딩할 때에 압축을 풀어서 재생하는 옵션.

    - 손실을 최대한 줄여야 하면서도 짧은 음원이라 메모리에 큰 타격이 없을 때에(유니티가 추천하기로는 압축 푸는데 드는 오버헤드가 200KB라서 이 미만일 경우 사용해도 무방) 사용할 수 있다.

 

 

 

유데미 코리아 바로가기 : 

 

Udemy Korea - 실용적인 온라인 강의, 글로벌 전문가에게 배워보세요. | Udemy Korea

유데미코리아 AI, 파이썬, 리엑트, 자바, 노션, 디자인, UI, UIX, 기획 등 전문가의 온라인 강의를 제공하고 있습니다.

www.udemykorea.com

 

💡 본 포스팅은 유데미-웅진씽크빅 취업 부트캠프 유니티 1기 과정 후기로 작성되었습니다.